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 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.27`, `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.
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 on the drag preview and the drop indicator. */
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 count = group.length;
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`, and a
25
- * count badge when more than one block moves (the one addition). No card,
26
- * background, shadow, opacity or fade: the blocks' own look. The pointer
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
- }, badge?: HTMLElement) => DragPreview;
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`, and a
162
- * count badge when more than one block moves (the one addition). No card,
163
- * background, shadow, opacity or fade: the blocks' own look. The pointer
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, badge) => {
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 drag-selection from `anchor` (the block its press landed in) to
53
- * `focus` (the block the drag reached), Notion's rule
54
- * (`sel.drag.across-columns`): once the drag leaves the anchor's column for
55
- * another column of the same layout, it selects blocks — every shown block
56
- * from one to the other in reading order, but the layouts and columns
57
- * themselves (each block a member as `selectedMembers` reads it), so a
58
- * sweep over every block of every column lifts to the layout
59
- * (`liftLayouts`, D3). The innermost layout holding both in different
60
- * columns decides. `null` (a text range) while both lie in one column, or
61
- * when no layout holding the anchor holds the focus (a drag leaving the
62
- * layout, or one that starts outside it). Decided from the roles
63
- * (`isLayout`/`isLayoutItem`).
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 drag-selection from `anchor` (the block its press landed in) to
96
- * `focus` (the block the drag reached), Notion's rule
97
- * (`sel.drag.across-columns`): once the drag leaves the anchor's column for
98
- * another column of the same layout, it selects blocks — every shown block
99
- * from one to the other in reading order, but the layouts and columns
100
- * themselves (each block a member as `selectedMembers` reads it), so a
101
- * sweep over every block of every column lifts to the layout
102
- * (`liftLayouts`, D3). The innermost layout holding both in different
103
- * columns decides. `null` (a text range) while both lie in one column, or
104
- * when no layout holding the anchor holds the focus (a drag leaving the
105
- * layout, or one that starts outside it). Decided from the roles
106
- * (`isLayout`/`isLayoutItem`).
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
- let across = false;
112
- for (let item = anchor.parent; item && !item.isRoot && !across; item = item.parent) {
113
- if (!facade.isLayoutItem(item.id))
114
- continue;
115
- if (focus.isChildOf(item))
116
- return null;
117
- across = Boolean(item.parent && focus.isChildOf(item.parent));
118
- }
119
- if (!across)
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 native range from one column into another
694
- * column of the same layout is a block selection (`acrossColumns`,
695
- * Notion): the blocks it covers are selected and the native range is
696
- * ignored — the browser keeps extending it from the press, so the drag
697
- * coming back into its own column is a text range again. Its highlight
698
- * is hidden while the value is a block selection
699
- * (`data-edytor-selection`). Only a range anchored where the press
700
- * landed is the drag's: a Shift+click extends the range it found, which
701
- * stays a text range in document order, as the keyboard's does (D7).
702
- * Answers whether it selected blocks.
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 || anchor.id !== this.pointerDragStart?.text.parent.id)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edytor",
3
- "version": "0.1.0-next.27",
3
+ "version": "0.1.0-next.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/beynar/edytor.git"