From 72bd58b49ab19aa43c6802189bb0be985da4e32d Mon Sep 17 00:00:00 2001 From: Brett Walker <bwalker@gitlab.com> Date: Fri, 23 Sep 2022 16:21:26 +0000 Subject: [PATCH] Use toolbar buttons for indent/outdent to better support cross-platform Changelog: fixed --- .../behaviors/shortcuts/keybindings.js | 14 +++++ .../javascripts/lib/utils/text_markdown.js | 60 +++++++++---------- .../vue_shared/components/markdown/header.vue | 30 ++++++++++ .../components/markdown/toolbar_button.vue | 6 ++ .../shared/blob/_markdown_buttons.html.haml | 8 +++ locale/gitlab.pot | 18 ++++++ spec/frontend/lib/utils/text_markdown_spec.js | 38 +++++++----- .../components/markdown/header_spec.js | 2 + 8 files changed, 128 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index 23b66405844c0..3239375bf7c82 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -145,6 +145,20 @@ export const LINK_TEXT = { customizable: false, }; +export const INDENT_LINE = { + id: 'editing.indentLine', + description: __('Indent line'), + defaultKeys: ['mod+]'], // eslint-disable-line @gitlab/require-i18n-strings + customizable: false, +}; + +export const OUTDENT_LINE = { + id: 'editing.outdentLine', + description: __('Outdent line'), + defaultKeys: ['mod+['], // eslint-disable-line @gitlab/require-i18n-strings + customizable: false, +}; + export const TOGGLE_MARKDOWN_PREVIEW = { id: 'editing.toggleMarkdownPreview', description: __('Toggle Markdown preview'), diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 48be8af3ff666..5e2b8b786a45e 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -391,13 +391,15 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo /** * Indents selected lines to the right by 2 spaces * - * @param {Object} textArea - the targeted text area + * @param {Object} textArea - jQuery object with the targeted text area */ -function indentLines(textArea) { +function indentLines($textArea) { + const textArea = $textArea.get(0); const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea); const shiftedLines = []; let totalAdded = 0; + textArea.focus(); textArea.setSelectionRange(startPos, endPos); lines.forEach((line) => { @@ -418,13 +420,15 @@ function indentLines(textArea) { * * @param {Object} textArea - the targeted text area */ -function outdentLines(textArea) { +function outdentLines($textArea) { + const textArea = $textArea.get(0); const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea); const shiftedLines = []; let totalRemoved = 0; let removedFromFirstline = -1; let removedFromLine = 0; + textArea.focus(); textArea.setSelectionRange(startPos, endPos); lines.forEach((line) => { @@ -460,28 +464,10 @@ function outdentLines(textArea) { ); } -function handleIndentOutdent(e, textArea) { - if (e.altKey || e.ctrlKey || e.shiftKey) return; - if (!e.metaKey) return; - - switch (e.key) { - case ']': - e.preventDefault(); - indentLines(textArea); - break; - case '[': - e.preventDefault(); - outdentLines(textArea); - break; - default: - break; - } -} - /* eslint-disable @gitlab/require-i18n-strings */ function handleSurroundSelectedText(e, textArea) { if (!gon.markdown_surround_selection) return; - if (e.metaKey) return; + if (e.metaKey || e.ctrlKey) return; if (textArea.selectionStart === textArea.selectionEnd) return; const keys = { @@ -586,7 +572,6 @@ export function keypressNoteText(e) { if ($(textArea).atwho?.('isSelecting')) return; - handleIndentOutdent(e, textArea); handleContinueList(e, textArea); handleSurroundSelectedText(e, textArea); } @@ -600,15 +585,26 @@ export function compositionEndNoteText() { } export function updateTextForToolbarBtn($toolbarBtn) { - return updateText({ - textArea: $toolbarBtn.closest('.md-area').find('textarea'), - tag: $toolbarBtn.data('mdTag'), - cursorOffset: $toolbarBtn.data('mdCursorOffset'), - blockTag: $toolbarBtn.data('mdBlock'), - wrap: !$toolbarBtn.data('mdPrepend'), - select: $toolbarBtn.data('mdSelect'), - tagContent: $toolbarBtn.attr('data-md-tag-content'), - }); + const $textArea = $toolbarBtn.closest('.md-area').find('textarea'); + + switch ($toolbarBtn.data('mdCommand')) { + case 'indentLines': + indentLines($textArea); + break; + case 'outdentLines': + outdentLines($textArea); + break; + default: + return updateText({ + textArea: $textArea, + tag: $toolbarBtn.data('mdTag'), + cursorOffset: $toolbarBtn.data('mdCursorOffset'), + blockTag: $toolbarBtn.data('mdBlock'), + wrap: !$toolbarBtn.data('mdPrepend'), + select: $toolbarBtn.data('mdSelect'), + tagContent: $toolbarBtn.attr('data-md-tag-content'), + }); + } } export function addMarkdownListeners(form) { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 458dfe0ed23bc..473d3b4a41a8e 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -7,6 +7,8 @@ import { ITALIC_TEXT, STRIKETHROUGH_TEXT, LINK_TEXT, + INDENT_LINE, + OUTDENT_LINE, } from '~/behaviors/shortcuts/keybindings'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__, __ } from '~/locale'; @@ -170,6 +172,8 @@ export default { italic: keysFor(ITALIC_TEXT), strikethrough: keysFor(STRIKETHROUGH_TEXT), link: keysFor(LINK_TEXT), + indent: keysFor(INDENT_LINE), + outdent: keysFor(OUTDENT_LINE), }, i18n: { writeTabTitle: __('Write'), @@ -317,6 +321,32 @@ export default { :button-title="__('Add a checklist')" icon="list-task" /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('indent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.indent" + command="indentLines" + icon="list-indent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('outdent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.outdent" + command="outdentLines" + icon="list-outdent" + /> <toolbar-button v-if="!restrictedToolBarItems.includes('collapsible-section')" :tag="mdCollapsibleSection" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 49217e38a1bb0..5ca21522d33f5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -47,6 +47,11 @@ export default { required: false, default: 0, }, + command: { + type: String, + required: false, + default: '', + }, /** * A string (or an array of strings) of @@ -81,6 +86,7 @@ export default { :data-md-tag-content="tagContent" :data-md-prepend="prepend" :data-md-shortcuts="shortcutsString" + :data-md-command="command" :title="buttonTitle" :aria-label="buttonTitle" :icon="icon" diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml index 4db1d20e81b76..00e8d39b26bc1 100644 --- a/app/views/shared/blob/_markdown_buttons.html.haml +++ b/app/views/shared/blob/_markdown_buttons.html.haml @@ -23,6 +23,14 @@ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") }) = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") }) = markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") }) + = markdown_toolbar_button({ icon: "list-indent", + data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' }, + css_class: 'gl-display-none', + title: sprintf(s_("MarkdownEditor|Indent line (%{modifier_key}])") % { modifier_key: modifier_key }) }) + = markdown_toolbar_button({ icon: "list-outdent", + data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' }, + css_class: 'gl-display-none', + title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) }) = markdown_toolbar_button({ icon: "details-block", data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" }, title: _("Add a collapsible section") }) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 362fb5d5ec523..ae973cecdf81f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21171,6 +21171,9 @@ msgstr "" msgid "Increase" msgstr "" +msgid "Indent line" +msgstr "" + msgid "Index" msgstr "" @@ -24407,6 +24410,18 @@ msgstr "" msgid "MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)" msgstr "" +msgid "MarkdownEditor|Indent line (%{modifierKey}])" +msgstr "" + +msgid "MarkdownEditor|Indent line (%{modifier_key}])" +msgstr "" + +msgid "MarkdownEditor|Outdent line (%{modifierKey}[)" +msgstr "" + +msgid "MarkdownEditor|Outdent line (%{modifier_key}[)" +msgstr "" + msgid "MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}" msgstr "" @@ -28114,6 +28129,9 @@ msgstr "" msgid "OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience." msgstr "" +msgid "Outdent line" +msgstr "" + msgid "Overridden" msgstr "" diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 8d179baa5051f..ba3c73d662de1 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -4,15 +4,30 @@ import { keypressNoteText, compositionStartNoteText, compositionEndNoteText, + updateTextForToolbarBtn, } from '~/lib/utils/text_markdown'; import '~/lib/utils/jquery_at_who'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; describe('init markdown', () => { + let mdArea; let textArea; + let indentButton; + let outdentButton; beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); + setHTMLFixture( + `<div class='md-area'> + <textarea></textarea> + <button data-md-command="indentLines" id="indentButton"></button> + <button data-md-command="outdentLines" id="outdentButton"></button> + </div>`, + ); + mdArea = document.querySelector('.md-area'); + textArea = mdArea.querySelector('textarea'); + indentButton = mdArea.querySelector('#indentButton'); + outdentButton = mdArea.querySelector('#outdentButton'); + textArea.focus(); // needed for the underlying insertText to work @@ -20,7 +35,7 @@ describe('init markdown', () => { }); afterAll(() => { - textArea.parentNode.removeChild(textArea); + resetHTMLFixture(); }); describe('insertMarkdownText', () => { @@ -306,15 +321,6 @@ describe('init markdown', () => { }); describe('shifting selected lines left or right', () => { - const indentEvent = new KeyboardEvent('keydown', { key: ']', metaKey: true }); - const outdentEvent = new KeyboardEvent('keydown', { key: '[', metaKey: true }); - - beforeEach(() => { - textArea.addEventListener('keydown', keypressNoteText); - textArea.addEventListener('compositionstart', compositionStartNoteText); - textArea.addEventListener('compositionend', compositionEndNoteText); - }); - it.each` selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd ${0} | ${0} | ${' 012\n456\n89'} | ${2} | ${2} @@ -338,7 +344,7 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(selectionStart, selectionEnd); - textArea.dispatchEvent(indentEvent); + updateTextForToolbarBtn($(indentButton)); expect(textArea.value).toEqual(expected); expect(textArea.selectionStart).toEqual(expectedSelectionStart); @@ -350,7 +356,7 @@ describe('init markdown', () => { textArea.value = '012\n\n89'; textArea.setSelectionRange(4, 4); - textArea.dispatchEvent(indentEvent); + updateTextForToolbarBtn($(indentButton)); expect(textArea.value).toEqual('012\n \n89'); expect(textArea.selectionStart).toEqual(6); @@ -381,7 +387,7 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(selectionStart, selectionEnd); - textArea.dispatchEvent(outdentEvent); + updateTextForToolbarBtn($(outdentButton)); expect(textArea.value).toEqual(expected); expect(textArea.selectionStart).toEqual(expectedSelectionStart); @@ -393,7 +399,7 @@ describe('init markdown', () => { textArea.value = '012\n\n89'; textArea.setSelectionRange(4, 4); - textArea.dispatchEvent(outdentEvent); + updateTextForToolbarBtn($(outdentButton)); expect(textArea.value).toEqual('012\n\n89'); expect(textArea.selectionStart).toEqual(4); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 9831908f806d4..afeaa5ec8507e 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -54,6 +54,8 @@ describe('Markdown field header component', () => { 'Add a bullet list', 'Add a numbered list', 'Add a checklist', + 'Indent line (⌘])', + 'Outdent line (⌘[)', 'Add a collapsible section', 'Add a table', 'Go full screen', -- GitLab