edytor 1.0.0 → 2.0.0
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 +14 -72
- package/dist/block/block.svelte.d.ts +6 -4
- package/dist/block/block.svelte.js +10 -4
- package/dist/block/inlineBlock.svelte.d.ts +6 -2
- package/dist/block/inlineBlock.svelte.js +7 -2
- package/dist/cloudflare/room/context.d.ts +6 -0
- package/dist/collaboration/awarenessSelection.d.ts +22 -1
- package/dist/collaboration/awarenessSelection.js +42 -0
- package/dist/components/Block.svelte +3 -1
- package/dist/components/Edytor.svelte +36 -10
- package/dist/components/Edytor.svelte.d.ts +8 -1
- package/dist/components/InlineBlock.svelte +1 -1
- package/dist/crdt/data.d.ts +10 -0
- package/dist/crdt/data.js +20 -2
- package/dist/crdt/doc/reads.d.ts +7 -0
- package/dist/crdt/doc/reads.js +11 -1
- package/dist/crdt/document.d.ts +8 -0
- package/dist/crdt/document.js +12 -0
- package/dist/crdt/edytor-doc.d.ts +18 -0
- package/dist/crdt/edytor-doc.js +2 -1
- package/dist/crdt/index.d.ts +6 -0
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/operations.js +1 -0
- package/dist/crdt/props.d.ts +32 -0
- package/dist/crdt/props.js +172 -0
- package/dist/{plugins/blockHandles → dnd}/dropIndicator.d.ts +2 -2
- package/dist/{plugins/blockHandles → dnd}/geometry.d.ts +2 -2
- package/dist/dnd/placement.d.ts +200 -0
- package/dist/dnd/placement.js +426 -0
- package/dist/edytor.svelte.d.ts +17 -4
- package/dist/edytor.svelte.js +16 -7
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +8 -4
- package/dist/events/onDrop.d.ts +6 -0
- package/dist/events/onDrop.js +38 -10
- package/dist/events/onPaste.d.ts +2 -1
- package/dist/events/onPaste.js +8 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +2 -0
- package/dist/labels.js +2 -1
- package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
- package/dist/plugins/arrowMove/arrowMove.js +1 -1
- package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +2 -122
- package/dist/plugins/blockHandles/BlockHandleController.svelte.js +29 -387
- package/dist/plugins/blockHandles/BlockHandles.svelte +14 -32
- package/dist/plugins/blockHandles/blockHandlesPlugin.js +2 -1
- package/dist/plugins/code/CodeHeader.svelte +5 -3
- package/dist/plugins/code/CodePlugin.svelte +3 -3
- package/dist/plugins/code/languages.d.ts +0 -3
- package/dist/plugins/code/languages.js +0 -2
- package/dist/plugins/columns/ColumnResize.svelte +3 -2
- package/dist/plugins/columns/ColumnsPlugin.svelte +1 -1
- package/dist/plugins/columns/gaps.d.ts +7 -5
- package/dist/plugins/columns/gaps.js +11 -6
- package/dist/plugins/columns/resize.svelte.d.ts +3 -3
- package/dist/plugins/columns/resize.svelte.js +11 -10
- package/dist/plugins/equation/EquationPlugin.svelte +1 -8
- package/dist/plugins/equation/EquationPlugin.svelte.d.ts +1 -0
- package/dist/plugins/equation/equation.css +7 -0
- package/dist/plugins/fileDrop.d.ts +28 -0
- package/dist/plugins/fileDrop.js +116 -0
- package/dist/plugins/image/ImagePlugin.svelte +42 -33
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +13 -1
- package/dist/plugins/image/image.css +17 -0
- package/dist/plugins/index.d.ts +3 -4
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/markdownShortcuts.js +5 -5
- package/dist/plugins/media/AudioPlugin.svelte +13 -1
- package/dist/plugins/media/AudioPlugin.svelte.d.ts +11 -1
- package/dist/plugins/media/FilePlugin.svelte +11 -1
- package/dist/plugins/media/FilePlugin.svelte.d.ts +9 -0
- package/dist/plugins/media/SourceBlock.svelte +48 -12
- package/dist/plugins/media/SourceBlock.svelte.d.ts +4 -3
- package/dist/plugins/media/UploadPreview.svelte +32 -0
- package/dist/plugins/media/UploadPreview.svelte.d.ts +11 -0
- package/dist/plugins/media/VideoPlugin.svelte +13 -1
- package/dist/plugins/media/VideoPlugin.svelte.d.ts +11 -1
- package/dist/plugins/media/empty.svelte.d.ts +13 -4
- package/dist/plugins/media/empty.svelte.js +30 -19
- package/dist/plugins/media/media.d.ts +7 -0
- package/dist/plugins/media/upload.css +23 -0
- package/dist/plugins/mention/MentionPlugin.svelte +72 -33
- package/dist/plugins/mention/MentionPlugin.svelte.d.ts +11 -2
- package/dist/plugins/mention/mention.css +6 -0
- package/dist/plugins/pageLink/PageLinkPlugin.svelte +122 -17
- package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +13 -1
- package/dist/plugins/pageLink/pageLink.css +6 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +14 -7
- package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +1 -0
- package/dist/plugins/richtext/richtext.css +6 -0
- package/dist/plugins/table/chrome.svelte.js +4 -3
- package/dist/plugins/uploads.svelte.d.ts +45 -32
- package/dist/plugins/uploads.svelte.js +105 -39
- package/dist/plugins.d.ts +40 -14
- package/dist/session/attempt.d.ts +16 -6
- package/dist/session/attempt.js +1 -0
- package/dist/session/bindings.js +34 -31
- package/dist/session/commands.js +2 -0
- package/dist/session/history.js +1 -0
- package/dist/session/inputRules.d.ts +6 -3
- package/dist/session/inputRules.js +9 -5
- package/dist/session/moves.d.ts +33 -0
- package/dist/session/moves.js +71 -1
- package/dist/session/navigation.d.ts +7 -0
- package/dist/session/navigation.js +19 -10
- package/dist/session/props.d.ts +28 -0
- package/dist/session/props.js +65 -0
- package/dist/session/uploads.d.ts +29 -0
- package/dist/session/uploads.js +10 -0
- package/dist/themes/notion.css +9 -0
- package/package.json +1 -1
- /package/dist/{plugins/blockHandles → dnd}/dropIndicator.js +0 -0
- /package/dist/{plugins/blockHandles → dnd}/geometry.js +0 -0
- /package/dist/{plugins/columns → dnd}/stacking.d.ts +0 -0
- /package/dist/{plugins/columns → dnd}/stacking.js +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { HIDDEN, hidden } from '../selection/visibility.js';
|
|
2
|
+
import { dropReach, getOwnRowBottom, isBeside, marginAt, nestIndent, overEmptyBody, ownRow, ownTextRow } from './geometry.js';
|
|
3
|
+
import { stacks } from './stacking.js';
|
|
4
|
+
/** How far beside a target's box it stays the drop target (`getIsSticky`), across and along. */
|
|
5
|
+
export const SLOP_X = 20;
|
|
6
|
+
export const SLOP_Y = 24;
|
|
7
|
+
/** The right beside band: the row's last 15% (Notion), at least this many px. */
|
|
8
|
+
const RIGHT_BAND = 0.15;
|
|
9
|
+
const RIGHT_BAND_MIN = 32;
|
|
10
|
+
/** The handle column's width when the drag's handle cannot be measured (`+`, grip and gap). */
|
|
11
|
+
const HANDLE_COLUMN = 50;
|
|
12
|
+
/** Where a block's own text starts: its level's column. */
|
|
13
|
+
const column = (block) => ownTextRow(block.node).left;
|
|
14
|
+
/**
|
|
15
|
+
* A block a drop may land at (a drop target): a movable block, not a
|
|
16
|
+
* table's row (its row menus move it) nor a cell.
|
|
17
|
+
*/
|
|
18
|
+
export const isDropTarget = (edytor, block) => block.movable && !edytor.facade.isTableRow(block.id);
|
|
19
|
+
export class DropResolver {
|
|
20
|
+
edytor;
|
|
21
|
+
ctx;
|
|
22
|
+
constructor(edytor, ctx) {
|
|
23
|
+
this.edytor = edytor;
|
|
24
|
+
this.ctx = ctx;
|
|
25
|
+
}
|
|
26
|
+
targetOf = (node) => this.ctx.targetOf(node);
|
|
27
|
+
isTarget = (node) => this.ctx.targetOf(node) !== undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The placement over `row` at the pointer: a beside band of the row first
|
|
30
|
+
* (`beside`; `false` when the document refuses it: nothing shows), else
|
|
31
|
+
* the row's halves, the top one first when `before` (the pointer's half,
|
|
32
|
+
* or the one a sticky target holds).
|
|
33
|
+
*/
|
|
34
|
+
at(drag, row, input, before) {
|
|
35
|
+
const aside = this.beside(drag, row, input);
|
|
36
|
+
if (aside !== undefined)
|
|
37
|
+
return aside ?? false;
|
|
38
|
+
const { before: top, after } = this.zones(drag, row, input);
|
|
39
|
+
return this.pick(drag, before ? [top, after] : [after, top]);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The drop target under the pointer for a drag the host's own events see
|
|
43
|
+
* (a file's): the innermost target element at the point, `null` outside
|
|
44
|
+
* the blocks and in the handle column (the margins answer there,
|
|
45
|
+
* `margin`).
|
|
46
|
+
*/
|
|
47
|
+
hitAt(input) {
|
|
48
|
+
const root = this.edytor.node;
|
|
49
|
+
if (!root || this.inHandleColumn(input.clientX))
|
|
50
|
+
return null;
|
|
51
|
+
for (const hit of root.ownerDocument.elementsFromPoint?.(input.clientX, input.clientY) ?? [])
|
|
52
|
+
for (let at = hit.closest('[data-edytor-block="true"]'); at && root.contains(at); at = at.parentElement?.closest('[data-edytor-block="true"]') ?? null)
|
|
53
|
+
if (this.isTarget(at))
|
|
54
|
+
return at;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** The placement over the row of drop target `node` at the pointer (`rowAt`, its own row's half: `at`). */
|
|
58
|
+
over(drag, node, input) {
|
|
59
|
+
const target = this.targetOf(node);
|
|
60
|
+
if (!target)
|
|
61
|
+
return null;
|
|
62
|
+
const row = this.rowAt(drag, target, node, input);
|
|
63
|
+
if (!row.node)
|
|
64
|
+
return null;
|
|
65
|
+
const rect = ownRow(row.node);
|
|
66
|
+
return this.at(drag, row, input, input.clientY < rect.top + rect.height / 2);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The editor's content column (`start`…`end`, the root's content box:
|
|
70
|
+
* where its blocks' boxes start and end), the width of the handle column
|
|
71
|
+
* left of it (the drag's handle, measured: `+`, grip and the gap to the
|
|
72
|
+
* block) and how far the drag reaches past them (`dropReach`, the
|
|
73
|
+
* `--edytor-drop-reach` the root inherits).
|
|
74
|
+
*/
|
|
75
|
+
gutter(root) {
|
|
76
|
+
const style = root.ownerDocument.defaultView?.getComputedStyle(root);
|
|
77
|
+
const rect = root.getBoundingClientRect();
|
|
78
|
+
const px = (value) => parseFloat(value ?? '') || 0;
|
|
79
|
+
const start = rect.left + root.clientLeft + px(style?.paddingLeft);
|
|
80
|
+
const end = rect.right - px(style?.borderRightWidth) - px(style?.paddingRight);
|
|
81
|
+
const width = this.ctx.handle()?.isConnected
|
|
82
|
+
? this.ctx.handle().getBoundingClientRect().width
|
|
83
|
+
: 0;
|
|
84
|
+
return { start, end, handles: width > 0 ? width : HANDLE_COLUMN, reach: dropReach(style) };
|
|
85
|
+
}
|
|
86
|
+
/** Whether `x` is in the handle column left of the editor's blocks (`gutter`): the page target's. */
|
|
87
|
+
inHandleColumn(x) {
|
|
88
|
+
const root = this.edytor.node;
|
|
89
|
+
return !!root && marginAt(x, this.gutter(root))?.zone === 'handles';
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A placement in the editor's margins at the pointer (`dnd.reach`), within
|
|
93
|
+
* the editor's height, for the row the content's near edge shows at the
|
|
94
|
+
* pointer's height (`rowAt`: a nested block's own row; over a layout, the
|
|
95
|
+
* row of its first column on the left, of its last on the right, never
|
|
96
|
+
* another), by the zone the pointer is in (`marginAt`):
|
|
97
|
+
* - the handle column left of the blocks (`gutter`): that row's reorder
|
|
98
|
+
* (`reorder`): a drag straight down the handles reorders, row after
|
|
99
|
+
* row, gaps included (never a beside band);
|
|
100
|
+
* - the first `BESIDE_MARGIN` px past it on the left, and past the
|
|
101
|
+
* content's right edge: the beside band of that side (`beside`, as if
|
|
102
|
+
* the pointer were at that edge) when the document has a layout kind
|
|
103
|
+
* and the row offers one (a refused band shows nothing);
|
|
104
|
+
* - elsewhere within the reach (and there without a band): the reorder,
|
|
105
|
+
* on the left the handle column's, on the right the one the row's
|
|
106
|
+
* right edge offers (its halves as just inside the content, so nothing
|
|
107
|
+
* jumps as the pointer leaves it).
|
|
108
|
+
* `undefined` past the reach, above or below the editor, and inside the
|
|
109
|
+
* content (the blocks' own targets answer there).
|
|
110
|
+
*/
|
|
111
|
+
margin(drag, { clientX: x, clientY: y }) {
|
|
112
|
+
const root = this.edytor.node;
|
|
113
|
+
if (!root)
|
|
114
|
+
return undefined;
|
|
115
|
+
const box = root.getBoundingClientRect();
|
|
116
|
+
if (y < box.top || y > box.bottom)
|
|
117
|
+
return undefined;
|
|
118
|
+
const gutter = this.gutter(root);
|
|
119
|
+
const zone = marginAt(x, gutter);
|
|
120
|
+
if (!zone)
|
|
121
|
+
return undefined;
|
|
122
|
+
const right = zone.side === 'right';
|
|
123
|
+
const band = zone.zone === 'beside' && this.layouts;
|
|
124
|
+
const edge = right ? gutter.end - 1 : gutter.start + 1;
|
|
125
|
+
const hits = root.ownerDocument.elementsFromPoint?.(edge, y) ?? [];
|
|
126
|
+
const hit = hits.find((at) => {
|
|
127
|
+
const block = at.closest('[data-edytor-block="true"]');
|
|
128
|
+
return block && root.contains(block) && this.isTarget(block);
|
|
129
|
+
});
|
|
130
|
+
const node = hit?.closest('[data-edytor-block="true"]');
|
|
131
|
+
const target = node && this.targetOf(node);
|
|
132
|
+
// Between two rows where the margin reorders (a margin, a layout's
|
|
133
|
+
// edge), the reorder shown stays, as a block's sticky slop keeps it.
|
|
134
|
+
const shown = !band && this.ctx.indicator.target === root.ownerDocument.body;
|
|
135
|
+
const held = shown ? this.ctx.indicator.placement : null;
|
|
136
|
+
const stay = held && !isBeside(held.position) ? held : undefined;
|
|
137
|
+
if (!node || !target)
|
|
138
|
+
return stay;
|
|
139
|
+
const { facade } = this.edytor;
|
|
140
|
+
let row = this.rowAt(drag, target, node, { clientX: edge, clientY: y });
|
|
141
|
+
// Over a layout's edge, its first or last column's row at that height.
|
|
142
|
+
if (facade.isLayout(row.id)) {
|
|
143
|
+
const items = row.children.filter((item) => item.node && this.isTarget(item.node));
|
|
144
|
+
const rect = (right ? items.at(-1) : items[0])?.node?.getBoundingClientRect();
|
|
145
|
+
if (!rect || rect.width === 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
const inner = right ? rect.right - 1 : rect.left + 1;
|
|
148
|
+
row = this.rowAt(drag, target, node, { clientX: inner, clientY: y });
|
|
149
|
+
}
|
|
150
|
+
if (facade.isLayout(row.id) || !row.node)
|
|
151
|
+
return stay;
|
|
152
|
+
if (band) {
|
|
153
|
+
const aside = this.beside(drag, row, { clientX: right ? Infinity : -Infinity, clientY: y }, zone.side);
|
|
154
|
+
// A band the document refuses shows nothing; a row offering none (a
|
|
155
|
+
// nested row on the left, a layout that would stack) reorders.
|
|
156
|
+
if (aside !== undefined)
|
|
157
|
+
return aside ?? undefined;
|
|
158
|
+
}
|
|
159
|
+
const rect = ownRow(row.node);
|
|
160
|
+
if (right) {
|
|
161
|
+
const at = { clientX: Math.min(x, rect.right - 1), clientY: y };
|
|
162
|
+
return this.pick(drag, this.halves(drag, row, at, rect)) ?? undefined;
|
|
163
|
+
}
|
|
164
|
+
return this.reorder(drag, row, { clientX: x, clientY: y }) ?? undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The halves of `row` (`zones`) at the pointer, the one it is in first:
|
|
168
|
+
* the top half of `rect` (its own row by default) before, the bottom one
|
|
169
|
+
* after; with `plain`, only before or after a block — never inside one.
|
|
170
|
+
*/
|
|
171
|
+
halves(drag, row, input, rect, plain = false) {
|
|
172
|
+
const zones = this.zones(drag, row, input);
|
|
173
|
+
const [before, after] = [zones.before, zones.after].map((half) => plain ? half.filter((placement) => placement.position !== 'inside') : half);
|
|
174
|
+
return input.clientY < rect.top + rect.height / 2 ? [before, after] : [after, before];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The first placement of `halves` the document takes (a refused one gives
|
|
178
|
+
* way to the next, then to the other half's), `blocked` when the first
|
|
179
|
+
* half took none; `null` when none fits.
|
|
180
|
+
*/
|
|
181
|
+
pick(drag, halves) {
|
|
182
|
+
const fits = drag.fits;
|
|
183
|
+
const placement = halves.flat().find(fits);
|
|
184
|
+
return placement ? { ...placement, blocked: !halves[0].some(fits) } : null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The handle column's reorder over `row` at the pointer: its plain halves
|
|
188
|
+
* (`halves`, the pointer's x picking the level); `null` when neither half
|
|
189
|
+
* fits. Over an item of a container the dragged blocks do not fit in (a
|
|
190
|
+
* paragraph over a list's item: the document refuses every placement at
|
|
191
|
+
* the item's level), the container is the row: its whole box, the upper
|
|
192
|
+
* half before it, the lower half after it (round 4), so a drag down the
|
|
193
|
+
* handles meets the list as one row it can pass.
|
|
194
|
+
*/
|
|
195
|
+
reorder(drag, row, input) {
|
|
196
|
+
const fits = drag.fits;
|
|
197
|
+
let halves = this.halves(drag, row, input, ownRow(row.node), true);
|
|
198
|
+
for (let at = row.parent; !halves[0].some(fits) && at?.node && !at.isRoot && at.isContainer; at = at.parent)
|
|
199
|
+
halves = this.halves(drag, at, input, at.node.getBoundingClientRect(), true);
|
|
200
|
+
return this.pick(drag, halves);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The block whose row the pointer is on over `node`: the last shown drop
|
|
204
|
+
* target in it whose row starts at or above the pointer (beside a nested
|
|
205
|
+
* block, over its ancestors' indent, the pointer is on that block's
|
|
206
|
+
* row), else `target`, over its own row (`emptied`). A layout's columns
|
|
207
|
+
* sit side by side: only the one under the pointer is measured (between
|
|
208
|
+
* two of them, the layout is the row: its gap).
|
|
209
|
+
*/
|
|
210
|
+
rowAt(drag, target, node, { clientX, clientY }) {
|
|
211
|
+
const lift = (row) => this.emptied(drag, row);
|
|
212
|
+
if (clientY < getOwnRowBottom(node))
|
|
213
|
+
return lift(target);
|
|
214
|
+
const { facade } = this.edytor;
|
|
215
|
+
let row = target;
|
|
216
|
+
let aside = null;
|
|
217
|
+
// In document order, rows start lower and lower (within a column): measure
|
|
218
|
+
// none below the pointer, none a closed toggle hides (each read would force
|
|
219
|
+
// a layout), and nothing in a column the pointer is not over.
|
|
220
|
+
for (const inner of node.querySelectorAll('[data-edytor-block="true"]')) {
|
|
221
|
+
if (aside?.contains(inner))
|
|
222
|
+
continue;
|
|
223
|
+
const block = this.targetOf(inner);
|
|
224
|
+
if (!block || inner.closest(HIDDEN))
|
|
225
|
+
continue;
|
|
226
|
+
const rect = inner.getBoundingClientRect();
|
|
227
|
+
if (facade.isLayoutItem(block.id) && (clientX < rect.left || clientX > rect.right)) {
|
|
228
|
+
aside = inner;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (rect.top > clientY)
|
|
232
|
+
break;
|
|
233
|
+
if (rect.height > 0)
|
|
234
|
+
row = block;
|
|
235
|
+
}
|
|
236
|
+
return lift(row);
|
|
237
|
+
}
|
|
238
|
+
/** Whether the document has a layout kind: only then are there beside placements. */
|
|
239
|
+
get layouts() {
|
|
240
|
+
for (const definition of this.edytor.blocks.values())
|
|
241
|
+
if (definition.layout)
|
|
242
|
+
return true;
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The block a beside placement over `block` stands beside: the document's
|
|
247
|
+
* own resolution (`facade.besideAt`, `placeBeside`'s): a list item's list,
|
|
248
|
+
* a code line's code block, a toggle's child itself, an item itself.
|
|
249
|
+
*/
|
|
250
|
+
besideOf(block) {
|
|
251
|
+
return this.edytor.idToBlock.get(this.edytor.facade.besideAt(block.id)) ?? block;
|
|
252
|
+
}
|
|
253
|
+
/** The item (and its layout) a beside placement at `outer` (`besideOf`) adds a column beside. */
|
|
254
|
+
layoutOf(outer) {
|
|
255
|
+
const { facade } = this.edytor;
|
|
256
|
+
const item = facade.isLayoutItem(outer.id)
|
|
257
|
+
? outer
|
|
258
|
+
: outer.parent && !outer.parent.isRoot && facade.isLayoutItem(outer.parent.id)
|
|
259
|
+
? outer.parent
|
|
260
|
+
: null;
|
|
261
|
+
return item ? { item, layout: item.parent ?? null } : null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether `x` is over a column other than one holding `block`: a sticky
|
|
265
|
+
* target never holds the pointer across a column boundary.
|
|
266
|
+
*/
|
|
267
|
+
overOtherColumn(block, x) {
|
|
268
|
+
const { facade } = this.edytor;
|
|
269
|
+
for (let at = block; at && !at.isRoot; at = at.parent) {
|
|
270
|
+
if (!facade.isLayoutItem(at.id))
|
|
271
|
+
continue;
|
|
272
|
+
const item = at;
|
|
273
|
+
const over = item.parent?.children.some((other) => {
|
|
274
|
+
const rect = other !== item ? other.node?.getBoundingClientRect() : undefined;
|
|
275
|
+
return rect !== undefined && rect.width > 0 && x >= rect.left && x <= rect.right;
|
|
276
|
+
});
|
|
277
|
+
if (over)
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Notion's beside bands over `row` (`docs/columns-plan.md`, "View layer"), only while the
|
|
284
|
+
* pointer is within its own row's height, the document has a layout kind
|
|
285
|
+
* and the layout the drop makes or grows does not stack (`stacks`):
|
|
286
|
+
* - right: the row's last 15% (at least 32px), and the slop past it;
|
|
287
|
+
* - left: the slop left of the row (its sticky margin, left of its text
|
|
288
|
+
* column), only for a row whose parent is the root or a column (a
|
|
289
|
+
* list's item: its list's) — a nested row keeps the pointer's x for its
|
|
290
|
+
* levels (`zones`) — and only
|
|
291
|
+
* inside the editor (a column's gap): left of the editor's blocks is the
|
|
292
|
+
* handle column, which reorders (`margin`);
|
|
293
|
+
* - over a layout's gap (the row is the layout): right of the column
|
|
294
|
+
* before it, a new column between.
|
|
295
|
+
* `undefined` outside the bands (the hitbox's halves stand), else the
|
|
296
|
+
* placement, or `null` when the document refuses it: a refused band shows
|
|
297
|
+
* nothing.
|
|
298
|
+
*/
|
|
299
|
+
beside(drag, row, { clientX: x, clientY: y }, band) {
|
|
300
|
+
if (!row.node || !this.layouts)
|
|
301
|
+
return undefined;
|
|
302
|
+
if (this.carrier(drag, row))
|
|
303
|
+
return undefined;
|
|
304
|
+
const { facade } = this.edytor;
|
|
305
|
+
let placement;
|
|
306
|
+
if (facade.isLayout(row.id)) {
|
|
307
|
+
const rect = row.node.getBoundingClientRect();
|
|
308
|
+
if (y < rect.top || y > rect.bottom || stacks(rect.width))
|
|
309
|
+
return undefined;
|
|
310
|
+
const items = row.children.filter((item) => item.node && this.isTarget(item.node));
|
|
311
|
+
for (const [index, item] of items.entries()) {
|
|
312
|
+
const next = items[index + 1]?.node?.getBoundingClientRect();
|
|
313
|
+
const own = item.node.getBoundingClientRect();
|
|
314
|
+
if (next && x >= own.right && x <= next.left)
|
|
315
|
+
placement = { target: item, node: item.node, position: 'right' };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const rect = ownRow(row.node);
|
|
320
|
+
if (y < rect.top || y > rect.bottom)
|
|
321
|
+
return undefined;
|
|
322
|
+
const outer = this.besideOf(row);
|
|
323
|
+
const width = (this.layoutOf(outer)?.layout ?? outer).node?.getBoundingClientRect().width;
|
|
324
|
+
if (width === undefined || stacks(width))
|
|
325
|
+
return undefined;
|
|
326
|
+
// A container's item (a list's) stands at its container's level, as the
|
|
327
|
+
// document resolves it (`besideAt`): left of a root list's item is left of the list.
|
|
328
|
+
let at = row;
|
|
329
|
+
while (at.parent &&
|
|
330
|
+
!at.parent.isRoot &&
|
|
331
|
+
at.parent.isContainer &&
|
|
332
|
+
!facade.isLayoutItem(at.parent.id))
|
|
333
|
+
at = at.parent;
|
|
334
|
+
const parent = at.parent;
|
|
335
|
+
const level = !parent || parent.isRoot || facade.isLayoutItem(parent.id);
|
|
336
|
+
if (band === 'right' ||
|
|
337
|
+
(!band && x >= rect.right - Math.max(RIGHT_BAND_MIN, rect.width * RIGHT_BAND)))
|
|
338
|
+
placement = { target: row, node: row.node, position: 'right' };
|
|
339
|
+
else if (level &&
|
|
340
|
+
(band === 'left' ||
|
|
341
|
+
(!band && x < rect.left && x >= rect.left - SLOP_X && x >= this.inside(rect.left))))
|
|
342
|
+
placement = { target: row, node: row.node, position: 'left' };
|
|
343
|
+
}
|
|
344
|
+
if (!placement)
|
|
345
|
+
return undefined;
|
|
346
|
+
return drag.fits(placement) ? placement : null;
|
|
347
|
+
}
|
|
348
|
+
/** Where the editor's blocks start (`gutter`), or `fallback` without a host. */
|
|
349
|
+
inside(fallback) {
|
|
350
|
+
const root = this.edytor.node;
|
|
351
|
+
return root ? this.gutter(root).start : fallback;
|
|
352
|
+
}
|
|
353
|
+
/** The children a block shows: none for a closed toggle's, or a container's items. */
|
|
354
|
+
shown(block) {
|
|
355
|
+
return block.isContainer
|
|
356
|
+
? []
|
|
357
|
+
: block.children.filter((child) => child.node && this.isTarget(child.node) && !hidden(child));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Over a dragged block, the parent it leaves when that parent shows no
|
|
361
|
+
* other child: the gap under the parent's own row, which reparents as
|
|
362
|
+
* the parent's lower half does (`zones`). Else `row`.
|
|
363
|
+
*/
|
|
364
|
+
emptied(drag, row) {
|
|
365
|
+
const group = drag.group;
|
|
366
|
+
const parent = this.carrier(drag, row)?.parent;
|
|
367
|
+
if (!parent?.node || parent.isRoot || !this.isTarget(parent.node))
|
|
368
|
+
return row;
|
|
369
|
+
const shown = this.shown(parent);
|
|
370
|
+
return shown.length && shown.every((child) => group.includes(child)) ? parent : row;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The placements over `row`, each half's in preference order (a refused
|
|
374
|
+
* one gives way to the next, then to the other half's):
|
|
375
|
+
* - the top half: before it;
|
|
376
|
+
* - the bottom half of a block whose children show (not a closed toggle):
|
|
377
|
+
* its first child's slot. Pragmatic drag and drop's tree rule: an
|
|
378
|
+
* expanded item offers no "after" on its own row;
|
|
379
|
+
* - else, after it at the level whose text column is nearest the pointer:
|
|
380
|
+
* the last block of a nested group reparents to any ancestor it ends,
|
|
381
|
+
* down to the root. Past one nesting step right of its text start,
|
|
382
|
+
* inside it (its last child) first. A block every shown child of which
|
|
383
|
+
* is dragged is not expanded: its levels, never inside it when the drag
|
|
384
|
+
* moves only its children (they would stay where they are).
|
|
385
|
+
* None over a dragged block's own row or its subtree: released there, the
|
|
386
|
+
* drag changes nothing (its levels would outdent it) — unless it empties
|
|
387
|
+
* its parent's shown children (`emptied`): that parent's row. Over the
|
|
388
|
+
* hint of the row's empty body (`body.hint`: an open toggle's, a
|
|
389
|
+
* callout's), inside it first, either half.
|
|
390
|
+
*/
|
|
391
|
+
zones(drag, row, { clientX, clientY }) {
|
|
392
|
+
if (this.carrier(drag, row))
|
|
393
|
+
return { before: [], after: [] };
|
|
394
|
+
const group = drag.group;
|
|
395
|
+
const at = (target, position) => target?.node ? [{ target, node: target.node, position }] : [];
|
|
396
|
+
const before = at(row, 'before');
|
|
397
|
+
const shown = this.shown(row);
|
|
398
|
+
const first = shown.find((child) => !group.includes(child));
|
|
399
|
+
if (first)
|
|
400
|
+
return { before, after: at(first, 'before') };
|
|
401
|
+
// A group moved out from under its parent leaves the block before it last.
|
|
402
|
+
const next = (block) => {
|
|
403
|
+
let next = block.nextBlock;
|
|
404
|
+
while (next && group.includes(next))
|
|
405
|
+
next = next.nextBlock;
|
|
406
|
+
return next;
|
|
407
|
+
};
|
|
408
|
+
const levels = [row];
|
|
409
|
+
for (let level = row; !next(level) && level.parent?.node && !level.parent.isRoot;)
|
|
410
|
+
levels.push((level = level.parent));
|
|
411
|
+
const distance = (level) => Math.abs(column(level) - clientX);
|
|
412
|
+
const after = levels
|
|
413
|
+
.sort((a, b) => distance(a) - distance(b))
|
|
414
|
+
.flatMap((level) => at(level, 'after'));
|
|
415
|
+
const stays = shown.length > 0 && group.every((moved) => moved.parent === row);
|
|
416
|
+
const inside = stays ? [] : at(row, 'inside');
|
|
417
|
+
if (overEmptyBody(row.node, clientY))
|
|
418
|
+
return { before: [...inside, ...before], after: [...inside, ...after] };
|
|
419
|
+
const nests = clientX > column(row) + nestIndent(row.node);
|
|
420
|
+
return { before, after: nests ? [...inside, ...after] : [...after, ...inside] };
|
|
421
|
+
}
|
|
422
|
+
/** The dragged block that is `row` or holds it: its row offers nothing (it moves with the drag). */
|
|
423
|
+
carrier(drag, row) {
|
|
424
|
+
return drag.group.find((moved) => moved === row || row.isChildOf(moved));
|
|
425
|
+
}
|
|
426
|
+
}
|
package/dist/edytor.svelte.d.ts
CHANGED
|
@@ -18,6 +18,15 @@ import { Dispatcher } from './session/commands.js';
|
|
|
18
18
|
import { type KindRow } from './kinds.js';
|
|
19
19
|
export type EdytorOptions = {
|
|
20
20
|
readonly?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Let users nest blocks under other blocks (default `false`): Tab and
|
|
23
|
+
* Shift+Tab nest and unnest, a drop or a handle's Alt+→ may place a block
|
|
24
|
+
* inside another. Off, those gestures nest nothing and Tab is left to the
|
|
25
|
+
* browser (the focus moves on); containers (lists, toggles, callouts,
|
|
26
|
+
* columns) keep their bodies, and nested blocks already in the document
|
|
27
|
+
* still render and edit.
|
|
28
|
+
*/
|
|
29
|
+
allowNesting?: boolean;
|
|
21
30
|
/** Chords (`mod+s`, `shift+alt+enter`) the view binds before plugins and built-ins. */
|
|
22
31
|
hotkeys?: Partial<Record<HotKeyCombination, HotKey>>;
|
|
23
32
|
plugins?: Plugin[];
|
|
@@ -100,11 +109,13 @@ export declare class Edytor {
|
|
|
100
109
|
/** What the components render: one cell per visible block, patched from change reports. */
|
|
101
110
|
cells: Cells | undefined;
|
|
102
111
|
/**
|
|
103
|
-
* The document's properties,
|
|
104
|
-
*
|
|
105
|
-
*
|
|
112
|
+
* The document's properties, as plain state: reactive at any depth, and
|
|
113
|
+
* writable (`edytor.data.title = 'Notes'`, `edytor.data.tags.push('x')`,
|
|
114
|
+
* `delete edytor.data.draft`). Each write is the `patchData` operation,
|
|
115
|
+
* dispatched; the writes of one task are one undo step. Type them by
|
|
116
|
+
* augmenting {@link EdytorDocData}.
|
|
106
117
|
*/
|
|
107
|
-
get data():
|
|
118
|
+
get data(): EdytorDocData;
|
|
108
119
|
/** The chrome layer outside the host: handles, menus, remote carets. */
|
|
109
120
|
readonly overlay: Overlay;
|
|
110
121
|
/** The chrome popups open on this view (`surface/popups`): what the root's ARIA names. */
|
|
@@ -113,6 +124,8 @@ export declare class Edytor {
|
|
|
113
124
|
readonly announcer: AnnouncerState;
|
|
114
125
|
/** The words the view was given (`<Edytor labels>`); its plugins read their section through `labelsOf`. */
|
|
115
126
|
readonly labels: PartialLabels;
|
|
127
|
+
/** Whether users may nest blocks (`allowNesting`, read once; the rule is `session/moves.ts` `nestsInto`). */
|
|
128
|
+
readonly allowNesting: boolean;
|
|
116
129
|
private off;
|
|
117
130
|
private onChange?;
|
|
118
131
|
private onDocChange?;
|
package/dist/edytor.svelte.js
CHANGED
|
@@ -6,7 +6,7 @@ import { onCut } from './events/onCut.js';
|
|
|
6
6
|
import { isControlEvent, isKindControl } from './events/nativeInteractiveControl.js';
|
|
7
7
|
import { onInput } from './events/onInput.js';
|
|
8
8
|
import { onPaste } from './events/onPaste.js';
|
|
9
|
-
import { onTextDragEnd, onTextDragStart, preventUnsupportedDrop } from './events/onDrop.js';
|
|
9
|
+
import { onDragLeave, onTextDragEnd, onTextDragStart, preventUnsupportedDrop } from './events/onDrop.js';
|
|
10
10
|
import { attachFocus, selectionIsInside, takeKeys } from './events/onFocus.js';
|
|
11
11
|
import { insertLineBreak, runIntent } from './events/beforeInputCommands.js';
|
|
12
12
|
import { textPointAt } from './selection/selection.utils.js';
|
|
@@ -43,6 +43,7 @@ import { Keymap } from './session/keymap.js';
|
|
|
43
43
|
import { TRANSACTION } from './constants.js';
|
|
44
44
|
import { Dispatcher } from './session/commands.js';
|
|
45
45
|
import { History } from './session/history.js';
|
|
46
|
+
import { DataWrites } from './session/props.js';
|
|
46
47
|
import { kindCatalogue, kindCommand, UNKNOWN_KIND } from './kinds.js';
|
|
47
48
|
import { clearDomSelection, getActiveElement, getDomSelection, getDomSelectionSnapshot } from './selection/domSelection.js';
|
|
48
49
|
/** A consumer that is set (a prop or a plugin hook left out is `undefined`). */
|
|
@@ -131,13 +132,18 @@ export class Edytor {
|
|
|
131
132
|
void this.#dataRevision;
|
|
132
133
|
return this.facade.docData();
|
|
133
134
|
};
|
|
135
|
+
/** @internal The view's property writes (`session/props`): the data proxies' one queue. */
|
|
136
|
+
dataWrites = new DataWrites(this);
|
|
137
|
+
#dataProxy;
|
|
134
138
|
/**
|
|
135
|
-
* The document's properties,
|
|
136
|
-
*
|
|
137
|
-
*
|
|
139
|
+
* The document's properties, as plain state: reactive at any depth, and
|
|
140
|
+
* writable (`edytor.data.title = 'Notes'`, `edytor.data.tags.push('x')`,
|
|
141
|
+
* `delete edytor.data.draft`). Each write is the `patchData` operation,
|
|
142
|
+
* dispatched; the writes of one task are one undo step. Type them by
|
|
143
|
+
* augmenting {@link EdytorDocData}.
|
|
138
144
|
*/
|
|
139
145
|
get data() {
|
|
140
|
-
return this.docData
|
|
146
|
+
return (this.#dataProxy ??= this.dataWrites.proxy(null, this.docData));
|
|
141
147
|
}
|
|
142
148
|
/** @internal The IME host pin (`surface/pin`): the composing cell's segment list and render, frozen. */
|
|
143
149
|
pin = new Pin();
|
|
@@ -149,6 +155,8 @@ export class Edytor {
|
|
|
149
155
|
announcer = new AnnouncerState(this);
|
|
150
156
|
/** The words the view was given (`<Edytor labels>`); its plugins read their section through `labelsOf`. */
|
|
151
157
|
labels = {};
|
|
158
|
+
/** Whether users may nest blocks (`allowNesting`, read once; the rule is `session/moves.ts` `nestsInto`). */
|
|
159
|
+
allowNesting = false;
|
|
152
160
|
off = [];
|
|
153
161
|
onChange;
|
|
154
162
|
onDocChange;
|
|
@@ -289,13 +297,14 @@ export class Edytor {
|
|
|
289
297
|
this.dispatcher.record({ operation: command, status: this.history.replay(command) });
|
|
290
298
|
};
|
|
291
299
|
constructor(options) {
|
|
292
|
-
const { readonly, hotkeys, plugins, document, doc, awareness, actor, requireHydration, presence, sync, value, onSelectionChange, labels, onChange, onDocChange } = options;
|
|
300
|
+
const { readonly, hotkeys, plugins, document, doc, awareness, actor, requireHydration, presence, sync, value, onSelectionChange, labels, onChange, onDocChange, allowNesting } = options;
|
|
293
301
|
for (const name of DOCUMENT_WRITES)
|
|
294
302
|
Object.defineProperty(this, name, {
|
|
295
303
|
value: (...args) => this.dispatcher.operate(name, args)
|
|
296
304
|
});
|
|
297
305
|
if (labels)
|
|
298
306
|
this.labels = labels;
|
|
307
|
+
this.allowNesting = allowNesting === true;
|
|
299
308
|
if (document !== undefined) {
|
|
300
309
|
if (DEV &&
|
|
301
310
|
(doc !== undefined ||
|
|
@@ -803,7 +812,7 @@ export class Edytor {
|
|
|
803
812
|
on(node, 'contextmenu', () => void this.surface.flush()), on(node, 'beforeinput', this.#withUserInput(this.onBeforeInput)),
|
|
804
813
|
// An `input` records what the browser did; it says nothing new about
|
|
805
814
|
// where the user wants the selection (a display still to land wins).
|
|
806
|
-
on(node, 'input', this.#withUserInput(this.onInput, { bumpSerial: false })), on(node, 'copy', this.onCopy), on(node, 'cut', this.#withUserInput(this.onCut)), on(node, 'paste', this.#withUserInput(this.onPaste)), on(node, 'dragstart', (event) => onTextDragStart(this, event)), on(node, 'dragend', () => onTextDragEnd(this)), on(node, 'dragover', (event) => preventUnsupportedDrop(event, this)), on(node, 'drop', this.#withUserInput((event) => preventUnsupportedDrop(event, this))), on(node, 'compositionstart', this.onCompositionStart), on(node, 'compositionend', this.onCompositionEnd), detachSurface);
|
|
815
|
+
on(node, 'input', this.#withUserInput(this.onInput, { bumpSerial: false })), on(node, 'copy', this.onCopy), on(node, 'cut', this.#withUserInput(this.onCut)), on(node, 'paste', this.#withUserInput(this.onPaste)), on(node, 'dragstart', (event) => onTextDragStart(this, event)), on(node, 'dragend', () => onTextDragEnd(this)), on(node, 'dragover', (event) => preventUnsupportedDrop(event, this)), on(node, 'dragleave', (event) => onDragLeave(this, event)), on(node, 'drop', this.#withUserInput((event) => preventUnsupportedDrop(event, this))), on(node, 'compositionstart', this.onCompositionStart), on(node, 'compositionend', this.onCompositionEnd), detachSurface);
|
|
807
816
|
// Chrome lives in the overlay, outside the host: remote carets, then the plugins'.
|
|
808
817
|
const detachOverlay = this.overlay.attach(node);
|
|
809
818
|
const presence = mount(RemoteSelections, {
|
|
@@ -9,6 +9,6 @@ import { type Attempt } from '../session/attempt.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const insertLineBreak: (edytor: Edytor, snapshot: Attempt, caret?: "after" | "before") => void;
|
|
11
11
|
/** The model command for a `beforeinput`, run as one user command (undo policy, prevention scope). */
|
|
12
|
-
export declare const runBeforeInputCommand: (edytor: Edytor, snapshot: Attempt) => boolean | void | Promise<undefined> |
|
|
12
|
+
export declare const runBeforeInputCommand: (edytor: Edytor, snapshot: Attempt) => boolean | void | Text | Promise<undefined> | null | undefined;
|
|
13
13
|
/** An editing intent at the current selection (a key binding's command): no event to fabricate. */
|
|
14
|
-
export declare const runIntent: (edytor: Edytor, inputType: string) => boolean | void | Promise<undefined> |
|
|
14
|
+
export declare const runIntent: (edytor: Edytor, inputType: string) => boolean | void | Text | Promise<undefined> | null | undefined;
|
|
@@ -97,10 +97,10 @@ export const insertLineBreak = (edytor, snapshot, caret = 'after') => {
|
|
|
97
97
|
: block.parent?.children[block.index + 1]?.firstText;
|
|
98
98
|
caretAt(edytor, line, before ? (line?.length ?? 0) : 0);
|
|
99
99
|
};
|
|
100
|
-
const runPastePlugins = (edytor, dataTransfer) => {
|
|
100
|
+
const runPastePlugins = (edytor, dataTransfer, at) => {
|
|
101
101
|
const event = { clipboardData: dataTransfer };
|
|
102
102
|
for (const plugin of edytor.plugins)
|
|
103
|
-
vetoable((prevent) => plugin.onPaste?.({ prevent, e: event }));
|
|
103
|
+
vetoable((prevent) => plugin.onPaste?.({ prevent, e: event, ...(at && { at }) }));
|
|
104
104
|
};
|
|
105
105
|
/** The drop point the browser reported: its text and offset (`null`: not in a text). */
|
|
106
106
|
const dropPoint = (edytor, { declared: range }) => {
|
|
@@ -150,9 +150,13 @@ const insertPayload = async (edytor, snapshot) => {
|
|
|
150
150
|
// A paste occurrence is the plain text its handler left to it (Shift-paste too).
|
|
151
151
|
const fragment = !paste && readEdytorClipboardFragment(dataTransfer);
|
|
152
152
|
if (!paste && !fragment && dataTransfer) {
|
|
153
|
-
// A plugin places the files at the
|
|
154
|
-
|
|
153
|
+
// A plugin places the files at the drop indicator's placement, else at the
|
|
154
|
+
// selection: the drop point, never over selected blocks.
|
|
155
|
+
if (dataTransfer.files?.length) {
|
|
156
|
+
if (snapshot.placement)
|
|
157
|
+
return void runPastePlugins(edytor, dataTransfer, snapshot.placement);
|
|
155
158
|
return void (land() !== null && runPastePlugins(edytor, dataTransfer));
|
|
159
|
+
}
|
|
156
160
|
if (dataTransfer.getData('text/html'))
|
|
157
161
|
runPastePlugins(edytor, dataTransfer);
|
|
158
162
|
}
|
package/dist/events/onDrop.d.ts
CHANGED
|
@@ -31,3 +31,9 @@ export declare const onTextDragEnd: (edytor: Edytor) => undefined;
|
|
|
31
31
|
* which is the control's (`isControlDrop`).
|
|
32
32
|
*/
|
|
33
33
|
export declare const preventUnsupportedDrop: (event: DragEvent, edytor: Edytor) => void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* A drag leaving the host (to the page around it, or out of the window):
|
|
36
|
+
* a file drag's indicator goes. Moving between the host's own elements is
|
|
37
|
+
* no leave.
|
|
38
|
+
*/
|
|
39
|
+
export declare const onDragLeave: (edytor: Edytor, event: DragEvent) => void;
|