You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
This commit is contained in:
@@ -281,6 +281,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -322,6 +323,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -254,6 +254,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -295,6 +296,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('app.reducer', () => {
|
||||
...createAppDefaultState({}),
|
||||
backgroundWindows: {
|
||||
testWindow: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
windowId: 'testWindow',
|
||||
|
||||
visibleDialogs: {
|
||||
|
||||
@@ -26,10 +26,21 @@ export interface AppStateDialog {
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EditorScrollPercents {
|
||||
export interface NoteIdToScrollPercent {
|
||||
[noteId: string]: number;
|
||||
}
|
||||
|
||||
type RichTextEditorSelectionBookmark = unknown;
|
||||
|
||||
export interface EditorCursorLocations {
|
||||
readonly richText?: RichTextEditorSelectionBookmark;
|
||||
readonly markdown?: number;
|
||||
}
|
||||
|
||||
export interface NoteIdToEditorCursorLocations {
|
||||
[noteId: string]: EditorCursorLocations;
|
||||
}
|
||||
|
||||
export interface VisibleDialogs {
|
||||
[dialogKey: string]: boolean;
|
||||
}
|
||||
@@ -42,6 +53,9 @@ export interface AppWindowState extends WindowState {
|
||||
devToolsVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
watchedResources: any;
|
||||
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
}
|
||||
|
||||
interface BackgroundWindowStates {
|
||||
@@ -55,7 +69,6 @@ export interface AppState extends State, AppWindowState {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navHistory: any[];
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
@@ -66,7 +79,7 @@ export interface AppState extends State, AppWindowState {
|
||||
isResettingLayout: boolean;
|
||||
}
|
||||
|
||||
export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export const createAppDefaultWindowState = (globalState: AppState|null): AppWindowState => {
|
||||
return {
|
||||
...defaultWindowState,
|
||||
visibleDialogs: {},
|
||||
@@ -75,6 +88,12 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
editorCodeView: true,
|
||||
devToolsVisible: false,
|
||||
watchedResources: {},
|
||||
|
||||
// Maintain the scroll and cursor location for secondary windows separate from the
|
||||
// main window. This prevents scrolling in a secondary window from changing/resetting
|
||||
// the default scroll position in the main window:
|
||||
lastEditorCursorLocations: globalState?.lastEditorCursorLocations ?? {},
|
||||
lastEditorScrollPercents: globalState?.lastEditorScrollPercents ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,7 +101,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
@@ -90,7 +109,6 @@ export function createAppDefaultState(resourceEditWatcherDefaultState: any): App
|
||||
},
|
||||
navHistory: [],
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
@@ -299,6 +317,18 @@ export default function(state: AppState, action: any) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_CURSOR_POSITION_SET':
|
||||
{
|
||||
newState = { ...state };
|
||||
const newCursorLocations = { ...newState.lastEditorCursorLocations };
|
||||
newCursorLocations[action.noteId] = {
|
||||
...(newCursorLocations[action.noteId] ?? {}),
|
||||
...action.location,
|
||||
};
|
||||
newState.lastEditorCursorLocations = newCursorLocations;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = { ...state };
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { createAppDefaultWindowState } from '../app.reducer';
|
||||
import { AppState, createAppDefaultWindowState } from '../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
@@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => {
|
||||
folderId: note.parent_id,
|
||||
windowId: `window-${noteId}-${idCounter++}`,
|
||||
defaultAppWindowState: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(context.state as AppState),
|
||||
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
||||
editorCodeView: Setting.value('editor.codeView'),
|
||||
},
|
||||
|
||||
@@ -342,6 +342,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.Change) {
|
||||
codeMirror_change(event.value);
|
||||
} else if (event.kind === EditorEventType.SelectionRangeChange) {
|
||||
props.onCursorMotion({ markdown: event.from });
|
||||
setSelectionRange({ from: event.from, to: event.to });
|
||||
} else if (event.kind === EditorEventType.UpdateSearchDialog) {
|
||||
if (lastSearchState.current?.searchText !== event.searchState.searchText) {
|
||||
@@ -355,7 +356,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.FollowLink) {
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch, props.onCursorMotion]);
|
||||
|
||||
const onSelectPastBeginning = useCallback(() => {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
@@ -400,12 +401,16 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
props.tabMovesFocus,
|
||||
]);
|
||||
|
||||
const initialCursorLocationRef = useRef(0);
|
||||
initialCursorLocationRef.current = props.initialCursorLocation.markdown ?? 0;
|
||||
|
||||
useSyncEditorValue({
|
||||
content: props.content,
|
||||
visiblePanes: props.visiblePanes,
|
||||
onMessage: props.onMessage,
|
||||
editorRef,
|
||||
noteId: props.noteId,
|
||||
initialCursorLocationRef,
|
||||
});
|
||||
|
||||
const renderEditor = () => {
|
||||
@@ -414,6 +419,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
<Editor
|
||||
style={styles.editor}
|
||||
initialText={props.content}
|
||||
initialSelectionRef={initialCursorLocationRef}
|
||||
initialNoteId={props.noteId}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { ForwardedRef, RefObject } from 'react';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback, ContentScriptData } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
@@ -23,6 +23,7 @@ import getResourceBaseUrl from '../../../utils/getResourceBaseUrl';
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
initialSelectionRef: RefObject<number>;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
externalSearch: SearchMarkers;
|
||||
@@ -127,6 +128,9 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
direction: 'unset',
|
||||
},
|
||||
});
|
||||
const cursor = props.initialSelectionRef.current;
|
||||
editor.select(cursor, cursor);
|
||||
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -9,15 +9,18 @@ interface Props {
|
||||
onMessage: OnMessage;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
noteId: string;
|
||||
initialCursorLocationRef: RefObject<number>;
|
||||
}
|
||||
|
||||
// Updates the editor's value as necessary
|
||||
const useSyncEditorValue = ({ content, visiblePanes, onMessage, editorRef, noteId }: Props) => {
|
||||
const useSyncEditorValue = ({ content, visiblePanes, onMessage, editorRef, noteId, initialCursorLocationRef }: Props) => {
|
||||
const visiblePanesRef = useRef(visiblePanes);
|
||||
visiblePanesRef.current = visiblePanes;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
const lastNoteIdRef = useRef(noteId);
|
||||
|
||||
useEffect(() => {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
@@ -25,13 +28,23 @@ const useSyncEditorValue = ({ content, visiblePanes, onMessage, editorRef, noteI
|
||||
if (editorRef.current?.updateBody(content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
|
||||
// Only reset the cursor location when switching notes. If, for example,
|
||||
// the note is updated from a secondary window, the cursor location shouldn't
|
||||
// reset.
|
||||
const noteChanged = lastNoteIdRef.current !== noteId;
|
||||
if (noteChanged) {
|
||||
const cursorLocation = initialCursorLocationRef.current;
|
||||
editorRef.current?.select(cursorLocation, cursorLocation);
|
||||
}
|
||||
lastNoteIdRef.current = noteId;
|
||||
|
||||
// If the viewer isn't visible, the content should be considered rendered
|
||||
// after the editor has finished updating:
|
||||
if (!visiblePanesRef.current.includes('viewer')) {
|
||||
onMessageRef.current({ channel: 'noteRenderComplete' });
|
||||
}
|
||||
}
|
||||
}, [content, noteId, editorRef]);
|
||||
}, [content, noteId, editorRef, initialCursorLocationRef]);
|
||||
};
|
||||
|
||||
export default useSyncEditorValue;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import { loadScript } from '../../../utils/loadScript';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { TinyMceEditorEvents } from './utils/types';
|
||||
import type { Editor, EditorEvent } from 'tinymce';
|
||||
import type { Bookmark, Editor, EditorEvent } from 'tinymce';
|
||||
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
|
||||
import shouldPasteResources from './utils/shouldPasteResources';
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
@@ -47,6 +47,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import useTextPatternsLookup, { TextPatternContext } from './utils/useTextPatternsLookup';
|
||||
import { toFileProtocolPath } from '@joplin/utils/path';
|
||||
import { RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import useCursorPositioning from './utils/useCursorPositioning';
|
||||
|
||||
const logger = Logger.create('TinyMCE');
|
||||
|
||||
@@ -1046,6 +1047,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { onInitialContentSet } = useCursorPositioning({
|
||||
initialCursorLocation: props.initialCursorLocation.richText as Bookmark,
|
||||
onCursorUpdate: props.onCursorMotion,
|
||||
editor,
|
||||
});
|
||||
|
||||
const lastNoteIdRef = useRef(props.noteId);
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
@@ -1136,6 +1143,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
await loadDocumentAssets(props.themeId, editor, allAssets);
|
||||
|
||||
dispatchDidUpdate(editor);
|
||||
onInitialContentSet();
|
||||
};
|
||||
|
||||
void loadContent();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Bookmark, Editor } from 'tinymce';
|
||||
import { OnCursorMotion } from '../../../utils/types';
|
||||
|
||||
interface Props {
|
||||
initialCursorLocation: Bookmark;
|
||||
editor: Editor;
|
||||
onCursorUpdate: OnCursorMotion;
|
||||
}
|
||||
|
||||
const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }: Props) => {
|
||||
const initialCursorLocationRef = useRef(initialCursorLocation);
|
||||
initialCursorLocationRef.current = initialCursorLocation;
|
||||
|
||||
const appliedInitialCursorLocationRef = useRef(false);
|
||||
const onInitialContentSet = useCallback(() => {
|
||||
if (editor) {
|
||||
if (initialCursorLocationRef.current) {
|
||||
editor.selection.moveToBookmark(initialCursorLocationRef.current);
|
||||
}
|
||||
|
||||
appliedInitialCursorLocationRef.current = true;
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
editor.on('ContentSet', onInitialContentSet);
|
||||
|
||||
const onSelectionChange = () => {
|
||||
// Wait until the initial cursor position has been set. This avoids resetting
|
||||
// the initial cursor position to zero when the editor first loads.
|
||||
if (!appliedInitialCursorLocationRef.current) return;
|
||||
|
||||
// Use an offset bookmark -- the default bookmark type is not preserved after unloading
|
||||
// and reloading the editor.
|
||||
const offsetBookmarkId = 2;
|
||||
onCursorUpdate({
|
||||
richText: editor.selection.getBookmark(offsetBookmarkId, true),
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('SelectionChange', onSelectionChange);
|
||||
|
||||
return () => {
|
||||
editor.off('ContentSet', onInitialContentSet);
|
||||
editor.off('SelectionChange', onSelectionChange);
|
||||
};
|
||||
}, [editor, onCursorUpdate, onInitialContentSet]);
|
||||
|
||||
return { onInitialContentSet };
|
||||
};
|
||||
|
||||
export default useCursorPositioning;
|
||||
@@ -18,7 +18,7 @@ import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEdi
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AppState, EditorCursorLocations } from '../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||
@@ -57,6 +57,7 @@ import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
||||
import useInitialCursorLocation from './utils/useInitialCursorLocation';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@@ -409,6 +410,14 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onCursorMotion = useCallback((location: EditorCursorLocations) => {
|
||||
props.dispatch({
|
||||
type: 'EDITOR_CURSOR_POSITION_SET',
|
||||
noteId: formNoteRef.current.id,
|
||||
location,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderNoNotes(rootStyle: React.CSSProperties) {
|
||||
const emptyDivStyle = {
|
||||
backgroundColor: 'black',
|
||||
@@ -419,6 +428,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
}
|
||||
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
const initialCursorLocation = useInitialCursorLocation({
|
||||
lastEditorCursorLocations: props.lastEditorCursorLocations, noteId: props.noteId,
|
||||
});
|
||||
|
||||
const markupLanguage = formNote.markup_language;
|
||||
const editorProps: NoteBodyEditorPropsAndRef = {
|
||||
@@ -432,6 +444,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
content: formNote.body,
|
||||
contentMarkupLanguage: markupLanguage,
|
||||
contentOriginalCss: formNote.originalCss,
|
||||
initialCursorLocation,
|
||||
resourceInfos: resourceInfos,
|
||||
resourceDirectory: Setting.value('resourceDir'),
|
||||
htmlToMarkdown: htmlToMarkdown,
|
||||
@@ -442,6 +455,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
dispatch: props.dispatch,
|
||||
noteToolbar: null,
|
||||
onScroll: onScroll,
|
||||
onCursorMotion,
|
||||
setLocalSearchResultCount: setLocalSearchResultCount,
|
||||
setLocalSearch: localSearch_change,
|
||||
setShowLocalSearch,
|
||||
@@ -729,6 +743,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteTags: windowState.selectedNoteTags,
|
||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
||||
lastEditorCursorLocations: state.lastEditorCursorLocations,
|
||||
selectedNoteHash: windowState.selectedNoteHash,
|
||||
searches: state.searches,
|
||||
selectedSearchId: windowState.selectedSearchId,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { RefObject, SetStateAction } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations, NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -40,8 +41,8 @@ export interface NoteEditorProps {
|
||||
notesParentType: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
selectedNoteTags: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
lastEditorScrollPercents: any;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
selectedNoteHash: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
searches: any[];
|
||||
@@ -83,6 +84,7 @@ export interface NoteBodyEditorRef {
|
||||
export { MarkupToHtmlOptions };
|
||||
export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string, parseOptions?: ParseOptions)=> Promise<string>;
|
||||
export type OnCursorMotion = (event: EditorCursorLocations)=> void;
|
||||
|
||||
export interface MessageEvent {
|
||||
channel: string;
|
||||
@@ -109,11 +111,13 @@ export interface NoteBodyEditorProps {
|
||||
contentKey: string;
|
||||
contentMarkupLanguage: number;
|
||||
contentOriginalCss: string;
|
||||
initialCursorLocation: EditorCursorLocations;
|
||||
onChange(event: OnChangeEvent): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onWillChange(event: any): void;
|
||||
onMessage: OnMessage;
|
||||
onScroll(event: { percent: number }): void;
|
||||
onCursorMotion: OnCursorMotion;
|
||||
markupToHtml: MarkupToHtmlHandler;
|
||||
htmlToMarkdown: HtmlToMarkdownHandler;
|
||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations } from '../../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
const useInitialCursorLocation = ({ noteId, lastEditorCursorLocations }: Props) => {
|
||||
const lastCursorLocation = lastEditorCursorLocations[noteId];
|
||||
|
||||
return useMemo((): EditorCursorLocations => {
|
||||
return lastCursorLocation ?? { };
|
||||
}, [lastCursorLocation]);
|
||||
};
|
||||
|
||||
export default useInitialCursorLocation;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { RefObject, useCallback, useRef } from 'react';
|
||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import type { EditorScrollPercents } from '../../../app.reducer';
|
||||
import type { NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
selectedNoteHash: string;
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollP
|
||||
const scrollWhenReadyRef = useRef<ScrollOptions|null>(null);
|
||||
|
||||
const previousNoteId = usePrevious(noteId);
|
||||
const lastScrollPercentsRef = useRef<EditorScrollPercents>(null);
|
||||
const lastScrollPercentsRef = useRef<NoteIdToScrollPercent>(null);
|
||||
lastScrollPercentsRef.current = lastEditorScrollPercents;
|
||||
|
||||
// This needs to be a nowEffect to prevent race conditions
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('NoteListUtils', () => {
|
||||
const mockStore = {
|
||||
getState: () => {
|
||||
return {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
settings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -89,8 +89,14 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
}
|
||||
|
||||
public select(anchor: number, head: number) {
|
||||
const maximumPosition = this.editor.state.doc.length;
|
||||
this.editor.dispatch(this.editor.state.update({
|
||||
selection: { anchor, head },
|
||||
selection: {
|
||||
// Ensure that (anchor, head) are in range.
|
||||
// (CodeMirror throws when (anchor, head) are out-of-range.)
|
||||
anchor: Math.min(anchor, maximumPosition),
|
||||
head: Math.min(head, maximumPosition),
|
||||
},
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user