Skip to content

Commit fd04b7f

Browse files
committed
feat(bundle): improve cursor movement to specified line in wysiwyg mode
1 parent 426bfcf commit fd04b7f

File tree

3 files changed

+106
-32
lines changed

3 files changed

+106
-32
lines changed

src/bundle/Editor.ts

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {EditorView as CMEditorView} from '@codemirror/view';
44
import {TextSelection} from 'prosemirror-state';
55
import type {EditorView as PMEditorView} from 'prosemirror-view';
66

7+
import {getDescedantByAttribute} from 'src/utils/node-descedants';
8+
79
import type {CommonEditor, MarkupString} from '../common';
810
import {
911
type ActionStorage,
@@ -516,48 +518,83 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
516518

517519
switch (mode) {
518520
case 'markup': {
519-
const view = this.markupEditor.cm;
520-
521-
let cmLine = line + 1; // lines in codemirror is 1-based
522-
cmLine = Math.max(cmLine, 1);
523-
cmLine = Math.min(cmLine, view.state.doc.lines);
524-
525-
const yMargin = getTopOffset(view.dom);
526-
const anchor = view.state.doc.line(cmLine).from;
527-
view.dispatch({
528-
scrollIntoView: true,
529-
selection: {anchor},
530-
effects: [
531-
CMEditorView.scrollIntoView(anchor, {y: 'start', x: 'start', yMargin}),
532-
],
533-
});
534-
521+
this.markupMoveToLine(line);
535522
break;
536-
537-
// eslint-disable-next-line no-inner-declarations
538523
}
539524
case 'wysiwyg': {
540-
const elem = this.wysiwygEditor.dom.querySelector(`[data-line="${line}"]`);
541-
542-
if (elem) {
543-
const elemTop = elem.getBoundingClientRect().top;
544-
const topOffset = getTopOffset(this.wysiwygEditor.dom);
545-
window.scrollTo({top: elemTop + window.scrollY - topOffset});
546-
547-
const position = this._wysiwygView.posAtDOM(elem, 0);
548-
const {tr} = this._wysiwygView.state;
549-
this._wysiwygView.dispatch(
550-
tr.setSelection(TextSelection.create(tr.doc, position)),
551-
);
552-
}
553-
525+
this.wysiwygMoveToLine(line);
554526
break;
555527
}
556528
default:
557529
throw new Error('Unknown editor mode: ' + mode);
558530
}
559531
}
560532

533+
private markupMoveToLine(line: number): void {
534+
const view = this.markupEditor.cm;
535+
const isConnected = Boolean(view.dom.parentElement);
536+
537+
let cmLine = line + 1; // lines in codemirror is 1-based
538+
cmLine = Math.max(cmLine, 1);
539+
cmLine = Math.min(cmLine, view.state.doc.lines);
540+
541+
const anchor = view.state.doc.line(cmLine).from;
542+
view.dispatch({
543+
scrollIntoView: true,
544+
selection: {anchor},
545+
effects: isConnected
546+
? [
547+
CMEditorView.scrollIntoView(anchor, {
548+
y: 'start',
549+
x: 'start',
550+
yMargin: getTopOffset(view.dom),
551+
}),
552+
]
553+
: undefined,
554+
});
555+
}
556+
557+
private wysiwygMoveToLine(line: number): void {
558+
const DATA_LINE = 'data-line';
559+
const SELECTOR = `[${DATA_LINE}="${line}"]` as const;
560+
561+
const view = this._wysiwygView;
562+
const isConnected = Boolean(view.dom.parentElement);
563+
564+
const setSelection = (pos: number) => {
565+
const {tr} = view.state;
566+
view.dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(pos), 1)));
567+
};
568+
569+
const scrollIntoView = (elemTop: number) => {
570+
const topOffset = getTopOffset(this.wysiwygEditor.dom);
571+
window.scrollTo({top: elemTop + window.scrollY - topOffset});
572+
};
573+
574+
const elem = this.wysiwygEditor.dom.querySelector(SELECTOR);
575+
if (elem) {
576+
const position = this._wysiwygView.posAtDOM(elem, 0);
577+
setSelection(position);
578+
579+
if (isConnected) {
580+
const elemTop = elem.getBoundingClientRect().top;
581+
scrollIntoView(elemTop);
582+
}
583+
584+
return;
585+
}
586+
587+
const node = getDescedantByAttribute(view.state.doc, DATA_LINE, [line, String(line)]);
588+
if (node) {
589+
setSelection(node.pos);
590+
591+
if (isConnected) {
592+
const elemTop = view.coordsAtPos(node.pos).top;
593+
scrollIntoView(elemTop);
594+
}
595+
}
596+
}
597+
561598
private shouldReplaceMarkupEditorValue(markupValue: string, wysiwygValue: string) {
562599
const serializedEditorMarkup = this.#wysiwygEditor?.serializer.serialize(
563600
this.#wysiwygEditor.parser.parse(markupValue),

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './inputrules';
1111
export * from './keymap';
1212
export * from './marks';
1313
export * from './node-children';
14+
export * from './node-descedants';
1415
export * from './nodes';
1516
export * from './placeholder';
1617
export * from './platform';

src/utils/node-descedants.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type {Node} from '#pm/model';
2+
3+
type DescendantWithMeta = {
4+
node: Node;
5+
pos: number;
6+
parent: Node | null;
7+
index: number;
8+
};
9+
10+
export function getDescedantBy(
11+
node: Node,
12+
f: (child: DescendantWithMeta) => boolean,
13+
): DescendantWithMeta | null {
14+
let result: DescendantWithMeta | null = null;
15+
node.descendants((node, pos, parent, index) => {
16+
if (result) return false;
17+
18+
const child: DescendantWithMeta = {node, pos, parent, index};
19+
if (f(child)) {
20+
result = child;
21+
return false;
22+
}
23+
24+
return true;
25+
});
26+
return result;
27+
}
28+
29+
export function getDescedantByAttribute(
30+
node: Node,
31+
attr: string,
32+
value: unknown | unknown[],
33+
): DescendantWithMeta | null {
34+
const values = Array<unknown>().concat(value);
35+
return getDescedantBy(node, (item) => values.includes(item.node.attrs[attr]));
36+
}

0 commit comments

Comments
 (0)