@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.
- package/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69667 -41708
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +94 -93
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- 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
|
+
}
|
package/src/page/drag.js
ADDED
|
@@ -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
|
+
}
|
package/src/page/drop.js
ADDED
|
@@ -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
|
+
}
|