From 76a41d4e92e00b592e5fa0127b29606099e418b8 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 3 Jun 2024 12:51:48 +0100 Subject: [PATCH] Fixed cursor movement across title/subtitle/editor closes https://linear.app/tryghost/issue/MOM-175 - matches cursor behaviour on Up/Down/Left/Right/Tab/Enter to our previous behaviour when we only had the title and editor --- .../components/gh-koenig-editor-lexical.hbs | 3 +- .../components/gh-koenig-editor-lexical.js | 77 ++++++++++++++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 2933f6000c..4438538c3e 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -67,6 +67,7 @@ @value={{readonly this.excerpt}} @input={{this.onExcerptInput}} @keyDown={{this.onExcerptKeydown}} + @didCreateTextarea={{this.registerSubtitleElement}} data-test-textarea="subtitle" /> {{#if @excerptErrorMessage}} @@ -84,7 +85,7 @@ @cardConfig={{@cardOptions}} @onChange={{@onBodyChange}} @registerAPI={{this.registerEditorAPI}} - @cursorDidExitAtTop={{this.focusTitle}} + @cursorDidExitAtTop={{if this.feature.editorSubtitle this.focusSubtitle this.focusTitle}} @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} /> diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.js b/ghost/admin/app/components/gh-koenig-editor-lexical.js index 643dc8cdf4..b924cc4d43 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.js +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.js @@ -10,6 +10,7 @@ export default class GhKoenigEditorReactComponent extends Component { containerElement = null; titleElement = null; + subtitleElement = null; mousedownY = 0; uploadUrl = `${ghostPaths().apiRoot}/images/upload/`; @@ -111,21 +112,34 @@ export default class GhKoenigEditorReactComponent extends Component { this.titleElement.focus(); } - // move cursor to the editor on - // - Tab - // - Arrow Down/Right when input is empty or caret at end of input - // - Enter, creating an empty paragraph when editor is not empty @action onTitleKeydown(event) { if (this.feature.get('editorSubtitle')) { - if (event.key === 'Enter') { + // move cursor to the subtitle on + // - Tab (handled by browser) + // - Arrow Down/Right when input is empty or caret at end of input + // - Enter + const {key} = event; + const {value, selectionStart} = event.target; + + if (key === 'Enter') { event.preventDefault(); - const subheadElement = document.querySelector('.gh-editor-subtitle'); - if (subheadElement) { - subheadElement.focus(); + this.subtitleElement?.focus(); + } + + if ((key === 'ArrowDown' || key === 'ArrowRight') && !event.shiftKey) { + const couldLeaveTitle = !value || selectionStart === value.length; + + if (couldLeaveTitle) { + event.preventDefault(); + this.subtitleElement?.focus(); } } } else { + // move cursor to the editor on + // - Tab + // - Arrow Down/Right when input is empty or caret at end of input + // - Enter, creating an empty paragraph when editor is not empty const {editorAPI} = this; if (!editorAPI || event.originalEvent.isComposing) { @@ -152,6 +166,21 @@ export default class GhKoenigEditorReactComponent extends Component { // Subhead ("excerpt") Actions ------------------------------------------- + @action + registerSubtitleElement(element) { + this.subtitleElement = element; + } + + @action + focusSubtitle() { + this.subtitleElement?.focus(); + + // timeout ensures this occurs after the keyboard events + setTimeout(() => { + this.subtitleElement?.setSelectionRange(-1, -1); + }, 0); + } + @action onExcerptInput(event) { this.args.setExcerpt?.(event.target.value); @@ -159,9 +188,37 @@ export default class GhKoenigEditorReactComponent extends Component { @action onExcerptKeydown(event) { - if (event.key === 'Enter') { + // move cursor to the title on + // - Shift+Tab (handled by the browser) + // - Arrow Up/Left when input is empty or caret at start of input + // move cursor to the editor on + // - Tab + // - Arrow Down/Right when input is empty or caret at end of input + // - Enter, creating an empty paragraph when editor is not empty + const {key} = event; + const {value, selectionStart} = event.target; + + if ((key === 'ArrowUp' || key === 'ArrowLeft') && !event.shiftKey) { + const couldLeaveTitle = !value || selectionStart === 0; + + if (couldLeaveTitle) { + event.preventDefault(); + this.focusTitle(); + } + } + + const {editorAPI} = this; + const couldLeaveTitle = !value || selectionStart === value.length; + const arrowLeavingTitle = (key === 'ArrowRight' || key === 'ArrowDown') && couldLeaveTitle; + + if (key === 'Enter' || (key === 'Tab' && !event.shiftKey) || arrowLeavingTitle) { event.preventDefault(); - this.editorAPI.focusEditor({position: 'top'}); + + if (key === 'Enter' && !editorAPI.editorIsEmpty()) { + editorAPI.insertParagraphAtTop({focus: true}); + } else { + editorAPI.focusEditor({position: 'top'}); + } } }