mirror of
https://github.com/laurent22/joplin.git
synced 2026-05-03 13:00:11 -05:00
Mobile: Resolves #520: Viewer, Rich Text Editor: Save/restore the cursor and scroll position when switching notes (#13962)
This commit is contained in:
@@ -1147,6 +1147,7 @@ packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/clampPointToDocument.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
|
||||
@@ -1119,6 +1119,7 @@ packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/clampPointToDocument.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
|
||||
@@ -27,7 +27,7 @@ interface WrapperProps {
|
||||
noteBody: string;
|
||||
highlightedKeywords?: string[];
|
||||
noteResources?: Record<string, ResourceInfo>;
|
||||
onScroll?: (percent: number)=> void;
|
||||
onScroll?: ()=> void;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ const WrappedNoteViewer: React.FC<WrapperProps> = (
|
||||
highlightedKeywords={highlightedKeywords}
|
||||
noteResources={noteResources}
|
||||
paddingBottom={0}
|
||||
initialScroll={0}
|
||||
initialScrollPercent={0}
|
||||
noteHash={''}
|
||||
onMarkForDownload={onMarkForDownload}
|
||||
onScroll={onScroll}
|
||||
|
||||
@@ -15,6 +15,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import useWebViewSetup from '../../contentScripts/rendererBundle/useWebViewSetup';
|
||||
import { OnScrollCallback } from '../../contentScripts/rendererBundle/types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -25,12 +26,12 @@ interface Props {
|
||||
highlightedKeywords: string[];
|
||||
noteResources: Record<string, ResourceInfo>;
|
||||
paddingBottom: number;
|
||||
initialScroll: number|null;
|
||||
initialScrollPercent: number|null;
|
||||
noteHash: string;
|
||||
onCheckboxChange?: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onScroll: (scrollTop: number)=> void;
|
||||
onScroll: OnScrollCallback;
|
||||
onLoadEnd?: ()=> void;
|
||||
pluginStates: PluginStates;
|
||||
}
|
||||
@@ -46,9 +47,7 @@ const onJoplinLinkClick = async (message: string) => {
|
||||
function NoteBodyViewer(props: Props) {
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
const onScroll = useCallback(async (scrollTop: number) => {
|
||||
props.onScroll(scrollTop);
|
||||
}, [props.onScroll]);
|
||||
const onScroll = props.onScroll;
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
{
|
||||
@@ -82,7 +81,7 @@ function NoteBodyViewer(props: Props) {
|
||||
highlightedKeywords: props.highlightedKeywords,
|
||||
noteResources: props.noteResources,
|
||||
noteHash: props.noteHash,
|
||||
initialScroll: props.initialScroll,
|
||||
initialScrollPercent: props.initialScrollPercent,
|
||||
|
||||
paddingBottom: props.paddingBottom,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ interface Props {
|
||||
highlightedKeywords: string[];
|
||||
noteResources: Record<string, ResourceInfo>;
|
||||
noteHash: string;
|
||||
initialScroll: number|undefined;
|
||||
initialScrollPercent: number|undefined;
|
||||
|
||||
paddingBottom: number;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ const useRerenderHandler = (props: Props) => {
|
||||
|
||||
// If the hash changed, we don't set initial scroll -- we want to scroll to the hash
|
||||
// instead.
|
||||
initialScroll: (previousHash && hashChanged) ? undefined : props.initialScroll,
|
||||
initialScrollPercent: (previousHash && hashChanged) ? undefined : props.initialScrollPercent,
|
||||
noteHash: props.noteHash,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
|
||||
export type OnScrollCallback = (scrollTop: number)=> void;
|
||||
export { OnScrollCallback } from '../../contentScripts/rendererBundle/types';
|
||||
export type OnWebViewMessageHandler = (event: OnMessageEvent)=> void;
|
||||
|
||||
@@ -28,12 +28,14 @@ const defaultEditorProps = {
|
||||
globalSearch: '',
|
||||
noteId: '',
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
style: {},
|
||||
toolbarEnabled: true,
|
||||
readOnly: false,
|
||||
onChange: ()=>{},
|
||||
onSelectionChange: ()=>{},
|
||||
onUndoRedoDepthChange: ()=>{},
|
||||
onScroll: ()=>{},
|
||||
onAttach: async ()=>{},
|
||||
noteResources: {},
|
||||
plugins: {},
|
||||
|
||||
@@ -13,7 +13,7 @@ import { editorFont } from '../global-style';
|
||||
import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types';
|
||||
import { EditorControl, EditorSettings, EditorType } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { ChangeEvent, EditorEvent, EditorEventType, EditorScrolledEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
|
||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
@@ -38,6 +38,7 @@ import Logger from '@joplin/utils/Logger';
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type ScrollEventHandler = (event: EditorScrolledEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
|
||||
type OnAttachCallback = (filePath?: string)=> Promise<void>;
|
||||
@@ -46,6 +47,7 @@ interface Props {
|
||||
ref: Ref<EditorControl>;
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
initialScroll: number;
|
||||
mode: EditorType;
|
||||
markupLanguage: MarkupLanguage;
|
||||
noteId: string;
|
||||
@@ -58,6 +60,7 @@ interface Props {
|
||||
plugins: PluginStates;
|
||||
noteResources: ResourceInfos;
|
||||
|
||||
onScroll: ScrollEventHandler;
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
@@ -334,9 +337,11 @@ function NoteEditor(props: Props) {
|
||||
break;
|
||||
}
|
||||
case EditorEventType.Remove:
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
break;
|
||||
case EditorEventType.Scroll:
|
||||
props.onScroll(event);
|
||||
break;
|
||||
default:
|
||||
exhaustivenessCheck = event;
|
||||
return exhaustivenessCheck;
|
||||
@@ -442,6 +447,7 @@ function NoteEditor(props: Props) {
|
||||
noteHash={props.noteHash}
|
||||
initialText={props.initialText}
|
||||
initialSelection={props.initialSelection}
|
||||
initialScroll={props.initialScroll}
|
||||
editorSettings={editorSettings}
|
||||
globalSearch={props.globalSearch}
|
||||
onEditorEvent={onEditorEvent}
|
||||
|
||||
@@ -96,6 +96,8 @@ const RichTextEditor: React.FC<EditorProps> = props => {
|
||||
themeId: props.themeId,
|
||||
pluginStates: props.plugins,
|
||||
noteResources: props.noteResources,
|
||||
initialSelection: props.initialSelection,
|
||||
initialScroll: props.initialScroll,
|
||||
onPostMessage: onPostMessage,
|
||||
onAttachFile: props.onAttach,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ const defaultWrapperProps: EditorProps = {
|
||||
noteHash: '',
|
||||
noteId: '',
|
||||
initialText: '',
|
||||
initialScroll: 0,
|
||||
editorSettings: defaultEditorSettings,
|
||||
initialSelection: { start: 0, end: 0 },
|
||||
globalSearch: '',
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface EditorProps {
|
||||
noteHash: string;
|
||||
initialText: string;
|
||||
initialSelection: SelectionRange;
|
||||
initialScroll: number;
|
||||
editorSettings: EditorSettings;
|
||||
globalSearch: string;
|
||||
plugins: PluginStates;
|
||||
|
||||
@@ -76,6 +76,7 @@ import { EditorType } from '../../NoteEditor/types';
|
||||
import { IconButton } from 'react-native-paper';
|
||||
import { writeTextToCacheFile } from '../../../utils/ShareUtils';
|
||||
import shareFile from '../../../utils/shareFile';
|
||||
import NotePositionService from '@joplin/lib/services/NotePositionService';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@@ -154,6 +155,8 @@ interface State {
|
||||
multiline: boolean;
|
||||
}
|
||||
|
||||
type ScrollEventSlice = { fraction: number };
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent<State> {
|
||||
// This isn't in this.state because we don't want changing scroll to trigger
|
||||
// a re-render.
|
||||
@@ -226,6 +229,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
multiline: false,
|
||||
};
|
||||
|
||||
const initialCursorLocation = NotePositionService.instance().getCursorPosition(props.noteId, defaultWindowId).markdown;
|
||||
if (initialCursorLocation) {
|
||||
this.selection = { start: initialCursorLocation, end: initialCursorLocation };
|
||||
}
|
||||
const initialScroll = NotePositionService.instance().getScrollPercent(props.noteId, defaultWindowId);
|
||||
this.lastBodyScroll = initialScroll;
|
||||
|
||||
this.titleTextFieldRef = React.createRef();
|
||||
|
||||
this.saveActionQueues_ = {};
|
||||
@@ -770,8 +780,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
this.selection = event.nativeEvent.selection;
|
||||
};
|
||||
|
||||
private onMarkdownEditorSelectionChange = (event: SelectionRangeChangeEvent) => {
|
||||
private onEditorSelectionChange = (event: SelectionRangeChangeEvent) => {
|
||||
this.selection = { start: event.from, end: event.to };
|
||||
|
||||
NotePositionService.instance().updateCursorPosition(
|
||||
this.props.noteId, defaultWindowId, { markdown: event.from },
|
||||
);
|
||||
};
|
||||
|
||||
public makeSaveAction(state: State) {
|
||||
@@ -1487,10 +1501,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
private onBodyViewerScroll = (scrollTop: number) => {
|
||||
this.lastBodyScroll = scrollTop;
|
||||
private onBodyViewerScroll = (event: ScrollEventSlice) => {
|
||||
this.lastBodyScroll = event.fraction;
|
||||
|
||||
NotePositionService.instance().updateScrollPosition(
|
||||
this.props.noteId, defaultWindowId, event.fraction,
|
||||
);
|
||||
};
|
||||
|
||||
private onMarkdownEditorScroll = () => {};
|
||||
|
||||
public onBodyViewerCheckboxChange(newBody: string) {
|
||||
void this.saveOneProperty('body', newBody);
|
||||
}
|
||||
@@ -1604,7 +1624,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onRequestEditResource={this.onEditResource}
|
||||
onScroll={this.onBodyViewerScroll}
|
||||
initialScroll={this.lastBodyScroll}
|
||||
initialScrollPercent={this.lastBodyScroll}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -1658,7 +1678,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
markupLanguage={this.state.note.markup_language}
|
||||
globalSearch={this.props.searchQuery}
|
||||
onChange={this.onMarkdownEditorTextChange}
|
||||
onSelectionChange={this.onMarkdownEditorSelectionChange}
|
||||
onSelectionChange={this.onEditorSelectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
onAttach={this.onAttach}
|
||||
noteResources={this.state.noteResources}
|
||||
@@ -1671,6 +1691,14 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
|
||||
// For now, only save/restore the scroll location for the Rich Text editor since that editor's
|
||||
// scroll should roughly match the viewer. In the future, it may make sense to refactor this to
|
||||
// use mapsToLine (similar to what's done on desktop) to sync the Markdown editor scroll, but this
|
||||
// will require refactoring.
|
||||
initialScroll={this.props.editorType === EditorType.RichText ? this.lastBodyScroll : undefined}
|
||||
onScroll={this.props.editorType === EditorType.RichText ? this.onBodyViewerScroll : this.onMarkdownEditorScroll}
|
||||
|
||||
mode={this.props.editorType}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { themeStyle } from '../global-style';
|
||||
import getHelpMessage from '@joplin/lib/components/shared/NoteRevisionViewer/getHelpMessage';
|
||||
import { DialogContext } from '../DialogManager';
|
||||
import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick';
|
||||
import { OnScrollCallback } from '../NoteBodyViewer/types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -153,6 +154,10 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
return result;
|
||||
}, [revisions]);
|
||||
|
||||
const onScroll: OnScrollCallback = useCallback((event) => {
|
||||
setInitialScroll(event.fraction);
|
||||
}, []);
|
||||
|
||||
const onOptionSelected = useCallback((value: string) => {
|
||||
setCurrentRevisionId(value);
|
||||
}, []);
|
||||
@@ -280,8 +285,8 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
noteResources={resources}
|
||||
highlightedKeywords={emptyStringList}
|
||||
paddingBottom={0}
|
||||
initialScroll={initialScroll}
|
||||
onScroll={setInitialScroll}
|
||||
initialScrollPercent={initialScroll}
|
||||
onScroll={onScroll}
|
||||
noteHash={''}
|
||||
/>
|
||||
</View>;
|
||||
|
||||
@@ -10,7 +10,7 @@ const defaultRendererSettings: RenderSettings = {
|
||||
resources: {},
|
||||
codeTheme: 'atom-one-light.css',
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
initialScrollPercent: 0,
|
||||
readAssetBlob: async (_path: string) => new Blob(),
|
||||
removeUnusedPluginAssets: true,
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface RenderSettings {
|
||||
resources: ResourceInfos;
|
||||
codeTheme: string;
|
||||
noteHash: string;
|
||||
initialScroll: number;
|
||||
initialScrollPercent: number;
|
||||
// If [null], plugin assets are not added to the document
|
||||
pluginAssetContainerSelector: string|null;
|
||||
removeUnusedPluginAssets: boolean;
|
||||
|
||||
@@ -64,7 +64,10 @@ export const initialize = (options: RendererWebViewOptions) => {
|
||||
const onMainContentScroll = () => {
|
||||
const newScrollTop = document.scrollingElement.scrollTop;
|
||||
if (lastScrollTop !== newScrollTop) {
|
||||
messenger.remoteApi.onScroll(newScrollTop);
|
||||
const scrollHeight = document.scrollingElement.scrollHeight;
|
||||
messenger.remoteApi.onScroll({
|
||||
fraction: newScrollTop / (scrollHeight || 1),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -19,13 +19,13 @@ const afterFullPageRender = (
|
||||
}
|
||||
|
||||
const hash = renderSettings.noteHash;
|
||||
const initialScroll = renderSettings.initialScroll;
|
||||
const initialScrollPercent = renderSettings.initialScrollPercent;
|
||||
|
||||
// Don't scroll to a hash if we're given initial scroll (initial scroll
|
||||
// overrides scrolling to a hash).
|
||||
if ((initialScroll ?? null) !== null) {
|
||||
if ((initialScrollPercent ?? null) !== null) {
|
||||
const scrollingElement = document.scrollingElement ?? document.documentElement;
|
||||
scrollingElement.scrollTop = initialScroll;
|
||||
scrollingElement.scrollTop = initialScrollPercent * scrollingElement.scrollHeight;
|
||||
} else if (hash) {
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
|
||||
@@ -30,20 +30,24 @@ export interface ExtraContentScriptSource {
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface ScrollEvent {
|
||||
fraction: number; // e.g. 0.5 when scrolled 50% of the way through the document
|
||||
}
|
||||
|
||||
export type OnScrollCallback = (scrollTop: ScrollEvent)=> void;
|
||||
|
||||
export interface RendererProcessApi {
|
||||
renderer: Renderer;
|
||||
jumpToHash: (hash: string)=> void;
|
||||
}
|
||||
|
||||
export interface MainProcessApi {
|
||||
onScroll(scrollTop: number): void;
|
||||
onScroll: OnScrollCallback;
|
||||
onPostMessage(message: string): void;
|
||||
onPostPluginMessage(contentScriptId: string, message: unknown): Promise<unknown>;
|
||||
fsDriver: RendererFsDriver;
|
||||
}
|
||||
|
||||
export type OnScrollCallback = (scrollTop: number)=> void;
|
||||
|
||||
export interface MarkupRecord {
|
||||
language: MarkupLanguage;
|
||||
markup: string;
|
||||
@@ -62,7 +66,7 @@ export interface RenderOptions {
|
||||
removeUnusedPluginAssets: boolean;
|
||||
|
||||
noteHash: string;
|
||||
initialScroll: number;
|
||||
initialScrollPercent: number;
|
||||
|
||||
// Forwarded renderer settings
|
||||
splitted?: boolean;
|
||||
|
||||
@@ -91,8 +91,8 @@ const useMessenger = (props: UseMessengerProps) => {
|
||||
|
||||
const messenger = useMemo(() => {
|
||||
const fsDriver = shim.fsDriver();
|
||||
const localApi = {
|
||||
onScroll: (fraction: number) => onScrollRef.current?.(fraction),
|
||||
const localApi: MainProcessApi = {
|
||||
onScroll: (event) => onScrollRef.current?.(event),
|
||||
onPostMessage: (message: string) => onPostMessageRef.current?.(message),
|
||||
onPostPluginMessage,
|
||||
fsDriver: {
|
||||
|
||||
@@ -29,6 +29,8 @@ export const initialize = async (
|
||||
settings,
|
||||
initialText,
|
||||
initialNoteId,
|
||||
initialSelection,
|
||||
initialScroll,
|
||||
parentElementClassName,
|
||||
initialSearch,
|
||||
}: EditorProps,
|
||||
@@ -41,6 +43,14 @@ export const initialize = async (
|
||||
throw new Error('Parent node is not an element.');
|
||||
}
|
||||
|
||||
document.addEventListener('scrollend', () => {
|
||||
const fraction = document.scrollingElement.scrollTop / (document.scrollingElement.scrollHeight || 1);
|
||||
void messenger.remoteApi.onEditorEvent({
|
||||
kind: EditorEventType.Scroll,
|
||||
fraction,
|
||||
});
|
||||
});
|
||||
|
||||
const assetContainer = document.createElement('div');
|
||||
assetContainer.id = 'joplin-container-pluginAssetsContainer';
|
||||
document.body.appendChild(assetContainer);
|
||||
@@ -100,6 +110,13 @@ export const initialize = async (
|
||||
});
|
||||
editor.setSearchState(initialSearch, 'initialSearch');
|
||||
|
||||
if (initialSelection) {
|
||||
editor.select(initialSelection.start, initialSelection.end);
|
||||
}
|
||||
if (initialScroll) {
|
||||
editor.setScrollPercent(initialScroll);
|
||||
}
|
||||
|
||||
messenger.setLocalInterface({
|
||||
editor,
|
||||
});
|
||||
|
||||
@@ -3,9 +3,13 @@ import { EditorControl, EditorSettings, OnLocalize, SearchState } from '@joplin/
|
||||
import { MarkupRecord, RendererControl } from '../rendererBundle/types';
|
||||
import { RenderResult } from '@joplin/renderer/types';
|
||||
|
||||
type SelectionRange = { start: number; end: number };
|
||||
|
||||
export interface EditorProps {
|
||||
initialText: string;
|
||||
initialSearch: SearchState;
|
||||
initialSelection: SelectionRange;
|
||||
initialScroll: number;
|
||||
initialNoteId: string;
|
||||
parentElementClassName: string;
|
||||
settings: EditorSettings;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RendererControl, RenderOptions } from '../rendererBundle/types';
|
||||
import { ResourceInfos } from '@joplin/renderer/types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { defaultSearchState } from '../../components/NoteEditor/SearchPanel';
|
||||
import { SelectionRange } from '../markdownEditorBundle/types';
|
||||
|
||||
const logger = Logger.create('useWebViewSetup');
|
||||
|
||||
@@ -21,6 +22,8 @@ interface Props {
|
||||
initialText: string;
|
||||
noteId: string;
|
||||
settings: EditorSettings;
|
||||
initialSelection: SelectionRange|null;
|
||||
initialScroll: number|null;
|
||||
parentElementClassName: string;
|
||||
globalSearch: string;
|
||||
themeId: number;
|
||||
@@ -53,7 +56,7 @@ const useMessenger = (props: UseMessengerProps) => {
|
||||
noteViewerFontSize: `${baseTheme.fontSize}${baseTheme.fontSizeUnits ?? 'px'}`,
|
||||
},
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
initialScrollPercent: 0,
|
||||
pluginAssetContainerSelector: null,
|
||||
removeUnusedPluginAssets: true,
|
||||
};
|
||||
@@ -119,6 +122,8 @@ const useSource = (props: UseSourceProps) => {
|
||||
...defaultSearchState,
|
||||
searchText: propsRef.current.globalSearch,
|
||||
},
|
||||
initialScroll: propsRef.current.initialScroll,
|
||||
initialSelection: propsRef.current.initialSelection,
|
||||
settings: propsRef.current.settings,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { RenderResult } from '../../renderer/types';
|
||||
import postprocessEditorOutput from './utils/postprocessEditorOutput';
|
||||
import detailsPlugin from './plugins/detailsPlugin';
|
||||
import tablePlugin from './plugins/tablePlugin';
|
||||
import clampPointToDocument from './utils/clampPointToDocument';
|
||||
|
||||
interface ProseMirrorControl extends EditorControl {
|
||||
getSettings(): EditorSettings;
|
||||
@@ -134,6 +135,14 @@ const createEditor = async (
|
||||
formatting: selectionFormatting,
|
||||
});
|
||||
}
|
||||
|
||||
props.onEvent({
|
||||
kind: EditorEventType.SelectionRangeChange,
|
||||
anchor: newState.selection.anchor,
|
||||
head: newState.selection.head,
|
||||
from: newState.selection.from,
|
||||
to: newState.selection.to,
|
||||
});
|
||||
};
|
||||
|
||||
const view = new EditorView(parentElement, {
|
||||
@@ -187,10 +196,14 @@ const createEditor = async (
|
||||
redo: () => {
|
||||
void editorControl.execCommand(EditorCommandType.Redo);
|
||||
},
|
||||
select: function(anchor: number, head: number): void {
|
||||
select: (anchor: number, head: number) => {
|
||||
const transaction = view.state.tr;
|
||||
transaction.setSelection(
|
||||
TextSelection.create(transaction.doc, anchor, head),
|
||||
TextSelection.create(
|
||||
transaction.doc,
|
||||
clampPointToDocument(view.state, anchor),
|
||||
clampPointToDocument(view.state, head),
|
||||
),
|
||||
);
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
|
||||
const documentMaximumIndex = (state: EditorState) => {
|
||||
// nodeSize is documented to be the size of a node's content plus two (one for the
|
||||
// start marker and one for the end marker). The main document doesn't have start or
|
||||
// end markers, so subtract these to get the document maximum index:
|
||||
return state.doc.nodeSize - 2;
|
||||
};
|
||||
|
||||
const clampPointToDocument = (state: EditorState, point: number) => {
|
||||
if (point < 0) return 0;
|
||||
|
||||
const maximumIndex = documentMaximumIndex(state);
|
||||
if (point > maximumIndex) return maximumIndex;
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
export default clampPointToDocument;
|
||||
@@ -225,3 +225,4 @@ mdpi
|
||||
xhdpi
|
||||
xxhdpi
|
||||
xxxhdpi
|
||||
scrollend
|
||||
|
||||
Reference in New Issue
Block a user