diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx index 2cf8528cd22..e796cbd2df0 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx @@ -244,6 +244,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef { + reg.logger().warn('"editor.scrollToText" is unsupported in legacy editor - please use the new editor'); + return false; + }, }; if (commands[cmd.name]) { diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts index e6983409d71..0eb9cc96953 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts @@ -1,6 +1,6 @@ import { RefObject, useMemo } from 'react'; -import { CommandValue, DropCommandValue } from '../../../utils/types'; +import { CommandValue, DropCommandValue, ScrollToTextValue } from '../../../utils/types'; import { commandAttachFileToBody } from '../../../utils/resourceHandling'; import { _ } from '@joplin/lib/locale'; import dialogs from '../../../../dialogs'; @@ -132,6 +132,28 @@ const useEditorCommands = (props: Props) => { search: () => { return editorRef.current.execCommand(EditorCommandType.ShowSearch); }, + 'editor.scrollToText': (value: ScrollToTextValue) => { + value = { + scrollStrategy: 'start', + ...value, + }; + + const valueToMarkdown = (value: ScrollToTextValue) => { + const conv: Record string> = { + h1: text => `# ${text}`, + h2: text => `## ${text}`, + h3: text => `### ${text}`, + h4: text => `#### ${text}`, + h5: text => `##### ${text}`, + strong: text => `**${text}**`, + ul: text => `- ${text}`, + }; + + return conv[value.element](value.text); + }; + + return editorRef.current.scrollToText(valueToMarkdown(value), value.scrollStrategy); + }, }; }, [ props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste, diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index cc4460fe19f..260e129dd42 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo } from 'react'; -import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler } from '../../utils/types'; +import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler, ScrollToTextValue } from '../../utils/types'; import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml } from '../../utils/resourceHandling'; import attachedResources from '@joplin/lib/utils/attachedResources'; import useScroll from './utils/useScroll'; @@ -244,6 +244,28 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { } else { logger.warn('unsupported drop item: ', cmd); } + } else if (cmd.name === 'editor.scrollToText') { + + const cmdValue = cmd.value as ScrollToTextValue; + + const findElementByText = (doc: Document, text: string, element: string) => { + const headers = doc.querySelectorAll(element); + for (const header of headers) { + if (header.textContent?.trim() === text) { + return header; + } + } + return null; + }; + + const contentDocument = editor.getDoc(); + const targetElement = findElementByText(contentDocument, cmdValue.text, cmdValue.element); + + if (targetElement) { + targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else { + logger.warn('editor.scrollToText: Could not find text to scroll to :', cmdValue); + } } else { commandProcessed = false; } diff --git a/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts b/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts index b46c60d29b5..f490a62982b 100644 --- a/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts +++ b/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts @@ -154,6 +154,9 @@ const declarations: CommandDeclaration[] = [ { name: 'editor.setText', }, + { + name: 'editor.scrollToText', + }, { name: 'editor.focus', }, diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index eb6fae6a0b3..3d0be500ba8 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -8,6 +8,7 @@ import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine'; import { DropHandler } from './useDropHandler'; import { SearchMarkers } from './useSearchMarkers'; import { ParseOptions } from '@joplin/lib/HtmlToMd'; +import { ScrollStrategy } from '@joplin/editor/CodeMirror/CodeMirrorControl'; export interface AllAssetsOptions { contentMaxWidthTarget?: string; @@ -271,3 +272,12 @@ export type DropCommandValue = ({ paths: string[]; createFileURL: boolean; }) & DropCommandBase; + +export interface ScrollToTextValue { + // Text should be plain text - it should not include Markdown characters as it needs to work + // with both TinyMCE and CodeMirror. To specific an element use the `element` property. For + // example to scroll to `## Scroll to this`, use `{ text: 'Scroll to this', element: 'h2' }`. + text: string; + element: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'strong' | 'ul'; + scrollStrategy?: ScrollStrategy; +} diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts index 707a7ae5ec1..6cdbabf91b6 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts @@ -6,6 +6,7 @@ import * as duplicateNote from './duplicateNote'; import * as editAlarm from './editAlarm'; import * as exportPdf from './exportPdf'; import * as gotoAnything from './gotoAnything'; +import * as hideEditorPlugin from './hideEditorPlugin'; import * as hideModalMessage from './hideModalMessage'; import * as leaveSharedFolder from './leaveSharedFolder'; import * as moveToFolder from './moveToFolder'; @@ -56,6 +57,7 @@ const index: any[] = [ editAlarm, exportPdf, gotoAnything, + hideEditorPlugin, hideModalMessage, leaveSharedFolder, moveToFolder, diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts index 6a12cc23186..0427fb70c87 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts @@ -47,6 +47,8 @@ export const runtime = (): CommandRuntime => { shownEditorViewIds.splice(idx, 1); } + logger.info('Shown editor IDs:', shownEditorViewIds); + Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds); }, }; diff --git a/packages/editor/CodeMirror/CodeMirrorControl.ts b/packages/editor/CodeMirror/CodeMirrorControl.ts index a67fe968774..7f9e45fb550 100644 --- a/packages/editor/CodeMirror/CodeMirrorControl.ts +++ b/packages/editor/CodeMirror/CodeMirrorControl.ts @@ -23,6 +23,9 @@ interface Callbacks { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied type EditorUserCommand = (...args: any[])=> any; +// Copied from CodeMirror source code since type is not exported +export type ScrollStrategy = 'nearest' | 'start' | 'end' | 'center'; + export default class CodeMirrorControl extends CodeMirror5Emulation implements EditorControl { private _pluginControl: PluginLoader; private _userCommands: Map = new Map(); @@ -177,6 +180,20 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E } + public scrollToText(text: string, scrollStrategy: ScrollStrategy) { + const doc = this.editor.state.doc; + const index = doc.toString().indexOf(text); + const textFound = index >= 0; + + if (textFound) { + this.editor.dispatch({ + effects: EditorView.scrollIntoView(index, { y: scrollStrategy }), + }); + } + + return textFound; + } + public addStyles(...styles: Parameters) { const compartment = new Compartment(); this.editor.dispatch({ diff --git a/packages/lib/services/PostMessageService.ts b/packages/lib/services/PostMessageService.ts index 5172eea2429..fdb48aa767a 100644 --- a/packages/lib/services/PostMessageService.ts +++ b/packages/lib/services/PostMessageService.ts @@ -127,14 +127,15 @@ export default class PostMessageService { } if (!responder) { - logger.warn('Cannot respond to message because no responder was found', message); + logger.info('Cannot respond to message because no responder was found', message); + logger.info('Error was:', error); + } else { + responder({ + responseId: message.id, + response: responseContent, + error, + }); } - - responder({ - responseId: message.id, - response: responseContent, - error, - }); } private responder(type: ResponderComponentType, viewId: string, windowId: string) {