edytor 0.1.0-next.27 → 0.1.0-next.29
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/plugins/blockHandles/BlockHandleController.svelte.js +2 -4
- package/dist/plugins/blockHandles/dragPreview.d.ts +4 -4
- package/dist/plugins/blockHandles/dragPreview.js +4 -8
- package/dist/selection/replaceSelection.d.ts +10 -12
- package/dist/selection/replaceSelection.js +19 -21
- package/dist/selection/selection.svelte.js +19 -12
- 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.29`, `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
|
|
|
@@ -120,7 +120,7 @@ const backdrop = (document) => {
|
|
|
120
120
|
node.setAttribute('aria-hidden', 'true');
|
|
121
121
|
return node;
|
|
122
122
|
};
|
|
123
|
-
/** How many blocks move: the badge
|
|
123
|
+
/** How many blocks move: the badge beside the drop indicator. */
|
|
124
124
|
const countBadge = (document, count) => {
|
|
125
125
|
const badge = document.createElement('span');
|
|
126
126
|
badge.dataset.edytorDragCount = 'true';
|
|
@@ -367,9 +367,7 @@ export class BlockHandleController {
|
|
|
367
367
|
const root = this.edytor.node;
|
|
368
368
|
const nodes = group.flatMap((moved) => (moved.node ? [moved.node] : []));
|
|
369
369
|
if (nativeSetDragImage && root && nodes.length) {
|
|
370
|
-
const
|
|
371
|
-
const badge = count > 1 ? countBadge(element.ownerDocument, count) : undefined;
|
|
372
|
-
const preview = dragPreview(root, nodes, location.current.input, badge);
|
|
370
|
+
const preview = dragPreview(root, nodes, location.current.input);
|
|
373
371
|
setCustomNativeDragPreview({
|
|
374
372
|
nativeSetDragImage,
|
|
375
373
|
getOffset: preview.offset,
|
|
@@ -21,13 +21,13 @@ export type DragPreview = {
|
|
|
21
21
|
/**
|
|
22
22
|
* The ghost of `nodes` (the dragged blocks' elements, in document order),
|
|
23
23
|
* grabbed at `pointer`: a copy of each block laid out at its own width (the
|
|
24
|
-
* widest of them; the editor's content width when none is laid out), scaled by `SCALE` from its top left, cut at `MAX_HEIGHT
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* widest of them; the editor's content width when none is laid out), scaled by `SCALE` from its top left, cut at `MAX_HEIGHT`.
|
|
25
|
+
* Nothing is added (no card, background, shadow, opacity, fade or count
|
|
26
|
+
* badge): the blocks' own look. The pointer
|
|
27
27
|
* keeps its place on the first block (a handle is left of it: a transparent
|
|
28
28
|
* gutter keeps the offset positive, which every browser supports).
|
|
29
29
|
*/
|
|
30
30
|
export declare const dragPreview: (root: HTMLElement, nodes: HTMLElement[], pointer: {
|
|
31
31
|
clientX: number;
|
|
32
32
|
clientY: number;
|
|
33
|
-
}
|
|
33
|
+
}) => DragPreview;
|
|
@@ -158,13 +158,13 @@ const contentWidth = (root) => {
|
|
|
158
158
|
/**
|
|
159
159
|
* The ghost of `nodes` (the dragged blocks' elements, in document order),
|
|
160
160
|
* grabbed at `pointer`: a copy of each block laid out at its own width (the
|
|
161
|
-
* widest of them; the editor's content width when none is laid out), scaled by `SCALE` from its top left, cut at `MAX_HEIGHT
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* widest of them; the editor's content width when none is laid out), scaled by `SCALE` from its top left, cut at `MAX_HEIGHT`.
|
|
162
|
+
* Nothing is added (no card, background, shadow, opacity, fade or count
|
|
163
|
+
* badge): the blocks' own look. The pointer
|
|
164
164
|
* keeps its place on the first block (a handle is left of it: a transparent
|
|
165
165
|
* gutter keeps the offset positive, which every browser supports).
|
|
166
166
|
*/
|
|
167
|
-
export const dragPreview = (root, nodes, pointer
|
|
167
|
+
export const dragPreview = (root, nodes, pointer) => {
|
|
168
168
|
const { ownerDocument: document } = root;
|
|
169
169
|
const first = nodes[0]?.getBoundingClientRect();
|
|
170
170
|
const dx = first ? pointer.clientX - first.left : 0;
|
|
@@ -201,10 +201,6 @@ export const dragPreview = (root, nodes, pointer, badge) => {
|
|
|
201
201
|
overflow: 'hidden'
|
|
202
202
|
});
|
|
203
203
|
box.append(scaled);
|
|
204
|
-
if (badge) {
|
|
205
|
-
Object.assign(badge.style, { top: '4px', right: '4px' });
|
|
206
|
-
box.append(badge);
|
|
207
|
-
}
|
|
208
204
|
const element = document.createElement('div');
|
|
209
205
|
element.dataset.edytorDragPreview = 'true';
|
|
210
206
|
element.dataset.count = String(nodes.length);
|
|
@@ -49,18 +49,16 @@ export declare const flipToggles: (blocks: readonly Block[]) => void;
|
|
|
49
49
|
*/
|
|
50
50
|
export declare const liftLayouts: (blocks: Iterable<Block>) => Block[];
|
|
51
51
|
/**
|
|
52
|
-
* A pointer
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* layout, or one that starts outside it). Decided from the roles
|
|
63
|
-
* (`isLayout`/`isLayoutItem`).
|
|
52
|
+
* A pointer selection (a drag, a Shift+click) from `anchor` to `focus`,
|
|
53
|
+
* Notion's rule (`sel.drag.across-columns`): once one end is in a column
|
|
54
|
+
* and the other is not in that column (another column of its layout, or
|
|
55
|
+
* outside the layout), it selects blocks — every shown block from one to
|
|
56
|
+
* the other in reading order, but the layouts and columns themselves (each
|
|
57
|
+
* block a member as `selectedMembers` reads it), so a sweep over every
|
|
58
|
+
* block of every column lifts to the layout (`liftLayouts`, D3). `null` (a
|
|
59
|
+
* text range) while both ends lie in the same columns (one column, or none:
|
|
60
|
+
* a range that runs over a whole layout from above it to below it stays
|
|
61
|
+
* text). Decided from the roles (`isLayoutItem`).
|
|
64
62
|
*/
|
|
65
63
|
export declare const acrossColumns: (anchor: Block, focus: Block) => Block[] | null;
|
|
66
64
|
/** Whether a block selection holds `blocks`, a layout it covers whole counting as held (D3). */
|
|
@@ -92,31 +92,29 @@ export const liftLayouts = (blocks) => {
|
|
|
92
92
|
return [...all];
|
|
93
93
|
};
|
|
94
94
|
/**
|
|
95
|
-
* A pointer
|
|
96
|
-
* `
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* layout, or one that starts outside it). Decided from the roles
|
|
106
|
-
* (`isLayout`/`isLayoutItem`).
|
|
95
|
+
* A pointer selection (a drag, a Shift+click) from `anchor` to `focus`,
|
|
96
|
+
* Notion's rule (`sel.drag.across-columns`): once one end is in a column
|
|
97
|
+
* and the other is not in that column (another column of its layout, or
|
|
98
|
+
* outside the layout), it selects blocks — every shown block from one to
|
|
99
|
+
* the other in reading order, but the layouts and columns themselves (each
|
|
100
|
+
* block a member as `selectedMembers` reads it), so a sweep over every
|
|
101
|
+
* block of every column lifts to the layout (`liftLayouts`, D3). `null` (a
|
|
102
|
+
* text range) while both ends lie in the same columns (one column, or none:
|
|
103
|
+
* a range that runs over a whole layout from above it to below it stays
|
|
104
|
+
* text). Decided from the roles (`isLayoutItem`).
|
|
107
105
|
*/
|
|
108
106
|
export const acrossColumns = (anchor, focus) => {
|
|
109
107
|
const { edytor } = anchor;
|
|
110
108
|
const { facade } = edytor;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
109
|
+
const columns = (block) => {
|
|
110
|
+
const items = new Set();
|
|
111
|
+
for (let at = block.parent; at && !at.isRoot; at = at.parent)
|
|
112
|
+
if (facade.isLayoutItem(at.id))
|
|
113
|
+
items.add(at.id);
|
|
114
|
+
return items;
|
|
115
|
+
};
|
|
116
|
+
const [from, to] = [columns(anchor), columns(focus)];
|
|
117
|
+
if (from.size === to.size && [...from].every((id) => to.has(id)))
|
|
120
118
|
return null;
|
|
121
119
|
const [first, last] = edytor.compareBlocks(anchor, focus) <= 0 ? [anchor, focus] : [focus, anchor];
|
|
122
120
|
const blocks = [];
|
|
@@ -662,6 +662,7 @@ export class EdytorSelection {
|
|
|
662
662
|
}
|
|
663
663
|
this.pointerDragStart = this.getTextPointFromClientPoint(event.clientX, event.clientY);
|
|
664
664
|
this.#across = false;
|
|
665
|
+
this.#extending = event.shiftKey;
|
|
665
666
|
};
|
|
666
667
|
clearPointerDragStart = () => {
|
|
667
668
|
this.pointerDragStart = null;
|
|
@@ -671,6 +672,8 @@ export class EdytorSelection {
|
|
|
671
672
|
#held = false;
|
|
672
673
|
/** The pointer drag selected blocks across columns (`acrossColumns`), until its release. */
|
|
673
674
|
#across = false;
|
|
675
|
+
/** The press held Shift: it extends the range it found (a Shift+click). */
|
|
676
|
+
#extending = false;
|
|
674
677
|
/**
|
|
675
678
|
* The drag ended: the DOM selection it left is derived again, now
|
|
676
679
|
* normalized. A drag that ends as a block selection across columns keeps
|
|
@@ -678,6 +681,11 @@ export class EdytorSelection {
|
|
|
678
681
|
* selection as no range).
|
|
679
682
|
*/
|
|
680
683
|
#dropped = () => {
|
|
684
|
+
// A Shift+press's range can reach the adopter only after its release
|
|
685
|
+
// (Chromium queues its `selectionchange`): read here, as the press's.
|
|
686
|
+
if (this.#extending && !this.#across)
|
|
687
|
+
this.#dragAcross(getDomSelectionSnapshot(this.edytor.node));
|
|
688
|
+
this.#extending = false;
|
|
681
689
|
const across = this.#across;
|
|
682
690
|
this.#across = false;
|
|
683
691
|
if (across && this.value.kind === 'blocks') {
|
|
@@ -690,25 +698,24 @@ export class EdytorSelection {
|
|
|
690
698
|
this.applySelectionSnapshot(getDomSelectionSnapshot(this.edytor.node));
|
|
691
699
|
};
|
|
692
700
|
/**
|
|
693
|
-
* Under a pointer drag, a
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* Answers whether it selected blocks.
|
|
701
|
+
* Under a pointer press (a drag, or a Shift+click extending the range it
|
|
702
|
+
* found), a native range with one end in a column and the other outside
|
|
703
|
+
* that column (another column, or outside the layout) is a block
|
|
704
|
+
* selection (`acrossColumns`, Notion): the blocks it covers are selected
|
|
705
|
+
* and the native range is ignored — the browser keeps extending it, so
|
|
706
|
+
* the drag coming back into the anchor's columns is a text range again.
|
|
707
|
+
* Its highlight is hidden while the value is a block selection
|
|
708
|
+
* (`data-edytor-selection`). The keyboard's ranges stay text ranges in
|
|
709
|
+
* document order (D7). Answers whether it selected blocks.
|
|
703
710
|
*/
|
|
704
711
|
#dragAcross = (dom) => {
|
|
705
712
|
if (!dom?.anchorNode || !dom.focusNode || dom.isCollapsed)
|
|
706
713
|
return false;
|
|
707
714
|
const anchor = this.getTextOfNode(dom.anchorNode, dom.anchorOffset)?.parent;
|
|
708
715
|
const focus = this.getTextOfNode(dom.focusNode, dom.focusOffset)?.parent;
|
|
709
|
-
if (!anchor
|
|
716
|
+
if (!anchor)
|
|
710
717
|
return false;
|
|
711
|
-
const blocks = focus && !anchor.isRoot && acrossColumns(anchor, focus);
|
|
718
|
+
const blocks = focus && !anchor.isRoot && !focus.isRoot && acrossColumns(anchor, focus);
|
|
712
719
|
if (!blocks || !blocks.length)
|
|
713
720
|
return false;
|
|
714
721
|
this.#across = true;
|