@panphora/clayjs 1.13.0 → 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 +69739 -41784
- 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 +95 -94
- 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/plugins/visitor-drafts/prompt.js +4 -6
- 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,307 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { schema } from "../schema.js";
|
|
3
|
+
import { attrKeys, defaultOf, sameJson, idKey, isHigh, isLow, mintOp } from "./layout.js";
|
|
4
|
+
import { markName, isCancel } from "./marks.js";
|
|
5
|
+
import { sameTok } from "./tokens.js";
|
|
6
|
+
import { removeKey, attrKey, textDeleteKey, recoveredSource } from "./recovery.js";
|
|
7
|
+
import { resolveTableFields, tableSeed, writeTable } from "./table.js";
|
|
8
|
+
import { resolveBoardFields, writeBoard } from "./board.js";
|
|
9
|
+
import { createOriginIndex } from "./origins.js";
|
|
10
|
+
|
|
11
|
+
export function setAttrs(pd, id, t, attrs) {
|
|
12
|
+
let m = pd.attrs.get(id);
|
|
13
|
+
if (!m) { m = new Y.Map(); pd.attrs.set(id, m); }
|
|
14
|
+
const type = schema.nodes[t];
|
|
15
|
+
const table = t === "table" && m.get("t") === "table" && !recoveredSource(id);
|
|
16
|
+
const board = t === "board" && !recoveredSource(id);
|
|
17
|
+
const before = table ? resolveTableFields(pd.tableFields, id, tableSeed(pd, id)) : board ? resolveBoardFields(pd.boardFields, id) : null;
|
|
18
|
+
if (m.get("t") !== t) m.set("t", t);
|
|
19
|
+
for (const k of attrKeys(type)) {
|
|
20
|
+
if (table && (k === "rows" || k === "cols")) continue;
|
|
21
|
+
if (board && (k === "columns" || k === "cards")) continue;
|
|
22
|
+
const cur = m.has(k) ? m.get(k) : defaultOf(type, k);
|
|
23
|
+
if (!sameJson(cur, attrs[k])) m.set(k, attrs[k]);
|
|
24
|
+
}
|
|
25
|
+
if (table) writeTable(pd, id, before, attrs);
|
|
26
|
+
if (board) writeBoard(pd, id, before, attrs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ctxAt(pd, yIdx) {
|
|
30
|
+
const attrs = new Map();
|
|
31
|
+
let left = yIdx;
|
|
32
|
+
for (let it = pd.flow._start; it && left > 0; it = it.right) {
|
|
33
|
+
if (it.deleted) continue;
|
|
34
|
+
const c = it.content;
|
|
35
|
+
if (c instanceof Y.ContentFormat) { if (c.value === null) attrs.delete(c.key); else attrs.set(c.key, c.value); }
|
|
36
|
+
else if (it.countable) left -= it.length;
|
|
37
|
+
}
|
|
38
|
+
return attrs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function liveKeys(ctx) {
|
|
42
|
+
const cancelled = new Set();
|
|
43
|
+
for (const [k, v] of ctx) if (isCancel(k) && v) cancelled.add(k.split("!")[0]);
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const [k, v] of ctx) if (!isCancel(k) && v !== null && v !== undefined && !cancelled.has(k) && schema.marks[markName(k)]) out.push([k, v]);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function runAttributes(pd, ctx, want) {
|
|
50
|
+
const rid = mintOp(pd);
|
|
51
|
+
const changes = {};
|
|
52
|
+
const have = liveKeys(ctx);
|
|
53
|
+
const kept = new Set();
|
|
54
|
+
for (const [k, v] of have) {
|
|
55
|
+
const n = markName(k);
|
|
56
|
+
const w = want[n];
|
|
57
|
+
if (w !== undefined && sameJson(w === true ? true : w, v)) kept.add(n);
|
|
58
|
+
else changes[`${k}!${rid}`] = true;
|
|
59
|
+
}
|
|
60
|
+
for (const [n, w] of Object.entries(want)) if (!kept.has(n)) changes[`${n}~${mintOp(pd)}`] = w;
|
|
61
|
+
if (!Object.keys(changes).length) return undefined;
|
|
62
|
+
return { ...Object.fromEntries(ctx), ...changes };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const CHUNK_CHARS = 2000;
|
|
66
|
+
|
|
67
|
+
export function chunksOf(toks) {
|
|
68
|
+
const out = [];
|
|
69
|
+
let cur = [], n = 0;
|
|
70
|
+
for (let q = 0; q < toks.length; q++) {
|
|
71
|
+
cur.push(toks[q]);
|
|
72
|
+
n += 1;
|
|
73
|
+
if (n >= CHUNK_CHARS && q + 1 < toks.length) {
|
|
74
|
+
const t = toks[q];
|
|
75
|
+
if (t.k === "c" && isHigh(t.ch.charCodeAt(0))) continue;
|
|
76
|
+
out.push(cur);
|
|
77
|
+
cur = [];
|
|
78
|
+
n = 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (cur.length) out.push(cur);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function insertTokens(pd, yIdx, toks, tx = (fn) => fn(), fences = null) {
|
|
86
|
+
let i = yIdx;
|
|
87
|
+
for (const part of chunksOf(toks)) tx(() => { i += insertChunk(pd, i, part, fences); });
|
|
88
|
+
return i - yIdx;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fenceAfter = (pd, yIdx, fences) => {
|
|
92
|
+
if (!fences || !fences.length) return null;
|
|
93
|
+
let left = yIdx, it = pd.flow._start;
|
|
94
|
+
while (it && left > 0) {
|
|
95
|
+
if (!it.deleted && it.countable) { if (left < it.length) return null; left -= it.length; }
|
|
96
|
+
it = it.right;
|
|
97
|
+
}
|
|
98
|
+
for (; it && (it.deleted || !it.countable); it = it.right) {
|
|
99
|
+
if (it.deleted && fences.some((f) => f.client === it.id.client && f.clock >= it.id.clock && f.clock < it.id.clock + it.length)) return it;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Yjs places an insertion after every deleted item to its right. A hidden range that starts at a
|
|
105
|
+
// deleted item would then swallow text typed just before it, so the fence counts as live for the call.
|
|
106
|
+
const beforeFence = (pd, yIdx, fences, fn) => {
|
|
107
|
+
const fence = fenceAfter(pd, yIdx, fences);
|
|
108
|
+
if (!fence) return fn();
|
|
109
|
+
fence.deleted = false;
|
|
110
|
+
try { return fn(); } finally { fence.deleted = true; if (pd.flow._searchMarker) pd.flow._searchMarker.length = 0; }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function insertChunk(pd, yIdx, toks, fences = null) {
|
|
114
|
+
let i = yIdx, run = "", runMk = null, runMa = null;
|
|
115
|
+
const flush = () => {
|
|
116
|
+
if (!run) return;
|
|
117
|
+
const attrs = runAttributes(pd, ctxAt(pd, i), runMa ?? {});
|
|
118
|
+
beforeFence(pd, i, fences, () => pd.flow.insert(i, run, attrs));
|
|
119
|
+
i += run.length;
|
|
120
|
+
run = "";
|
|
121
|
+
};
|
|
122
|
+
const embed = (e) => { const attrs = Object.fromEntries(ctxAt(pd, i)); beforeFence(pd, i, fences, () => pd.flow.insertEmbed(i, e, attrs)); i += 1; };
|
|
123
|
+
for (const t of toks) {
|
|
124
|
+
if (t.k === "c") { if (runMk !== null && t.mk !== runMk) flush(); runMk = t.mk; runMa = t.ma; run += t.ch; }
|
|
125
|
+
else if (t.k === "h") { flush(); runMk = null; embed({ br: 1 }); }
|
|
126
|
+
else {
|
|
127
|
+
flush(); runMk = null;
|
|
128
|
+
embed({ b: t.id });
|
|
129
|
+
setAttrs(pd, t.id, t.t, t.attrs);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
flush();
|
|
133
|
+
return i - yIdx;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const recordTextDeletes = (pd, ctx, from, to) => {
|
|
137
|
+
let at = 0;
|
|
138
|
+
for (let it = pd.flow._start; it && at < to; it = it.right) {
|
|
139
|
+
if (it.deleted || !it.countable) continue;
|
|
140
|
+
const start = at;
|
|
141
|
+
at += it.length;
|
|
142
|
+
if (at <= from) continue;
|
|
143
|
+
if (it.content instanceof Y.ContentEmbed && it.content.embed?.b !== undefined) continue;
|
|
144
|
+
const lo = Math.max(0, from - start);
|
|
145
|
+
const hi = Math.min(it.length, to - start);
|
|
146
|
+
if (hi <= lo) continue;
|
|
147
|
+
pd.gone.set(textDeleteKey(it.id.client, it.id.clock + lo, ctx.operationId), { length: hi - lo, actor: pd.ydoc.clientID, sv: ctx.sv });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export function deletePieces(pd, map, ranges, ctx = null) {
|
|
152
|
+
for (let r = ranges.length - 1; r >= 0; r--) {
|
|
153
|
+
const [ka, kb] = ranges[r];
|
|
154
|
+
if (kb <= ka) continue;
|
|
155
|
+
const spans = map.physicalRanges(ka, kb);
|
|
156
|
+
for (let q = spans.length - 1; q >= 0; q--) {
|
|
157
|
+
const [a, b] = spans[q];
|
|
158
|
+
if (b <= a) continue;
|
|
159
|
+
if (ctx) recordTextDeletes(pd, ctx, a, b);
|
|
160
|
+
pd.flow.delete(a, b - a);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function nextDistinct(pd, sourceKey, blockId) {
|
|
166
|
+
let seen = false;
|
|
167
|
+
for (let it = pd.flow._start; it; it = it.right) {
|
|
168
|
+
const key = idKey(it.id);
|
|
169
|
+
if (key === sourceKey) { seen = true; continue; }
|
|
170
|
+
if (!seen) continue;
|
|
171
|
+
const embed = it.content instanceof Y.ContentEmbed ? it.content.embed : null;
|
|
172
|
+
if (embed && embed.b !== undefined && embed.b !== blockId) return key;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function applyRegion(ctx, ta, oldToks, newToks, oldSurvives = null) {
|
|
178
|
+
const { pd, map, bt } = ctx;
|
|
179
|
+
const survives = (j) => oldSurvives === null || oldSurvives[j];
|
|
180
|
+
let p = 0;
|
|
181
|
+
while (p < oldToks.length && p < newToks.length && sameTok(oldToks[p], newToks[p]) && survives(p)) p++;
|
|
182
|
+
let s = 0;
|
|
183
|
+
while (s < oldToks.length - p && s < newToks.length - p && sameTok(oldToks[oldToks.length - 1 - s], newToks[newToks.length - 1 - s]) && survives(oldToks.length - 1 - s)) s++;
|
|
184
|
+
const charAt = (t) => (t && t.k === "c" ? t.ch.charCodeAt(0) : -1);
|
|
185
|
+
if (p > 0 && isHigh(charAt(oldToks[p - 1])) && (isLow(charAt(oldToks[p])) || isLow(charAt(newToks[p])))) p--;
|
|
186
|
+
if (s > 0 && isLow(charAt(oldToks[oldToks.length - s])) && (isHigh(charAt(oldToks[oldToks.length - s - 1])) || isHigh(charAt(newToks[newToks.length - s - 1])))) s--;
|
|
187
|
+
const om = oldToks.slice(p, oldToks.length - s), nm = newToks.slice(p, newToks.length - s);
|
|
188
|
+
const start = ta + p;
|
|
189
|
+
const end = start + om.length;
|
|
190
|
+
const obs = om.filter((t) => t.k === "b").map((t) => t.id).join(",");
|
|
191
|
+
const sameBreaks = om.some((t) => t.k === "b") && obs === nm.filter((t) => t.k === "b").map((t) => t.id).join(",");
|
|
192
|
+
if (!sameBreaks) {
|
|
193
|
+
const origins = createOriginIndex(pd.origins);
|
|
194
|
+
try {
|
|
195
|
+
for (const t of om) {
|
|
196
|
+
if (t.k !== "b" || !t.brk) continue;
|
|
197
|
+
const source = origins.resolve(t.brk);
|
|
198
|
+
if (!origins.boundaryOf(source)) ctx.tx(() => origins.recordBoundary(source, { blockId: t.id, kind: "removed" }));
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
origins.dispose();
|
|
202
|
+
}
|
|
203
|
+
const softAt = new Set();
|
|
204
|
+
om.forEach((t, j) => {
|
|
205
|
+
if (t.k !== "b" || !t.brk) return;
|
|
206
|
+
const abs = start + j;
|
|
207
|
+
let next = abs + 1;
|
|
208
|
+
while (next < bt.length && bt[next].k !== "b") next++;
|
|
209
|
+
const whole = next <= end && next !== abs + 1;
|
|
210
|
+
const nextBrk = next < bt.length ? bt[next].brk : null;
|
|
211
|
+
if (!whole) return;
|
|
212
|
+
ctx.tx(() => pd.gone.set(removeKey(idKey(t.brk), ctx.operationId), { blockId: t.id, end: nextBrk ? idKey(nextBrk) : null, sv: ctx.sv }));
|
|
213
|
+
softAt.add(abs);
|
|
214
|
+
});
|
|
215
|
+
const keep = [];
|
|
216
|
+
let from = start;
|
|
217
|
+
for (let q = start; q < end; q++) if (softAt.has(q)) { if (q > from) keep.push([from, q]); from = q + 1; }
|
|
218
|
+
if (end > from) keep.push([from, end]);
|
|
219
|
+
ctx.tx(() => deletePieces(pd, map, keep, om.some((t) => t.k === "b") ? null : ctx));
|
|
220
|
+
const kept = new Set(nm.filter((t) => t.k === "b").map((t) => t.id));
|
|
221
|
+
for (const t of om) {
|
|
222
|
+
if (t.k !== "b" || !t.brk || kept.has(t.id)) continue;
|
|
223
|
+
for (const key of ctx.copies.get(t.id) ?? []) {
|
|
224
|
+
const removal = removeKey(key, ctx.operationId);
|
|
225
|
+
if (!pd.gone.has(removal)) ctx.tx(() => pd.gone.set(removal, { blockId: t.id, end: nextDistinct(pd, key, t.id), sv: ctx.sv }));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (nm.length) insertTokens(pd, map.toPhysical(start, "before"), nm, ctx.tx, ctx.fences);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const dels = [];
|
|
232
|
+
const inss = [];
|
|
233
|
+
const attrs = [];
|
|
234
|
+
let cursor = start, oi = 0, ni = 0;
|
|
235
|
+
while (oi < om.length || ni < nm.length) {
|
|
236
|
+
let ob = oi; while (ob < om.length && om[ob].k !== "b") ob++;
|
|
237
|
+
let nb = ni; while (nb < nm.length && nm[nb].k !== "b") nb++;
|
|
238
|
+
const o = om.slice(oi, ob), n = nm.slice(ni, nb);
|
|
239
|
+
if (o.length) dels.push([cursor, cursor + o.length]);
|
|
240
|
+
if (n.length) inss.push({ at: cursor, toks: n });
|
|
241
|
+
cursor += o.length;
|
|
242
|
+
if (ob < om.length) { attrs.push([om[ob], nm[nb]]); cursor += 1; }
|
|
243
|
+
oi = ob + (ob < om.length ? 1 : 0); ni = nb + (nb < nm.length ? 1 : 0);
|
|
244
|
+
if (ob >= om.length && nb >= nm.length) break;
|
|
245
|
+
}
|
|
246
|
+
const events = [...dels.map((d) => ({ at: d[0], d })), ...inss.map((i) => ({ at: i.at, i }))].sort((x, y) => y.at - x.at || (x.d ? -1 : 1));
|
|
247
|
+
for (const e of events) {
|
|
248
|
+
if (e.d) ctx.tx(() => deletePieces(pd, map, [e.d], ctx));
|
|
249
|
+
else insertTokens(pd, map.toPhysical(e.at, "before"), e.i.toks, ctx.tx, ctx.fences);
|
|
250
|
+
}
|
|
251
|
+
for (const [ob, nb] of attrs) ctx.tx(() => {
|
|
252
|
+
const source = ctx.recovered.get(ob.id);
|
|
253
|
+
if (source) {
|
|
254
|
+
if (ob.t !== nb.t) pd.gone.set(attrKey(source, "t"), nb.t);
|
|
255
|
+
for (const kk of attrKeys(schema.nodes[nb.t])) {
|
|
256
|
+
if (!sameJson(ob.attrs[kk], nb.attrs[kk])) pd.gone.set(attrKey(source, kk), nb.attrs[kk]);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const base = ob.id;
|
|
261
|
+
const m = pd.attrs.get(base);
|
|
262
|
+
if (m) {
|
|
263
|
+
const table = ob.t === "table" && nb.t === "table" && m.get("t") === "table";
|
|
264
|
+
const board = nb.t === "board";
|
|
265
|
+
const boardBefore = board ? (ob.t === "board" && m.get("t") === "board" ? ob.attrs : resolveBoardFields(pd.boardFields, base)) : null;
|
|
266
|
+
if (ob.t !== nb.t) m.set("t", nb.t);
|
|
267
|
+
for (const kk of attrKeys(schema.nodes[nb.t])) {
|
|
268
|
+
if (table && (kk === "rows" || kk === "cols")) continue;
|
|
269
|
+
if (board && (kk === "columns" || kk === "cards")) continue;
|
|
270
|
+
const cur = m.has(kk) ? m.get(kk) : defaultOf(schema.nodes[nb.t], kk);
|
|
271
|
+
if (!sameJson(cur, nb.attrs[kk])) m.set(kk, nb.attrs[kk]);
|
|
272
|
+
}
|
|
273
|
+
if (table) writeTable(pd, base, ob.attrs, nb.attrs);
|
|
274
|
+
if (board) writeBoard(pd, base, boardBefore, nb.attrs);
|
|
275
|
+
} else setAttrs(pd, base, nb.t, nb.attrs);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function markRange(ctx, a, b, name, val, toks) {
|
|
280
|
+
const { pd, map } = ctx;
|
|
281
|
+
const op = `${name}~${mintOp(pd)}`;
|
|
282
|
+
const rid = mintOp(pd);
|
|
283
|
+
let i = a;
|
|
284
|
+
while (i < b) {
|
|
285
|
+
if (toks[i].k !== "c") { i++; continue; }
|
|
286
|
+
let j = i; while (j < b && toks[j].k === "c") j++;
|
|
287
|
+
const spans = map.physicalRanges(i, j);
|
|
288
|
+
if (val !== null) for (const [ya, yb] of spans) pd.flow.format(ya, yb - ya, { [op]: val });
|
|
289
|
+
else {
|
|
290
|
+
let pos = 0;
|
|
291
|
+
for (const part of pd.flow.toDelta()) {
|
|
292
|
+
const len = typeof part.insert === "string" ? part.insert.length : 1;
|
|
293
|
+
for (const [ya, yb] of spans) {
|
|
294
|
+
if (pos >= yb) break;
|
|
295
|
+
const s0 = Math.max(pos, ya), e0 = Math.min(pos + len, yb);
|
|
296
|
+
if (e0 > s0 && part.attributes) {
|
|
297
|
+
const clear = {};
|
|
298
|
+
for (const [kk, v] of Object.entries(part.attributes)) if (markName(kk) === name && !isCancel(kk) && v !== null) clear[`${kk}!${rid}`] = true;
|
|
299
|
+
if (Object.keys(clear).length) pd.flow.format(s0, e0 - s0, clear);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
pos += len;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
i = j;
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/plugins/indicator.js
CHANGED
|
@@ -16,12 +16,15 @@ const LABELS = {
|
|
|
16
16
|
saved: "Saved",
|
|
17
17
|
error: "Couldn't save",
|
|
18
18
|
offline: "Offline, not saved",
|
|
19
|
+
retrying: "Saving…",
|
|
20
|
+
blocked: "Can't save here",
|
|
19
21
|
};
|
|
20
22
|
|
|
21
|
-
// States that stay on screen instead of fading
|
|
22
|
-
|
|
23
|
+
// States that stay on screen instead of fading: a save still in flight, one that
|
|
24
|
+
// keeps trying, and one the host will not take without a person's help.
|
|
25
|
+
const STICKY = new Set(["saving", "retrying", "blocked"]);
|
|
23
26
|
|
|
24
|
-
const ALARMING = new Set(["error", "offline"]);
|
|
27
|
+
const ALARMING = new Set(["error", "offline", "blocked"]);
|
|
25
28
|
|
|
26
29
|
const REST = [`background:${TOKENS.surface}`, `color:${TOKENS.ink}`, `border-color:${TOKENS["line-2"]}`];
|
|
27
30
|
const ALARM = [
|
|
@@ -34,7 +37,9 @@ let el = null;
|
|
|
34
37
|
let hideTimer = null;
|
|
35
38
|
|
|
36
39
|
function ensure() {
|
|
37
|
-
|
|
40
|
+
// Looked up on every call, never trusted once detached: a live-sync morph or a
|
|
41
|
+
// caller can empty the body, and a detached chip would swallow every later state.
|
|
42
|
+
if (el && el.isConnected) return el;
|
|
38
43
|
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
39
44
|
el = bevelSurface("div", [
|
|
40
45
|
"position:fixed", "right:16px", "bottom:16px", "z-index:2147483000",
|
|
@@ -49,9 +54,13 @@ function ensure() {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function show(state) {
|
|
57
|
+
if (!LABELS[state]) return;
|
|
52
58
|
// A failed or offline save is the persistent toast's to report when clay-ui is loaded,
|
|
53
|
-
// and so is saved when the page turned on clay.saveToast.
|
|
54
|
-
|
|
59
|
+
// and so is saved when the page turned on clay.saveToast. Blocked is the exception:
|
|
60
|
+
// the page cannot save at all, so the chip stays even beside the generic toast.
|
|
61
|
+
const ownedByToast = state !== "blocked" &&
|
|
62
|
+
(ALARMING.has(state) || (state === "saved" && saveFeedback.savedToast));
|
|
63
|
+
if (saveFeedback.toasts && ownedByToast) {
|
|
55
64
|
clearTimeout(hideTimer);
|
|
56
65
|
if (el) {
|
|
57
66
|
set(el, "opacity", "0");
|
|
@@ -76,6 +85,7 @@ function init() {
|
|
|
76
85
|
for (const state of Object.keys(LABELS)) {
|
|
77
86
|
document.addEventListener("clay:save-" + state, () => show(state));
|
|
78
87
|
}
|
|
88
|
+
document.addEventListener("clay:page-save-state", (event) => show(event.detail?.state));
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
onDomReady(init);
|