Skip to content

Commit 991107a

Browse files
authored
feat(Selection, Cursor)!: rewrite code for fake-paragraph behaviour (#65)
I rewrote the code that for the behavior with the fake paragraph. This fixed some bugs and improved the behavior in general. 1. Don't create fake paragraph near with textblocks. 2. Fixed error, that's occurred with node-selection and `after`-direction 3. Add _waterfall_ fake-paragraphs behaviour. For example, cursor in `doc -> blockquote -> yfm_cut -> yfm_note -> blockquote`. On first `down` press fake paragraph will be created in `yfm_note`, then in `yfm_cut`, then in `blockquote`, and then in `doc`
1 parent c259747 commit 991107a

File tree

18 files changed

+457
-148
lines changed

18 files changed

+457
-148
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"lint:prettier": "prettier --check \"{src,demo}/**/*.{js,jsx,ts,tsx,css,scss}\"",
1717
"test": "jest",
1818
"test:cov": "jest --coverage",
19+
"test:watch": "jest --watchAll",
1920
"prepublishOnly": "npm run lint && npm run build"
2021
},
2122
"repository": {
@@ -57,7 +58,7 @@
5758
"prosemirror-state": "1.4.1",
5859
"prosemirror-transform": "1.6.0",
5960
"prosemirror-utils": "1.0.0-0",
60-
"prosemirror-view": "1.26.2",
61+
"prosemirror-view": "1.30.0",
6162
"react-use": "^17.3.2",
6263
"tslib": "^2.3.1"
6364
},

src/extensions/behavior/Clipboard/clipboard.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export const clipboard = ({
3636
}: ClipboardPluginOptions) => {
3737
return new Plugin({
3838
props: {
39-
// @ts-expect-error handleDOMEvents has broken types
4039
handleDOMEvents: {
4140
copy(view, e) {
4241
if (!e.clipboardData) return false;

src/extensions/behavior/Cursor/GapCursorSelection.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,42 @@ import {Selection} from 'prosemirror-state';
22
import type {Node, ResolvedPos} from 'prosemirror-model';
33
import type {Mapping} from 'prosemirror-transform';
44

5+
export function isGapCursorSelection<T>(selection: Selection): selection is GapCursorSelection<T> {
6+
return selection instanceof GapCursorSelection;
7+
}
8+
59
// @ts-expect-error // TODO: implements toJSON
6-
export class GapCursorSelection extends Selection {
7-
constructor(pos: ResolvedPos) {
8-
super(pos, pos);
10+
export class GapCursorSelection<T = any> extends Selection {
11+
#_$pos: ResolvedPos;
12+
13+
readonly meta?: T;
14+
15+
readonly selectionName = 'GapCursorSelection';
16+
17+
get $pos(): ResolvedPos {
18+
return this.#_$pos;
19+
}
20+
21+
get pos(): number {
22+
return this.#_$pos.pos;
23+
}
24+
25+
constructor($pos: ResolvedPos, params?: {meta?: T}) {
26+
super($pos, $pos);
27+
this.#_$pos = $pos;
28+
this.meta = params?.meta;
929
}
1030

1131
eq(other: Selection): boolean {
12-
return other instanceof GapCursorSelection && other.head === this.head;
32+
return (
33+
isGapCursorSelection(other) &&
34+
this.pos === other.pos &&
35+
this.$pos.doc.eq(other.$pos.doc)
36+
);
1337
}
1438

1539
map(doc: Node, mapping: Mapping): Selection {
1640
const $pos = doc.resolve(mapping.map(this.head));
17-
1841
return Selection.near($pos);
1942
}
2043
}

src/extensions/behavior/Cursor/gapcursor.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {DOMSerializer} from 'prosemirror-model';
22
import {Decoration, DecorationSet, EditorView} from 'prosemirror-view';
3-
import {EditorState, NodeSelection, Plugin, PluginKey} from 'prosemirror-state';
3+
import {EditorState, Plugin, PluginKey} from 'prosemirror-state';
44

5+
import {isNodeSelection} from '../../../utils/selection';
56
import {pType} from '../../base/BaseSchema';
67
import {createPlaceholder} from '../../behavior/Placeholder';
7-
import {GapCursorSelection} from './GapCursorSelection';
8+
import {isGapCursorSelection} from './GapCursorSelection';
89

910
import './gapcursor.scss';
1011

@@ -16,10 +17,8 @@ export const gapCursor = () =>
1617
state: {
1718
init: () => false,
1819
apply: (_tr, _pluginState, _oldState, newState) => {
19-
return (
20-
newState.selection instanceof GapCursorSelection ||
21-
newState.selection instanceof NodeSelection
22-
);
20+
const sel = newState.selection;
21+
return isGapCursorSelection(sel) || isNodeSelection(sel);
2322
},
2423
},
2524
view() {
@@ -33,7 +32,7 @@ export const gapCursor = () =>
3332
},
3433
props: {
3534
decorations: ({doc, selection}: EditorState) => {
36-
if (selection instanceof GapCursorSelection) {
35+
if (isGapCursorSelection(selection)) {
3736
const position = selection.head;
3837

3938
return DecorationSet.create(doc, [
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import type {Node} from 'prosemirror-model';
2+
import {TextSelection} from 'prosemirror-state';
3+
import {builders} from 'prosemirror-test-builder';
4+
5+
import {ExtensionsManager} from '../../../core';
6+
import {BaseNode, BaseSchema} from '../../base/BaseSchema';
7+
import {Blockquote, blockquote} from '../../markdown/Blockquote';
8+
import {CodeBlock, codeBlockNodeName} from '../../markdown/CodeBlock';
9+
import {YfmTable, YfmTableNode} from '../../yfm/YfmTable';
10+
import {GapCursorSelection} from '../Cursor/GapCursorSelection';
11+
12+
import {
13+
Direction,
14+
findFakeParaPosForTextSelection,
15+
findNextFakeParaPosForGapCursorSelection,
16+
} from './commands';
17+
18+
const {schema} = new ExtensionsManager({
19+
extensions: (builder) =>
20+
builder
21+
.use(BaseSchema, {})
22+
.use(Blockquote, {})
23+
.use(CodeBlock, {})
24+
.use(YfmTable, {})
25+
.addNode('testnode', () => ({
26+
spec: {content: `block*`, group: 'block', gapcursor: false},
27+
fromYfm: {tokenSpec: {name: 'testnode', type: 'block', ignore: true}},
28+
toYfm: () => {},
29+
})),
30+
}).buildDeps();
31+
32+
const {doc, p, bq, codeBlock, table, tbody, tr, td, testnode} = builders(schema, {
33+
doc: {nodeType: BaseNode.Doc},
34+
p: {nodeType: BaseNode.Paragraph},
35+
bq: {nodeType: blockquote},
36+
codeBlock: {nodeType: codeBlockNodeName},
37+
table: {nodeType: YfmTableNode.Table},
38+
tbody: {nodeType: YfmTableNode.Body},
39+
tr: {nodeType: YfmTableNode.Row},
40+
td: {nodeType: YfmTableNode.Cell},
41+
}) as PMTestBuilderResult<
42+
'doc' | 'p' | 'bq' | 'codeBlock' | 'table' | 'tbody' | 'tr' | 'td' | 'testnode'
43+
>;
44+
45+
function shouldFindPos(doc: Node, dir: Direction, selPos: number, fakePos: number) {
46+
const sel = TextSelection.create(doc, selPos);
47+
const $pos = findFakeParaPosForTextSelection(sel, dir);
48+
49+
expect($pos).toBeTruthy();
50+
expect($pos!.pos).toBe(fakePos);
51+
}
52+
53+
function shouldReturnNull(doc: Node, dir: Direction, selPos: number) {
54+
const sel = TextSelection.create(doc, selPos);
55+
const $pos = findFakeParaPosForTextSelection(sel, dir);
56+
57+
expect($pos).toBeNull();
58+
}
59+
60+
function shouldFindNextPos(doc: Node, dir: Direction, selPos: number, fakePos: number) {
61+
const sel = new GapCursorSelection(doc.resolve(selPos));
62+
const $pos = findNextFakeParaPosForGapCursorSelection(sel, dir);
63+
64+
expect($pos).toBeTruthy();
65+
expect($pos!.pos).toBe(fakePos);
66+
}
67+
68+
function shouldNotFindNextPos(doc: Node, dir: Direction, selPos: number) {
69+
const sel = new GapCursorSelection(doc.resolve(selPos));
70+
const $pos = findNextFakeParaPosForGapCursorSelection(sel, dir);
71+
72+
expect($pos).toBeNull();
73+
}
74+
75+
describe('Selection arrow commands: findFakeParaPosForTextSelection', () => {
76+
it.each(['before', 'after'] as const)(
77+
'should not find fake paragraph position %s empty paragraph in doc',
78+
(dir) => {
79+
shouldReturnNull(doc(p()), dir, 1);
80+
},
81+
);
82+
83+
it.each(['before', 'after'] as const)(
84+
'should not find fake paragraph position %s empty paragraph [2]',
85+
(dir) => {
86+
shouldReturnNull(doc(p(), p(), p()), dir, 3); // cursor in second paragraph
87+
},
88+
);
89+
90+
describe('codeblock', () => {
91+
it.each([
92+
['before', 0],
93+
['after', 2],
94+
] as const)('should find fake paragraph position %s codeblock', (dir, fakePos) => {
95+
shouldFindPos(doc(codeBlock()), dir, 1, fakePos);
96+
});
97+
98+
it.each(['before', 'after'] as const)(
99+
'should not find fake paragraph position %s codeblock ',
100+
(dir) => {
101+
shouldReturnNull(doc(p(), codeBlock(), p()), dir, 3); // cursor in codeblock
102+
},
103+
);
104+
105+
it.each([
106+
['before', 3, 2],
107+
['after', 1, 2],
108+
] as const)(
109+
'should find fake paragraph position between code blocks [%s]',
110+
(dir, selPos, fakePos) => {
111+
shouldFindPos(doc(codeBlock(), codeBlock()), dir, selPos, fakePos);
112+
},
113+
);
114+
});
115+
116+
it.each([
117+
['before', 0],
118+
['after', 4],
119+
] as const)('should find fake paragraph position %s block (blockquote)', (dir, fakePos) => {
120+
shouldFindPos(doc(bq(p())), dir, 2, fakePos); // cursor in para in blockquote
121+
});
122+
123+
it.each([
124+
['before', 6, 4],
125+
['after', 2, 4],
126+
] as const)(
127+
'should find fake paragraph position between blocks (blockquotes) [%s]',
128+
(dir, selPos, fakePos) => {
129+
shouldFindPos(doc(bq(p()), bq(p())), dir, selPos, fakePos);
130+
},
131+
);
132+
133+
it.each([
134+
['before', 4], // cursor in second para
135+
['after', 2], // cursor in first para
136+
] as const)(
137+
'should not find fake paragraph position beetween paragraphs in block (in blockquote) [%s]',
138+
(dir, selPos) => {
139+
shouldReturnNull(doc(bq(p(), p())), dir, selPos);
140+
},
141+
);
142+
143+
it.each([
144+
['before', 9, 4],
145+
['after', 31, 36],
146+
] as const)(
147+
'should find fake paragraph position on edge of complex block (yfm-table) [%s]',
148+
(dir, selPos, fakePos) => {
149+
shouldFindPos(
150+
doc(
151+
bq(p()),
152+
table(tbody(tr(td(p()), td(p()), td(p())), tr(td(p()), td(p()), td(p())))),
153+
bq(p()),
154+
),
155+
dir,
156+
selPos,
157+
fakePos,
158+
);
159+
},
160+
);
161+
162+
it.each([
163+
['before', 19],
164+
['after', 5],
165+
] as const)(
166+
'should not find fake paragraph position inside of complex block (yfm-table) [%s]',
167+
(dir, selPos) => {
168+
shouldReturnNull(
169+
doc(table(tbody(tr(td(p()), td(p()), td(p())), tr(td(p()), td(p()), td(p()))))),
170+
dir,
171+
selPos,
172+
);
173+
},
174+
);
175+
176+
it.each([
177+
['before', 7],
178+
['after', 29],
179+
] as const)(
180+
'should not find fake paragraph position on edge of complex block (yfm-table) with textblock [%s]',
181+
(dir, selPos) => {
182+
shouldReturnNull(
183+
doc(
184+
p(),
185+
table(tbody(tr(td(p()), td(p()), td(p())), tr(td(p()), td(p()), td(p())))),
186+
p(),
187+
),
188+
dir,
189+
selPos,
190+
);
191+
},
192+
);
193+
194+
it.each([
195+
['before', 3, 0],
196+
['after', 3, 6],
197+
] as const)('should skip nodes with `gapcursor: false` flag [%s]', (dir, selPos, fakePos) => {
198+
shouldFindPos(doc(testnode(bq(p()))), dir, selPos, fakePos);
199+
});
200+
201+
describe('pyramid of quotes', () => {
202+
const initDoc = doc(bq(p('1'), bq(p('3')), p('2')));
203+
204+
it.each([
205+
['before', 6], // before '3'
206+
['after', 6], // before '3'
207+
] as const)('should ignore – top level', (dir, selPos) => {
208+
shouldReturnNull(initDoc, dir, selPos);
209+
});
210+
211+
it.each([
212+
['before', 2, 0], // after '1'
213+
['after', 10, 13], // after '2'
214+
] as const)('should find a position %s the base of the pyramid', (dir, selPos, fakePos) => {
215+
shouldFindPos(initDoc, dir, selPos, fakePos);
216+
});
217+
});
218+
219+
describe('stack of quotes', () => {
220+
const initDoc = doc(bq(bq(p('1'))));
221+
222+
it.each([
223+
['before', 1], // before nested quote
224+
['after', 6], // after nested quote
225+
] as const)('should find a position %s nested quote', (dir, fakePos) => {
226+
shouldFindPos(initDoc, dir, 3, fakePos);
227+
});
228+
229+
it.each([
230+
['before', 1, 0],
231+
['after', 6, 7],
232+
] as const)('should find next fake para position %s root quote', (dir, selPos, fakePos) => {
233+
shouldFindNextPos(initDoc, dir, selPos, fakePos);
234+
});
235+
});
236+
237+
it.each(['before', 'after'] as const)(
238+
'should not find next fake para pos if current fake pos located between two blocks [%s]',
239+
(dir) => {
240+
shouldNotFindNextPos(doc(bq(bq(p()), bq(p()))), dir, 5);
241+
},
242+
);
243+
});

0 commit comments

Comments
 (0)