From 066a99b6e9d0849b477858c9aac274c2f81bb367 Mon Sep 17 00:00:00 2001 From: Sam Bigelow Date: Fri, 14 Dec 2018 12:56:25 -0500 Subject: Add markdown buttons to file editor Currently, we have markdown files in many places (e.g. comments, new issues, etc.). This Merge Request detects if the file being edited is a markdown file and adds markdown buttons and their functionality to the single file editor (Not the web IDE) --- app/assets/javascripts/blob_edit/blob_bundle.js | 9 +- app/assets/javascripts/blob_edit/edit_blob.js | 20 +- app/assets/javascripts/lib/utils/text_markdown.js | 162 +++++++-- .../stylesheets/framework/markdown_area.scss | 2 +- app/assets/stylesheets/pages/editor.scss | 4 + app/helpers/blob_helper.rb | 3 +- app/views/projects/_md_preview.html.haml | 12 +- app/views/projects/blob/_editor.html.haml | 4 + .../projects/blob/_markdown_buttons.html.haml | 13 + ...markdown-editing-buttons-to-the-file-editor.yml | 5 + spec/javascripts/blob_edit/blob_bundle_spec.js | 13 +- spec/javascripts/lib/utils/text_markdown_spec.js | 387 +++++++++++++-------- 12 files changed, 421 insertions(+), 213 deletions(-) create mode 100644 app/views/projects/blob/_markdown_buttons.html.haml create mode 100644 changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index b07f951346e..5f64175362d 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -16,6 +16,7 @@ export default () => { const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); + const isMarkdown = editBlobForm.data('is-markdown'); const commitButton = $('.js-commit-button'); const cancelLink = $('.btn.btn-cancel'); @@ -27,7 +28,13 @@ export default () => { window.onbeforeunload = null; }); - new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); + new EditBlob({ + assetsPath: `${urlRoot}${assetsPath}`, + filePath, + currentAction, + projectId, + isMarkdown, + }); new NewCommitForm(editBlobForm); // returning here blocks page navigation diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 6e19548eed2..011898a5e7a 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -6,22 +6,31 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import getModeByFileExtension from '~/lib/utils/ace_utils'; +import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; export default class EditBlob { - constructor(assetsPath, aceMode, currentAction, projectId) { - this.configureAceEditor(aceMode, assetsPath); + // The options object has: + // assetsPath, filePath, currentAction, projectId, isMarkdown + constructor(options) { + this.options = options; + this.configureAceEditor(); this.initModePanesAndLinks(); this.initSoftWrap(); - this.initFileSelectors(currentAction, projectId); + this.initFileSelectors(); } - configureAceEditor(filePath, assetsPath) { + configureAceEditor() { + const { filePath, assetsPath, isMarkdown } = this.options; ace.config.set('modePath', `${assetsPath}/ace`); ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/modelist'); this.editor = ace.edit('editor'); + if (isMarkdown) { + addEditorMarkdownListeners(this.editor); + } + // This prevents warnings re: automatic scrolling being logged this.editor.$blockScrolling = Infinity; @@ -32,7 +41,8 @@ export default class EditBlob { } } - initFileSelectors(currentAction, projectId) { + initFileSelectors() { + const { currentAction, projectId } = this.options; this.fileTemplateMediator = new TemplateSelectorMediator({ currentAction, editor: this.editor, diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 1254ec798a6..84a617acb42 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -8,6 +8,10 @@ function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } +function addBlockTags(blockTag, selected) { + return `${blockTag}\n${selected}\n${blockTag}`; +} + function lineBefore(text, textarea) { var split; split = text @@ -24,19 +28,45 @@ function lineAfter(text, textarea) { .split('\n')[0]; } +function editorBlockTagText(text, blockTag, selected, editor) { + const lines = text.split('\n'); + const selectionRange = editor.getSelectionRange(); + const shouldRemoveBlock = + lines[selectionRange.start.row - 1] === blockTag && + lines[selectionRange.end.row + 1] === blockTag; + + if (shouldRemoveBlock) { + if (blockTag !== null) { + // ace is globally defined + // eslint-disable-next-line no-undef + const { Range } = ace.require('ace/range'); + const lastLine = lines[selectionRange.end.row + 1]; + const rangeWithBlockTags = new Range( + lines[selectionRange.start.row - 1], + 0, + selectionRange.end.row + 1, + lastLine.length, + ); + editor.getSelection().setSelectionRange(rangeWithBlockTags); + } + return selected; + } + return addBlockTags(blockTag, selected); +} + function blockTagText(text, textArea, blockTag, selected) { - const before = lineBefore(text, textArea); - const after = lineAfter(text, textArea); - if (before === blockTag && after === blockTag) { + const shouldRemoveBlock = + lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag; + + if (shouldRemoveBlock) { // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); } return selected; - } else { - return blockTag + '\n' + selected + '\n' + blockTag; } + return addBlockTags(blockTag, selected); } function moveCursor({ @@ -46,33 +76,48 @@ function moveCursor({ positionBetweenTags, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }) { var pos; - if (!textArea.setSelectionRange) { + if (textArea && !textArea.setSelectionRange) { return; } if (select && select.length > 0) { - // calculate the part of the text to be selected - const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); - const endPosition = startPosition + select.length; - return textArea.setSelectionRange(startPosition, endPosition); - } - if (textArea.selectionStart === textArea.selectionEnd) { - if (positionBetweenTags) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; + if (textArea) { + // calculate the part of the text to be selected + const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); + const endPosition = startPosition + select.length; + return textArea.setSelectionRange(startPosition, endPosition); + } else if (editor) { + editor.navigateLeft(tag.length - tag.indexOf(select)); + editor.getSelection().selectAWord(); + return; } + } + if (textArea) { + if (textArea.selectionStart === textArea.selectionEnd) { + if (positionBetweenTags) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } - if (removedLastNewLine) { - pos -= 1; - } + if (removedLastNewLine) { + pos -= 1; + } - if (cursorOffset) { - pos -= cursorOffset; - } + if (cursorOffset) { + pos -= cursorOffset; + } - return textArea.setSelectionRange(pos, pos); + return textArea.setSelectionRange(pos, pos); + } + } else if (editor && editorSelectionStart.row === editorSelectionEnd.row) { + if (positionBetweenTags) { + editor.navigateLeft(tag.length); + } } } @@ -85,6 +130,7 @@ export function insertMarkdownText({ selected = '', wrap, select, + editor, }) { var textToInsert, selectedSplit, @@ -92,11 +138,20 @@ export function insertMarkdownText({ removedLastNewLine, removedFirstNewLine, currentLineEmpty, - lastNewLine; + lastNewLine, + editorSelectionStart, + editorSelectionEnd; removedLastNewLine = false; removedFirstNewLine = false; currentLineEmpty = false; + if (editor) { + const selectionRange = editor.getSelectionRange(); + + editorSelectionStart = selectionRange.start; + editorSelectionEnd = selectionRange.end; + } + // check for link pattern and selected text is an URL // if so fill in the url part instead of the text part of the pattern. if (tag === LINK_TAG_PATTERN) { @@ -119,14 +174,27 @@ export function insertMarkdownText({ } // Remove the last newline - if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { - removedLastNewLine = true; - selected = selected.replace(/\n$/, ''); + if (textArea) { + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + } else if (editor) { + if (editorSelectionStart.row !== editorSelectionEnd.row) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } } selectedSplit = selected.split('\n'); - if (!wrap) { + if (editor && !wrap) { + lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row]; + + if (/^\s*$/.test(lastNewLine)) { + currentLineEmpty = true; + } + } else if (textArea && !wrap) { lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); // Check whether the current line is empty or consists only of spaces(=handle as empty) @@ -135,13 +203,19 @@ export function insertMarkdownText({ } } - startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; + const isBeginning = + (textArea && textArea.selectionStart === 0) || + (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0); + + startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; const textPlaceholder = '{text}'; if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (blockTag != null && blockTag !== '') { - textToInsert = blockTagText(text, textArea, blockTag, selected); + textToInsert = editor + ? editorBlockTagText(text, blockTag, selected, editor) + : blockTagText(text, textArea, blockTag, selected); } else { textToInsert = selectedSplit .map(function(val) { @@ -170,7 +244,11 @@ export function insertMarkdownText({ textToInsert += '\n'; } - insertText(textArea, textToInsert); + if (editor) { + editor.insert(textToInsert); + } else { + insertText(textArea, textToInsert); + } return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), @@ -178,6 +256,9 @@ export function insertMarkdownText({ positionBetweenTags: wrap && selected.length === 0, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }); } @@ -217,6 +298,25 @@ export function addMarkdownListeners(form) { }); } +export function addEditorMarkdownListeners(editor) { + $('.js-md') + .off('click') + .on('click', function(e) { + const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data(); + + insertMarkdownText({ + tag: mdTag, + blockTag: mdBlock, + wrap: !mdPrepend, + select: mdSelect, + selected: editor.getSelectedText(), + text: editor.getValue(), + editor, + }); + editor.focus(); + }); +} + export function removeMarkdownListeners(form) { return $('.js-md', form).off('click'); } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5609a2086e6..eb191cf4d39 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -173,7 +173,7 @@ svg { width: 14px; height: 14px; - margin-top: 3px; + vertical-align: middle; fill: $gl-text-color-secondary; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index f46ff360496..5a988b184b6 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -128,6 +128,10 @@ width: 100%; } } + + @media(max-width: map-get($grid-breakpoints, md)-1) { + clear: both; + } } .editor-ref { diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 3dea0975beb..23d6684a8e6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -177,7 +177,8 @@ module BlobHelper 'relative-url-root' => Rails.application.config.relative_url_root, 'assets-prefix' => Gitlab::Application.config.assets.prefix, 'blob-filename' => @blob && @blob.path, - 'project-id' => project.id + 'project-id' => project.id, + 'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path) } end diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 0f709c65d0e..03ba1104507 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -18,17 +18,7 @@ Preview %li.md-header-toolbar.active - = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") }) - = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") }) - = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") }) - = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") }) - = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") }) - = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") }) - = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") }) - = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") }) - = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") }) - %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } } - = sprite_icon("screen-full") + = render 'projects/blob/markdown_buttons', show_fullscreen_button: true .md-write-holder = yield diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 3c1f33ea95e..a54460f1196 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,4 +1,6 @@ - action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' +- file_name = params[:id].split("/").last ||= "" +- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) .file-holder-bottom-radius.file-holder.file.append-bottom-default .js-file-title.file-title.clearfix{ data: { current_action: action } } @@ -17,6 +19,8 @@ required: true, class: 'form-control new-file-name js-file-path-name-input' .file-buttons + - if is_markdown + = render 'projects/blob/markdown_buttons', show_fullscreen_button: false = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/blob/_markdown_buttons.html.haml b/app/views/projects/blob/_markdown_buttons.html.haml new file mode 100644 index 00000000000..1d6acd86108 --- /dev/null +++ b/app/views/projects/blob/_markdown_buttons.html.haml @@ -0,0 +1,13 @@ +.md-header-toolbar.active + = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") }) + = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") }) + = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") }) + = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") }) + = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") }) + = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") }) + = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") }) + = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") }) + = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") }) + - if show_fullscreen_button + %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } } + = sprite_icon("screen-full") diff --git a/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml b/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml new file mode 100644 index 00000000000..00eb5223d58 --- /dev/null +++ b/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml @@ -0,0 +1,5 @@ +--- +title: Add markdown helper buttons to file editor +merge_request: 23480 +author: +type: added diff --git a/spec/javascripts/blob_edit/blob_bundle_spec.js b/spec/javascripts/blob_edit/blob_bundle_spec.js index 57f60a4a3dd..48af0148e3f 100644 --- a/spec/javascripts/blob_edit/blob_bundle_spec.js +++ b/spec/javascripts/blob_edit/blob_bundle_spec.js @@ -1,18 +1,11 @@ import blobBundle from '~/blob_edit/blob_bundle'; import $ from 'jquery'; -window.ace = { - config: { - set: () => {}, - loadModule: () => {}, - }, - edit: () => ({ focus: () => {} }), -}; - -describe('EditBlob', () => { +describe('BlobBundle', () => { beforeEach(() => { + spyOnDependency(blobBundle, 'EditBlob').and.stub(); setFixtures(` -
+
`); diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js index f71d27eb4e4..df4029555bb 100644 --- a/spec/javascripts/lib/utils/text_markdown_spec.js +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -13,215 +13,296 @@ describe('init markdown', () => { textArea.parentNode.removeChild(textArea); }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; + describe('textArea', () => { + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; - - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '*', - blockTag: null, - selected: '', - wrap: false, - }); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); - - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + insertMarkdownText({ + textArea, + text: textArea.value, + tag: '*', + blockTag: null, + selected: '', + wrap: false, + }); - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '*', - blockTag: null, - selected: '', - wrap: false, + expect(textArea.value).toEqual(`${initialValue}* `); }); - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + insertMarkdownText({ + textArea, + text: textArea.value, + tag: '*', + blockTag: null, + selected: '', + wrap: false, + }); - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '*', - blockTag: null, - selected: '', - wrap: false, + expect(textArea.value).toEqual(`${initialValue}\n* `); }); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + insertMarkdownText({ + textArea, + text: textArea.value, + tag: '*', + blockTag: null, + selected: '', + wrap: false, + }); - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '*', - blockTag: null, - selected: '', - wrap: false, + expect(textArea.value).toEqual(`${initialValue}* `); }); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; - it('places the cursor inside the tags', () => { - const start = 'lorem '; - const end = ' ipsum'; - const tag = '*'; + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - textArea.value = `${start}${end}`; - textArea.setSelectionRange(start.length, start.length); + insertMarkdownText({ + textArea, + text: textArea.value, + tag: '*', + blockTag: null, + selected: '', + wrap: false, + }); - insertMarkdownText({ - textArea, - text: textArea.value, - tag, - blockTag: null, - selected: '', - wrap: true, + expect(textArea.value).toEqual(`${initialValue}* `); }); - expect(textArea.value).toEqual(`${start}**${end}`); + it('places the cursor inside the tags', () => { + const start = 'lorem '; + const end = ' ipsum'; + const tag = '*'; - // cursor placement should be between tags - expect(textArea.selectionStart).toBe(start.length + tag.length); - }); - }); + textArea.value = `${start}${end}`; + textArea.setSelectionRange(start.length, start.length); - describe('with selection', () => { - const text = 'initial selected value'; - const selected = 'selected'; - beforeEach(() => { - textArea.value = text; - const selectedIndex = text.indexOf(selected); - textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); - }); + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: '', + wrap: true, + }); - it('applies the tag to the selected value', () => { - const selectedIndex = text.indexOf(selected); - const tag = '*'; + expect(textArea.value).toEqual(`${start}**${end}`); - insertMarkdownText({ - textArea, - text: textArea.value, - tag, - blockTag: null, - selected, - wrap: true, + // cursor placement should be between tags + expect(textArea.selectionStart).toBe(start.length + tag.length); }); - - expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`)); - - // cursor placement should be after selection + 2 tag lengths - expect(textArea.selectionStart).toBe(selectedIndex + selected.length + 2 * tag.length); }); - it('replaces the placeholder in the tag', () => { - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '[{text}](url)', - blockTag: null, - selected, - wrap: false, + describe('with selection', () => { + const text = 'initial selected value'; + const selected = 'selected'; + beforeEach(() => { + textArea.value = text; + const selectedIndex = text.indexOf(selected); + textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); }); - expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`)); - }); + it('applies the tag to the selected value', () => { + const selectedIndex = text.indexOf(selected); + const tag = '*'; - describe('and text to be selected', () => { - const tag = '[{text}](url)'; - const select = 'url'; - - it('selects the text', () => { insertMarkdownText({ textArea, text: textArea.value, tag, blockTag: null, selected, - wrap: false, - select, + wrap: true, }); - const expectedText = text.replace(selected, `[${selected}](url)`); + expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`)); - expect(textArea.value).toEqual(expectedText); - expect(textArea.selectionStart).toEqual(expectedText.indexOf(select)); - expect(textArea.selectionEnd).toEqual(expectedText.indexOf(select) + select.length); + // cursor placement should be after selection + 2 tag lengths + expect(textArea.selectionStart).toBe(selectedIndex + selected.length + 2 * tag.length); }); - it('selects the right text when multiple tags are present', () => { - const initialValue = `${tag} ${tag} ${selected}`; - textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(selected); - textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); + it('replaces the placeholder in the tag', () => { insertMarkdownText({ textArea, text: textArea.value, - tag, + tag: '[{text}](url)', blockTag: null, selected, wrap: false, - select, }); - const expectedText = initialValue.replace(selected, `[${selected}](url)`); - - expect(textArea.value).toEqual(expectedText); - expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select)); - expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length); + expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`)); }); - it('should support selected urls', () => { - const expectedUrl = 'http://www.gitlab.com'; - const expectedSelectionText = 'text'; - const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`; - const initialValue = `text ${expectedUrl} text`; + describe('and text to be selected', () => { + const tag = '[{text}](url)'; + const select = 'url'; + + it('selects the text', () => { + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected, + wrap: false, + select, + }); + + const expectedText = text.replace(selected, `[${selected}](url)`); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(select)); + expect(textArea.selectionEnd).toEqual(expectedText.indexOf(select) + select.length); + }); - textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(expectedUrl); - textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); + it('selects the right text when multiple tags are present', () => { + const initialValue = `${tag} ${tag} ${selected}`; + textArea.value = initialValue; + const selectedIndex = initialValue.indexOf(selected); + textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected, + wrap: false, + select, + }); + + const expectedText = initialValue.replace(selected, `[${selected}](url)`); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select)); + expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length); + }); - insertMarkdownText({ - textArea, - text: textArea.value, - tag, - blockTag: null, - selected: expectedUrl, - wrap: false, - select, + it('should support selected urls', () => { + const expectedUrl = 'http://www.gitlab.com'; + const expectedSelectionText = 'text'; + const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`; + const initialValue = `text ${expectedUrl} text`; + + textArea.value = initialValue; + const selectedIndex = initialValue.indexOf(expectedUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: expectedUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, + ); }); + }); + }); + }); + + describe('Ace Editor', () => { + let editor; + + beforeEach(() => { + editor = { + getSelectionRange: () => ({ + start: 0, + end: 0, + }), + getValue: () => 'this is text \n in two lines', + insert: () => {}, + navigateLeft: () => {}, + }; + }); + + it('uses ace editor insert text when editor is passed in', () => { + spyOn(editor, 'insert'); - expect(textArea.value).toEqual(expectedText); - expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); - expect(textArea.selectionEnd).toEqual( - expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, - ); + insertMarkdownText({ + text: editor.getValue, + tag: '*', + blockTag: null, + selected: '', + wrap: false, + editor, + }); + + expect(editor.insert).toHaveBeenCalled(); + }); + + it('adds block tags on line above and below selection', () => { + spyOn(editor, 'insert'); + + const selected = 'this text \n is multiple \n lines'; + const text = `before \n ${selected} \n after`; + + insertMarkdownText({ + text, + tag: '', + blockTag: '***', + selected, + wrap: true, + editor, + }); + + expect(editor.insert).toHaveBeenCalledWith(`***\n${selected}\n***`); + }); + + it('uses ace editor to navigate back tag length when nothing is selected', () => { + spyOn(editor, 'navigateLeft'); + + insertMarkdownText({ + text: editor.getValue, + tag: '*', + blockTag: null, + selected: '', + wrap: true, + editor, }); + + expect(editor.navigateLeft).toHaveBeenCalledWith(1); + }); + + it('ace editor does not navigate back when there is selected text', () => { + spyOn(editor, 'navigateLeft'); + + insertMarkdownText({ + text: editor.getValue, + tag: '*', + blockTag: null, + selected: 'foobar', + wrap: true, + editor, + }); + + expect(editor.navigateLeft).not.toHaveBeenCalled(); }); }); }); -- cgit v1.2.1