You've already forked joplin
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:
@ -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]) {
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,9 @@ const declarations: CommandDeclaration[] = [
|
|||||||
{
|
{
|
||||||
name: 'editor.setText',
|
name: 'editor.setText',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'editor.scrollToText',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'editor.focus',
|
name: 'editor.focus',
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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({
|
||||||
|
@ -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) {
|
||||||
|
Reference in New Issue
Block a user