@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,130 @@
|
|
|
1
|
+
import { canon, renderModel, parseModel, fragmentCleaner } from "./projection.js";
|
|
2
|
+
|
|
3
|
+
const TEXT = new Set(["paragraph", "heading", "list_item", "callout", "code_block", "toggle"]);
|
|
4
|
+
const same = (a, b) => canon(a) === canon(b);
|
|
5
|
+
|
|
6
|
+
function inlineOf(item, doc) {
|
|
7
|
+
const article = doc.createElement("article");
|
|
8
|
+
article.setAttribute("data-page", "");
|
|
9
|
+
article.innerHTML = renderModel([{ id: item.id, t: "paragraph", c: item.c ?? "", ...(item.m ? { m: item.m } : {}) }], { clean: fragmentCleaner(doc) });
|
|
10
|
+
return article;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// An incoming block whose id is not in base but whose content is the one base
|
|
14
|
+
// block of its kind that no other incoming block took: a writer that dropped the
|
|
15
|
+
// data-ids. It gets that block's id back, so the rest of the merge sees the edit
|
|
16
|
+
// rather than a deletion and an insertion. Scans base in order, from after the
|
|
17
|
+
// base item matched to the previous incoming item, and points the children of a
|
|
18
|
+
// block that got its id back at that id. Returns copies.
|
|
19
|
+
function restoreIds(base, incoming) {
|
|
20
|
+
const index = new Map(base.map((x, n) => [x.id, n]));
|
|
21
|
+
const used = new Set(incoming.map((x) => x.id));
|
|
22
|
+
const renamed = new Map();
|
|
23
|
+
let from = 0;
|
|
24
|
+
const out = incoming.map((x) => {
|
|
25
|
+
const at = index.get(x.id);
|
|
26
|
+
if (at !== undefined) {
|
|
27
|
+
from = at + 1;
|
|
28
|
+
return x;
|
|
29
|
+
}
|
|
30
|
+
const content = { c: x.c ?? "", m: x.m ?? null };
|
|
31
|
+
for (let k = from; k < base.length; k++) {
|
|
32
|
+
const y = base[k];
|
|
33
|
+
if (used.has(y.id) || y.t !== x.t || !same({ c: y.c ?? "", m: y.m ?? null }, content)) continue;
|
|
34
|
+
used.add(y.id);
|
|
35
|
+
from = k + 1;
|
|
36
|
+
renamed.set(x.id, y.id);
|
|
37
|
+
return { ...x, id: y.id };
|
|
38
|
+
}
|
|
39
|
+
return x;
|
|
40
|
+
});
|
|
41
|
+
return out.map((x) => (renamed.has(x.parent) ? { ...x, parent: renamed.get(x.parent) } : x));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Three page models (arrays from parseModel) merged block by block: the
|
|
45
|
+
// structure and every field through mergeJson, and merge3 only inside a text
|
|
46
|
+
// block both sides changed. The same words changed on both sides go to the
|
|
47
|
+
// incoming side; a block one side edited and the other deleted is kept. A code
|
|
48
|
+
// block both sides changed keeps its incoming text, newlines and all, and
|
|
49
|
+
// conflicts: merge3 on it would flatten them.
|
|
50
|
+
// merge3 and mergeJson are passed in: the browser has the vendored
|
|
51
|
+
// HyperMorph, the server the hyper-morph package.
|
|
52
|
+
export function mergePageModels(base, current, incoming, { doc, merge3, mergeJson }) {
|
|
53
|
+
const conflicts = [];
|
|
54
|
+
const inc = restoreIds(base, incoming);
|
|
55
|
+
const b = new Map(base.map((x) => [x.id, x]));
|
|
56
|
+
const c = new Map(current.map((x) => [x.id, x]));
|
|
57
|
+
const i = new Map(inc.map((x) => [x.id, x]));
|
|
58
|
+
const current2 = current.map((x) => ({ ...x }));
|
|
59
|
+
const incoming2 = inc.map((x) => ({ ...x }));
|
|
60
|
+
const c2 = new Map(current2.map((x) => [x.id, x]));
|
|
61
|
+
const i2 = new Map(incoming2.map((x) => [x.id, x]));
|
|
62
|
+
for (const [id, was] of b) {
|
|
63
|
+
const l = c.get(id);
|
|
64
|
+
const r = i.get(id);
|
|
65
|
+
if (!l || !r || !TEXT.has(was.t) || !TEXT.has(l.t) || !TEXT.has(r.t)) continue;
|
|
66
|
+
const tb = { c: was.c ?? "", m: was.m ?? null };
|
|
67
|
+
const tl = { c: l.c ?? "", m: l.m ?? null };
|
|
68
|
+
const tr = { c: r.c ?? "", m: r.m ?? null };
|
|
69
|
+
if (same(tl, tb) || same(tr, tb) || same(tl, tr)) continue;
|
|
70
|
+
if (was.t === "code_block") {
|
|
71
|
+
for (const t of [c2.get(id), i2.get(id)]) {
|
|
72
|
+
t.c = r.c ?? "";
|
|
73
|
+
if (r.m) t.m = r.m;
|
|
74
|
+
else delete t.m;
|
|
75
|
+
}
|
|
76
|
+
conflicts.push({ id, kind: "text", local: l.c ?? "", remote: r.c ?? "" });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const res = merge3(inlineOf(was, doc), inlineOf(l, doc), inlineOf(r, doc));
|
|
80
|
+
for (const k of res.conflicts) conflicts.push({ id, kind: "text", local: k.local?.textContent ?? null, remote: k.remote?.textContent ?? null });
|
|
81
|
+
const [merged] = parseModel(res.root, { seed: "", known: new Map() });
|
|
82
|
+
for (const t of [c2.get(id), i2.get(id)]) {
|
|
83
|
+
t.c = merged.c;
|
|
84
|
+
if (merged.m) t.m = merged.m;
|
|
85
|
+
else delete t.m;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const order = current.map((x) => x.id);
|
|
89
|
+
for (const [id, was] of b) {
|
|
90
|
+
const l = c2.get(id);
|
|
91
|
+
if (!l || i.has(id) || same(c.get(id), was)) continue;
|
|
92
|
+
let k = order.indexOf(id) - 1;
|
|
93
|
+
while (k >= 0 && !i2.has(order[k])) k--;
|
|
94
|
+
const at = k < 0 ? 0 : incoming2.findIndex((x) => x.id === order[k]) + 1;
|
|
95
|
+
incoming2.splice(at, 0, { ...l });
|
|
96
|
+
i2.set(id, incoming2[at]);
|
|
97
|
+
conflicts.push({ id, kind: "kept-edit" });
|
|
98
|
+
}
|
|
99
|
+
const out = mergeJson(base, current2, incoming2, {
|
|
100
|
+
onConflict: (k) => conflicts.push({ kind: "field", path: k.path, local: k.local, remote: k.remote, resolved: k.resolved }),
|
|
101
|
+
});
|
|
102
|
+
const ids = new Set(out.map((x) => x.id));
|
|
103
|
+
const parentOf = (id) => (c.get(id) ?? b.get(id) ?? i.get(id))?.parent ?? null;
|
|
104
|
+
const fixed = out.map((x) => {
|
|
105
|
+
let p = x.parent ?? null;
|
|
106
|
+
while (p && !ids.has(p)) p = parentOf(p);
|
|
107
|
+
return p === (x.parent ?? null) ? x : { ...x, parent: p };
|
|
108
|
+
});
|
|
109
|
+
// Both sides moving blocks into each other leaves a parent cycle: the block
|
|
110
|
+
// whose incoming parent closes the loop keeps the tab's parent instead, and a
|
|
111
|
+
// loop that remains goes back to base.
|
|
112
|
+
const parents = new Map(fixed.map((x) => [x.id, x.parent ?? null]));
|
|
113
|
+
const loops = (id) => {
|
|
114
|
+
const seen = new Set([id]);
|
|
115
|
+
for (let p = parents.get(id); p; p = parents.get(p)) {
|
|
116
|
+
if (seen.has(p)) return true;
|
|
117
|
+
seen.add(p);
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
for (const x of fixed) {
|
|
122
|
+
if (!loops(x.id)) continue;
|
|
123
|
+
for (const p of [c.get(x.id)?.parent, b.get(x.id)?.parent, null]) {
|
|
124
|
+
parents.set(x.id, p && ids.has(p) ? p : null);
|
|
125
|
+
if (!loops(x.id)) break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const items = fixed.map((x) => ((x.parent ?? null) === parents.get(x.id) ? x : { ...x, parent: parents.get(x.id) }));
|
|
129
|
+
return { items, conflicts };
|
|
130
|
+
}
|
package/src/page/model.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
// text and `m` its mark keys, one per UTF-16 unit, left out when there are none
|
|
6
6
|
// (inline.js). Attributes equal to their schema default are left out, so
|
|
7
7
|
// a block's JSON is as small as the block. A table keeps its rows as an object
|
|
8
|
-
// keyed by row id ({ pos, cells: { <col id>:
|
|
8
|
+
// keyed by row id ({ pos, cells: { <col id>: cell } }), never a list, because a
|
|
9
9
|
// list in a live store is written whole and two tabs adding rows keep one.
|
|
10
10
|
//
|
|
11
11
|
// Order is (pos, id): pos strings compare as plain strings, and the id breaks
|
|
12
12
|
// the tie two tabs inserting at the same spot leave behind. Ids and positions
|
|
13
13
|
// are only ever assigned by a local edit (identityPlugin), never on open.
|
|
14
14
|
import { model, state, transform } from "../vendor/prosemirror.vendor.js";
|
|
15
|
-
import { flatten, unflatten } from "./inline.js";
|
|
15
|
+
import { flatten, unflatten, unitsOf, runsOf } from "./inline.js";
|
|
16
|
+
import { cellText } from "./rich/cell.js";
|
|
16
17
|
|
|
17
18
|
export const REMOTE = "clay-page-remote";
|
|
18
19
|
|
|
@@ -85,13 +86,29 @@ function attrsJson(node) {
|
|
|
85
86
|
return out;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// A table cell as the record keeps it: its text when it has no marks and no
|
|
90
|
+
// line breaks, otherwise { c, m } in the text blocks' inline encoding.
|
|
91
|
+
export function cellValue(cell) {
|
|
92
|
+
const { c, m } = flatten(cell.content);
|
|
93
|
+
return m.some(Boolean) ? { c, m: runsOf(m) } : c;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function cellContent(schema, value) {
|
|
97
|
+
if (value !== null && typeof value === "object" && typeof value.c === "string") {
|
|
98
|
+
const units = unitsOf(value.m, value.c.length);
|
|
99
|
+
if (units) return unflatten(schema, value.c, units, schema.nodes.table_cell);
|
|
100
|
+
}
|
|
101
|
+
const text = cellText(value);
|
|
102
|
+
return text ? schema.text(text) : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
export function blockJson(node) {
|
|
89
106
|
const out = { t: node.type.name, pos: node.attrs.pos, ...attrsJson(node) };
|
|
90
107
|
if (node.type.name === "table") {
|
|
91
108
|
const rows = {};
|
|
92
109
|
node.forEach((row) => {
|
|
93
110
|
const cells = {};
|
|
94
|
-
row.forEach((cell) => { cells[cell.attrs.col] = cell
|
|
111
|
+
row.forEach((cell) => { cells[cell.attrs.col] = cellValue(cell); });
|
|
95
112
|
rows[row.attrs.id] = { pos: row.attrs.pos, cells };
|
|
96
113
|
});
|
|
97
114
|
out.rows = rows;
|
|
@@ -116,17 +133,14 @@ export function nodeFromBlock(schema, id, blockValue) {
|
|
|
116
133
|
const { table_row: rowType, table_cell: cellType } = schema.nodes;
|
|
117
134
|
const children = orderedIds(rows || {}).map((rid) => rowType.createChecked(
|
|
118
135
|
{ id: rid, pos: rows[rid].pos },
|
|
119
|
-
cols.map((col) => {
|
|
120
|
-
const text = rows[rid].cells?.[col];
|
|
121
|
-
return cellType.createChecked({ col }, text ? schema.text(String(text)) : null);
|
|
122
|
-
}),
|
|
136
|
+
cols.map((col) => cellType.createChecked({ col }, cellContent(schema, rows[rid].cells?.[col]))),
|
|
123
137
|
));
|
|
124
138
|
return schema.nodes.table.createChecked({ ...attrs, id, pos }, children);
|
|
125
139
|
}
|
|
126
140
|
const type = schema.nodes[t];
|
|
127
141
|
if (!type) throw new RangeError(`page: unknown block type ${t}`);
|
|
128
142
|
// A record from before the flat form holds ProseMirror's inline JSON.
|
|
129
|
-
const content = typeof c === "string" ? unflatten(schema, c, m, type) : Array.isArray(c) && c.length ? model.Fragment.fromJSON(schema, c) : null;
|
|
143
|
+
const content = typeof c === "string" ? unflatten(schema, c, unitsOf(m, c.length), type) : Array.isArray(c) && c.length ? model.Fragment.fromJSON(schema, c) : null;
|
|
130
144
|
return type.create({ ...attrs, id, pos }, content);
|
|
131
145
|
}
|
|
132
146
|
|
|
@@ -198,10 +212,57 @@ export function repairSiblings(items, prefix) {
|
|
|
198
212
|
return out;
|
|
199
213
|
}
|
|
200
214
|
|
|
215
|
+
// True when every step of tr edits inside one top-level textblock: its text
|
|
216
|
+
// and marks change, never which blocks the page has, their order or attrs.
|
|
217
|
+
export function insideTextblocks(tr) {
|
|
218
|
+
for (let i = 0; i < tr.steps.length; i++) {
|
|
219
|
+
const step = tr.steps[i];
|
|
220
|
+
if (step instanceof transform.ReplaceStep) {
|
|
221
|
+
if (step.slice.openStart || step.slice.openEnd) return false;
|
|
222
|
+
for (let j = 0; j < step.slice.content.childCount; j++) if (!step.slice.content.child(j).isInline) return false;
|
|
223
|
+
} else if (!(step instanceof transform.AddMarkStep || step instanceof transform.RemoveMarkStep)) return false;
|
|
224
|
+
const $from = tr.docs[i].resolve(step.from), $to = tr.docs[i].resolve(step.to);
|
|
225
|
+
if ($from.depth !== 1 || $to.depth !== 1 || $from.index(0) !== $to.index(0) || !$from.parent.isTextblock) return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
201
230
|
export function identityPlugin() {
|
|
231
|
+
// Documents with nothing to repair. An edit inside textblocks keeps a
|
|
232
|
+
// document that way, so it skips the walk over every block.
|
|
233
|
+
const clean = new WeakSet();
|
|
234
|
+
const ready = (doc) => {
|
|
235
|
+
const ids = new Set();
|
|
236
|
+
let previous = null, valid = true;
|
|
237
|
+
doc.forEach((node) => {
|
|
238
|
+
if (!node.attrs.id || ids.has(node.attrs.id) || !isKey(node.attrs.pos) || (previous !== null && previous >= node.attrs.pos)) valid = false;
|
|
239
|
+
ids.add(node.attrs.id);
|
|
240
|
+
previous = node.attrs.pos;
|
|
241
|
+
if (node.type.name !== "table") return;
|
|
242
|
+
const rows = new Set();
|
|
243
|
+
let rowPrevious = null;
|
|
244
|
+
node.forEach((row) => {
|
|
245
|
+
if (!row.attrs.id || rows.has(row.attrs.id) || !isKey(row.attrs.pos) || (rowPrevious !== null && rowPrevious >= row.attrs.pos)) valid = false;
|
|
246
|
+
rows.add(row.attrs.id);
|
|
247
|
+
rowPrevious = row.attrs.pos;
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
return valid;
|
|
251
|
+
};
|
|
202
252
|
return new state.Plugin({
|
|
253
|
+
state: {
|
|
254
|
+
init(_config, editor) { if (ready(editor.doc)) clean.add(editor.doc); return null; },
|
|
255
|
+
apply() { return null; },
|
|
256
|
+
},
|
|
203
257
|
appendTransaction(transactions, old, next) {
|
|
204
|
-
if (
|
|
258
|
+
if (clean.has(old.doc) && transactions.every(insideTextblocks)) {
|
|
259
|
+
clean.add(next.doc);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (!transactions.some((tr) => tr.docChanged && !tr.getMeta(REMOTE))) {
|
|
263
|
+
if (clean.has(old.doc) && ready(next.doc)) clean.add(next.doc);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
205
266
|
const tr = next.tr;
|
|
206
267
|
const fix = (node, at, change) => { if (change) tr.setNodeMarkup(at, null, { ...node.attrs, ...change }); };
|
|
207
268
|
// Of two holders of an id, the one with the old block's content keeps it
|
|
@@ -235,6 +296,7 @@ export function identityPlugin() {
|
|
|
235
296
|
node.forEach((row, rowOffset) => rows.push({ node: row, at: offset + 1 + rowOffset }));
|
|
236
297
|
repairSiblings(items(rows), "r").forEach((change, i) => fix(rows[i].node, rows[i].at, change));
|
|
237
298
|
}
|
|
299
|
+
if (!tr.docChanged) clean.add(next.doc);
|
|
238
300
|
return tr.docChanged ? tr : null;
|
|
239
301
|
},
|
|
240
302
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { state, view as pmView } from "../vendor/prosemirror.vendor.js";
|
|
2
|
+
import { schema } from "./schema.js";
|
|
3
|
+
|
|
4
|
+
const { Decoration, DecorationSet } = pmView;
|
|
5
|
+
|
|
6
|
+
export const PLACEHOLDER_TEXT = "Type / for blocks";
|
|
7
|
+
|
|
8
|
+
export function placeholderPlugin(text = PLACEHOLDER_TEXT) {
|
|
9
|
+
return new state.Plugin({
|
|
10
|
+
props: {
|
|
11
|
+
decorations(st) {
|
|
12
|
+
const { selection } = st;
|
|
13
|
+
if (!selection.empty) return null;
|
|
14
|
+
const $from = selection.$from;
|
|
15
|
+
if ($from.depth !== 1) return null;
|
|
16
|
+
const node = $from.parent;
|
|
17
|
+
if (node.type !== schema.nodes.paragraph || node.content.size) return null;
|
|
18
|
+
const pos = $from.before(1);
|
|
19
|
+
return DecorationSet.create(st.doc, [Decoration.node(pos, pos + node.nodeSize, { class: "page-placeholder", "data-placeholder": text })]);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|