diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 8d23d177410b438fc9aecbe317b09c7b04715ab3..45d06e8e9755a63f332c48efc7332277d72ca27b 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -303,7 +303,41 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo }); } +/* eslint-disable @gitlab/require-i18n-strings */ +export function keypressNoteText(e) { + if (this.selectionStart === this.selectionEnd) { + return; + } + const keys = { + '*': '**{text}**', // wraps with bold character + _: '_{text}_', // wraps with italic character + '`': '`{text}`', // wraps with inline character + "'": "'{text}'", // single quotes + '"': '"{text}"', // double quotes + '[': '[{text}]', // brackets + '{': '{{text}}', // braces + '(': '({text})', // parentheses + '<': '<{text}>', // angle brackets + }; + const tag = keys[e.key]; + + if (tag) { + e.preventDefault(); + + updateText({ + tag, + textArea: this, + blockTag: '', + wrap: true, + select: '', + tagContent: '', + }); + } +} +/* eslint-enable @gitlab/require-i18n-strings */ + export function addMarkdownListeners(form) { + $('.markdown-area', form).on('keydown', keypressNoteText); return $('.js-md', form) .off('click') .on('click', function() { @@ -342,5 +376,6 @@ export function addEditorMarkdownListeners(editor) { } export function removeMarkdownListeners(form) { + $('.markdown-area', form).off('keydown', keypressNoteText); return $('.js-md', form).off('click'); } diff --git a/changelogs/unreleased/bw-surround-text-wth-char.yml b/changelogs/unreleased/bw-surround-text-wth-char.yml new file mode 100644 index 0000000000000000000000000000000000000000..506271f13c1f54867321048a19c787cf419aa3d7 --- /dev/null +++ b/changelogs/unreleased/bw-surround-text-wth-char.yml @@ -0,0 +1,5 @@ +--- +title: Surround selected text in markdown fields on certain key presses +merge_request: 37151 +author: +type: added diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 8eba2c985956f845a398551b1653207a38d3fc59..8f2fb9e827c9423a2e37e8dca17cbf98b4cc8cb1 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -8,6 +8,7 @@ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: '[some link](other-page)') } let(:wiki_content) do <<-HEREDOC +Some text so key event for [ does not trigger an incorrect replacement. [regular link](regular) [relative link 1](../relative) [relative link 2](./relative) diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 2e52958a8289f700ee3f9bc9bdc39dc877a1bdc9..1aaae80dcdf3b479150546d93939e565a6a28cf4 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -1,4 +1,4 @@ -import { insertMarkdownText } from '~/lib/utils/text_markdown'; +import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown'; describe('init markdown', () => { let textArea; @@ -115,14 +115,15 @@ describe('init markdown', () => { describe('with selection', () => { const text = 'initial selected value'; const selected = 'selected'; + let selectedIndex; + beforeEach(() => { textArea.value = text; - const selectedIndex = text.indexOf(selected); + selectedIndex = text.indexOf(selected); textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); }); it('applies the tag to the selected value', () => { - const selectedIndex = text.indexOf(selected); const tag = '*'; insertMarkdownText({ @@ -153,6 +154,29 @@ describe('init markdown', () => { expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`)); }); + it.each` + key | expected + ${'['} | ${`[${selected}]`} + ${'*'} | ${`**${selected}**`} + ${"'"} | ${`'${selected}'`} + ${'_'} | ${`_${selected}_`} + ${'`'} | ${`\`${selected}\``} + ${'"'} | ${`"${selected}"`} + ${'{'} | ${`{${selected}}`} + ${'('} | ${`(${selected})`} + ${'<'} | ${`<${selected}>`} + `('generates $expected when $key is pressed', ({ key, expected }) => { + const event = new KeyboardEvent('keydown', { key }); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(event); + + expect(textArea.value).toEqual(text.replace(selected, expected)); + + // cursor placement should be after selection + 2 tag lengths + expect(textArea.selectionStart).toBe(selectedIndex + expected.length); + }); + describe('and text to be selected', () => { const tag = '[{text}](url)'; const select = 'url'; @@ -178,7 +202,7 @@ describe('init markdown', () => { it('selects the right text when multiple tags are present', () => { const initialValue = `${tag} ${tag} ${selected}`; textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(selected); + selectedIndex = initialValue.indexOf(selected); textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); insertMarkdownText({ textArea, @@ -204,7 +228,7 @@ describe('init markdown', () => { const initialValue = `text ${expectedUrl} text`; textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(expectedUrl); + selectedIndex = initialValue.indexOf(expectedUrl); textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); insertMarkdownText({