1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-16 00:14:34 +02:00

Plugins: Add support for editor.scrollToText on desktop

This commit is contained in:
Laurent Cozic
2024-11-16 11:19:11 +00:00
parent 8e3c81717e
commit 6eac8d9ccf
9 changed files with 92 additions and 9 deletions

View File

@ -244,6 +244,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
reg.logger().warn('CodeMirror execCommand: unsupported command: ', value.name); reg.logger().warn('CodeMirror execCommand: unsupported command: ', value.name);
} }
}, },
'editor.scrollToText': (_text: string) => {
reg.logger().warn('"editor.scrollToText" is unsupported in legacy editor - please use the new editor');
return false;
},
}; };
if (commands[cmd.name]) { if (commands[cmd.name]) {

View File

@ -1,6 +1,6 @@
import { RefObject, useMemo } from 'react'; import { RefObject, useMemo } from 'react';
import { CommandValue, DropCommandValue } from '../../../utils/types'; import { CommandValue, DropCommandValue, ScrollToTextValue } from '../../../utils/types';
import { commandAttachFileToBody } from '../../../utils/resourceHandling'; import { commandAttachFileToBody } from '../../../utils/resourceHandling';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import dialogs from '../../../../dialogs'; import dialogs from '../../../../dialogs';
@ -132,6 +132,28 @@ const useEditorCommands = (props: Props) => {
search: () => { search: () => {
return editorRef.current.execCommand(EditorCommandType.ShowSearch); return editorRef.current.execCommand(EditorCommandType.ShowSearch);
}, },
'editor.scrollToText': (value: ScrollToTextValue) => {
value = {
scrollStrategy: 'start',
...value,
};
const valueToMarkdown = (value: ScrollToTextValue) => {
const conv: Record<typeof value.element, (text: string)=> 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, props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste,

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo } 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 { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml } from '../../utils/resourceHandling';
import attachedResources from '@joplin/lib/utils/attachedResources'; import attachedResources from '@joplin/lib/utils/attachedResources';
import useScroll from './utils/useScroll'; import useScroll from './utils/useScroll';
@ -244,6 +244,28 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
} else { } else {
logger.warn('unsupported drop item: ', cmd); 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 { } else {
commandProcessed = false; commandProcessed = false;
} }

View File

@ -154,6 +154,9 @@ const declarations: CommandDeclaration[] = [
{ {
name: 'editor.setText', name: 'editor.setText',
}, },
{
name: 'editor.scrollToText',
},
{ {
name: 'editor.focus', name: 'editor.focus',
}, },

View File

@ -8,6 +8,7 @@ import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
import { DropHandler } from './useDropHandler'; import { DropHandler } from './useDropHandler';
import { SearchMarkers } from './useSearchMarkers'; import { SearchMarkers } from './useSearchMarkers';
import { ParseOptions } from '@joplin/lib/HtmlToMd'; import { ParseOptions } from '@joplin/lib/HtmlToMd';
import { ScrollStrategy } from '@joplin/editor/CodeMirror/CodeMirrorControl';
export interface AllAssetsOptions { export interface AllAssetsOptions {
contentMaxWidthTarget?: string; contentMaxWidthTarget?: string;
@ -271,3 +272,12 @@ export type DropCommandValue = ({
paths: string[]; paths: string[];
createFileURL: boolean; createFileURL: boolean;
}) & DropCommandBase; }) & 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;
}

View File

@ -6,6 +6,7 @@ import * as duplicateNote from './duplicateNote';
import * as editAlarm from './editAlarm'; import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf'; import * as exportPdf from './exportPdf';
import * as gotoAnything from './gotoAnything'; import * as gotoAnything from './gotoAnything';
import * as hideEditorPlugin from './hideEditorPlugin';
import * as hideModalMessage from './hideModalMessage'; import * as hideModalMessage from './hideModalMessage';
import * as leaveSharedFolder from './leaveSharedFolder'; import * as leaveSharedFolder from './leaveSharedFolder';
import * as moveToFolder from './moveToFolder'; import * as moveToFolder from './moveToFolder';
@ -56,6 +57,7 @@ const index: any[] = [
editAlarm, editAlarm,
exportPdf, exportPdf,
gotoAnything, gotoAnything,
hideEditorPlugin,
hideModalMessage, hideModalMessage,
leaveSharedFolder, leaveSharedFolder,
moveToFolder, moveToFolder,

View File

@ -47,6 +47,8 @@ export const runtime = (): CommandRuntime => {
shownEditorViewIds.splice(idx, 1); shownEditorViewIds.splice(idx, 1);
} }
logger.info('Shown editor IDs:', shownEditorViewIds);
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds); Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
}, },
}; };

View File

@ -23,6 +23,9 @@ interface Callbacks {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
type EditorUserCommand = (...args: any[])=> any; 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 { export default class CodeMirrorControl extends CodeMirror5Emulation implements EditorControl {
private _pluginControl: PluginLoader; private _pluginControl: PluginLoader;
private _userCommands: Map<string, EditorUserCommand> = new Map(); private _userCommands: Map<string, EditorUserCommand> = 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<typeof EditorView.theme>) { public addStyles(...styles: Parameters<typeof EditorView.theme>) {
const compartment = new Compartment(); const compartment = new Compartment();
this.editor.dispatch({ this.editor.dispatch({

View File

@ -127,14 +127,15 @@ export default class PostMessageService {
} }
if (!responder) { 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) { private responder(type: ResponderComponentType, viewId: string, windowId: string) {