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
|
@@ -2,16 +2,15 @@ import { untrack } from 'svelte';
|
|
|
2
2
|
import { autoScrollFor } from '../../dnd/autoScroll.js';
|
|
3
3
|
import { blockSelection, noSelection, project } from '../../session/selection.js';
|
|
4
4
|
import { draggable, dropTargetForElements, setCustomNativeDragPreview } from '../../dnd/pragmatic.js';
|
|
5
|
-
import { HIDDEN, hidden } from '../../selection/visibility.js';
|
|
6
5
|
import { getSelectionBlocks, holdsBlocks, movable, outermost, selectMoved, shownText } from '../../selection/replaceSelection.js';
|
|
7
6
|
import { takeKeys } from '../../events/onFocus.js';
|
|
8
7
|
import { EDYTOR_BLOCK_DRAG_MIME } from '../../events/onDrop.js';
|
|
9
8
|
import { kindLabel } from '../../kinds.js';
|
|
10
9
|
import { siblingSlot } from '../../session/handles.js';
|
|
11
10
|
import { dragPreview } from './dragPreview.js';
|
|
12
|
-
import { DropIndicator } from '
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { DropIndicator } from '../../dnd/dropIndicator.js';
|
|
12
|
+
import { DropResolver, SLOP_X, SLOP_Y } from '../../dnd/placement.js';
|
|
13
|
+
import { getOwnRowBottom, ownRow } from '../../dnd/geometry.js';
|
|
15
14
|
import { all, listen } from '../../utils/listen.js';
|
|
16
15
|
/**
|
|
17
16
|
* The DOM event a grip click dispatches on the editor (the one way to
|
|
@@ -38,14 +37,6 @@ const ROW = 'edytorRow';
|
|
|
38
37
|
const BEFORE = 'edytorBefore';
|
|
39
38
|
/** The drop target data key of a beside band beyond the editor's edges (`margin`). */
|
|
40
39
|
const MARGIN = 'edytorMargin';
|
|
41
|
-
/** How far beside a target's box it stays the drop target (`getIsSticky`), across and along. */
|
|
42
|
-
const SLOP_X = 20;
|
|
43
|
-
const SLOP_Y = 24;
|
|
44
|
-
/** The right beside band: the row's last 15% (Notion), at least this many px. */
|
|
45
|
-
const RIGHT_BAND = 0.15;
|
|
46
|
-
const RIGHT_BAND_MIN = 32;
|
|
47
|
-
/** The handle column's width when the drag's handle cannot be measured (`+`, grip and gap). */
|
|
48
|
-
const HANDLE_COLUMN = 50;
|
|
49
40
|
/**
|
|
50
41
|
* Whether the pointer is over the own row of a block other than `node` and
|
|
51
42
|
* its descendants. A container (a list) has no row of its own: its padding
|
|
@@ -65,8 +56,6 @@ const endsDocument = (block) => {
|
|
|
65
56
|
return false;
|
|
66
57
|
return true;
|
|
67
58
|
};
|
|
68
|
-
/** Where a block's own text starts: its level's column. */
|
|
69
|
-
const column = (block) => ownTextRow(block.node).left;
|
|
70
59
|
export class BlockHandleController {
|
|
71
60
|
edytor;
|
|
72
61
|
options;
|
|
@@ -82,6 +71,8 @@ export class BlockHandleController {
|
|
|
82
71
|
group = [];
|
|
83
72
|
/** The drop bar and the nest backdrop in the overlay, while our drag runs. */
|
|
84
73
|
indicator;
|
|
74
|
+
/** Where a drop lands (the one resolution a file drag reads too). */
|
|
75
|
+
drop;
|
|
85
76
|
/** The selection the drag in progress replaced when it started: its undo step restores it. */
|
|
86
77
|
held = null;
|
|
87
78
|
/** The handle the drag in progress started from: its width is the handle column's. */
|
|
@@ -91,10 +82,14 @@ export class BlockHandleController {
|
|
|
91
82
|
constructor(edytor, options = { draggable: true }) {
|
|
92
83
|
this.edytor = edytor;
|
|
93
84
|
this.options = options;
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
this.drop = new DropResolver(edytor, {
|
|
86
|
+
targetOf: (node) => this.targets.get(node),
|
|
87
|
+
handle: () => this.handle,
|
|
88
|
+
indicator: (this.indicator = new DropIndicator(edytor, {
|
|
89
|
+
group: () => this.group,
|
|
90
|
+
besideOf: (block) => this.drop.besideOf(block),
|
|
91
|
+
layoutOf: (outer) => this.drop.layoutOf(outer)
|
|
92
|
+
}))
|
|
98
93
|
});
|
|
99
94
|
}
|
|
100
95
|
get readonly() {
|
|
@@ -396,13 +391,13 @@ export class BlockHandleController {
|
|
|
396
391
|
if (!dragSource)
|
|
397
392
|
return false;
|
|
398
393
|
if (!target)
|
|
399
|
-
return Boolean(this.margin(dragSource, input));
|
|
400
|
-
if (this
|
|
394
|
+
return Boolean(this.drop.margin(this.#drag(dragSource), input));
|
|
395
|
+
if (this.drop.inHandleColumn(input.clientX))
|
|
401
396
|
return false;
|
|
402
|
-
const row = this.rowAt(dragSource, target, node, input);
|
|
403
|
-
const { before, after } = this.zones(dragSource, row, input);
|
|
404
|
-
return ([...before, ...after].some(this
|
|
405
|
-
Boolean(this.beside(dragSource, row, input)));
|
|
397
|
+
const row = this.drop.rowAt(this.#drag(dragSource), target, node, input);
|
|
398
|
+
const { before, after } = this.drop.zones(this.#drag(dragSource), row, input);
|
|
399
|
+
return ([...before, ...after].some(this.#drag(dragSource).fits) ||
|
|
400
|
+
Boolean(this.drop.beside(this.#drag(dragSource), row, input)));
|
|
406
401
|
},
|
|
407
402
|
// The row at the pointer and the half of its own row (not its subtree's)
|
|
408
403
|
// the pointer is in: held while the target is sticky (the drag library
|
|
@@ -411,7 +406,7 @@ export class BlockHandleController {
|
|
|
411
406
|
const dragSource = this.#getDragSource(source.data);
|
|
412
407
|
if (!target || !dragSource)
|
|
413
408
|
return { [MARGIN]: true };
|
|
414
|
-
const row = this.rowAt(dragSource, target, node, input);
|
|
409
|
+
const row = this.drop.rowAt(this.#drag(dragSource), target, node, input);
|
|
415
410
|
const rect = ownRow(row.node);
|
|
416
411
|
return { [ROW]: row.id, [BEFORE]: input.clientY < rect.top + rect.height / 2 };
|
|
417
412
|
},
|
|
@@ -429,7 +424,7 @@ export class BlockHandleController {
|
|
|
429
424
|
input.clientX <= rect.right + SLOP_X &&
|
|
430
425
|
input.clientY >= rect.top - SLOP_Y &&
|
|
431
426
|
(input.clientY <= rect.bottom + SLOP_Y || endsDocument(target)) &&
|
|
432
|
-
!this
|
|
427
|
+
!this.drop.overOtherColumn(target, input.clientX) &&
|
|
433
428
|
!overOtherRow(node, input, (over) => !this.edytor.idToBlock.get(over.dataset.edytorId ?? '')?.isContainer));
|
|
434
429
|
},
|
|
435
430
|
onDragEnter: show,
|
|
@@ -466,138 +461,6 @@ export class BlockHandleController {
|
|
|
466
461
|
});
|
|
467
462
|
this.registered.set(node, cleanup);
|
|
468
463
|
}
|
|
469
|
-
/**
|
|
470
|
-
* The editor's content column (`start`…`end`, the root's content box:
|
|
471
|
-
* where its blocks' boxes start and end), the width of the handle column
|
|
472
|
-
* left of it (the drag's handle, measured: `+`, grip and the gap to the
|
|
473
|
-
* block) and how far the drag reaches past them (`dropReach`, the
|
|
474
|
-
* `--edytor-drop-reach` the root inherits).
|
|
475
|
-
*/
|
|
476
|
-
gutter(root) {
|
|
477
|
-
const style = root.ownerDocument.defaultView?.getComputedStyle(root);
|
|
478
|
-
const rect = root.getBoundingClientRect();
|
|
479
|
-
const px = (value) => parseFloat(value ?? '') || 0;
|
|
480
|
-
const start = rect.left + root.clientLeft + px(style?.paddingLeft);
|
|
481
|
-
const end = rect.right - px(style?.borderRightWidth) - px(style?.paddingRight);
|
|
482
|
-
const width = this.handle?.isConnected ? this.handle.getBoundingClientRect().width : 0;
|
|
483
|
-
return { start, end, handles: width > 0 ? width : HANDLE_COLUMN, reach: dropReach(style) };
|
|
484
|
-
}
|
|
485
|
-
/** Whether `x` is in the handle column left of the editor's blocks (`gutter`): the page target's. */
|
|
486
|
-
#inHandleColumn(x) {
|
|
487
|
-
const root = this.edytor.node;
|
|
488
|
-
return !!root && marginAt(x, this.gutter(root))?.zone === 'handles';
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* A placement in the editor's margins at the pointer (`dnd.reach`), within
|
|
492
|
-
* the editor's height, for the row the content's near edge shows at the
|
|
493
|
-
* pointer's height (`rowAt`: a nested block's own row; over a layout, the
|
|
494
|
-
* row of its first column on the left, of its last on the right, never
|
|
495
|
-
* another), by the zone the pointer is in (`marginAt`):
|
|
496
|
-
* - the handle column left of the blocks (`gutter`): that row's reorder
|
|
497
|
-
* (`reorder`): a drag straight down the handles reorders, row after
|
|
498
|
-
* row, gaps included (never a beside band);
|
|
499
|
-
* - the first `BESIDE_MARGIN` px past it on the left, and past the
|
|
500
|
-
* content's right edge: the beside band of that side (`beside`, as if
|
|
501
|
-
* the pointer were at that edge) when the document has a layout kind
|
|
502
|
-
* and the row offers one (a refused band shows nothing);
|
|
503
|
-
* - elsewhere within the reach (and there without a band): the reorder,
|
|
504
|
-
* on the left the handle column's, on the right the one the row's
|
|
505
|
-
* right edge offers (its halves as just inside the content, so nothing
|
|
506
|
-
* jumps as the pointer leaves it).
|
|
507
|
-
* `undefined` past the reach, above or below the editor, and inside the
|
|
508
|
-
* content (the blocks' own targets answer there).
|
|
509
|
-
*/
|
|
510
|
-
margin(source, { clientX: x, clientY: y }) {
|
|
511
|
-
const root = this.edytor.node;
|
|
512
|
-
if (!root)
|
|
513
|
-
return undefined;
|
|
514
|
-
const box = root.getBoundingClientRect();
|
|
515
|
-
if (y < box.top || y > box.bottom)
|
|
516
|
-
return undefined;
|
|
517
|
-
const gutter = this.gutter(root);
|
|
518
|
-
const zone = marginAt(x, gutter);
|
|
519
|
-
if (!zone)
|
|
520
|
-
return undefined;
|
|
521
|
-
const right = zone.side === 'right';
|
|
522
|
-
const band = zone.zone === 'beside' && this.layouts;
|
|
523
|
-
const edge = right ? gutter.end - 1 : gutter.start + 1;
|
|
524
|
-
const hits = root.ownerDocument.elementsFromPoint?.(edge, y) ?? [];
|
|
525
|
-
const hit = hits.find((at) => {
|
|
526
|
-
const block = at.closest('[data-edytor-block="true"]');
|
|
527
|
-
return block && root.contains(block) && this.targets.has(block);
|
|
528
|
-
});
|
|
529
|
-
const node = hit?.closest('[data-edytor-block="true"]');
|
|
530
|
-
const target = node && this.targets.get(node);
|
|
531
|
-
// Between two rows where the margin reorders (a margin, a layout's
|
|
532
|
-
// edge), the reorder shown stays, as a block's sticky slop keeps it.
|
|
533
|
-
const shown = !band && this.indicator.target === root.ownerDocument.body;
|
|
534
|
-
const held = shown ? this.indicator.placement : null;
|
|
535
|
-
const stay = held && !isBeside(held.position) ? held : undefined;
|
|
536
|
-
if (!node || !target)
|
|
537
|
-
return stay;
|
|
538
|
-
const { facade } = this.edytor;
|
|
539
|
-
let row = this.rowAt(source, target, node, { clientX: edge, clientY: y });
|
|
540
|
-
// Over a layout's edge, its first or last column's row at that height.
|
|
541
|
-
if (facade.isLayout(row.id)) {
|
|
542
|
-
const items = row.children.filter((item) => item.node && this.targets.has(item.node));
|
|
543
|
-
const rect = (right ? items.at(-1) : items[0])?.node?.getBoundingClientRect();
|
|
544
|
-
if (!rect || rect.width === 0)
|
|
545
|
-
return undefined;
|
|
546
|
-
const inner = right ? rect.right - 1 : rect.left + 1;
|
|
547
|
-
row = this.rowAt(source, target, node, { clientX: inner, clientY: y });
|
|
548
|
-
}
|
|
549
|
-
if (facade.isLayout(row.id) || !row.node)
|
|
550
|
-
return stay;
|
|
551
|
-
if (band) {
|
|
552
|
-
const aside = this.beside(source, row, { clientX: right ? Infinity : -Infinity, clientY: y }, zone.side);
|
|
553
|
-
// A band the document refuses shows nothing; a row offering none (a
|
|
554
|
-
// nested row on the left, a layout that would stack) reorders.
|
|
555
|
-
if (aside !== undefined)
|
|
556
|
-
return aside ?? undefined;
|
|
557
|
-
}
|
|
558
|
-
const rect = ownRow(row.node);
|
|
559
|
-
if (right) {
|
|
560
|
-
const at = { clientX: Math.min(x, rect.right - 1), clientY: y };
|
|
561
|
-
return this.pick(source, this.halves(source, row, at, rect)) ?? undefined;
|
|
562
|
-
}
|
|
563
|
-
return this.reorder(source, row, { clientX: x, clientY: y }) ?? undefined;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* The halves of `row` (`zones`) at the pointer, the one it is in first:
|
|
567
|
-
* the top half of `rect` (its own row by default) before, the bottom one
|
|
568
|
-
* after; with `plain`, only before or after a block — never inside one.
|
|
569
|
-
*/
|
|
570
|
-
halves(source, row, input, rect, plain = false) {
|
|
571
|
-
const zones = this.zones(source, row, input);
|
|
572
|
-
const [before, after] = [zones.before, zones.after].map((half) => plain ? half.filter((placement) => placement.position !== 'inside') : half);
|
|
573
|
-
return input.clientY < rect.top + rect.height / 2 ? [before, after] : [after, before];
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* The first placement of `halves` the document takes (a refused one gives
|
|
577
|
-
* way to the next, then to the other half's), `blocked` when the first
|
|
578
|
-
* half took none; `null` when none fits.
|
|
579
|
-
*/
|
|
580
|
-
pick(source, halves) {
|
|
581
|
-
const fits = this.fits(source);
|
|
582
|
-
const placement = halves.flat().find(fits);
|
|
583
|
-
return placement ? { ...placement, blocked: !halves[0].some(fits) } : null;
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* The handle column's reorder over `row` at the pointer: its plain halves
|
|
587
|
-
* (`halves`, the pointer's x picking the level); `null` when neither half
|
|
588
|
-
* fits. Over an item of a container the dragged blocks do not fit in (a
|
|
589
|
-
* paragraph over a list's item: the document refuses every placement at
|
|
590
|
-
* the item's level), the container is the row: its whole box, the upper
|
|
591
|
-
* half before it, the lower half after it (round 4), so a drag down the
|
|
592
|
-
* handles meets the list as one row it can pass.
|
|
593
|
-
*/
|
|
594
|
-
reorder(source, row, input) {
|
|
595
|
-
const fits = this.fits(source);
|
|
596
|
-
let halves = this.halves(source, row, input, ownRow(row.node), true);
|
|
597
|
-
for (let at = row.parent; !halves[0].some(fits) && at?.node && !at.isRoot && at.isContainer; at = at.parent)
|
|
598
|
-
halves = this.halves(source, at, input, at.node.getBoundingClientRect(), true);
|
|
599
|
-
return this.pick(source, halves);
|
|
600
|
-
}
|
|
601
464
|
/**
|
|
602
465
|
* Alt+arrow: one step for the blocks a drag of this handle moves. A text
|
|
603
466
|
* range over them stays, as Mod+Shift+arrow keeps it; else they end
|
|
@@ -672,227 +535,13 @@ export class BlockHandleController {
|
|
|
672
535
|
const blocks = selection.selectedBlocks.size ? [source] : this.covered(source);
|
|
673
536
|
selection.select(blockSelection(blocks.map((block) => block.id)));
|
|
674
537
|
}
|
|
675
|
-
/**
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* The block whose row the pointer is on over `node`: the last shown drop
|
|
679
|
-
* target in it whose row starts at or above the pointer (beside a nested
|
|
680
|
-
* block, over its ancestors' indent, the pointer is on that block's
|
|
681
|
-
* row), else `target`, over its own row (`emptied`). A layout's columns
|
|
682
|
-
* sit side by side: only the one under the pointer is measured (between
|
|
683
|
-
* two of them, the layout is the row: its gap).
|
|
684
|
-
*/
|
|
685
|
-
rowAt(source, target, node, { clientX, clientY }) {
|
|
686
|
-
const lift = (row) => this.emptied(source, row);
|
|
687
|
-
if (clientY < getOwnRowBottom(node))
|
|
688
|
-
return lift(target);
|
|
689
|
-
const { facade } = this.edytor;
|
|
690
|
-
let row = target;
|
|
691
|
-
let aside = null;
|
|
692
|
-
// In document order, rows start lower and lower (within a column): measure
|
|
693
|
-
// none below the pointer, none a closed toggle hides (each read would force
|
|
694
|
-
// a layout), and nothing in a column the pointer is not over.
|
|
695
|
-
for (const inner of node.querySelectorAll('[data-edytor-block="true"]')) {
|
|
696
|
-
if (aside?.contains(inner))
|
|
697
|
-
continue;
|
|
698
|
-
const block = this.targets.get(inner);
|
|
699
|
-
if (!block || inner.closest(HIDDEN))
|
|
700
|
-
continue;
|
|
701
|
-
const rect = inner.getBoundingClientRect();
|
|
702
|
-
if (facade.isLayoutItem(block.id) && (clientX < rect.left || clientX > rect.right)) {
|
|
703
|
-
aside = inner;
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
if (rect.top > clientY)
|
|
707
|
-
break;
|
|
708
|
-
if (rect.height > 0)
|
|
709
|
-
row = block;
|
|
710
|
-
}
|
|
711
|
-
return lift(row);
|
|
712
|
-
}
|
|
713
|
-
/** Whether the document has a layout kind: only then are there beside placements. */
|
|
714
|
-
get layouts() {
|
|
715
|
-
for (const definition of this.edytor.blocks.values())
|
|
716
|
-
if (definition.layout)
|
|
717
|
-
return true;
|
|
718
|
-
return false;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* The block a beside placement over `block` stands beside: the document's
|
|
722
|
-
* own resolution (`facade.besideAt`, `placeBeside`'s): a list item's list,
|
|
723
|
-
* a code line's code block, a toggle's child itself, an item itself.
|
|
724
|
-
*/
|
|
725
|
-
besideOf(block) {
|
|
726
|
-
return this.edytor.idToBlock.get(this.edytor.facade.besideAt(block.id)) ?? block;
|
|
727
|
-
}
|
|
728
|
-
/** The item (and its layout) a beside placement at `outer` (`besideOf`) adds a column beside. */
|
|
729
|
-
layoutOf(outer) {
|
|
730
|
-
const { facade } = this.edytor;
|
|
731
|
-
const item = facade.isLayoutItem(outer.id)
|
|
732
|
-
? outer
|
|
733
|
-
: outer.parent && !outer.parent.isRoot && facade.isLayoutItem(outer.parent.id)
|
|
734
|
-
? outer.parent
|
|
735
|
-
: null;
|
|
736
|
-
return item ? { item, layout: item.parent ?? null } : null;
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Whether `x` is over a column other than one holding `block`: a sticky
|
|
740
|
-
* target never holds the pointer across a column boundary.
|
|
741
|
-
*/
|
|
742
|
-
#overOtherColumn(block, x) {
|
|
743
|
-
const { facade } = this.edytor;
|
|
744
|
-
for (let at = block; at && !at.isRoot; at = at.parent) {
|
|
745
|
-
if (!facade.isLayoutItem(at.id))
|
|
746
|
-
continue;
|
|
747
|
-
const item = at;
|
|
748
|
-
const over = item.parent?.children.some((other) => {
|
|
749
|
-
const rect = other !== item ? other.node?.getBoundingClientRect() : undefined;
|
|
750
|
-
return rect !== undefined && rect.width > 0 && x >= rect.left && x <= rect.right;
|
|
751
|
-
});
|
|
752
|
-
if (over)
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Notion's beside bands over `row` (`docs/columns-plan.md`, "View layer"), only while the
|
|
759
|
-
* pointer is within its own row's height, the document has a layout kind
|
|
760
|
-
* and the layout the drop makes or grows does not stack (`stacks`):
|
|
761
|
-
* - right: the row's last 15% (at least 32px), and the slop past it;
|
|
762
|
-
* - left: the slop left of the row (its sticky margin, left of its text
|
|
763
|
-
* column), only for a row whose parent is the root or a column (a
|
|
764
|
-
* list's item: its list's) — a nested row keeps the pointer's x for its
|
|
765
|
-
* levels (`zones`) — and only
|
|
766
|
-
* inside the editor (a column's gap): left of the editor's blocks is the
|
|
767
|
-
* handle column, which reorders (`margin`);
|
|
768
|
-
* - over a layout's gap (the row is the layout): right of the column
|
|
769
|
-
* before it, a new column between.
|
|
770
|
-
* `undefined` outside the bands (the hitbox's halves stand), else the
|
|
771
|
-
* placement, or `null` when the document refuses it: a refused band shows
|
|
772
|
-
* nothing.
|
|
773
|
-
*/
|
|
774
|
-
beside(source, row, { clientX: x, clientY: y }, band) {
|
|
775
|
-
if (!row.node || !this.layouts)
|
|
776
|
-
return undefined;
|
|
777
|
-
if (this.carrier(source, row))
|
|
778
|
-
return undefined;
|
|
779
|
-
const { facade } = this.edytor;
|
|
780
|
-
let placement;
|
|
781
|
-
if (facade.isLayout(row.id)) {
|
|
782
|
-
const rect = row.node.getBoundingClientRect();
|
|
783
|
-
if (y < rect.top || y > rect.bottom || stacks(rect.width))
|
|
784
|
-
return undefined;
|
|
785
|
-
const items = row.children.filter((item) => item.node && this.targets.has(item.node));
|
|
786
|
-
for (const [index, item] of items.entries()) {
|
|
787
|
-
const next = items[index + 1]?.node?.getBoundingClientRect();
|
|
788
|
-
const own = item.node.getBoundingClientRect();
|
|
789
|
-
if (next && x >= own.right && x <= next.left)
|
|
790
|
-
placement = { target: item, node: item.node, position: 'right' };
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
const rect = ownRow(row.node);
|
|
795
|
-
if (y < rect.top || y > rect.bottom)
|
|
796
|
-
return undefined;
|
|
797
|
-
const outer = this.besideOf(row);
|
|
798
|
-
const width = (this.layoutOf(outer)?.layout ?? outer).node?.getBoundingClientRect().width;
|
|
799
|
-
if (width === undefined || stacks(width))
|
|
800
|
-
return undefined;
|
|
801
|
-
// A container's item (a list's) stands at its container's level, as the
|
|
802
|
-
// document resolves it (`besideAt`): left of a root list's item is left of the list.
|
|
803
|
-
let at = row;
|
|
804
|
-
while (at.parent &&
|
|
805
|
-
!at.parent.isRoot &&
|
|
806
|
-
at.parent.isContainer &&
|
|
807
|
-
!facade.isLayoutItem(at.parent.id))
|
|
808
|
-
at = at.parent;
|
|
809
|
-
const parent = at.parent;
|
|
810
|
-
const level = !parent || parent.isRoot || facade.isLayoutItem(parent.id);
|
|
811
|
-
if (band === 'right' ||
|
|
812
|
-
(!band && x >= rect.right - Math.max(RIGHT_BAND_MIN, rect.width * RIGHT_BAND)))
|
|
813
|
-
placement = { target: row, node: row.node, position: 'right' };
|
|
814
|
-
else if (level &&
|
|
815
|
-
(band === 'left' ||
|
|
816
|
-
(!band && x < rect.left && x >= rect.left - SLOP_X && x >= this.inside(rect.left))))
|
|
817
|
-
placement = { target: row, node: row.node, position: 'left' };
|
|
818
|
-
}
|
|
819
|
-
if (!placement)
|
|
820
|
-
return undefined;
|
|
821
|
-
return this.fits(source)(placement) ? placement : null;
|
|
822
|
-
}
|
|
823
|
-
/** Where the editor's blocks start (`gutter`), or `fallback` without a host. */
|
|
824
|
-
inside(fallback) {
|
|
825
|
-
const root = this.edytor.node;
|
|
826
|
-
return root ? this.gutter(root).start : fallback;
|
|
827
|
-
}
|
|
828
|
-
/** The children a block shows: none for a closed toggle's, or a container's items. */
|
|
829
|
-
shown(block) {
|
|
830
|
-
return block.isContainer
|
|
831
|
-
? []
|
|
832
|
-
: block.children.filter((child) => child.node && this.targets.has(child.node) && !hidden(child));
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Over a dragged block, the parent it leaves when that parent shows no
|
|
836
|
-
* other child: the gap under the parent's own row, which reparents as
|
|
837
|
-
* the parent's lower half does (`zones`). Else `row`.
|
|
838
|
-
*/
|
|
839
|
-
emptied(source, row) {
|
|
538
|
+
/** The drag of `source`: the blocks it moves, and the placements the document takes for them. */
|
|
539
|
+
#drag(source) {
|
|
840
540
|
const group = this.moving(source);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const shown = this.shown(parent);
|
|
845
|
-
return shown.length && shown.every((child) => group.includes(child)) ? parent : row;
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* The placements over `row`, each half's in preference order (a refused
|
|
849
|
-
* one gives way to the next, then to the other half's):
|
|
850
|
-
* - the top half: before it;
|
|
851
|
-
* - the bottom half of a block whose children show (not a closed toggle):
|
|
852
|
-
* its first child's slot. Pragmatic drag and drop's tree rule: an
|
|
853
|
-
* expanded item offers no "after" on its own row;
|
|
854
|
-
* - else, after it at the level whose text column is nearest the pointer:
|
|
855
|
-
* the last block of a nested group reparents to any ancestor it ends,
|
|
856
|
-
* down to the root. Past one nesting step right of its text start,
|
|
857
|
-
* inside it (its last child) first. A block every shown child of which
|
|
858
|
-
* is dragged is not expanded: its levels, never inside it when the drag
|
|
859
|
-
* moves only its children (they would stay where they are).
|
|
860
|
-
* None over a dragged block's own row or its subtree: released there, the
|
|
861
|
-
* drag changes nothing (its levels would outdent it) — unless it empties
|
|
862
|
-
* its parent's shown children (`emptied`): that parent's row. Over the
|
|
863
|
-
* hint of the row's empty body (`body.hint`: an open toggle's, a
|
|
864
|
-
* callout's), inside it first, either half.
|
|
865
|
-
*/
|
|
866
|
-
zones(source, row, { clientX, clientY }) {
|
|
867
|
-
if (this.carrier(source, row))
|
|
868
|
-
return { before: [], after: [] };
|
|
869
|
-
const group = this.moving(source);
|
|
870
|
-
const at = (target, position) => target?.node ? [{ target, node: target.node, position }] : [];
|
|
871
|
-
const before = at(row, 'before');
|
|
872
|
-
const shown = this.shown(row);
|
|
873
|
-
const first = shown.find((child) => !group.includes(child));
|
|
874
|
-
if (first)
|
|
875
|
-
return { before, after: at(first, 'before') };
|
|
876
|
-
// A group moved out from under its parent leaves the block before it last.
|
|
877
|
-
const next = (block) => {
|
|
878
|
-
let next = block.nextBlock;
|
|
879
|
-
while (next && group.includes(next))
|
|
880
|
-
next = next.nextBlock;
|
|
881
|
-
return next;
|
|
541
|
+
return {
|
|
542
|
+
group,
|
|
543
|
+
fits: ({ target, position }) => this.edytor.canMove({ blocks: group, target, position })
|
|
882
544
|
};
|
|
883
|
-
const levels = [row];
|
|
884
|
-
for (let level = row; !next(level) && level.parent?.node && !level.parent.isRoot;)
|
|
885
|
-
levels.push((level = level.parent));
|
|
886
|
-
const distance = (level) => Math.abs(column(level) - clientX);
|
|
887
|
-
const after = levels
|
|
888
|
-
.sort((a, b) => distance(a) - distance(b))
|
|
889
|
-
.flatMap((level) => at(level, 'after'));
|
|
890
|
-
const stays = shown.length > 0 && group.every((moved) => moved.parent === row);
|
|
891
|
-
const inside = stays ? [] : at(row, 'inside');
|
|
892
|
-
if (overEmptyBody(row.node, clientY))
|
|
893
|
-
return { before: [...inside, ...before], after: [...inside, ...after] };
|
|
894
|
-
const nests = clientX > column(row) + nestIndent(row.node);
|
|
895
|
-
return { before, after: nests ? [...inside, ...after] : [...after, ...inside] };
|
|
896
545
|
}
|
|
897
546
|
/**
|
|
898
547
|
* The placement a drop target record offers: the page's margins
|
|
@@ -902,20 +551,13 @@ export class BlockHandleController {
|
|
|
902
551
|
* live.
|
|
903
552
|
*/
|
|
904
553
|
placement(record, source, input) {
|
|
554
|
+
const drag = this.#drag(source);
|
|
905
555
|
if (record.data[MARGIN])
|
|
906
|
-
return this.margin(
|
|
556
|
+
return this.drop.margin(drag, input) ?? false;
|
|
907
557
|
const row = this.edytor.idToBlock.get(record.data[ROW]);
|
|
908
558
|
if (!row?.node)
|
|
909
559
|
return null;
|
|
910
|
-
|
|
911
|
-
if (aside !== undefined)
|
|
912
|
-
return aside ?? false;
|
|
913
|
-
const { before, after } = this.zones(source, row, input);
|
|
914
|
-
return this.pick(source, record.data[BEFORE] ? [before, after] : [after, before]);
|
|
915
|
-
}
|
|
916
|
-
/** The dragged block that is `row` or holds it: its row offers nothing (it moves with the drag). */
|
|
917
|
-
carrier(source, row) {
|
|
918
|
-
return this.moving(source).find((moved) => moved === row || row.isChildOf(moved));
|
|
560
|
+
return this.drop.at(drag, row, input, Boolean(record.data[BEFORE]));
|
|
919
561
|
}
|
|
920
562
|
/** The block our drag moves from (none in a read-only view). */
|
|
921
563
|
#getDragSource(data) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { BlockHandleController } from './BlockHandleController.svelte.js';
|
|
11
11
|
import { gapBefore, handleSpan } from '../columns/gaps.js';
|
|
12
12
|
import { renderSkipped, rendered } from '../../surface/overlay.js';
|
|
13
|
-
import { getOwnRowBottom } from '
|
|
13
|
+
import { getOwnRowBottom } from '../../dnd/geometry.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The handles, in the overlay: one per registered movable block that
|
|
@@ -201,14 +201,15 @@
|
|
|
201
201
|
pointer-events: none;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
/* Over a gap between two columns (one hit rule, the handles'): the handle
|
|
205
|
-
* spans the gap right of the column resize band,
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* shows
|
|
204
|
+
/* Over a gap between two columns (one hit rule, the handles'): the handle's
|
|
205
|
+
* box spans the gap right of the column resize band, centered in the gap
|
|
206
|
+
* (`columns/gaps.ts`, one measurement), and takes the pointer at its
|
|
207
|
+
* block's whole row, shown or not — a pointer there hovers the block, so
|
|
208
|
+
* its grip is reached from any side. Its `+` (compact) and grip stand
|
|
209
|
+
* together flush with its block (Notion's "+ ⋮⋮"), never shrunk: wider
|
|
210
|
+
* than the box, the `+` reaches left under the band (the resizers' layer
|
|
211
|
+
* is above the handles'): where the band's guide shows, at every height,
|
|
212
|
+
* a press resizes; the `+`'s part left of the band takes its own. */
|
|
212
213
|
[data-edytor-block-handle-host][data-over-gap='true'] {
|
|
213
214
|
box-sizing: border-box;
|
|
214
215
|
width: var(--edytor-handle-gap-width, auto);
|
|
@@ -216,6 +217,10 @@
|
|
|
216
217
|
padding-inline-end: 0;
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
[data-edytor-block-handle-host][data-over-gap='true'] > :global(*) {
|
|
221
|
+
flex-shrink: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
219
224
|
[data-edytor-block-handle-host][data-over-gap='true']::before {
|
|
220
225
|
content: '';
|
|
221
226
|
position: absolute;
|
|
@@ -238,27 +243,4 @@
|
|
|
238
243
|
padding-inline-end: 0;
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
|
-
|
|
242
|
-
/* Where a handle's drag lands (`DropIndicator`): the bar (4px) and the nest
|
|
243
|
-
* backdrop, placed in the overlay and shown (`data-shown`) by the plugin;
|
|
244
|
-
* their look is a theme's (`edytor/themes/notion.css`). */
|
|
245
|
-
:global([data-edytor-drop-indicator]),
|
|
246
|
-
:global([data-edytor-drop-backdrop]) {
|
|
247
|
-
position: absolute;
|
|
248
|
-
pointer-events: none;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
:global([data-edytor-drop-indicator]) {
|
|
252
|
-
z-index: 100;
|
|
253
|
-
box-sizing: border-box;
|
|
254
|
-
height: 4px;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
:global([data-edytor-drop-backdrop]) {
|
|
258
|
-
z-index: 99;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
:global([data-edytor-drop-backdrop]:not([data-shown='true'])) {
|
|
262
|
-
visibility: hidden;
|
|
263
|
-
}
|
|
264
246
|
</style>
|
|
@@ -3,6 +3,7 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
|
3
3
|
import BlockHandles from './BlockHandles.svelte';
|
|
4
4
|
import { BlockHandleController } from './BlockHandleController.svelte.js';
|
|
5
5
|
import { hidden } from '../../selection/visibility.js';
|
|
6
|
+
import { isDropTarget } from '../../dnd/placement.js';
|
|
6
7
|
import { all, listen } from '../../utils/listen.js';
|
|
7
8
|
/**
|
|
8
9
|
* Block handles in the overlay, created lazily: a handle mounts
|
|
@@ -123,7 +124,7 @@ export const createBlockHandlesPlugin = (options = {}) => (edytor) => {
|
|
|
123
124
|
},
|
|
124
125
|
onBlockAttached: ({ node, block }) => {
|
|
125
126
|
// A table's rows move by its own row menus, its cells never (`table.fits`).
|
|
126
|
-
if (!
|
|
127
|
+
if (!isDropTarget(edytor, block))
|
|
127
128
|
return;
|
|
128
129
|
const offDropTarget = controller.addDropTarget(node, block);
|
|
129
130
|
// A layout and its columns are drop targets but have no handle (as
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import type { BlockView } from '../../plugins.js';
|
|
4
4
|
import type { Edytor } from '../../edytor.svelte.js';
|
|
5
|
-
import { DEFAULT_CODE_SETTINGS
|
|
5
|
+
import { DEFAULT_CODE_SETTINGS } from './languages.js';
|
|
6
6
|
import { languageMenus } from './languageMenu.svelte.js';
|
|
7
7
|
import { CodeHeader } from './header.svelte.js';
|
|
8
8
|
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
/** The options of the view rendering it (a suggestion's preview included). */
|
|
16
16
|
const edytor = getContext<Edytor>('edytor');
|
|
17
|
-
|
|
18
|
-
const
|
|
17
|
+
// The first code plugin listed owns the view's options (it draws the kind).
|
|
18
|
+
const menu = languageMenus.get(edytor);
|
|
19
|
+
const settings = menu?.settings ?? DEFAULT_CODE_SETTINGS;
|
|
20
|
+
const header = new CodeHeader(() => block, settings, menu);
|
|
19
21
|
$effect(() => header.dispose);
|
|
20
22
|
</script>
|
|
21
23
|
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { codeEnglish, keywordsOf, labelsOf } from '../../labels.js';
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_CODE_SETTINGS,
|
|
16
|
-
codeSettings,
|
|
17
16
|
languageOf,
|
|
18
17
|
tokenizeCode,
|
|
19
18
|
type CodePluginOptions,
|
|
@@ -47,8 +46,9 @@
|
|
|
47
46
|
header: options.header,
|
|
48
47
|
menu: options.menu
|
|
49
48
|
};
|
|
50
|
-
|
|
51
|
-
//
|
|
49
|
+
// The view's code options and language list: the first code plugin
|
|
50
|
+
// listed owns them, as it draws the kind (first listed wins); the
|
|
51
|
+
// header reads them from it.
|
|
52
52
|
if (edytor && !languageMenus.has(edytor))
|
|
53
53
|
languageMenus.set(edytor, new LanguageMenu(edytor, own));
|
|
54
54
|
const ownMenu = () => {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type LanguageDefinition } from '@tanstack/highlight/core';
|
|
9
9
|
import type { JSONText } from '../../utils/json.js';
|
|
10
|
-
import type { Edytor } from '../../edytor.svelte.js';
|
|
11
10
|
import type { Snippet } from 'svelte';
|
|
12
11
|
import type { CodeHeader } from './header.svelte.js';
|
|
13
12
|
import type { LanguageMenu } from './languageMenu.svelte.js';
|
|
@@ -68,8 +67,6 @@ export type CodeSettings = {
|
|
|
68
67
|
menu?: Snippet<[LanguageMenu]>;
|
|
69
68
|
};
|
|
70
69
|
export declare const DEFAULT_CODE_SETTINGS: CodeSettings;
|
|
71
|
-
/** Each view's code options, for its code blocks' header (which reads them through its view). */
|
|
72
|
-
export declare const codeSettings: WeakMap<Edytor, CodeSettings>;
|
|
73
70
|
/** A code block's language id: its `data.language`, else the default. */
|
|
74
71
|
export declare const languageOf: (data: {
|
|
75
72
|
language?: unknown;
|
|
@@ -99,7 +99,5 @@ export const DEFAULT_CODE_SETTINGS = {
|
|
|
99
99
|
languages: CODE_LANGUAGES,
|
|
100
100
|
defaultLanguage: 'javascript'
|
|
101
101
|
};
|
|
102
|
-
/** Each view's code options, for its code blocks' header (which reads them through its view). */
|
|
103
|
-
export const codeSettings = new WeakMap();
|
|
104
102
|
/** A code block's language id: its `data.language`, else the default. */
|
|
105
103
|
export const languageOf = (data, { defaultLanguage }) => (typeof data?.language === 'string' && data.language ? data.language : defaultLanguage);
|