@panphora/clayjs 1.13.1 → 1.14.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.
Files changed (136) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69667 -41708
  3. package/dist/server/checkpoint.cjs +70690 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/rooms.cjs +72039 -0
  6. package/dist/server/rooms.mjs +66 -0
  7. package/docs/guide.md +94 -93
  8. package/entries/clay.js +22 -0
  9. package/package.json +16 -2
  10. package/packed-contract.json +17 -3
  11. package/src/core/conflict-download.js +11 -2
  12. package/src/core/host-meta.js +75 -0
  13. package/src/core/persist.js +11 -0
  14. package/src/core/save-core.js +266 -31
  15. package/src/core/save.js +62 -32
  16. package/src/core/snapshot.js +39 -13
  17. package/src/core/style-comparisons.js +37 -0
  18. package/src/dom/insert-style-tag.js +11 -12
  19. package/src/lib/effect.js +130 -0
  20. package/src/lib/style-adoption.js +9 -0
  21. package/src/lib/style.js +44 -0
  22. package/src/live/fast-lane.js +347 -60
  23. package/src/live/local-lane.js +32 -6
  24. package/src/live/ops.js +77 -0
  25. package/src/live/provenance.js +129 -0
  26. package/src/live/save-history.js +188 -0
  27. package/src/live/server-lane.js +9 -2
  28. package/src/live/store.js +194 -16
  29. package/src/loader-logic.js +7 -3
  30. package/src/loader.js +6 -0
  31. package/src/page/address.js +20 -0
  32. package/src/page/adopt-boot.js +189 -0
  33. package/src/page/adopt.js +240 -0
  34. package/src/page/ai-tool.js +188 -0
  35. package/src/page/announce.js +21 -0
  36. package/src/page/blocks.js +1121 -0
  37. package/src/page/blocksel.js +269 -0
  38. package/src/page/board-drag.js +226 -0
  39. package/src/page/board-panel.js +353 -0
  40. package/src/page/board-style.js +146 -0
  41. package/src/page/board-view.js +447 -0
  42. package/src/page/board.js +41 -0
  43. package/src/page/chart.js +220 -0
  44. package/src/page/checklist.js +143 -0
  45. package/src/page/chrome.js +587 -0
  46. package/src/page/csv-import.js +169 -0
  47. package/src/page/drag.js +202 -0
  48. package/src/page/drop.js +112 -0
  49. package/src/page/editor.js +384 -0
  50. package/src/page/highlight.js +268 -0
  51. package/src/page/inline.js +60 -1
  52. package/src/page/items.js +159 -0
  53. package/src/page/link.js +247 -0
  54. package/src/page/markdown.js +71 -0
  55. package/src/page/media.js +570 -0
  56. package/src/page/merge.js +130 -0
  57. package/src/page/model.js +71 -9
  58. package/src/page/placeholder.js +23 -0
  59. package/src/page/projection.js +647 -0
  60. package/src/page/recover.js +193 -0
  61. package/src/page/rich/cell.js +16 -0
  62. package/src/page/rich/chart.js +104 -0
  63. package/src/page/rich/csv.js +118 -0
  64. package/src/page/rich/page-tool.js +86 -0
  65. package/src/page/rich/tool-runtime.js +195 -0
  66. package/src/page/save-projection.js +53 -0
  67. package/src/page/save-states.js +85 -0
  68. package/src/page/schema.js +12 -5
  69. package/src/page/table-kinds.js +20 -0
  70. package/src/page/table-paste.js +57 -0
  71. package/src/page/table-select.js +244 -0
  72. package/src/page/table-style.js +25 -0
  73. package/src/page/table.js +650 -0
  74. package/src/page/themes/content.css +46 -0
  75. package/src/page/themes/light-1.css +269 -0
  76. package/src/page/themes/light-2.css +211 -0
  77. package/src/page/themes/light-3.css +130 -0
  78. package/src/page/themes/light-4.css +299 -0
  79. package/src/page/tool-data.js +25 -0
  80. package/src/page/tool-view.js +68 -0
  81. package/src/page/tools.js +133 -0
  82. package/src/page/view.js +136 -0
  83. package/src/page/ybind/author.js +25 -0
  84. package/src/page/ybind/binding.js +92 -0
  85. package/src/page/ybind/board-commands.js +170 -0
  86. package/src/page/ybind/board.js +89 -0
  87. package/src/page/ybind/bridge.js +382 -0
  88. package/src/page/ybind/conversion.js +26 -0
  89. package/src/page/ybind/core.js +7 -0
  90. package/src/page/ybind/decode.js +208 -0
  91. package/src/page/ybind/doc.js +58 -0
  92. package/src/page/ybind/external.js +190 -0
  93. package/src/page/ybind/group.js +124 -0
  94. package/src/page/ybind/index.js +12 -0
  95. package/src/page/ybind/layout.js +27 -0
  96. package/src/page/ybind/marks.js +18 -0
  97. package/src/page/ybind/migrate.js +152 -0
  98. package/src/page/ybind/moves.js +288 -0
  99. package/src/page/ybind/origins.js +257 -0
  100. package/src/page/ybind/parts.js +44 -0
  101. package/src/page/ybind/projection-map.js +92 -0
  102. package/src/page/ybind/provider.js +285 -0
  103. package/src/page/ybind/recovery.js +411 -0
  104. package/src/page/ybind/seed.js +27 -0
  105. package/src/page/ybind/table.js +166 -0
  106. package/src/page/ybind/tokens.js +93 -0
  107. package/src/page/ybind/walk.js +333 -0
  108. package/src/page/ybind/writer.js +307 -0
  109. package/src/plugins/indicator.js +16 -6
  110. package/src/plugins/live.js +742 -65
  111. package/src/plugins/page.js +547 -0
  112. package/src/plugins/undo.js +60 -3
  113. package/src/server/checkpoint/index.js +218 -0
  114. package/src/server/rooms/index.js +59 -0
  115. package/src/server/rooms/live-data-ops.js +369 -0
  116. package/src/server/rooms/live-data-rooms.js +965 -0
  117. package/src/server/rooms/live-data-tags.js +49 -0
  118. package/src/server/rooms/live-page-history.js +41 -0
  119. package/src/server/rooms/live-provenance.js +142 -0
  120. package/src/server/rooms/page-admission.js +79 -0
  121. package/src/server/rooms/page-conversion.js +62 -0
  122. package/src/server/rooms/page-rebase.js +108 -0
  123. package/src/server/rooms/page-state.js +91 -0
  124. package/src/server/rooms/page-write-admission.js +63 -0
  125. package/src/server/rooms/page-write-guard.js +23 -0
  126. package/src/server/rooms/recent.js +18 -0
  127. package/src/server/rooms/spec-wire.js +17 -0
  128. package/src/standalone.js +22 -0
  129. package/src/sync/live-sync.js +13 -2
  130. package/src/sync/stream.js +62 -16
  131. package/src/ui/index.js +4 -0
  132. package/src/ui/toast.js +21 -5
  133. package/src/vendor/pierre/pierre.vendor.js +3 -0
  134. package/src/vendor/yjs.vendor.js +7 -0
  135. package/src/page/binding.js +0 -647
  136. package/src/page/replay.js +0 -578
@@ -0,0 +1,169 @@
1
+ import { state } from "../vendor/prosemirror.vendor.js";
2
+ import { schema } from "./schema.js";
3
+ import { nodeFromBlock, newId, keyBetween } from "./model.js";
4
+ import { parseCsv, tableFromCsv, CsvError } from "./rich/csv.js";
5
+ import { TABLE_IMPORT_LIMIT, insertTableNode } from "./table.js";
6
+ import { safeFileHref } from "./address.js";
7
+ import { asStep } from "./blocksel.js";
8
+
9
+ const { Plugin } = state;
10
+ const inputs = new WeakMap();
11
+
12
+ const readText = (file) => typeof file.text === "function" ? file.text() : new Promise((resolve, reject) => {
13
+ const reader = new FileReader();
14
+ reader.onload = () => resolve(reader.result);
15
+ reader.onerror = () => reject(reader.error);
16
+ reader.readAsText(file);
17
+ });
18
+
19
+ function error(view, message) {
20
+ const doc = view.dom.ownerDocument;
21
+ let notice = view.dom.parentElement.querySelector(".page-csv-error");
22
+ if (!notice) {
23
+ notice = doc.createElement("div");
24
+ notice.className = "page-csv-error";
25
+ notice.setAttribute("role", "alert");
26
+ view.dom.parentElement.append(notice);
27
+ }
28
+ notice.textContent = message;
29
+ }
30
+
31
+ function tableNode(parsed) {
32
+ let count = 0;
33
+ const data = tableFromCsv(parsed, { maxBytes: TABLE_IMPORT_LIMIT, newId: () => newId(count++ < parsed.header.length ? "c" : "r") });
34
+ if (!Object.keys(data.cols).length) return null;
35
+ if (!Object.keys(data.rows).length) {
36
+ const cells = Object.fromEntries(Object.keys(data.cols).map((id) => [id, ""]));
37
+ data.rows[newId("r")] = { pos: keyBetween(), cells };
38
+ }
39
+ return { node: nodeFromBlock(schema, newId(), { t: "table", pos: null, cols: data.cols, rows: data.rows }), truncated: data.truncated };
40
+ }
41
+
42
+ async function confirmTruncation({ kept, total }) {
43
+ const message = `This file has ${total} rows. The first ${kept} fit in a table.`;
44
+ let confirm = globalThis.clay?.confirm;
45
+ let modal = globalThis.clay?.modal;
46
+ if (!confirm) {
47
+ confirm = (await import("../ui/dialogs.js")).consent;
48
+ modal = (await import("../ui/modal.js")).default;
49
+ }
50
+ const promise = confirm(message);
51
+ if (modal) modal.yes = `Insert first ${kept} rows and attach the file`;
52
+ try { await promise; return true; } catch { return false; }
53
+ }
54
+
55
+ const tableIndex = (doc, id) => {
56
+ let found = -1;
57
+ doc.forEach((node, _offset, index) => { if (found < 0 && node.type === schema.nodes.table && node.attrs.id === id) found = index; });
58
+ return found;
59
+ };
60
+
61
+ const emptyTable = (table) => {
62
+ let empty = true;
63
+ table.descendants((node) => { if (node.isText) empty = false; return empty; });
64
+ return empty;
65
+ };
66
+
67
+ // From a table's menu: an empty table is filled in place (it keeps its id,
68
+ // order and parent), a table with content gets the imported one below it.
69
+ function placeImported(view, node, tableId, attachment) {
70
+ const st = view.state;
71
+ const index = tableIndex(st.doc, tableId);
72
+ if (index < 0) return insertTableNode(st, view.dispatch, node, null, attachment);
73
+ let at = 0;
74
+ for (let i = 0; i < index; i++) at += st.doc.child(i).nodeSize;
75
+ const table = st.doc.child(index);
76
+ if (!emptyTable(table) || attachment) {
77
+ const next = st.doc.maybeChild(index + 1);
78
+ const parent = table.attrs.parent ?? null;
79
+ const pos = keyBetween(table.attrs.pos, next?.attrs.pos ?? null);
80
+ const placed = node.type.create({ ...node.attrs, pos, parent }, node.content);
81
+ const end = at + table.nodeSize;
82
+ const tr = st.tr.insert(end, placed);
83
+ if (attachment) tr.insert(end + placed.nodeSize, attachment.type.create({ ...attachment.attrs, pos: keyBetween(pos, next?.attrs.pos ?? null), parent }, attachment.content));
84
+ tr.setSelection(state.TextSelection.create(tr.doc, end + 3));
85
+ view.dispatch(asStep(tr).scrollIntoView());
86
+ return true;
87
+ }
88
+ const filled = node.type.create({ ...node.attrs, id: table.attrs.id, pos: table.attrs.pos, parent: table.attrs.parent }, node.content);
89
+ const tr = st.tr.replaceWith(at, at + table.nodeSize, filled);
90
+ tr.setSelection(state.TextSelection.create(tr.doc, at + 3));
91
+ view.dispatch(asStep(tr).scrollIntoView());
92
+ return true;
93
+ }
94
+
95
+ export async function importCsv(view, file, { upload, remove = null, table = null } = {}) {
96
+ try {
97
+ view.dom.parentElement.querySelector(".page-csv-error")?.remove();
98
+ const parsed = parseCsv(await readText(file));
99
+ const result = tableNode(parsed);
100
+ if (!result) { error(view, "Couldn't read this CSV: no columns."); return false; }
101
+ let attachment = null;
102
+ if (result.truncated) {
103
+ if (!(await confirmTruncation(result.truncated))) return false;
104
+ const response = await upload?.(file);
105
+ const src = safeFileHref(response?.uploads?.[0]?.url);
106
+ if (!response?.ok || !src) { error(view, "Couldn't attach the original CSV."); return false; }
107
+ attachment = schema.nodes.file.create({ id: newId(), src, name: file.name, size: file.size ?? null, mime: file.type || "text/csv" });
108
+ }
109
+ if (view.isDestroyed) return false;
110
+ if (table) return placeImported(view, result.node, table, attachment);
111
+ const inserted = insertTableNode(view.state, view.dispatch, result.node, remove, attachment);
112
+ return inserted;
113
+ } catch (cause) {
114
+ if (cause instanceof CsvError) error(view, `Couldn't read this CSV: unclosed quote on line ${cause.line}.`);
115
+ else error(view, "Couldn't read this CSV.");
116
+ return false;
117
+ }
118
+ }
119
+
120
+ export function pickCsv(view, remove = null, table = null) {
121
+ const input = inputs.get(view);
122
+ if (!input) return false;
123
+ input._remove = remove;
124
+ input._table = table;
125
+ input.click();
126
+ return true;
127
+ }
128
+
129
+ const csvFiles = (files) => [...files].filter((file) => /\.csv$/i.test(file.name || "") || /^(text\/csv|application\/csv)$/i.test(file.type || ""));
130
+
131
+ export function csvImportPlugin({ upload }) {
132
+ return new Plugin({
133
+ view(view) {
134
+ const input = view.dom.ownerDocument.createElement("input");
135
+ input.type = "file";
136
+ input.accept = ".csv,text/csv";
137
+ input.hidden = true;
138
+ input.className = "page-csv-input";
139
+ view.dom.parentElement.append(input);
140
+ inputs.set(view, input);
141
+ input.addEventListener("change", () => {
142
+ const file = input.files?.[0];
143
+ if (file) importCsv(view, file, { upload, remove: input._remove, table: input._table });
144
+ input._remove = null;
145
+ input._table = null;
146
+ input.value = "";
147
+ });
148
+ return { destroy() { inputs.delete(view); input.remove(); } };
149
+ },
150
+ props: {
151
+ handlePaste(view, event) {
152
+ const files = csvFiles(event.clipboardData?.files || []);
153
+ if (!files.length) return false;
154
+ event.preventDefault();
155
+ importCsv(view, files[0], { upload });
156
+ return true;
157
+ },
158
+ handleDrop(view, event, _slice, moved) {
159
+ const files = csvFiles(event.dataTransfer?.files || []);
160
+ if (moved || !files.length) return false;
161
+ event.preventDefault();
162
+ const hit = view.posAtCoords({ left: event.clientX, top: event.clientY });
163
+ if (hit) view.dispatch(view.state.tr.setSelection(state.TextSelection.near(view.state.doc.resolve(hit.pos))));
164
+ importCsv(view, files[0], { upload });
165
+ return true;
166
+ },
167
+ },
168
+ });
169
+ }
@@ -0,0 +1,202 @@
1
+ import { outline, siblingSpan, BlockSelection, captureSpan, resolveSpan } from "./blocks.js";
2
+ import { selectBlocks, asStep } from "./blocksel.js";
3
+ import { gapTargets, summaryTarget, pickTarget, moveSpanTo } from "./drop.js";
4
+ import { announce } from "./announce.js";
5
+
6
+ const SLOP = 4;
7
+ const EDGE = 56;
8
+ const MAX_SPEED = 18;
9
+
10
+ export function scrollParent(el) {
11
+ for (let n = el.parentElement; n; n = n.parentElement) {
12
+ const o = getComputedStyle(n).overflowY;
13
+ if ((o === "auto" || o === "scroll") && n.scrollHeight > n.clientHeight) return n;
14
+ }
15
+ return el.ownerDocument.scrollingElement;
16
+ }
17
+
18
+ export class BlockDrag {
19
+ constructor(view, grip, { index, onClick }) {
20
+ this.view = view;
21
+ this.grip = grip;
22
+ this.index = index;
23
+ this.onClick = onClick;
24
+ this.shell = view.dom.parentElement;
25
+ this.line = document.createElement("div");
26
+ this.line.className = "page-drop";
27
+ this.line.hidden = true;
28
+ this.line.setAttribute("aria-hidden", "true");
29
+ this.shell.append(this.line);
30
+ this.state = null;
31
+ this.onMove = (e) => this.move(e);
32
+ this.onUp = (e) => this.end(e);
33
+ this.onCancel = (e) => { if (e.pointerId === this.state?.pointer) this.cancel(); };
34
+ this.onKey = (e) => {
35
+ if (e.key === "Escape" && this.state?.active) {
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ this.cancel();
39
+ }
40
+ };
41
+ grip.addEventListener("pointerdown", (e) => this.start(e));
42
+ }
43
+
44
+ start(event) {
45
+ if (event.button !== 0 || this.state) return;
46
+ const st = this.view.state, ents = outline(st.doc), sel = st.selection;
47
+ const index = Math.min(this.index(), ents.length - 1);
48
+ const inSel = sel instanceof BlockSelection && ents[index].at >= sel.from && ents[index].end <= sel.to;
49
+ const { lo, hi } = inSel ? sel.span(st.doc) : siblingSpan(ents, index, index);
50
+ this.state = {
51
+ index, span: captureSpan(ents, lo, hi), touch: event.pointerType === "touch",
52
+ pointer: event.pointerId, x0: event.clientX, y0: event.clientY, x: event.clientX, y: event.clientY,
53
+ active: false, raf: 0, gap: undefined, targets: [], target: null,
54
+ };
55
+ this.grip.setPointerCapture?.(event.pointerId);
56
+ this.grip.addEventListener("pointermove", this.onMove);
57
+ this.grip.addEventListener("pointerup", this.onUp);
58
+ this.grip.addEventListener("pointercancel", this.onCancel);
59
+ this.grip.addEventListener("lostpointercapture", this.onCancel);
60
+ event.preventDefault();
61
+ }
62
+
63
+ move(event) {
64
+ const s = this.state;
65
+ if (!s || event.pointerId !== s.pointer) return;
66
+ s.x = event.clientX;
67
+ s.y = event.clientY;
68
+ if (!s.active) {
69
+ if (Math.hypot(s.x - s.x0, s.y - s.y0) <= SLOP) return;
70
+ if (s.touch) return this.stop();
71
+ this.begin();
72
+ }
73
+ this.track();
74
+ }
75
+
76
+ begin() {
77
+ const s = this.state;
78
+ if (!s || s.active) return;
79
+ const st = this.view.state, ents = outline(st.doc);
80
+ const span = resolveSpan(ents, s.span);
81
+ if (!span) return this.cancel();
82
+ s.active = true;
83
+ this.view.dispatch(st.tr.setSelection(selectBlocks(st.doc, span.lo, span.hi)));
84
+ this.shell.classList.add("is-dragging");
85
+ const probe = this.shell.appendChild(document.createElement("div"));
86
+ probe.style.cssText = "position:absolute;visibility:hidden;width:var(--page-indent)";
87
+ this.indentPx = probe.getBoundingClientRect().width || 27;
88
+ probe.remove();
89
+ this.scroller = scrollParent(this.view.dom);
90
+ window.addEventListener("keydown", this.onKey, true);
91
+ this.loop();
92
+ this.track();
93
+ }
94
+
95
+ track() {
96
+ const s = this.state, view = this.view, st = view.state;
97
+ if (!s) return;
98
+ const ents = outline(st.doc), span = resolveSpan(ents, s.span);
99
+ if (!span) return this.cancel();
100
+ const { lo, hi } = span;
101
+ let next = -1, line = null, over = null;
102
+ for (const e of ents) {
103
+ if (e.hidden) continue;
104
+ const dom = view.nodeDOM(e.at);
105
+ if (!dom?.getBoundingClientRect) continue;
106
+ const r = dom.getBoundingClientRect();
107
+ if (s.y >= r.top + r.height / 4 && s.y <= r.bottom - r.height / 4) over = { i: e.i, bottom: r.bottom };
108
+ if (s.y < r.top + r.height / 2) { next = e.i; line = r.top - 2; break; }
109
+ line = r.bottom + 2;
110
+ }
111
+ const inside = lo < 0 ? null : over && summaryTarget(st, ents, lo, hi, over.i);
112
+ const key = inside ? `summary:${over.i}` : next;
113
+ if (key !== s.gap || st.doc !== s.doc) {
114
+ s.gap = key;
115
+ s.doc = st.doc;
116
+ s.targets = lo < 0 ? [] : inside ? [inside] : gapTargets(st, ents, lo, hi, next);
117
+ }
118
+ if (inside) line = over.bottom + 2;
119
+ const edLeft = view.dom.getBoundingClientRect().left;
120
+ s.target = pickTarget(s.targets, (s.x - edLeft) / this.indentPx);
121
+ const shellBox = this.shell.getBoundingClientRect();
122
+ if (!s.target || line === null) {
123
+ this.line.hidden = true;
124
+ this.shell.dataset.drop = "none";
125
+ return;
126
+ }
127
+ this.shell.dataset.drop = "ok";
128
+ this.line.hidden = false;
129
+ this.line.style.setProperty("--page-drop-y", `${line - shellBox.top}px`);
130
+ this.line.style.setProperty("--page-drop-x", `${edLeft - shellBox.left + s.target.depth * this.indentPx}px`);
131
+ this.line.style.setProperty("--page-drop-w", `${view.dom.getBoundingClientRect().right - edLeft - s.target.depth * this.indentPx}px`);
132
+ }
133
+
134
+ loop() {
135
+ const step = () => {
136
+ const s = this.state;
137
+ if (!s?.active) return;
138
+ const sc = this.scroller;
139
+ const box = sc === sc.ownerDocument.scrollingElement ? { top: 0, bottom: window.innerHeight } : sc.getBoundingClientRect();
140
+ let v = 0;
141
+ if (s.y < box.top + EDGE) v = -MAX_SPEED * Math.min(1, (box.top + EDGE - s.y) / EDGE);
142
+ else if (s.y > box.bottom - EDGE) v = MAX_SPEED * Math.min(1, (s.y - (box.bottom - EDGE)) / EDGE);
143
+ if (v) {
144
+ sc.scrollTop += v;
145
+ this.track();
146
+ }
147
+ s.raf = requestAnimationFrame(step);
148
+ };
149
+ this.state.raf = requestAnimationFrame(step);
150
+ }
151
+
152
+ end(event) {
153
+ const s = this.state;
154
+ if (!s || event.pointerId !== s.pointer) return;
155
+ if (!s.active) {
156
+ this.stop();
157
+ if (event.type === "pointerup") this.onClick(s.index);
158
+ return;
159
+ }
160
+ this.track();
161
+ if (this.state !== s) return;
162
+ const target = s.target;
163
+ const st = this.view.state, ents = outline(st.doc);
164
+ const span = resolveSpan(ents, s.span);
165
+ this.stop();
166
+ if (!target || !span) return;
167
+ const { lo, hi } = span;
168
+ const same = !target.open && (target.at === ents[lo].at || target.at === ents[hi].end) && target.parent === (ents[lo].node.attrs.parent ?? null)
169
+ && (ents[lo].node.type.name !== "list_item" || target.indent === ents[lo].node.attrs.indent);
170
+ if (same) return;
171
+ this.view.dispatch(asStep(moveSpanTo(st, ents, lo, hi, target)).scrollIntoView());
172
+ const n = hi - lo + 1;
173
+ announce(this.view, `Moved ${n} ${n === 1 ? "block" : "blocks"}`);
174
+ this.view.focus();
175
+ }
176
+
177
+ cancel() {
178
+ this.stop();
179
+ announce(this.view, "Move cancelled");
180
+ }
181
+
182
+ stop() {
183
+ const s = this.state;
184
+ if (!s) return;
185
+ this.state = null;
186
+ cancelAnimationFrame(s.raf);
187
+ this.grip.removeEventListener("pointermove", this.onMove);
188
+ this.grip.removeEventListener("pointerup", this.onUp);
189
+ this.grip.removeEventListener("pointercancel", this.onCancel);
190
+ this.grip.removeEventListener("lostpointercapture", this.onCancel);
191
+ this.grip.releasePointerCapture?.(s.pointer);
192
+ window.removeEventListener("keydown", this.onKey, true);
193
+ this.shell.classList.remove("is-dragging");
194
+ delete this.shell.dataset.drop;
195
+ this.line.hidden = true;
196
+ }
197
+
198
+ destroy() {
199
+ this.stop();
200
+ this.line.remove();
201
+ }
202
+ }
@@ -0,0 +1,112 @@
1
+ import { outline, structureFixes, moveSpanTr, BlockSelection, MAX_INDENT, MOVE } from "./blocks.js";
2
+ import { schema } from "./schema.js";
3
+
4
+ const N = schema.nodes;
5
+ const isList = (n) => n.type === N.list_item;
6
+ const isToggle = (n) => n.type === N.toggle;
7
+ const parentOf = (n) => n.attrs.parent ?? null;
8
+
9
+ function chainAfter(ents, byId, k) {
10
+ if (k < 0) return [null];
11
+ const chain = [];
12
+ const n = ents[k].node;
13
+ if (isToggle(n) && n.attrs.open && !ents[k].hidden) chain.unshift(n.attrs.id);
14
+ for (let p = parentOf(n); p !== null && !chain.includes(p);) {
15
+ const e = ents[byId.get(p)];
16
+ if (!e) break;
17
+ chain.unshift(p);
18
+ p = parentOf(e.node);
19
+ }
20
+ const out = [null];
21
+ for (const id of chain) {
22
+ const e = ents[byId.get(id)];
23
+ if (!e.node.attrs.open || e.hidden) break;
24
+ out.push(id);
25
+ }
26
+ return out;
27
+ }
28
+
29
+ export function spanAttrs(ents, lo, hi, target) {
30
+ const rootParent = parentOf(ents[lo].node);
31
+ const delta = isList(ents[lo].node) ? target.indent - ents[lo].node.attrs.indent : 0;
32
+ return (e) => {
33
+ if (e.i < lo || e.i > hi || parentOf(e.node) !== rootParent) return null;
34
+ return isList(e.node) ? { parent: target.parent, indent: e.node.attrs.indent + delta } : { parent: target.parent };
35
+ };
36
+ }
37
+
38
+ function openToggle(tr, id) {
39
+ let hit = null;
40
+ tr.doc.forEach((n, off) => { if (n.attrs.id === id && isToggle(n)) hit = { n, off }; });
41
+ if (hit && !hit.n.attrs.open) tr.setNodeMarkup(hit.off, null, { ...hit.n.attrs, open: true });
42
+ }
43
+
44
+ export function moveSpanTo(st, ents, lo, hi, target, sel = null) {
45
+ const attrs = spanAttrs(ents, lo, hi, target);
46
+ const from = ents[lo].at, to = ents[hi].end;
47
+ const pick = sel ?? new BlockSelection(st.doc.resolve(from), st.doc.resolve(to));
48
+ let tr;
49
+ if (target.at === from || target.at === to) {
50
+ tr = st.tr;
51
+ for (let k = lo; k <= hi; k++) {
52
+ const c = attrs(ents[k]);
53
+ if (c) tr.setNodeMarkup(ents[k].at, null, { ...ents[k].node.attrs, ...c });
54
+ }
55
+ tr.setSelection(pick.map(tr.doc, tr.mapping));
56
+ } else {
57
+ tr = moveSpanTr(st, ents, lo, hi, target.at, attrs, pick);
58
+ }
59
+ if (target.open) {
60
+ openToggle(tr, target.open);
61
+ const move = tr.getMeta(MOVE);
62
+ if (move) tr.setMeta(MOVE, { ...move, attrs: { ...move.attrs, [target.open]: { open: true } } });
63
+ }
64
+ return tr;
65
+ }
66
+
67
+ function legal(st, ents, lo, hi, target) {
68
+ if (target.parent != null && ents.slice(lo, hi + 1).some((e) => e.node.attrs.id === target.parent)) return false;
69
+ const size = ents[hi].end - ents[lo].at;
70
+ const attrs = spanAttrs(ents, lo, hi, target);
71
+ for (let k = lo; k <= hi; k++) if (isList(ents[k].node) && (attrs(ents[k])?.indent ?? ents[k].node.attrs.indent) > MAX_INDENT) return false;
72
+ const tr = moveSpanTo(st, ents, lo, hi, target);
73
+ const inPlace = target.at === ents[lo].at || target.at === ents[hi].end;
74
+ const at = inPlace ? ents[lo].at : target.at > ents[lo].at ? target.at - size : target.at;
75
+ return structureFixes(tr.doc, st.doc).every((f) => f.at >= at && f.at < at + size);
76
+ }
77
+
78
+ export function gapTargets(st, ents, lo, hi, next) {
79
+ const byId = new Map();
80
+ ents.forEach((e) => { if (isToggle(e.node) && e.node.attrs.id) byId.set(e.node.attrs.id, e.i); });
81
+ const at = next < 0 ? st.doc.content.size : ents[next].at;
82
+ if (at > ents[lo].at && at < ents[hi].end) return [];
83
+ const k = (next < 0 ? ents.length : next) - 1;
84
+ const chain = chainAfter(ents, byId, k);
85
+ const start = chain.indexOf(next < 0 ? null : parentOf(ents[next].node));
86
+ const out = [];
87
+ for (const parent of chain.slice(Math.max(0, start))) {
88
+ const base = parent === null ? 0 : ents[byId.get(parent)].depth + 1;
89
+ const prev = k >= 0 && isList(ents[k].node) && parentOf(ents[k].node) === parent ? ents[k].node : null;
90
+ const top = isList(ents[lo].node) ? (prev ? prev.attrs.indent + 1 : 0) : 0;
91
+ for (let indent = 0; indent <= top; indent++) {
92
+ const t = { at, parent, indent, depth: base + (isList(ents[lo].node) ? indent : 0) };
93
+ if (legal(st, ents, lo, hi, t)) out.push(t);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+
99
+ export function summaryTarget(st, ents, lo, hi, i) {
100
+ const e = ents[i];
101
+ if (!e || !isToggle(e.node) || e.node.attrs.open || e.hidden || (i >= lo && i <= hi)) return null;
102
+ if (e.at > ents[lo].at && e.at < ents[hi].end) return null;
103
+ const id = e.node.attrs.id;
104
+ const target = { at: ents[e.subEnd].end, parent: id, indent: 0, depth: e.depth + 1, open: id };
105
+ return legal(st, ents, lo, hi, target) ? target : null;
106
+ }
107
+
108
+ export function pickTarget(targets, depth) {
109
+ let best = null;
110
+ for (const t of targets) if (!best || Math.abs(t.depth - depth) < Math.abs(best.depth - depth)) best = t;
111
+ return best;
112
+ }