edytor 0.1.0-next.18 → 0.1.0-next.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/components/Edytor.svelte +13 -0
- package/dist/events/onFocus.d.ts +16 -0
- package/dist/events/onFocus.js +53 -17
- package/dist/plugins/blockMenu/blockMenuPlugin.js +4 -2
- package/dist/plugins/columns/ColumnResize.svelte +1 -1
- package/dist/plugins/columns/resize.svelte.d.ts +14 -2
- package/dist/plugins/columns/resize.svelte.js +31 -10
- package/dist/plugins/slashMenu/slashMenuPlugin.js +4 -2
- package/dist/plugins/toolbar/toolbarPlugin.d.ts +2 -2
- package/dist/plugins/toolbar/toolbarPlugin.js +22 -1
- package/dist/selection/replaceSelection.d.ts +15 -0
- package/dist/selection/replaceSelection.js +34 -0
- package/dist/selection/selection.svelte.js +47 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
|
|
17
17
|
|
|
18
|
-
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.
|
|
18
|
+
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.20`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
@@ -341,6 +341,7 @@
|
|
|
341
341
|
use:editableRootBrowserAttributes={browserMutationGuardAttributes}
|
|
342
342
|
use:nonNativeEditableBlockChromeSelection
|
|
343
343
|
data-edytor
|
|
344
|
+
data-edytor-selection={edytor.selection.value.kind === 'blocks' ? 'blocks' : undefined}
|
|
344
345
|
contenteditable={!readonly}
|
|
345
346
|
role="textbox"
|
|
346
347
|
aria-multiline="true"
|
|
@@ -356,3 +357,15 @@
|
|
|
356
357
|
></span>
|
|
357
358
|
</div>
|
|
358
359
|
{/if}
|
|
360
|
+
|
|
361
|
+
<style>
|
|
362
|
+
/*
|
|
363
|
+
* A block selection shows as its selected blocks, never as a native range:
|
|
364
|
+
* the range a pointer drag across columns still extends under it
|
|
365
|
+
* (`sel.drag.across-columns`) is not highlighted, over any theme.
|
|
366
|
+
*/
|
|
367
|
+
:global([data-edytor][data-edytor-selection='blocks'] ::selection),
|
|
368
|
+
:global([data-edytor][data-edytor-selection='blocks']::selection) {
|
|
369
|
+
background: transparent !important;
|
|
370
|
+
}
|
|
371
|
+
</style>
|
package/dist/events/onFocus.d.ts
CHANGED
|
@@ -12,4 +12,20 @@ export declare const selectionIsInside: (node: HTMLElement) => boolean;
|
|
|
12
12
|
* already inside the host, or the view is gone.
|
|
13
13
|
*/
|
|
14
14
|
export declare const takeKeys: (edytor: Edytor) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Whether `event` is the press of a primary `mousedown` no `pointerdown`
|
|
17
|
+
* came before (WebKit's first press after a drag). Asked during the
|
|
18
|
+
* event's dispatch, after the document's capture phase saw it: by a
|
|
19
|
+
* listener the view's attach installs later (a plugin's `onEdytorAttached`)
|
|
20
|
+
* or by an element's own (bubbling, target phase), such as the chrome in the
|
|
21
|
+
* overlay, which starts from it as from a `pointerdown`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const isLonePress: (edytor: Edytor, event: MouseEvent) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Every press on `target` (`isLonePress`): its `pointerdown`, or the lone
|
|
26
|
+
* `mousedown` standing for a missing one, never both for one press. For a
|
|
27
|
+
* listener installed after the view's attach (a plugin's press outside its
|
|
28
|
+
* menu). Answers the teardown.
|
|
29
|
+
*/
|
|
30
|
+
export declare const onPress: (edytor: Edytor, target: EventTarget, listener: (event: MouseEvent) => void, options?: boolean | AddEventListenerOptions) => () => void;
|
|
15
31
|
export declare const attachFocus: (edytor: Edytor, node: HTMLElement) => (() => void)[];
|
package/dist/events/onFocus.js
CHANGED
|
@@ -37,21 +37,54 @@ export const takeKeys = (edytor) => {
|
|
|
37
37
|
node.focus({ preventScroll: true });
|
|
38
38
|
edytor.projector.parked();
|
|
39
39
|
};
|
|
40
|
+
/*
|
|
41
|
+
* The press: one per primary button press, its `pointerdown`, or a
|
|
42
|
+
* `mousedown` no `pointerdown` came before. WebKit fires no `pointercancel`
|
|
43
|
+
* at a native `dragstart` (a block handle's drag) and, believing the button
|
|
44
|
+
* still down, no `pointerdown` for the next press, only its `mousedown`
|
|
45
|
+
* (WebKit bugs 202287, 222632, 279749): that primary `mousedown` is the
|
|
46
|
+
* press (lone). `attachFocus` classifies each `mousedown` of its view's
|
|
47
|
+
* document at the capture phase, first of the view's listeners; the lone one
|
|
48
|
+
* of each view is kept here, read by `isLonePress` and `onPress`.
|
|
49
|
+
*/
|
|
50
|
+
const lonePresses = new WeakMap();
|
|
51
|
+
/**
|
|
52
|
+
* Whether `event` is the press of a primary `mousedown` no `pointerdown`
|
|
53
|
+
* came before (WebKit's first press after a drag). Asked during the
|
|
54
|
+
* event's dispatch, after the document's capture phase saw it: by a
|
|
55
|
+
* listener the view's attach installs later (a plugin's `onEdytorAttached`)
|
|
56
|
+
* or by an element's own (bubbling, target phase), such as the chrome in the
|
|
57
|
+
* overlay, which starts from it as from a `pointerdown`.
|
|
58
|
+
*/
|
|
59
|
+
export const isLonePress = (edytor, event) => lonePresses.get(edytor) === event;
|
|
60
|
+
/**
|
|
61
|
+
* Every press on `target` (`isLonePress`): its `pointerdown`, or the lone
|
|
62
|
+
* `mousedown` standing for a missing one, never both for one press. For a
|
|
63
|
+
* listener installed after the view's attach (a plugin's press outside its
|
|
64
|
+
* menu). Answers the teardown.
|
|
65
|
+
*/
|
|
66
|
+
export const onPress = (edytor, target, listener, options) => {
|
|
67
|
+
const lone = (event) => {
|
|
68
|
+
if (isLonePress(edytor, event))
|
|
69
|
+
listener(event);
|
|
70
|
+
};
|
|
71
|
+
target.addEventListener('pointerdown', listener, options);
|
|
72
|
+
target.addEventListener('mousedown', lone, options);
|
|
73
|
+
return () => {
|
|
74
|
+
target.removeEventListener('pointerdown', listener, options);
|
|
75
|
+
target.removeEventListener('mousedown', lone, options);
|
|
76
|
+
};
|
|
77
|
+
};
|
|
40
78
|
export const attachFocus = (edytor, node) => {
|
|
41
79
|
let lastPointerDownInsideEditorAt = Number.NEGATIVE_INFINITY;
|
|
42
80
|
/*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* only its `mousedown` (WebKit bugs 202287, 222632, 279749): that
|
|
48
|
-
* primary `mousedown` is the press (`lone`). A primary `pointerdown` arms
|
|
49
|
-
* `compat` until the `mousedown` it is followed by (before or after its
|
|
50
|
-
* release, a touch's comes after), so a pair counts once. Only the
|
|
51
|
-
* browser's own `mousedown` stands for a missing press; a script's is none.
|
|
81
|
+
* A primary `pointerdown` arms `compat` until the `mousedown` it is
|
|
82
|
+
* followed by (before or after its release, a touch's comes after), so a
|
|
83
|
+
* pair counts once. Only the browser's own `mousedown` stands for a
|
|
84
|
+
* missing press; a script's is none (`lonePresses`).
|
|
52
85
|
*/
|
|
53
86
|
let compat = false;
|
|
54
|
-
|
|
87
|
+
const lone = (event) => isLonePress(edytor, event);
|
|
55
88
|
/** Where the press landed: the gesture serial, and whether the user went outside. */
|
|
56
89
|
const gesture = (event) => {
|
|
57
90
|
edytor.markUserGesture();
|
|
@@ -150,27 +183,29 @@ export const attachFocus = (edytor, node) => {
|
|
|
150
183
|
on(node.ownerDocument, 'pointerdown', (event) => {
|
|
151
184
|
if (event.isPrimary)
|
|
152
185
|
compat = true;
|
|
153
|
-
|
|
186
|
+
lonePresses.delete(edytor);
|
|
154
187
|
gesture(event);
|
|
155
188
|
}, { capture: true }),
|
|
156
189
|
on(node.ownerDocument, 'mousedown', (event) => {
|
|
157
|
-
|
|
190
|
+
const press = !compat && event.isTrusted && event.button === 0;
|
|
158
191
|
compat = false;
|
|
159
|
-
if (
|
|
160
|
-
|
|
192
|
+
if (!press)
|
|
193
|
+
return void lonePresses.delete(edytor);
|
|
194
|
+
lonePresses.set(edytor, event);
|
|
195
|
+
gesture(event);
|
|
161
196
|
}, { capture: true }),
|
|
162
197
|
// A press on a block's non-editable chrome places the caret in its text.
|
|
163
198
|
on(node, 'pointerdown', edytor.selection.handleNonNativeEditableBlockChromePointerDown, {
|
|
164
199
|
capture: true
|
|
165
200
|
}),
|
|
166
201
|
on(node, 'mousedown', (event) => {
|
|
167
|
-
if (event
|
|
202
|
+
if (lone(event))
|
|
168
203
|
edytor.selection.handleNonNativeEditableBlockChromePointerDown(event);
|
|
169
204
|
}, { capture: true }),
|
|
170
205
|
// The document's capture listener already marked the gesture.
|
|
171
206
|
on(node, 'pointerdown', pressInside),
|
|
172
207
|
on(node, 'mousedown', (event) => {
|
|
173
|
-
if (event
|
|
208
|
+
if (lone(event))
|
|
174
209
|
pressInside(event);
|
|
175
210
|
}),
|
|
176
211
|
on(node, 'pointerup', (event) => {
|
|
@@ -225,6 +260,7 @@ export const attachFocus = (edytor, node) => {
|
|
|
225
260
|
// Focus leaving abandons a live composition: the browser committed what it shows.
|
|
226
261
|
edytor.composition.abandon();
|
|
227
262
|
setTimeout(clearNativeSelectionAfterExternalFocus);
|
|
228
|
-
})
|
|
263
|
+
}),
|
|
264
|
+
() => void lonePresses.delete(edytor)
|
|
229
265
|
];
|
|
230
266
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT } from '../blockHandles/BlockHandleController.svelte.js';
|
|
2
|
+
import { onPress } from '../../events/onFocus.js';
|
|
2
3
|
import { getSelectedBlocksInDocumentOrder } from '../../selection/replaceSelection.js';
|
|
3
4
|
import BlockMenu from './BlockMenu.svelte';
|
|
4
5
|
import { BlockMenuController } from './BlockMenuController.svelte.js';
|
|
@@ -103,12 +104,13 @@ export const createBlockMenuPlugin = (options = {}) => (edytor) => {
|
|
|
103
104
|
const add = () => controller.close(false);
|
|
104
105
|
node.addEventListener(BLOCK_ACTIVATE_EVENT, activate);
|
|
105
106
|
node.addEventListener(BLOCK_ADD_EVENT, add);
|
|
106
|
-
|
|
107
|
+
// Every press, WebKit's lone `mousedown` after a drag too (`onPress`).
|
|
108
|
+
const offPress = onPress(edytor, node.ownerDocument, outside, true);
|
|
107
109
|
const unmount = edytor.overlay.mount(BlockMenu, { controller, menu: options.menu }, 'edytor-block-menu-host', 70, place);
|
|
108
110
|
return () => {
|
|
109
111
|
node.removeEventListener(BLOCK_ACTIVATE_EVENT, activate);
|
|
110
112
|
node.removeEventListener(BLOCK_ADD_EVENT, add);
|
|
111
|
-
|
|
113
|
+
offPress();
|
|
112
114
|
unmount();
|
|
113
115
|
};
|
|
114
116
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
data-dragging={resize.drag ? 'true' : undefined}
|
|
34
34
|
data-selecting={resize.selecting ? 'true' : undefined}
|
|
35
35
|
onpointerdown={(event) => resize.start(event, strip)}
|
|
36
|
-
onmousedown={(event) =>
|
|
36
|
+
onmousedown={(event) => resize.mousedown(event, strip)}
|
|
37
37
|
></div>
|
|
38
38
|
{/each}
|
|
39
39
|
{/if}
|
|
@@ -93,8 +93,20 @@ export declare class ColumnResize {
|
|
|
93
93
|
private gaps;
|
|
94
94
|
/** The left column's width at the drag's pointer, held to the minimum on both sides. */
|
|
95
95
|
private widthAt;
|
|
96
|
-
/**
|
|
97
|
-
|
|
96
|
+
/**
|
|
97
|
+
* A `mousedown` on `strip`: no native selection starts there; the press
|
|
98
|
+
* itself when no `pointerdown` came before it (`isLonePress`: WebKit's
|
|
99
|
+
* first press after a block drag), which starts the drag as one would.
|
|
100
|
+
*/
|
|
101
|
+
mousedown: (event: MouseEvent, strip: Strip) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Press on `strip`: the drag starts, nothing is written until the release.
|
|
104
|
+
* A `pointerdown` tracks its pointer (captured, on the document); a lone
|
|
105
|
+
* `mousedown` (`mousedown`) tracks the mouse on the window, its
|
|
106
|
+
* `mousemove`s and its `mouseup`, with the same preview, minimum, Escape
|
|
107
|
+
* and release.
|
|
108
|
+
*/
|
|
109
|
+
start: (event: PointerEvent | MouseEvent, strip: Strip) => void;
|
|
98
110
|
/**
|
|
99
111
|
* The view turned readonly (or the document read-only): a drag in progress
|
|
100
112
|
* ends at once, its preview dropped, nothing written (its release would be
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { takeKeys } from '../../events/onFocus.js';
|
|
1
|
+
import { isLonePress, takeKeys } from '../../events/onFocus.js';
|
|
2
2
|
import { bandOf, gapBefore } from './gaps.js';
|
|
3
3
|
import { stacks } from './stacking.js';
|
|
4
4
|
/** The guide line's width (the hover guide's and the drag's). */
|
|
@@ -141,7 +141,23 @@ export class ColumnResize {
|
|
|
141
141
|
const hi = Math.max(sum - min, width);
|
|
142
142
|
return Math.min(hi, Math.max(lo, width + at - from));
|
|
143
143
|
}
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* A `mousedown` on `strip`: no native selection starts there; the press
|
|
146
|
+
* itself when no `pointerdown` came before it (`isLonePress`: WebKit's
|
|
147
|
+
* first press after a block drag), which starts the drag as one would.
|
|
148
|
+
*/
|
|
149
|
+
mousedown = (event, strip) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
if (isLonePress(this.edytor, event))
|
|
152
|
+
this.start(event, strip);
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Press on `strip`: the drag starts, nothing is written until the release.
|
|
156
|
+
* A `pointerdown` tracks its pointer (captured, on the document); a lone
|
|
157
|
+
* `mousedown` (`mousedown`) tracks the mouse on the window, its
|
|
158
|
+
* `mousemove`s and its `mouseup`, with the same preview, minimum, Escape
|
|
159
|
+
* and release.
|
|
160
|
+
*/
|
|
145
161
|
start = (event, strip) => {
|
|
146
162
|
const { edytor } = this;
|
|
147
163
|
if (event.button !== 0 || edytor.readonly || this.drag)
|
|
@@ -155,7 +171,9 @@ export class ColumnResize {
|
|
|
155
171
|
event.preventDefault();
|
|
156
172
|
event.stopPropagation();
|
|
157
173
|
const target = event.currentTarget;
|
|
158
|
-
|
|
174
|
+
const pointer = event.type === 'pointerdown';
|
|
175
|
+
if (pointer)
|
|
176
|
+
target?.setPointerCapture?.(event.pointerId);
|
|
159
177
|
this.drag = {
|
|
160
178
|
strip,
|
|
161
179
|
left,
|
|
@@ -171,6 +189,8 @@ export class ColumnResize {
|
|
|
171
189
|
height: box.height
|
|
172
190
|
};
|
|
173
191
|
const document = target?.ownerDocument ?? edytor.node?.ownerDocument;
|
|
192
|
+
const tracked = pointer ? document : document?.defaultView;
|
|
193
|
+
const [moves, ups] = pointer ? ['pointermove', 'pointerup'] : ['mousemove', 'mouseup'];
|
|
174
194
|
const move = (moved) => {
|
|
175
195
|
if (!this.drag)
|
|
176
196
|
return;
|
|
@@ -181,16 +201,16 @@ export class ColumnResize {
|
|
|
181
201
|
edytor.overlay.invalidate();
|
|
182
202
|
};
|
|
183
203
|
const end = (ended) => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
tracked?.removeEventListener(moves, move);
|
|
205
|
+
tracked?.removeEventListener(ups, end);
|
|
206
|
+
tracked?.removeEventListener('pointercancel', end);
|
|
187
207
|
document?.removeEventListener('keydown', escape, true);
|
|
188
208
|
const drag = this.drag;
|
|
189
209
|
this.drag = null;
|
|
190
210
|
this.guide = null;
|
|
191
211
|
this.#cancel = null;
|
|
192
212
|
edytor.overlay.invalidate();
|
|
193
|
-
if (drag && ended.type ===
|
|
213
|
+
if (drag && ended.type === ups) {
|
|
194
214
|
drag.at = ended.clientX;
|
|
195
215
|
this.release(drag);
|
|
196
216
|
// The editor holds the keys after the release (Notion: Mod+Z right after).
|
|
@@ -207,9 +227,10 @@ export class ColumnResize {
|
|
|
207
227
|
end(key);
|
|
208
228
|
};
|
|
209
229
|
this.#cancel = () => end(new Event('cancel'));
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
tracked?.addEventListener(moves, move);
|
|
231
|
+
tracked?.addEventListener(ups, end);
|
|
232
|
+
if (pointer)
|
|
233
|
+
tracked?.addEventListener('pointercancel', end);
|
|
213
234
|
document?.addEventListener('keydown', escape, true);
|
|
214
235
|
edytor.overlay.invalidate();
|
|
215
236
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT } from '../blockHandles/BlockHandleController.svelte.js';
|
|
2
|
+
import { onPress } from '../../events/onFocus.js';
|
|
2
3
|
import SlashMenu from './SlashMenu.svelte';
|
|
3
4
|
import { SlashMenuController } from './SlashMenuController.svelte.js';
|
|
4
5
|
/** The slash menu, with your own markup through `menu` or `item` snippets. */
|
|
@@ -90,12 +91,13 @@ export const createSlashMenuPlugin = (options = {}) => (edytor) => {
|
|
|
90
91
|
};
|
|
91
92
|
node.addEventListener(BLOCK_ADD_EVENT, offer);
|
|
92
93
|
node.addEventListener(BLOCK_ACTIVATE_EVENT, activate);
|
|
93
|
-
|
|
94
|
+
// Every press, WebKit's lone `mousedown` after a drag too (`onPress`).
|
|
95
|
+
const offPress = onPress(edytor, node.ownerDocument, outside, true);
|
|
94
96
|
const unmount = edytor.overlay.mount(SlashMenu, { controller, menu: options.menu, item: options.item }, 'edytor-slash-menu-host', 50, positionMenu);
|
|
95
97
|
return () => {
|
|
96
98
|
node.removeEventListener(BLOCK_ADD_EVENT, offer);
|
|
97
99
|
node.removeEventListener(BLOCK_ACTIVATE_EVENT, activate);
|
|
98
|
-
|
|
100
|
+
offPress();
|
|
99
101
|
unmount();
|
|
100
102
|
};
|
|
101
103
|
}
|
|
@@ -5,8 +5,8 @@ export type ToolbarOptions = {
|
|
|
5
5
|
/**
|
|
6
6
|
* Replace the toolbar; it renders while `controller.isVisible`, placed
|
|
7
7
|
* above the selection (mark your bar `data-edytor-toolbar-bar` if panels
|
|
8
|
-
* hang below it).
|
|
9
|
-
*
|
|
8
|
+
* hang below it). A press on it never takes the editor's focus (its
|
|
9
|
+
* fields, an input or a select, take their own).
|
|
10
10
|
*/
|
|
11
11
|
toolbar?: Snippet<[ToolbarController]>;
|
|
12
12
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import Toolbar from './Toolbar.svelte';
|
|
2
2
|
import { ToolbarController } from './ToolbarController.svelte.js';
|
|
3
|
+
/** A press in a field of the chrome (an input, a select): it takes focus. */
|
|
4
|
+
const isNativeFieldEvent = (event) => event
|
|
5
|
+
.composedPath()
|
|
6
|
+
.some((target) => target instanceof Element && target.matches('input, textarea, select'));
|
|
3
7
|
/** The selection toolbar, with your own markup through a `toolbar` snippet. */
|
|
4
8
|
export const createToolbarPlugin = (options = {}) => (edytor) => {
|
|
5
9
|
const controller = new ToolbarController(edytor);
|
|
@@ -33,7 +37,24 @@ export const createToolbarPlugin = (options = {}) => (edytor) => {
|
|
|
33
37
|
controller.updateFromSelection(selection);
|
|
34
38
|
edytor.overlay.invalidate();
|
|
35
39
|
},
|
|
36
|
-
onEdytorAttached: () =>
|
|
40
|
+
onEdytorAttached: () => {
|
|
41
|
+
const unmount = edytor.overlay.mount(Toolbar, { controller, toolbar: options.toolbar }, 'edytor-toolbar-host', 60, positionToolbar);
|
|
42
|
+
// No press on the chrome takes focus, its background and a custom
|
|
43
|
+
// snippet's markup included (not only its buttons): the editor keeps
|
|
44
|
+
// its focus and its selection. A field (the link panel's input) takes
|
|
45
|
+
// its own. Every press has a `mousedown`, WebKit's lone one too
|
|
46
|
+
// (`onFocus.ts`), so no `pointerdown` is cancelled.
|
|
47
|
+
const host = edytor.overlay.layer?.querySelector('[data-edytor-toolbar-host]');
|
|
48
|
+
const keep = (event) => {
|
|
49
|
+
if (!isNativeFieldEvent(event))
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
};
|
|
52
|
+
host?.addEventListener('mousedown', keep);
|
|
53
|
+
return () => {
|
|
54
|
+
host?.removeEventListener('mousedown', keep);
|
|
55
|
+
unmount();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
37
58
|
};
|
|
38
59
|
};
|
|
39
60
|
/** The Notion-style selection toolbar. */
|
|
@@ -48,6 +48,21 @@ export declare const flipToggles: (blocks: readonly Block[]) => void;
|
|
|
48
48
|
* (`selectedMembers`).
|
|
49
49
|
*/
|
|
50
50
|
export declare const liftLayouts: (blocks: Iterable<Block>) => Block[];
|
|
51
|
+
/**
|
|
52
|
+
* A pointer drag-selection from `anchor` (the block its press landed in) to
|
|
53
|
+
* `focus` (the block the drag reached), Notion's rule
|
|
54
|
+
* (`sel.drag.across-columns`): once the drag leaves the anchor's column for
|
|
55
|
+
* another column of the same layout, it selects blocks — every shown block
|
|
56
|
+
* from one to the other in reading order, but the layouts and columns
|
|
57
|
+
* themselves (each block a member as `selectedMembers` reads it), so a
|
|
58
|
+
* sweep over every block of every column lifts to the layout
|
|
59
|
+
* (`liftLayouts`, D3). The innermost layout holding both in different
|
|
60
|
+
* columns decides. `null` (a text range) while both lie in one column, or
|
|
61
|
+
* when no layout holding the anchor holds the focus (a drag leaving the
|
|
62
|
+
* layout, or one that starts outside it). Decided from the roles
|
|
63
|
+
* (`isLayout`/`isLayoutItem`).
|
|
64
|
+
*/
|
|
65
|
+
export declare const acrossColumns: (anchor: Block, focus: Block) => Block[] | null;
|
|
51
66
|
/** Whether a block selection holds `blocks`, a layout it covers whole counting as held (D3). */
|
|
52
67
|
export declare const holdsBlocks: (selected: Iterable<Block>, blocks: readonly Block[]) => boolean;
|
|
53
68
|
/**
|
|
@@ -91,6 +91,40 @@ export const liftLayouts = (blocks) => {
|
|
|
91
91
|
}
|
|
92
92
|
return [...all];
|
|
93
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* A pointer drag-selection from `anchor` (the block its press landed in) to
|
|
96
|
+
* `focus` (the block the drag reached), Notion's rule
|
|
97
|
+
* (`sel.drag.across-columns`): once the drag leaves the anchor's column for
|
|
98
|
+
* another column of the same layout, it selects blocks — every shown block
|
|
99
|
+
* from one to the other in reading order, but the layouts and columns
|
|
100
|
+
* themselves (each block a member as `selectedMembers` reads it), so a
|
|
101
|
+
* sweep over every block of every column lifts to the layout
|
|
102
|
+
* (`liftLayouts`, D3). The innermost layout holding both in different
|
|
103
|
+
* columns decides. `null` (a text range) while both lie in one column, or
|
|
104
|
+
* when no layout holding the anchor holds the focus (a drag leaving the
|
|
105
|
+
* layout, or one that starts outside it). Decided from the roles
|
|
106
|
+
* (`isLayout`/`isLayoutItem`).
|
|
107
|
+
*/
|
|
108
|
+
export const acrossColumns = (anchor, focus) => {
|
|
109
|
+
const { edytor } = anchor;
|
|
110
|
+
const { facade } = edytor;
|
|
111
|
+
let across = false;
|
|
112
|
+
for (let item = anchor.parent; item && !item.isRoot && !across; item = item.parent) {
|
|
113
|
+
if (!facade.isLayoutItem(item.id))
|
|
114
|
+
continue;
|
|
115
|
+
if (focus.isChildOf(item))
|
|
116
|
+
return null;
|
|
117
|
+
across = Boolean(item.parent && focus.isChildOf(item.parent));
|
|
118
|
+
}
|
|
119
|
+
if (!across)
|
|
120
|
+
return null;
|
|
121
|
+
const [first, last] = edytor.compareBlocks(anchor, focus) <= 0 ? [anchor, focus] : [focus, anchor];
|
|
122
|
+
const blocks = [];
|
|
123
|
+
for (let at = first; at && edytor.compareBlocks(at, last) <= 0; at = shown(at, 'blockAfter'))
|
|
124
|
+
if (!facade.isLayout(at.id) && !facade.isLayoutItem(at.id))
|
|
125
|
+
blocks.push(at);
|
|
126
|
+
return blocks;
|
|
127
|
+
};
|
|
94
128
|
/** Whether a block selection holds `blocks`, a layout it covers whole counting as held (D3). */
|
|
95
129
|
export const holdsBlocks = (selected, blocks) => {
|
|
96
130
|
const as = [...selected];
|
|
@@ -11,7 +11,7 @@ import { seam } from '../crdt/anchors.js';
|
|
|
11
11
|
import { getTextPath } from '../events/events.utils.js';
|
|
12
12
|
import { landed } from '../session/navigation.js';
|
|
13
13
|
import * as visibility from './visibility.js';
|
|
14
|
-
import { caretBeside, shownText } from './replaceSelection.js';
|
|
14
|
+
import { acrossColumns, caretBeside, shownText } from './replaceSelection.js';
|
|
15
15
|
/** The compatibility state of no (or an unresolvable) selection. */
|
|
16
16
|
const EMPTY_STATE = Object.freeze({
|
|
17
17
|
yStart: 0,
|
|
@@ -661,6 +661,7 @@ export class EdytorSelection {
|
|
|
661
661
|
return;
|
|
662
662
|
}
|
|
663
663
|
this.pointerDragStart = this.getTextPointFromClientPoint(event.clientX, event.clientY);
|
|
664
|
+
this.#across = false;
|
|
664
665
|
};
|
|
665
666
|
clearPointerDragStart = () => {
|
|
666
667
|
this.pointerDragStart = null;
|
|
@@ -668,13 +669,53 @@ export class EdytorSelection {
|
|
|
668
669
|
};
|
|
669
670
|
/** A normalization a pointer drag held back (the gesture is the user's, O57). */
|
|
670
671
|
#held = false;
|
|
671
|
-
/** The drag
|
|
672
|
+
/** The pointer drag selected blocks across columns (`acrossColumns`), until its release. */
|
|
673
|
+
#across = false;
|
|
674
|
+
/**
|
|
675
|
+
* The drag ended: the DOM selection it left is derived again, now
|
|
676
|
+
* normalized. A drag that ends as a block selection across columns keeps
|
|
677
|
+
* it: the native range it ignored goes (the projector shows a block
|
|
678
|
+
* selection as no range).
|
|
679
|
+
*/
|
|
672
680
|
#dropped = () => {
|
|
681
|
+
const across = this.#across;
|
|
682
|
+
this.#across = false;
|
|
683
|
+
if (across && this.value.kind === 'blocks') {
|
|
684
|
+
this.#held = false;
|
|
685
|
+
return this.display();
|
|
686
|
+
}
|
|
673
687
|
if (!this.#held)
|
|
674
688
|
return;
|
|
675
689
|
this.#held = false;
|
|
676
690
|
this.applySelectionSnapshot(getDomSelectionSnapshot(this.edytor.node));
|
|
677
691
|
};
|
|
692
|
+
/**
|
|
693
|
+
* Under a pointer drag, a native range from one column into another
|
|
694
|
+
* column of the same layout is a block selection (`acrossColumns`,
|
|
695
|
+
* Notion): the blocks it covers are selected and the native range is
|
|
696
|
+
* ignored — the browser keeps extending it from the press, so the drag
|
|
697
|
+
* coming back into its own column is a text range again. Its highlight
|
|
698
|
+
* is hidden while the value is a block selection
|
|
699
|
+
* (`data-edytor-selection`). Only a range anchored where the press
|
|
700
|
+
* landed is the drag's: a Shift+click extends the range it found, which
|
|
701
|
+
* stays a text range in document order, as the keyboard's does (D7).
|
|
702
|
+
* Answers whether it selected blocks.
|
|
703
|
+
*/
|
|
704
|
+
#dragAcross = (dom) => {
|
|
705
|
+
if (!dom?.anchorNode || !dom.focusNode || dom.isCollapsed)
|
|
706
|
+
return false;
|
|
707
|
+
const anchor = this.getTextOfNode(dom.anchorNode, dom.anchorOffset)?.parent;
|
|
708
|
+
const focus = this.getTextOfNode(dom.focusNode, dom.focusOffset)?.parent;
|
|
709
|
+
if (!anchor || anchor.id !== this.pointerDragStart?.text.parent.id)
|
|
710
|
+
return false;
|
|
711
|
+
const blocks = focus && !anchor.isRoot && acrossColumns(anchor, focus);
|
|
712
|
+
if (!blocks || !blocks.length)
|
|
713
|
+
return false;
|
|
714
|
+
this.#across = true;
|
|
715
|
+
this.select(blockSelection(blocks.map((block) => block.id)), 'dom');
|
|
716
|
+
this.edytor.projector.observe();
|
|
717
|
+
return true;
|
|
718
|
+
};
|
|
678
719
|
/**
|
|
679
720
|
* The block selection value over which the DOM caret was put — a primary
|
|
680
721
|
* pointer press in the editor, or a foreign `selectionchange` (a script's,
|
|
@@ -798,7 +839,10 @@ export class EdytorSelection {
|
|
|
798
839
|
this.applySelectionSnapshot(selection);
|
|
799
840
|
};
|
|
800
841
|
applySelectionSnapshot = (selection, options = {}) => {
|
|
801
|
-
if (this.
|
|
842
|
+
if (this.dragging && this.#dragAcross(selection))
|
|
843
|
+
return;
|
|
844
|
+
// A block selection ignores the DOM, but the one a drag across columns made while it lasts.
|
|
845
|
+
if (this.selectedBlocks.size > 0 && !(this.dragging && this.#across))
|
|
802
846
|
return;
|
|
803
847
|
const container = this.edytor.node;
|
|
804
848
|
if (!selection?.anchorNode ||
|