You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +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); | ||||
| 							} | ||||
| 						}, | ||||
| 						'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]) { | ||||
|   | ||||
| @@ -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<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, | ||||
|   | ||||
| @@ -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; | ||||
| 				} | ||||
|   | ||||
| @@ -154,6 +154,9 @@ const declarations: CommandDeclaration[] = [ | ||||
| 	{ | ||||
| 		name: 'editor.setText', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'editor.scrollToText', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'editor.focus', | ||||
| 	}, | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -47,6 +47,8 @@ export const runtime = (): CommandRuntime => { | ||||
| 				shownEditorViewIds.splice(idx, 1); | ||||
| 			} | ||||
|  | ||||
| 			logger.info('Shown editor IDs:', 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 | ||||
| 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<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>) { | ||||
| 		const compartment = new Compartment(); | ||||
| 		this.editor.dispatch({ | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user