@panphora/clayjs 1.13.1 → 1.15.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 +69385 -40992
- package/dist/server/checkpoint.cjs +1 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/core.cjs +72210 -0
- package/dist/server/rooms.cjs +1 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +127 -99
- package/entries/clay.js +22 -0
- package/entries/sap.js +5 -5
- package/package.json +16 -2
- package/packed-contract.json +18 -3
- package/src/core/boot-phase.js +6 -0
- package/src/core/conflict-download.js +12 -3
- 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 +154 -36
- package/src/core/snapshot.js +51 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/derived.js +116 -0
- package/src/lib/effect.js +130 -0
- package/src/lib/mutation.js +3 -1
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +47 -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 +16 -4
- package/src/loader.js +37 -1
- 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 +1170 -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 +386 -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/derive.js +12 -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 +62 -4
- 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/conflict-revert.js +2 -0
- package/src/sync/conflicts.js +2 -2
- package/src/sync/live-sync.js +36 -8
- 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/sap-core.vendor.js +9 -0
- package/src/vendor/sap-ownership.vendor.js +111 -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,220 @@
|
|
|
1
|
+
import { state } from "../vendor/prosemirror.vendor.js";
|
|
2
|
+
import { schema } from "./schema.js";
|
|
3
|
+
import { blockJson, keyBetween, newId, orderedIds } from "./model.js";
|
|
4
|
+
import { chartModel, renderChartSvg } from "./rich/chart.js";
|
|
5
|
+
import { openPageMenu } from "./chrome.js";
|
|
6
|
+
import { project } from "./projection.js";
|
|
7
|
+
import { columnKinds } from "./table-kinds.js";
|
|
8
|
+
import { cellText } from "./rich/cell.js";
|
|
9
|
+
|
|
10
|
+
const views = new WeakMap();
|
|
11
|
+
|
|
12
|
+
function sourceTable(doc, id) {
|
|
13
|
+
let table = null;
|
|
14
|
+
doc.forEach((node) => { if (node.type === schema.nodes.table && node.attrs.id === id) table = node; });
|
|
15
|
+
return table;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tableData(node) {
|
|
19
|
+
if (!node) return null;
|
|
20
|
+
return project({ [node.attrs.id]: blockJson(node) })[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function chartAttrs(table, pos, parent) {
|
|
24
|
+
const colIds = orderedIds(table.attrs.cols);
|
|
25
|
+
if (!colIds.length) return null;
|
|
26
|
+
const kinds = columnKinds(table);
|
|
27
|
+
const series = colIds.filter((id) => id !== colIds[0] && kinds[id] === "number");
|
|
28
|
+
return {
|
|
29
|
+
id: newId(),
|
|
30
|
+
pos,
|
|
31
|
+
parent,
|
|
32
|
+
source: table.attrs.id,
|
|
33
|
+
kind: "bar",
|
|
34
|
+
x: colIds[0],
|
|
35
|
+
series: series.length ? series : colIds[1] ? [colIds[1]] : [],
|
|
36
|
+
title: "",
|
|
37
|
+
highlight: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function insertChartAfterTable(view, tablePos) {
|
|
42
|
+
const doc = view.state.doc;
|
|
43
|
+
const table = doc.nodeAt(tablePos);
|
|
44
|
+
if (table?.type !== schema.nodes.table) return false;
|
|
45
|
+
const after = tablePos + table.nodeSize;
|
|
46
|
+
const next = doc.maybeChild(doc.resolve(after).index(0));
|
|
47
|
+
const attrs = chartAttrs(table, keyBetween(table.attrs.pos, next?.attrs.pos ?? null), table.attrs.parent);
|
|
48
|
+
if (!attrs) return false;
|
|
49
|
+
view.dispatch(view.state.tr.insert(after, schema.nodes.chart.create(attrs)));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function chartTables(doc) {
|
|
54
|
+
const tables = [];
|
|
55
|
+
doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
|
|
56
|
+
return tables;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// /chart: an empty line becomes the chart, otherwise it goes below the line.
|
|
60
|
+
// One table is charted at once; several open a picker named by their columns.
|
|
61
|
+
function placeChart(view, index, table, remove) {
|
|
62
|
+
let tr = view.state.tr;
|
|
63
|
+
if (remove) tr.delete(remove.from, remove.to);
|
|
64
|
+
if (!sourceTable(tr.doc, table.attrs.id) || index >= tr.doc.childCount) return false;
|
|
65
|
+
let at = 0;
|
|
66
|
+
for (let i = 0; i < index; i++) at += tr.doc.child(i).nodeSize;
|
|
67
|
+
const node = tr.doc.child(index);
|
|
68
|
+
const replace = node.isTextblock && node.content.size === 0;
|
|
69
|
+
const next = tr.doc.maybeChild(index + 1);
|
|
70
|
+
const pos = replace ? node.attrs.pos : keyBetween(node.attrs.pos, next?.attrs.pos ?? null);
|
|
71
|
+
const attrs = chartAttrs(table, pos, node.attrs.parent ?? null);
|
|
72
|
+
if (!attrs) return false;
|
|
73
|
+
const chart = schema.nodes.chart.create(attrs);
|
|
74
|
+
tr = replace ? tr.replaceWith(at, at + node.nodeSize, chart) : tr.insert(at + node.nodeSize, chart);
|
|
75
|
+
view.dispatch(tr.scrollIntoView());
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const tableLabel = (table) => orderedIds(table.attrs.cols).map((id) => table.attrs.cols[id].name || "Untitled").join(", ");
|
|
80
|
+
|
|
81
|
+
export function chartFromSlash(view, index, remove = null) {
|
|
82
|
+
const tables = chartTables(view.state.doc);
|
|
83
|
+
if (!tables.length) return false;
|
|
84
|
+
if (tables.length === 1) return placeChart(view, index, tables[0], remove);
|
|
85
|
+
const anchor = view.nodeDOM(view.state.doc.resolve(view.state.selection.head).before(1)) ?? view.dom;
|
|
86
|
+
return openPageMenu(view, anchor, tables.map((table, i) => ({
|
|
87
|
+
label: `Table ${i + 1}`,
|
|
88
|
+
description: tableLabel(table),
|
|
89
|
+
icon: "▦",
|
|
90
|
+
run: () => placeChart(view, index, table, remove),
|
|
91
|
+
})));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class ChartView {
|
|
95
|
+
constructor(node, view, getPos) {
|
|
96
|
+
this.node = node;
|
|
97
|
+
this.view = view;
|
|
98
|
+
this.getPos = getPos;
|
|
99
|
+
this.source = undefined;
|
|
100
|
+
this.frame = null;
|
|
101
|
+
const doc = view.dom.ownerDocument;
|
|
102
|
+
this.dom = doc.createElement("figure");
|
|
103
|
+
this.dom.className = "page-chart";
|
|
104
|
+
this.dom.dataset.id = node.attrs.id ?? "";
|
|
105
|
+
this.dom.dataset.type = "chart";
|
|
106
|
+
this.plot = doc.createElement("div");
|
|
107
|
+
this.plot.className = "page-chart-plot";
|
|
108
|
+
this.caption = doc.createElement("figcaption");
|
|
109
|
+
this.settings = doc.createElement("button");
|
|
110
|
+
this.settings.type = "button";
|
|
111
|
+
this.settings.className = "page-chart-settings";
|
|
112
|
+
this.settings.textContent = "Chart settings";
|
|
113
|
+
this.settings.setAttribute("aria-label", "Chart settings");
|
|
114
|
+
this.settings.contentEditable = "false";
|
|
115
|
+
this.settings.addEventListener("click", () => this.openSettings());
|
|
116
|
+
this.dom.append(this.plot, this.caption, this.settings);
|
|
117
|
+
if (!views.has(view)) views.set(view, new Set());
|
|
118
|
+
views.get(view).add(this);
|
|
119
|
+
this.refresh();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
refresh() {
|
|
123
|
+
const source = sourceTable(this.view.state.doc, this.node.attrs.source);
|
|
124
|
+
if (source === this.source) return;
|
|
125
|
+
this.source = source;
|
|
126
|
+
this.schedule();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
schedule() {
|
|
130
|
+
if (this.frame !== null) return;
|
|
131
|
+
const win = this.dom.ownerDocument.defaultView;
|
|
132
|
+
const raf = win.requestAnimationFrame?.bind(win) ?? ((fn) => win.setTimeout(fn, 0));
|
|
133
|
+
this.frame = raf(() => { this.frame = null; this.draw(); });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
draw() {
|
|
137
|
+
const attrs = this.node.attrs;
|
|
138
|
+
const chart = chartModel({ id: attrs.id, type: attrs.kind, x: attrs.x, series: attrs.series, title: attrs.title, highlight: attrs.highlight }, tableData(this.source));
|
|
139
|
+
this.plot.innerHTML = renderChartSvg(chart, { width: 640, height: 320, narrow: this.dom.getBoundingClientRect().width < 480 });
|
|
140
|
+
this.caption.textContent = attrs.title;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setAttrs(changes) {
|
|
144
|
+
const pos = this.getPos();
|
|
145
|
+
if (pos == null) return;
|
|
146
|
+
const node = this.view.state.doc.nodeAt(pos);
|
|
147
|
+
if (node?.type !== schema.nodes.chart) return;
|
|
148
|
+
this.view.dispatch(this.view.state.tr.setNodeMarkup(pos, null, { ...node.attrs, ...changes }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
submenu(items) {
|
|
152
|
+
openPageMenu(this.view, this.settings, items);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
openSettings() {
|
|
156
|
+
const attrs = this.node.attrs;
|
|
157
|
+
const table = tableData(sourceTable(this.view.state.doc, attrs.source));
|
|
158
|
+
const cols = table?.cols ?? [];
|
|
159
|
+
const rows = table?.rows ?? [];
|
|
160
|
+
this.submenu([
|
|
161
|
+
{ label: "Line", icon: attrs.kind === "line" ? "✓" : "", run: () => this.setAttrs({ kind: "line" }) },
|
|
162
|
+
{ label: "Bar", icon: attrs.kind === "bar" ? "✓" : "", run: () => this.setAttrs({ kind: "bar" }) },
|
|
163
|
+
{ label: "X column ▸", focus: false, run: () => this.submenu(cols.map((col) => ({ label: col.name || col.id, icon: attrs.x === col.id ? "✓" : "", run: () => this.setAttrs({ x: col.id, series: attrs.series.filter((id) => id !== col.id) }) }))) },
|
|
164
|
+
{ label: "Series ▸", focus: false, run: () => this.submenu(cols.filter((col) => col.id !== attrs.x).map((col) => ({ label: col.name || col.id, icon: attrs.series.includes(col.id) ? "✓" : "", checked: attrs.series.includes(col.id), run: () => this.setAttrs({ series: attrs.series.includes(col.id) ? attrs.series.filter((id) => id !== col.id) : [...attrs.series, col.id] }) }))) },
|
|
165
|
+
{ label: "Highlight row ▸", focus: false, run: () => this.submenu([{ label: "None", icon: !attrs.highlight ? "✓" : "", run: () => this.setAttrs({ highlight: null }) }, ...rows.map((row) => ({ label: cellText(row.cells[attrs.x]) || row.id, icon: attrs.highlight === row.id ? "✓" : "", run: () => this.setAttrs({ highlight: row.id }) }))]) },
|
|
166
|
+
{ label: "Edit title", focus: false, run: () => this.editTitle() },
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
editTitle() {
|
|
171
|
+
const input = this.dom.ownerDocument.createElement("input");
|
|
172
|
+
input.type = "text";
|
|
173
|
+
input.className = "page-chart-title-input";
|
|
174
|
+
input.setAttribute("aria-label", "Chart title");
|
|
175
|
+
input.value = this.node.attrs.title ?? "";
|
|
176
|
+
const finish = (save) => {
|
|
177
|
+
input.remove();
|
|
178
|
+
this.caption.hidden = false;
|
|
179
|
+
if (save) this.setAttrs({ title: input.value });
|
|
180
|
+
else this.settings.focus();
|
|
181
|
+
};
|
|
182
|
+
input.addEventListener("keydown", (event) => {
|
|
183
|
+
if (event.key === "Enter" || event.key === "Escape") {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
finish(event.key === "Enter");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
this.caption.hidden = true;
|
|
190
|
+
this.caption.after(input);
|
|
191
|
+
input.focus();
|
|
192
|
+
input.select();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
update(node) {
|
|
196
|
+
if (node.type !== schema.nodes.chart) return false;
|
|
197
|
+
const changed = !node.sameMarkup(this.node);
|
|
198
|
+
this.node = node;
|
|
199
|
+
this.dom.dataset.id = node.attrs.id ?? "";
|
|
200
|
+
if (changed) { this.source = sourceTable(this.view.state.doc, node.attrs.source); this.schedule(); }
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
stopEvent(event) { return !!event.target.closest?.(".page-chart-settings, .page-chart-title-input"); }
|
|
205
|
+
|
|
206
|
+
destroy() {
|
|
207
|
+
views.get(this.view)?.delete(this);
|
|
208
|
+
if (this.frame !== null) {
|
|
209
|
+
const win = this.dom.ownerDocument.defaultView;
|
|
210
|
+
if (win.cancelAnimationFrame) win.cancelAnimationFrame(this.frame);
|
|
211
|
+
else win.clearTimeout(this.frame);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function chartPlugin() {
|
|
217
|
+
return new state.Plugin({
|
|
218
|
+
view: () => ({ update(view) { for (const chart of views.get(view) ?? []) chart.refresh(); } }),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { state, view as pmView } from "../vendor/prosemirror.vendor.js";
|
|
2
|
+
import { schema } from "./schema.js";
|
|
3
|
+
import { asStep } from "./blocksel.js";
|
|
4
|
+
import { el, cardFace } from "./board-view.js";
|
|
5
|
+
|
|
6
|
+
const { Plugin } = state;
|
|
7
|
+
const { Decoration, DecorationSet } = pmView;
|
|
8
|
+
|
|
9
|
+
function itemAt(view, id) {
|
|
10
|
+
let found = null;
|
|
11
|
+
view.state.doc.forEach((node, pos) => { if (node.attrs.id === id && node.type === schema.nodes.list_item && node.attrs.kind === "check") found = { node, pos }; });
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setItem(view, id, attr, value) {
|
|
16
|
+
const found = itemAt(view, id);
|
|
17
|
+
if (!found) return;
|
|
18
|
+
view.dispatch(asStep(view.state.tr.setNodeMarkup(found.pos, null, { ...found.node.attrs, [attr]: value || null })));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function chip(doc, label, aria, attr) {
|
|
22
|
+
const button = doc.createElement("button");
|
|
23
|
+
button.type = "button";
|
|
24
|
+
button.className = "page-check-chip";
|
|
25
|
+
button.dataset.check = attr;
|
|
26
|
+
button.textContent = label;
|
|
27
|
+
button.setAttribute("aria-label", aria);
|
|
28
|
+
return button;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The same owner field a board card's panel shows: people as chips, "No owner",
|
|
32
|
+
// then a name box for anyone not in the list.
|
|
33
|
+
function ownerPicker(view, id, anchor) {
|
|
34
|
+
const doc = anchor.ownerDocument;
|
|
35
|
+
const current = itemAt(view, id)?.node.attrs.owner || "";
|
|
36
|
+
const popup = doc.createElement("span");
|
|
37
|
+
popup.className = "page-check-popup page-check-owner";
|
|
38
|
+
popup.contentEditable = "false";
|
|
39
|
+
const box = el(doc, "span", "page-board-owners");
|
|
40
|
+
const people = el(doc, "span", "page-board-people", { role: "group", "aria-label": "Owner" });
|
|
41
|
+
const input = el(doc, "input", "page-board-owner-input", { type: "text", "aria-label": "Owner name", placeholder: "Or type a name…" });
|
|
42
|
+
input.value = current;
|
|
43
|
+
const choose = (name) => setItem(view, id, "owner", name);
|
|
44
|
+
const person = (name, label, attr, pressed) => {
|
|
45
|
+
const button = el(doc, "button", `page-board-person${name ? "" : " is-none"}`, { type: "button", "aria-pressed": String(pressed), "data-check": attr });
|
|
46
|
+
if (name) button.append(cardFace(doc, name), el(doc, "span", null, {}, name));
|
|
47
|
+
else button.textContent = label;
|
|
48
|
+
button.addEventListener("click", () => choose(name));
|
|
49
|
+
return button;
|
|
50
|
+
};
|
|
51
|
+
const draw = (available) => {
|
|
52
|
+
const typed = input.value.trim().toLowerCase();
|
|
53
|
+
const shown = (available || []).filter((p) => !typed || typed === current.toLowerCase() || p.name.toLowerCase().includes(typed));
|
|
54
|
+
people.replaceChildren(...shown.map((p) => person(p.name, p.name, "owner-option", p.name === current)), person(null, "No owner", "clear-owner", !current));
|
|
55
|
+
};
|
|
56
|
+
input.addEventListener("keydown", (event) => {
|
|
57
|
+
if (event.key === "Enter") { event.preventDefault(); choose(input.value.trim()); }
|
|
58
|
+
if (event.key === "Escape") { event.preventDefault(); popup.remove(); anchor.focus(); }
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
});
|
|
61
|
+
box.append(people, input);
|
|
62
|
+
popup.append(box);
|
|
63
|
+
anchor.after(popup);
|
|
64
|
+
input.focus();
|
|
65
|
+
let available = [];
|
|
66
|
+
draw(available);
|
|
67
|
+
input.addEventListener("input", () => draw(available));
|
|
68
|
+
const source = globalThis.clay?.people?.available;
|
|
69
|
+
if (source) Promise.resolve().then(() => source()).then((list) => { if (popup.isConnected) { available = list || []; draw(available); } }).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function datePicker(view, id, anchor) {
|
|
73
|
+
const doc = anchor.ownerDocument;
|
|
74
|
+
const popup = doc.createElement("span");
|
|
75
|
+
popup.className = "page-check-popup";
|
|
76
|
+
popup.contentEditable = "false";
|
|
77
|
+
const input = doc.createElement("input");
|
|
78
|
+
input.type = "date";
|
|
79
|
+
input.setAttribute("aria-label", "Due date");
|
|
80
|
+
input.value = itemAt(view, id)?.node.attrs.due || "";
|
|
81
|
+
input.addEventListener("change", () => setItem(view, id, "due", input.value));
|
|
82
|
+
input.addEventListener("keydown", (event) => event.stopPropagation());
|
|
83
|
+
const clear = chip(doc, "Clear", "Clear due date", "clear-date");
|
|
84
|
+
clear.addEventListener("click", () => setItem(view, id, "due", null));
|
|
85
|
+
popup.append(input, clear);
|
|
86
|
+
anchor.after(popup);
|
|
87
|
+
input.focus();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function controls(view, node) {
|
|
91
|
+
const doc = view.dom.ownerDocument;
|
|
92
|
+
const box = doc.createElement("span");
|
|
93
|
+
box.className = "page-check-controls";
|
|
94
|
+
box.contentEditable = "false";
|
|
95
|
+
box.dataset.empty = String(!node.attrs.owner && !node.attrs.due);
|
|
96
|
+
const owner = chip(doc, node.attrs.owner || "Assign", node.attrs.owner ? `Owner: ${node.attrs.owner}` : "Assign owner", "owner");
|
|
97
|
+
const dueLabel = node.attrs.due ? new Date(`${node.attrs.due}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }) : "Date";
|
|
98
|
+
const date = chip(doc, dueLabel, node.attrs.due ? `Due ${dueLabel}` : "Set due date", "date");
|
|
99
|
+
owner.addEventListener("click", () => ownerPicker(view, node.attrs.id, owner));
|
|
100
|
+
date.addEventListener("click", () => datePicker(view, node.attrs.id, date));
|
|
101
|
+
box.append(owner, date);
|
|
102
|
+
return box;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function focusControl(view, name) {
|
|
106
|
+
const $pos = view.state.selection.$head;
|
|
107
|
+
if ($pos.depth !== 1 || $pos.parent.type !== schema.nodes.list_item || $pos.parent.attrs.kind !== "check") return false;
|
|
108
|
+
const id = $pos.parent.attrs.id;
|
|
109
|
+
const widget = [...view.dom.querySelectorAll(".page-check-controls")].find((el) => el.dataset.item === id);
|
|
110
|
+
const button = widget?.querySelector(`[data-check="${name}"]`);
|
|
111
|
+
if (!button) return false;
|
|
112
|
+
button.focus();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const checklistKeys = new Plugin({
|
|
117
|
+
props: {
|
|
118
|
+
handleKeyDown(view, event) {
|
|
119
|
+
if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey) return false;
|
|
120
|
+
const name = event.key.toLowerCase() === "o" ? "owner" : event.key.toLowerCase() === "d" ? "date" : null;
|
|
121
|
+
return name ? focusControl(view, name) : false;
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export function checklistPlugin() {
|
|
127
|
+
return new Plugin({
|
|
128
|
+
props: {
|
|
129
|
+
decorations(st) {
|
|
130
|
+
const widgets = [];
|
|
131
|
+
st.doc.forEach((node, pos) => {
|
|
132
|
+
if (node.type !== schema.nodes.list_item || node.attrs.kind !== "check") return;
|
|
133
|
+
widgets.push(Decoration.widget(pos + node.nodeSize - 1, (view) => {
|
|
134
|
+
const box = controls(view, node);
|
|
135
|
+
box.dataset.item = node.attrs.id;
|
|
136
|
+
return box;
|
|
137
|
+
}, { key: `${node.attrs.id}:${node.attrs.owner}:${node.attrs.due}`, side: 1, stopEvent: () => true, ignoreSelection: true }));
|
|
138
|
+
});
|
|
139
|
+
return DecorationSet.create(st.doc, widgets);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|