@dou-ui/form-builder 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/builder/canvas.js +695 -0
- package/dist/builder/canvas.js.map +1 -0
- package/dist/builder/completion.js +49 -0
- package/dist/builder/completion.js.map +1 -0
- package/dist/builder/condition-editor.js +6 -0
- package/dist/builder/condition-editor.js.map +1 -0
- package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js +410 -0
- package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/condition.js +159 -0
- package/dist/builder/condition.js.map +1 -0
- package/dist/builder/context.js +477 -0
- package/dist/builder/context.js.map +1 -0
- package/dist/builder/core/commands.js +373 -0
- package/dist/builder/core/commands.js.map +1 -0
- package/dist/builder/core/history.js +76 -0
- package/dist/builder/core/history.js.map +1 -0
- package/dist/builder/core/transaction.js +298 -0
- package/dist/builder/core/transaction.js.map +1 -0
- package/dist/builder/drag.js +466 -0
- package/dist/builder/drag.js.map +1 -0
- package/dist/builder/expression-box.js +19 -0
- package/dist/builder/expression-box.js.map +1 -0
- package/dist/builder/expression-input.js +6 -0
- package/dist/builder/expression-input.js.map +1 -0
- package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js +117 -0
- package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/extension.js +47 -0
- package/dist/builder/extension.js.map +1 -0
- package/dist/builder/form-builder.js +6 -0
- package/dist/builder/form-builder.js.map +1 -0
- package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js +450 -0
- package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/index.js +22 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/inspector.js +6 -0
- package/dist/builder/inspector.js.map +1 -0
- package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js +442 -0
- package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/json-dialog.js +6 -0
- package/dist/builder/json-dialog.js.map +1 -0
- package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js +281 -0
- package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/locale.js +1271 -0
- package/dist/builder/locale.js.map +1 -0
- package/dist/builder/logic-fields.js +18 -0
- package/dist/builder/logic-fields.js.map +1 -0
- package/dist/builder/logic.js +332 -0
- package/dist/builder/logic.js.map +1 -0
- package/dist/builder/menu.js +92 -0
- package/dist/builder/menu.js.map +1 -0
- package/dist/builder/outline-row.js +6 -0
- package/dist/builder/outline-row.js.map +1 -0
- package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js +88 -0
- package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/outline.js +6 -0
- package/dist/builder/outline.js.map +1 -0
- package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js +111 -0
- package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/palette.js +6 -0
- package/dist/builder/palette.js.map +1 -0
- package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js +96 -0
- package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/preview.js +6 -0
- package/dist/builder/preview.js.map +1 -0
- package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js +111 -0
- package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/builder/settings-binding.js +57 -0
- package/dist/builder/settings-binding.js.map +1 -0
- package/dist/builder/settings.js +487 -0
- package/dist/builder/settings.js.map +1 -0
- package/dist/builder/span-field.js +104 -0
- package/dist/builder/span-field.js.map +1 -0
- package/dist/code/expression-input.js +6 -0
- package/dist/code/expression-input.js.map +1 -0
- package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js +88 -0
- package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/code/extension.js +202 -0
- package/dist/code/extension.js.map +1 -0
- package/dist/code/index.js +19 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/language.js +43 -0
- package/dist/code/language.js.map +1 -0
- package/dist/collaboration/index.js +382 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/core/check.js +314 -0
- package/dist/core/check.js.map +1 -0
- package/dist/core/document.js +112 -0
- package/dist/core/document.js.map +1 -0
- package/dist/core/expression.js +854 -0
- package/dist/core/expression.js.map +1 -0
- package/dist/core/formulas.js +242 -0
- package/dist/core/formulas.js.map +1 -0
- package/dist/core/rules.js +83 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/values.js +80 -0
- package/dist/core/values.js.map +1 -0
- package/dist/fields/builtin.js +167 -0
- package/dist/fields/builtin.js.map +1 -0
- package/dist/fields/components.js +155 -0
- package/dist/fields/components.js.map +1 -0
- package/dist/fields/define.js +44 -0
- package/dist/fields/define.js.map +1 -0
- package/dist/fields/format.js +125 -0
- package/dist/fields/format.js.map +1 -0
- package/dist/fields/options.js +54 -0
- package/dist/fields/options.js.map +1 -0
- package/dist/index.js +20 -0
- package/dist/locale.js +161 -0
- package/dist/locale.js.map +1 -0
- package/dist/renderer/containers.js +430 -0
- package/dist/renderer/containers.js.map +1 -0
- package/dist/renderer/context.js +21 -0
- package/dist/renderer/context.js.map +1 -0
- package/dist/renderer/form-renderer.js +6 -0
- package/dist/renderer/form-renderer.js.map +1 -0
- package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js +272 -0
- package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/renderer/index.js +19 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/node-view.js +385 -0
- package/dist/renderer/node-view.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +27 -0
- package/dist/types/builder/canvas.d.ts +5 -0
- package/dist/types/builder/completion.d.ts +35 -0
- package/dist/types/builder/condition-editor.vue.d.ts +17 -0
- package/dist/types/builder/condition.d.ts +37 -0
- package/dist/types/builder/context.d.ts +149 -0
- package/dist/types/builder/core/commands.d.ts +74 -0
- package/dist/types/builder/core/history.d.ts +23 -0
- package/dist/types/builder/core/transaction.d.ts +108 -0
- package/dist/types/builder/drag.d.ts +52 -0
- package/dist/types/builder/expression-box.d.ts +8 -0
- package/dist/types/builder/expression-input.vue.d.ts +29 -0
- package/dist/types/builder/extension.d.ts +149 -0
- package/dist/types/builder/form-builder.vue.d.ts +24 -0
- package/dist/types/builder/index.d.ts +76 -0
- package/dist/types/builder/inspector.vue.d.ts +3 -0
- package/dist/types/builder/json-dialog.vue.d.ts +10 -0
- package/dist/types/builder/locale.d.ts +195 -0
- package/dist/types/builder/logic-fields.d.ts +2 -0
- package/dist/types/builder/logic.d.ts +57 -0
- package/dist/types/builder/menu.d.ts +23 -0
- package/dist/types/builder/outline-row.vue.d.ts +19 -0
- package/dist/types/builder/outline.vue.d.ts +3 -0
- package/dist/types/builder/palette.vue.d.ts +3 -0
- package/dist/types/builder/preview.vue.d.ts +3 -0
- package/dist/types/builder/settings-binding.d.ts +23 -0
- package/dist/types/builder/settings.d.ts +49 -0
- package/dist/types/builder/span-field.d.ts +4 -0
- package/dist/types/builder/types.d.ts +59 -0
- package/dist/types/code/expression-input.vue.d.ts +23 -0
- package/dist/types/code/extension.d.ts +45 -0
- package/dist/types/code/index.d.ts +12 -0
- package/dist/types/code/language.d.ts +6 -0
- package/dist/types/collaboration/index.d.ts +64 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/core/check.d.ts +68 -0
- package/dist/types/core/document.d.ts +36 -0
- package/dist/types/core/expression.d.ts +175 -0
- package/dist/types/core/formulas.d.ts +44 -0
- package/dist/types/core/rules.d.ts +23 -0
- package/dist/types/core/types.d.ts +211 -0
- package/dist/types/core/values.d.ts +22 -0
- package/dist/types/fields/builtin.d.ts +9 -0
- package/dist/types/fields/components.d.ts +177 -0
- package/dist/types/fields/define.d.ts +93 -0
- package/dist/types/fields/format.d.ts +76 -0
- package/dist/types/fields/options.d.ts +11 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/locale.d.ts +45 -0
- package/dist/types/renderer/containers.d.ts +20 -0
- package/dist/types/renderer/context.d.ts +186 -0
- package/dist/types/renderer/form-renderer.vue.d.ts +25 -0
- package/dist/types/renderer/index.d.ts +81 -0
- package/dist/types/renderer/node-view.d.ts +56 -0
- package/package.json +88 -4
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { isContainer, walkNodes } from "../core/document.js";
|
|
2
|
+
import { canContainFormNode, formNodePlaces } from "../builder/core/transaction.js";
|
|
3
|
+
import { replaceDocument } from "../builder/core/commands.js";
|
|
4
|
+
import { defineFormBuilderExtension } from "../builder/extension.js";
|
|
5
|
+
import { orderKeys } from "./order.js";
|
|
6
|
+
import { collaborationColors } from "dou-ui";
|
|
7
|
+
import { computed, shallowRef, watch } from "vue";
|
|
8
|
+
import * as Y from "yjs";
|
|
9
|
+
// src/collaboration/index.ts
|
|
10
|
+
/** The container a node is in (`null`: the top of the form) and its place there, kept beside its keys. */
|
|
11
|
+
var PARENT = "$parent";
|
|
12
|
+
var ORDER = "$order";
|
|
13
|
+
var same = (a, b) => a === b || typeof a === "object" && typeof b === "object" && JSON.stringify(a) === JSON.stringify(b);
|
|
14
|
+
/** Where in `value` something JSON can't carry sits, e.g. a component or a function. */
|
|
15
|
+
function unshareable(value, path) {
|
|
16
|
+
if (value === null || [
|
|
17
|
+
"string",
|
|
18
|
+
"number",
|
|
19
|
+
"boolean",
|
|
20
|
+
"undefined"
|
|
21
|
+
].includes(typeof value)) return null;
|
|
22
|
+
if (typeof value !== "object") return path;
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const [i, item] of value.entries()) {
|
|
25
|
+
const found = unshareable(item, `${path}[${i}]`);
|
|
26
|
+
if (found) return found;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const proto = Object.getPrototypeOf(value);
|
|
31
|
+
if (proto !== Object.prototype && proto !== null) return path;
|
|
32
|
+
for (const [key, item] of Object.entries(value)) {
|
|
33
|
+
const found = unshareable(item, `${path}.${key}`);
|
|
34
|
+
if (found) return found;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
var warned = /* @__PURE__ */ new Set();
|
|
39
|
+
/** Others would get a broken copy of such a value while this builder shows it fine. */
|
|
40
|
+
function warnUnshareable(id, key, value) {
|
|
41
|
+
if (process.env.NODE_ENV === "production") return;
|
|
42
|
+
const path = unshareable(value, key);
|
|
43
|
+
if (!path || warned.has(`${id}:${path}`)) return;
|
|
44
|
+
warned.add(`${id}:${path}`);
|
|
45
|
+
console.warn(`[@dou-ui/form-builder] "${id}" has ${path}, which can't be shared: only JSON values reach the others. Store a name and look the component or function up where it is used.`);
|
|
46
|
+
}
|
|
47
|
+
/** Writes a node's keys besides its place, only those that changed; `keys` limits which. */
|
|
48
|
+
function writeNode(map, node, keys) {
|
|
49
|
+
const existing = map.get(node.id);
|
|
50
|
+
const fields = existing instanceof Y.Map ? existing : new Y.Map();
|
|
51
|
+
const fresh = existing !== fields;
|
|
52
|
+
const item = node;
|
|
53
|
+
for (const [key, value] of Object.entries(item)) {
|
|
54
|
+
if (key === "id" || key === "children" || value === void 0) continue;
|
|
55
|
+
if (keys && !fresh && !keys.has(key)) continue;
|
|
56
|
+
warnUnshareable(node.id, key, value);
|
|
57
|
+
if (fresh || !same(fields.get(key), value)) fields.set(key, value);
|
|
58
|
+
}
|
|
59
|
+
if (fresh) return void map.set(node.id, fields);
|
|
60
|
+
for (const key of Array.from(fields.keys())) if (key[0] !== "$" && item[key] === void 0 && (!keys || keys.has(key))) fields.delete(key);
|
|
61
|
+
}
|
|
62
|
+
/** Writes the place of each child of a container, giving new order keys only where it must. */
|
|
63
|
+
function writeOrder(map, parent, children) {
|
|
64
|
+
const shared = children.map((child) => {
|
|
65
|
+
const fields = map.get(child.id);
|
|
66
|
+
return fields instanceof Y.Map ? fields : null;
|
|
67
|
+
});
|
|
68
|
+
const current = shared.map((fields) => fields && (fields.get(PARENT) ?? null) === parent ? fields.get(ORDER) ?? null : null);
|
|
69
|
+
const keys = orderKeys(current);
|
|
70
|
+
shared.forEach((fields, index) => {
|
|
71
|
+
if (!fields) return;
|
|
72
|
+
if ((fields.get(PARENT) ?? null) !== parent) fields.set(PARENT, parent);
|
|
73
|
+
if (fields.get(ORDER) !== keys[index]) fields.set(ORDER, keys[index]);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Writes a form into empty maps, e.g. when a form so far saved as JSON starts being edited together. */
|
|
77
|
+
function fillMaps(maps, document) {
|
|
78
|
+
const fill = () => {
|
|
79
|
+
for (const node of walkNodes(document.nodes)) writeNode(maps.nodes, node);
|
|
80
|
+
writeOrder(maps.nodes, null, document.nodes);
|
|
81
|
+
for (const node of walkNodes(document.nodes)) if (isContainer(node)) writeOrder(maps.nodes, node.id, node.children);
|
|
82
|
+
for (const [key, value] of Object.entries(document)) if (key !== "nodes" && key !== "version" && value !== void 0) maps.settings.set(key, value);
|
|
83
|
+
};
|
|
84
|
+
const ydoc = maps.nodes.doc;
|
|
85
|
+
if (ydoc) ydoc.transact(fill);
|
|
86
|
+
else fill();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The form the maps hold. A node whose container is gone, e.g. one moved
|
|
90
|
+
* into a section while another person deleted the section, goes to the top
|
|
91
|
+
* of the form. So do containers two people moved into each other at once:
|
|
92
|
+
* the one with the smallest id goes to the top, the same for everyone.
|
|
93
|
+
* Both are listed in `lifted`.
|
|
94
|
+
*/
|
|
95
|
+
function readMaps(maps) {
|
|
96
|
+
const entries = /* @__PURE__ */ new Map();
|
|
97
|
+
maps.nodes.forEach((value, id) => {
|
|
98
|
+
if (!(value instanceof Y.Map)) return;
|
|
99
|
+
const { [PARENT]: parent, [ORDER]: order, ...rest } = value.toJSON();
|
|
100
|
+
entries.set(id, {
|
|
101
|
+
node: {
|
|
102
|
+
...rest,
|
|
103
|
+
id
|
|
104
|
+
},
|
|
105
|
+
parent: typeof parent === "string" ? parent : null,
|
|
106
|
+
order: typeof order === "string" ? order : ""
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
const lifted = [];
|
|
110
|
+
const reached = /* @__PURE__ */ new Set();
|
|
111
|
+
const reach = (id) => {
|
|
112
|
+
const path = [];
|
|
113
|
+
let current = id;
|
|
114
|
+
while (current !== null && !reached.has(current)) {
|
|
115
|
+
const entry = entries.get(current);
|
|
116
|
+
const loop = path.indexOf(current);
|
|
117
|
+
if (loop >= 0) {
|
|
118
|
+
const top = path.slice(loop).sort()[0];
|
|
119
|
+
entries.get(top).parent = null;
|
|
120
|
+
lifted.push(top);
|
|
121
|
+
return reach(id);
|
|
122
|
+
}
|
|
123
|
+
path.push(current);
|
|
124
|
+
if (entry.parent !== null && !entries.has(entry.parent)) {
|
|
125
|
+
entry.parent = null;
|
|
126
|
+
lifted.push(current);
|
|
127
|
+
}
|
|
128
|
+
current = entry.parent;
|
|
129
|
+
}
|
|
130
|
+
for (const each of path) reached.add(each);
|
|
131
|
+
};
|
|
132
|
+
for (const id of entries.keys()) if (!reached.has(id)) reach(id);
|
|
133
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const entry of entries.values()) {
|
|
135
|
+
const list = byParent.get(entry.parent);
|
|
136
|
+
if (list) list.push(entry);
|
|
137
|
+
else byParent.set(entry.parent, [entry]);
|
|
138
|
+
}
|
|
139
|
+
const build = (parent) => {
|
|
140
|
+
const list = byParent.get(parent?.id ?? null) ?? [];
|
|
141
|
+
list.sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : a.node.id < b.node.id ? -1 : 1);
|
|
142
|
+
const nodes = [];
|
|
143
|
+
for (const { node } of list) {
|
|
144
|
+
if (!canContainFormNode(parent, node)) continue;
|
|
145
|
+
nodes.push(isContainer(node) ? {
|
|
146
|
+
...node,
|
|
147
|
+
children: build(node)
|
|
148
|
+
} : node);
|
|
149
|
+
}
|
|
150
|
+
return nodes;
|
|
151
|
+
};
|
|
152
|
+
const settings = maps.settings.toJSON();
|
|
153
|
+
delete settings.nodes;
|
|
154
|
+
delete settings.version;
|
|
155
|
+
return {
|
|
156
|
+
document: {
|
|
157
|
+
...settings,
|
|
158
|
+
version: 1,
|
|
159
|
+
nodes: build(null)
|
|
160
|
+
},
|
|
161
|
+
lifted
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** The nodes, places and settings a transaction changed, written to the maps. */
|
|
165
|
+
function writeChanges(maps, tr) {
|
|
166
|
+
const after = formNodePlaces(tr.doc);
|
|
167
|
+
const touched = /* @__PURE__ */ new Map();
|
|
168
|
+
const parents = /* @__PURE__ */ new Set();
|
|
169
|
+
const settings = /* @__PURE__ */ new Set();
|
|
170
|
+
let removed = false;
|
|
171
|
+
const mark = (id, keys) => {
|
|
172
|
+
const current = touched.get(id);
|
|
173
|
+
if (current === true) return;
|
|
174
|
+
if (keys === true) return void touched.set(id, true);
|
|
175
|
+
const set = current ?? /* @__PURE__ */ new Set();
|
|
176
|
+
for (const key of keys) set.add(key);
|
|
177
|
+
touched.set(id, set);
|
|
178
|
+
};
|
|
179
|
+
for (const step of tr.steps) if (step.type === "insert") {
|
|
180
|
+
parents.add(step.parent);
|
|
181
|
+
for (const node of walkNodes([step.node])) {
|
|
182
|
+
mark(node.id, true);
|
|
183
|
+
if (isContainer(node)) parents.add(node.id);
|
|
184
|
+
}
|
|
185
|
+
} else if (step.type === "remove") removed = true;
|
|
186
|
+
else if (step.type === "update") mark(step.id, Object.keys(step.patch));
|
|
187
|
+
else if (step.type === "move") parents.add(step.parent);
|
|
188
|
+
else for (const key of Object.keys(step.patch)) settings.add(key);
|
|
189
|
+
if (removed) {
|
|
190
|
+
for (const id of formNodePlaces(tr.before).keys()) if (!after.has(id)) maps.nodes.delete(id);
|
|
191
|
+
}
|
|
192
|
+
for (const [id, keys] of touched) {
|
|
193
|
+
const node = after.get(id)?.node;
|
|
194
|
+
if (node) writeNode(maps.nodes, node, keys === true ? void 0 : keys);
|
|
195
|
+
}
|
|
196
|
+
for (const parent of parents) if (parent === null) writeOrder(maps.nodes, null, tr.doc.nodes);
|
|
197
|
+
else {
|
|
198
|
+
const node = after.get(parent)?.node;
|
|
199
|
+
if (node && isContainer(node)) writeOrder(maps.nodes, parent, node.children);
|
|
200
|
+
}
|
|
201
|
+
const document = tr.doc;
|
|
202
|
+
for (const key of settings) {
|
|
203
|
+
const value = document[key];
|
|
204
|
+
if (value === void 0) maps.settings.delete(key);
|
|
205
|
+
else if (!same(maps.settings.get(key), value)) maps.settings.set(key, value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Edit a form together through a Yjs document. Each node lives in `nodes`
|
|
210
|
+
* as its own `Y.Map`, each key on its own, with its container and an order
|
|
211
|
+
* key beside them; so two people changing different keys of one field both
|
|
212
|
+
* keep their change, and moving a node only writes that node. The form's
|
|
213
|
+
* settings live in `settings`. The builder's `v-model` only mirrors them:
|
|
214
|
+
* a form set through `v-model` from outside is not written back. Undo only
|
|
215
|
+
* takes back this user's own changes. With `awareness`, each person sees
|
|
216
|
+
* what the others have selected, marked in their colors.
|
|
217
|
+
*/
|
|
218
|
+
function collaboration(options) {
|
|
219
|
+
const { nodes, settings, awareness, user } = options;
|
|
220
|
+
const ydoc = nodes.doc;
|
|
221
|
+
if (!ydoc || settings.doc !== ydoc) throw new Error("[@dou-ui/form-builder] nodes and settings must be maps of the same Y.Doc");
|
|
222
|
+
if (awareness && user) {
|
|
223
|
+
const color = user.color ?? collaborationColors[ydoc.clientID % collaborationColors.length];
|
|
224
|
+
awareness.setLocalStateField("user", {
|
|
225
|
+
name: user.name,
|
|
226
|
+
color
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const maps = {
|
|
230
|
+
nodes,
|
|
231
|
+
settings
|
|
232
|
+
};
|
|
233
|
+
/** This builder's own changes, which undo can take back. */
|
|
234
|
+
const tracked = { formBuilder: "tracked" };
|
|
235
|
+
/** Changes undo leaves alone, e.g. filling the maps or lifting nodes out of deleted containers. */
|
|
236
|
+
const untracked = { formBuilder: "untracked" };
|
|
237
|
+
const undoManager = new Y.UndoManager([nodes, settings], {
|
|
238
|
+
trackedOrigins: /* @__PURE__ */ new Set([tracked]),
|
|
239
|
+
captureTimeout: 500
|
|
240
|
+
});
|
|
241
|
+
let context;
|
|
242
|
+
/** Set while shared changes are applied to the builder, so they aren't written back. */
|
|
243
|
+
let applying = false;
|
|
244
|
+
/** What undo or redo is running, to name the change. */
|
|
245
|
+
let travelling = null;
|
|
246
|
+
/** The selection to go back to once undo or redo has run. */
|
|
247
|
+
let restore;
|
|
248
|
+
let lastGroup;
|
|
249
|
+
undoManager.on("stack-item-added", (event) => {
|
|
250
|
+
event.stackItem.meta.set("selection", context?.selection.slice() ?? []);
|
|
251
|
+
});
|
|
252
|
+
undoManager.on("stack-item-popped", (event) => {
|
|
253
|
+
restore = event.stackItem.meta.get("selection");
|
|
254
|
+
});
|
|
255
|
+
/** Brings the builder in line with the maps. */
|
|
256
|
+
function pull() {
|
|
257
|
+
if (!context) return;
|
|
258
|
+
const { document, lifted } = readMaps(maps);
|
|
259
|
+
const tr = context.transaction();
|
|
260
|
+
try {
|
|
261
|
+
replaceDocument(tr, document);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error("[@dou-ui/form-builder] could not apply a shared change", error);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (tr.changed) {
|
|
267
|
+
tr.setMeta("history", false);
|
|
268
|
+
tr.setMeta("origin", travelling ?? "remote");
|
|
269
|
+
applying = true;
|
|
270
|
+
try {
|
|
271
|
+
context.dispatch(tr);
|
|
272
|
+
} finally {
|
|
273
|
+
applying = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (lifted.length) queueMicrotask(() => ydoc.transact(() => {
|
|
277
|
+
for (const id of lifted) {
|
|
278
|
+
const fields = nodes.get(id);
|
|
279
|
+
if (fields instanceof Y.Map && fields.get(PARENT) !== null) fields.set(PARENT, null);
|
|
280
|
+
}
|
|
281
|
+
}, untracked));
|
|
282
|
+
}
|
|
283
|
+
function run(action) {
|
|
284
|
+
if (!(action === "undo" ? undoManager.undoStack : undoManager.redoStack).length) return false;
|
|
285
|
+
travelling = action;
|
|
286
|
+
restore = void 0;
|
|
287
|
+
try {
|
|
288
|
+
undoManager[action]();
|
|
289
|
+
} finally {
|
|
290
|
+
travelling = null;
|
|
291
|
+
}
|
|
292
|
+
if (restore && context) context.select(restore);
|
|
293
|
+
restore = void 0;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
const peers = shallowRef([]);
|
|
297
|
+
/** The others who have each node selected. */
|
|
298
|
+
const marksByNode = computed(() => {
|
|
299
|
+
const map = /* @__PURE__ */ new Map();
|
|
300
|
+
for (const peer of peers.value) for (const id of peer.selection) {
|
|
301
|
+
const mark = {
|
|
302
|
+
key: `peer-${peer.client}`,
|
|
303
|
+
label: peer.name,
|
|
304
|
+
color: peer.color
|
|
305
|
+
};
|
|
306
|
+
const list = map.get(id);
|
|
307
|
+
if (list) list.push(mark);
|
|
308
|
+
else map.set(id, [mark]);
|
|
309
|
+
}
|
|
310
|
+
return map;
|
|
311
|
+
});
|
|
312
|
+
return defineFormBuilderExtension({
|
|
313
|
+
name: "collaboration",
|
|
314
|
+
priority: 0,
|
|
315
|
+
transaction(tr) {
|
|
316
|
+
if (applying) return;
|
|
317
|
+
const tracking = tr.meta.history !== false;
|
|
318
|
+
if (tracking && (!tr.meta.group || tr.meta.group !== lastGroup)) undoManager.stopCapturing();
|
|
319
|
+
if (tracking) lastGroup = tr.meta.group;
|
|
320
|
+
ydoc.transact(() => writeChanges(maps, tr), tracking ? tracked : untracked);
|
|
321
|
+
},
|
|
322
|
+
history: () => ({
|
|
323
|
+
undo: () => run("undo"),
|
|
324
|
+
redo: () => run("redo"),
|
|
325
|
+
canUndo: () => undoManager.undoStack.length > 0,
|
|
326
|
+
canRedo: () => undoManager.redoStack.length > 0
|
|
327
|
+
}),
|
|
328
|
+
marks: awareness ? (_context, node) => marksByNode.value.get(node.id) ?? [] : void 0,
|
|
329
|
+
setup(builder) {
|
|
330
|
+
context = builder;
|
|
331
|
+
if (!nodes.size && !settings.size) ydoc.transact(() => fillMaps(maps, builder.document), untracked);
|
|
332
|
+
pull();
|
|
333
|
+
let dirty = false;
|
|
334
|
+
const observe = (_events, transaction) => {
|
|
335
|
+
if (transaction.origin !== tracked && transaction.origin !== untracked) dirty = true;
|
|
336
|
+
};
|
|
337
|
+
const flush = () => {
|
|
338
|
+
if (!dirty) return;
|
|
339
|
+
dirty = false;
|
|
340
|
+
pull();
|
|
341
|
+
};
|
|
342
|
+
nodes.observeDeep(observe);
|
|
343
|
+
settings.observeDeep(observe);
|
|
344
|
+
ydoc.on("afterTransaction", flush);
|
|
345
|
+
let stopPresence;
|
|
346
|
+
let readPeers;
|
|
347
|
+
if (awareness) {
|
|
348
|
+
readPeers = () => {
|
|
349
|
+
const list = [];
|
|
350
|
+
awareness.getStates().forEach((state, client) => {
|
|
351
|
+
if (client === awareness.clientID) return;
|
|
352
|
+
const who = state.user;
|
|
353
|
+
const presence = state.formBuilder;
|
|
354
|
+
if (!who || !presence?.selection?.length) return;
|
|
355
|
+
list.push({
|
|
356
|
+
client,
|
|
357
|
+
name: who.name ?? "",
|
|
358
|
+
color: who.color ?? "currentColor",
|
|
359
|
+
selection: presence.selection
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
peers.value = list;
|
|
363
|
+
};
|
|
364
|
+
readPeers();
|
|
365
|
+
awareness.on("change", readPeers);
|
|
366
|
+
stopPresence = watch(() => builder.selection, (selection) => awareness.setLocalStateField("formBuilder", { selection: selection.slice(0, 100) }), { immediate: true });
|
|
367
|
+
}
|
|
368
|
+
return () => {
|
|
369
|
+
nodes.unobserveDeep(observe);
|
|
370
|
+
settings.unobserveDeep(observe);
|
|
371
|
+
ydoc.off("afterTransaction", flush);
|
|
372
|
+
stopPresence?.();
|
|
373
|
+
if (readPeers) awareness?.off("change", readPeers);
|
|
374
|
+
awareness?.setLocalStateField("formBuilder", null);
|
|
375
|
+
context = void 0;
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
export { collaboration, collaborationColors, fillMaps, readMaps };
|
|
381
|
+
|
|
382
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/collaboration/index.ts"],"sourcesContent":["import { collaborationColors } from 'dou-ui'\nimport { computed, shallowRef, watch } from 'vue'\nimport * as Y from 'yjs'\nimport { isContainer, walkNodes } from '../core/document'\nimport type { FormContainerNode, FormDocument, FormNode } from '../core/types'\nimport type { FormBuilderContext } from '../builder/context'\nimport { replaceDocument } from '../builder/core/commands'\nimport { canContainFormNode, formNodePlaces } from '../builder/core/transaction'\nimport type { FormBuilderTransaction } from '../builder/core/transaction'\nimport { defineFormBuilderExtension } from '../builder/extension'\nimport type { FormBuilderExtension, FormBuilderNodeMark } from '../builder/extension'\nimport { orderKeys } from './order'\n\n/** The part of an `Awareness` (`y-protocols/awareness`) the builder uses. */\nexport interface AwarenessLike {\n clientID: number\n getLocalState(): Record<string, unknown> | null\n setLocalStateField(field: string, value: unknown): void\n getStates(): Map<number, Record<string, unknown>>\n on(event: 'change', listener: (...args: unknown[]) => void): void\n off(event: 'change', listener: (...args: unknown[]) => void): void\n}\n\n/** How the people editing together see each other. */\nexport interface CollaborationUser {\n name: string\n /** The color of this user's marks, any CSS color. Picked from the client id when left out. */\n color?: string\n}\n\nexport interface CollaborationOptions {\n /** Where the nodes live, e.g. `ydoc.getMap('nodes')`. */\n nodes: Y.Map<unknown>\n /** Where the form's own settings live, e.g. `ydoc.getMap('settings')`; in the same `Y.Doc` as `nodes`. */\n settings: Y.Map<unknown>\n /** Shares what each person has selected (`y-protocols/awareness`). */\n awareness?: AwarenessLike\n /** This user, shown to the others. Needs `awareness`. */\n user?: CollaborationUser\n}\n\n/** What each person shares under the awareness field `formBuilder`. */\nexport interface FormBuilderPresence {\n selection: string[]\n}\n\nexport { collaborationColors }\n\ntype Shared = Y.Map<unknown>\ntype Item = Record<string, unknown>\n\n/** The container a node is in (`null`: the top of the form) and its place there, kept beside its keys. */\nconst PARENT = '$parent'\nconst ORDER = '$order'\n\nconst same = (a: unknown, b: unknown) =>\n a === b ||\n (typeof a === 'object' && typeof b === 'object' && JSON.stringify(a) === JSON.stringify(b))\n\n/** Where in `value` something JSON can't carry sits, e.g. a component or a function. */\nfunction unshareable(value: unknown, path: string): string | null {\n if (value === null || ['string', 'number', 'boolean', 'undefined'].includes(typeof value))\n return null\n if (typeof value !== 'object') return path\n if (Array.isArray(value)) {\n for (const [i, item] of value.entries()) {\n const found = unshareable(item, `${path}[${i}]`)\n if (found) return found\n }\n return null\n }\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return path\n for (const [key, item] of Object.entries(value)) {\n const found = unshareable(item, `${path}.${key}`)\n if (found) return found\n }\n return null\n}\n\nconst warned = new Set<string>()\n/** Others would get a broken copy of such a value while this builder shows it fine. */\nfunction warnUnshareable(id: string, key: string, value: unknown) {\n // the app's bundler replaces this, as for Vue's own warnings\n if (process.env.NODE_ENV === 'production') return\n const path = unshareable(value, key)\n if (!path || warned.has(`${id}:${path}`)) return\n warned.add(`${id}:${path}`)\n console.warn(\n `[@dou-ui/form-builder] \"${id}\" has ${path}, which can't be shared: only JSON values reach the others. Store a name and look the component or function up where it is used.`,\n )\n}\n\n/** Writes a node's keys besides its place, only those that changed; `keys` limits which. */\nfunction writeNode(map: Shared, node: FormNode, keys?: Set<string>) {\n const existing = map.get(node.id)\n const fields: Shared = existing instanceof Y.Map ? existing : new Y.Map<unknown>()\n const fresh = existing !== fields\n const item = node as unknown as Item\n for (const [key, value] of Object.entries(item)) {\n if (key === 'id' || key === 'children' || value === undefined) continue\n if (keys && !fresh && !keys.has(key)) continue\n warnUnshareable(node.id, key, value)\n // a new map can't be read before it is in the document\n if (fresh || !same(fields.get(key), value)) fields.set(key, value)\n }\n if (fresh) return void map.set(node.id, fields)\n for (const key of Array.from(fields.keys()))\n if (key[0] !== '$' && item[key] === undefined && (!keys || keys.has(key))) fields.delete(key)\n}\n\n/** Writes the place of each child of a container, giving new order keys only where it must. */\nfunction writeOrder(map: Shared, parent: string | null, children: readonly FormNode[]) {\n const shared = children.map((child) => {\n const fields = map.get(child.id)\n return fields instanceof Y.Map ? fields : null\n })\n const current = shared.map((fields) =>\n fields && (fields.get(PARENT) ?? null) === parent\n ? ((fields.get(ORDER) as string | undefined) ?? null)\n : null,\n )\n const keys = orderKeys(current)\n shared.forEach((fields, index) => {\n if (!fields) return\n if ((fields.get(PARENT) ?? null) !== parent) fields.set(PARENT, parent)\n if (fields.get(ORDER) !== keys[index]) fields.set(ORDER, keys[index])\n })\n}\n\n/** Writes a form into empty maps, e.g. when a form so far saved as JSON starts being edited together. */\nexport function fillMaps(\n maps: { nodes: Y.Map<unknown>; settings: Y.Map<unknown> },\n document: FormDocument,\n) {\n const fill = () => {\n for (const node of walkNodes(document.nodes)) writeNode(maps.nodes, node)\n writeOrder(maps.nodes, null, document.nodes)\n for (const node of walkNodes(document.nodes))\n if (isContainer(node)) writeOrder(maps.nodes, node.id, node.children)\n for (const [key, value] of Object.entries(document))\n if (key !== 'nodes' && key !== 'version' && value !== undefined) maps.settings.set(key, value)\n }\n const ydoc = maps.nodes.doc\n if (ydoc) ydoc.transact(fill)\n else fill()\n}\n\ninterface Entry {\n node: FormNode\n parent: string | null\n order: string\n}\n\n/**\n * The form the maps hold. A node whose container is gone, e.g. one moved\n * into a section while another person deleted the section, goes to the top\n * of the form. So do containers two people moved into each other at once:\n * the one with the smallest id goes to the top, the same for everyone.\n * Both are listed in `lifted`.\n */\nexport function readMaps(maps: { nodes: Y.Map<unknown>; settings: Y.Map<unknown> }): {\n document: FormDocument\n lifted: string[]\n} {\n const entries = new Map<string, Entry>()\n maps.nodes.forEach((value, id) => {\n if (!(value instanceof Y.Map)) return\n const { [PARENT]: parent, [ORDER]: order, ...rest } = value.toJSON() as Item\n entries.set(id, {\n node: { ...rest, id } as unknown as FormNode,\n parent: typeof parent === 'string' ? parent : null,\n order: typeof order === 'string' ? order : '',\n })\n })\n\n const lifted: string[] = []\n const reached = new Set<string>()\n // Walk up from each node until the top or a node already reached.\n const reach = (id: string) => {\n const path: string[] = []\n let current: string | null = id\n while (current !== null && !reached.has(current)) {\n const entry: Entry = entries.get(current)!\n const loop = path.indexOf(current)\n if (loop >= 0) {\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a copy\n const top = path.slice(loop).sort()[0]!\n entries.get(top)!.parent = null\n lifted.push(top)\n return reach(id)\n }\n path.push(current)\n if (entry.parent !== null && !entries.has(entry.parent)) {\n entry.parent = null\n lifted.push(current)\n }\n current = entry.parent\n }\n for (const each of path) reached.add(each)\n }\n for (const id of entries.keys()) if (!reached.has(id)) reach(id)\n\n const byParent = new Map<string | null, Entry[]>()\n for (const entry of entries.values()) {\n const list = byParent.get(entry.parent)\n if (list) list.push(entry)\n else byParent.set(entry.parent, [entry])\n }\n const build = (parent: FormNode | null): FormNode[] => {\n const list = byParent.get(parent?.id ?? null) ?? []\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts the list built here\n list.sort((a, b) =>\n a.order < b.order ? -1 : a.order > b.order ? 1 : a.node.id < b.node.id ? -1 : 1,\n )\n const nodes: FormNode[] = []\n for (const { node } of list) {\n // A node that can't be here, e.g. a field among pages, waits until it can.\n if (!canContainFormNode(parent, node)) continue\n nodes.push(\n isContainer(node)\n ? ({ ...node, children: build(node) } as FormContainerNode)\n : (node as FormNode),\n )\n }\n return nodes\n }\n const settings = maps.settings.toJSON() as Item\n delete settings.nodes\n delete settings.version\n return {\n document: { ...settings, version: 1, nodes: build(null) } as FormDocument,\n lifted,\n }\n}\n\n/** The nodes, places and settings a transaction changed, written to the maps. */\nfunction writeChanges(maps: { nodes: Shared; settings: Shared }, tr: FormBuilderTransaction) {\n const after = formNodePlaces(tr.doc)\n const touched = new Map<string, Set<string> | true>()\n const parents = new Set<string | null>()\n const settings = new Set<string>()\n let removed = false\n const mark = (id: string, keys: string[] | true) => {\n const current = touched.get(id)\n if (current === true) return\n if (keys === true) return void touched.set(id, true)\n const set = current ?? new Set<string>()\n for (const key of keys) set.add(key)\n touched.set(id, set)\n }\n for (const step of tr.steps) {\n if (step.type === 'insert') {\n parents.add(step.parent)\n for (const node of walkNodes([step.node])) {\n mark(node.id, true)\n if (isContainer(node)) parents.add(node.id)\n }\n } else if (step.type === 'remove') removed = true\n else if (step.type === 'update') mark(step.id, Object.keys(step.patch))\n else if (step.type === 'move') parents.add(step.parent)\n else for (const key of Object.keys(step.patch)) settings.add(key)\n }\n\n if (removed)\n for (const id of formNodePlaces(tr.before).keys()) if (!after.has(id)) maps.nodes.delete(id)\n for (const [id, keys] of touched) {\n const node = after.get(id)?.node\n if (node) writeNode(maps.nodes, node, keys === true ? undefined : keys)\n }\n for (const parent of parents) {\n if (parent === null) writeOrder(maps.nodes, null, tr.doc.nodes)\n else {\n const node = after.get(parent)?.node\n if (node && isContainer(node)) writeOrder(maps.nodes, parent, node.children)\n }\n }\n const document = tr.doc as unknown as Item\n for (const key of settings) {\n const value = document[key]\n if (value === undefined) maps.settings.delete(key)\n else if (!same(maps.settings.get(key), value)) maps.settings.set(key, value)\n }\n}\n\ninterface Peer {\n client: number\n name: string\n color: string\n selection: string[]\n}\n\n/**\n * Edit a form together through a Yjs document. Each node lives in `nodes`\n * as its own `Y.Map`, each key on its own, with its container and an order\n * key beside them; so two people changing different keys of one field both\n * keep their change, and moving a node only writes that node. The form's\n * settings live in `settings`. The builder's `v-model` only mirrors them:\n * a form set through `v-model` from outside is not written back. Undo only\n * takes back this user's own changes. With `awareness`, each person sees\n * what the others have selected, marked in their colors.\n */\nexport function collaboration(options: CollaborationOptions): FormBuilderExtension {\n const { nodes, settings, awareness, user } = options\n const ydoc = nodes.doc\n if (!ydoc || settings.doc !== ydoc)\n throw new Error('[@dou-ui/form-builder] nodes and settings must be maps of the same Y.Doc')\n if (awareness && user) {\n const color = user.color ?? collaborationColors[ydoc.clientID % collaborationColors.length]\n awareness.setLocalStateField('user', { name: user.name, color })\n }\n const maps = { nodes, settings }\n\n /** This builder's own changes, which undo can take back. */\n const tracked = { formBuilder: 'tracked' }\n /** Changes undo leaves alone, e.g. filling the maps or lifting nodes out of deleted containers. */\n const untracked = { formBuilder: 'untracked' }\n const undoManager = new Y.UndoManager([nodes, settings], {\n trackedOrigins: new Set([tracked]),\n captureTimeout: 500,\n })\n let context: FormBuilderContext | undefined\n /** Set while shared changes are applied to the builder, so they aren't written back. */\n let applying = false\n /** What undo or redo is running, to name the change. */\n let travelling: 'undo' | 'redo' | null = null\n /** The selection to go back to once undo or redo has run. */\n let restore: string[] | undefined\n let lastGroup: string | undefined\n\n // Each step of undo keeps the selection from before it, and each step of redo the one after.\n undoManager.on('stack-item-added', (event: { stackItem: { meta: Map<string, unknown> } }) => {\n event.stackItem.meta.set('selection', context?.selection.slice() ?? [])\n })\n undoManager.on('stack-item-popped', (event: { stackItem: { meta: Map<string, unknown> } }) => {\n restore = event.stackItem.meta.get('selection') as string[] | undefined\n })\n\n /** Brings the builder in line with the maps. */\n function pull() {\n if (!context) return\n const { document, lifted } = readMaps(maps)\n const tr = context.transaction()\n try {\n replaceDocument(tr, document)\n } catch (error) {\n console.error('[@dou-ui/form-builder] could not apply a shared change', error)\n return\n }\n if (tr.changed) {\n tr.setMeta('history', false)\n tr.setMeta('origin', travelling ?? 'remote')\n applying = true\n try {\n context.dispatch(tr)\n } finally {\n applying = false\n }\n }\n // Everyone works these out alike; writing them keeps the maps tidy.\n if (lifted.length)\n queueMicrotask(() =>\n ydoc!.transact(() => {\n for (const id of lifted) {\n const fields = nodes.get(id)\n if (fields instanceof Y.Map && fields.get(PARENT) !== null) fields.set(PARENT, null)\n }\n }, untracked),\n )\n }\n\n function run(action: 'undo' | 'redo') {\n const stack = action === 'undo' ? undoManager.undoStack : undoManager.redoStack\n if (!stack.length) return false\n travelling = action\n restore = undefined\n try {\n undoManager[action]()\n } finally {\n travelling = null\n }\n if (restore && context) context.select(restore)\n restore = undefined\n return true\n }\n\n const peers = shallowRef<Peer[]>([])\n /** The others who have each node selected. */\n const marksByNode = computed(() => {\n const map = new Map<string, FormBuilderNodeMark[]>()\n for (const peer of peers.value)\n for (const id of peer.selection) {\n const mark = { key: `peer-${peer.client}`, label: peer.name, color: peer.color }\n const list = map.get(id)\n if (list) list.push(mark)\n else map.set(id, [mark])\n }\n return map\n })\n\n return defineFormBuilderExtension({\n name: 'collaboration',\n // after the other extensions, so it writes what they added\n priority: 0,\n transaction(tr) {\n if (applying) return\n const tracking = tr.meta.history !== false\n // like the builder's own history: only changes of one group undo together\n if (tracking && (!tr.meta.group || tr.meta.group !== lastGroup)) undoManager.stopCapturing()\n if (tracking) lastGroup = tr.meta.group\n ydoc.transact(() => writeChanges(maps, tr), tracking ? tracked : untracked)\n },\n history: () => ({\n undo: () => run('undo'),\n redo: () => run('redo'),\n canUndo: () => undoManager.undoStack.length > 0,\n canRedo: () => undoManager.redoStack.length > 0,\n }),\n marks: awareness ? (_context, node) => marksByNode.value.get(node.id) ?? [] : undefined,\n setup(builder) {\n context = builder\n // the first one to open the form brings its nodes; everyone else reads the maps\n if (!nodes.size && !settings.size)\n ydoc.transact(() => fillMaps(maps, builder.document), untracked)\n pull()\n\n // changes from others, and from undo: applied once per Yjs transaction\n let dirty = false\n const observe = (_events: unknown, transaction: Y.Transaction) => {\n if (transaction.origin !== tracked && transaction.origin !== untracked) dirty = true\n }\n const flush = () => {\n if (!dirty) return\n dirty = false\n pull()\n }\n nodes.observeDeep(observe)\n settings.observeDeep(observe)\n ydoc.on('afterTransaction', flush)\n\n let stopPresence: (() => void) | undefined\n let readPeers: (() => void) | undefined\n if (awareness) {\n readPeers = () => {\n const list: Peer[] = []\n awareness.getStates().forEach((state, client) => {\n if (client === awareness.clientID) return\n const who = state.user as { name?: string; color?: string } | undefined\n const presence = state.formBuilder as FormBuilderPresence | null | undefined\n if (!who || !presence?.selection?.length) return\n list.push({\n client,\n name: who.name ?? '',\n color: who.color ?? 'currentColor',\n selection: presence.selection,\n })\n })\n peers.value = list\n }\n readPeers()\n awareness.on('change', readPeers)\n stopPresence = watch(\n () => builder.selection,\n (selection) =>\n awareness.setLocalStateField('formBuilder', {\n selection: selection.slice(0, 100),\n } satisfies FormBuilderPresence),\n { immediate: true },\n )\n }\n\n return () => {\n nodes.unobserveDeep(observe)\n settings.unobserveDeep(observe)\n ydoc.off('afterTransaction', flush)\n stopPresence?.()\n if (readPeers) awareness?.off('change', readPeers)\n awareness?.setLocalStateField('formBuilder', null)\n context = undefined\n }\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/collaboration/order.ts
|
|
2
|
+
/**
|
|
3
|
+
* Order keys: strings that sort like the nodes they belong to. A key can
|
|
4
|
+
* always be made between two others, so moving or inserting a node only
|
|
5
|
+
* writes that node's key, never its neighbours'.
|
|
6
|
+
*/
|
|
7
|
+
var DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
8
|
+
/** A key between `a` and `b` (`b === null` means no upper bound). Keys never end in `0`. */
|
|
9
|
+
function midpoint(a, b) {
|
|
10
|
+
if (b !== null) {
|
|
11
|
+
let n = 0;
|
|
12
|
+
while ((a[n] ?? "0") === b[n]) n++;
|
|
13
|
+
if (n > 0) return b.slice(0, n) + midpoint(a.slice(n), b.slice(n));
|
|
14
|
+
}
|
|
15
|
+
const low = a ? DIGITS.indexOf(a[0]) : 0;
|
|
16
|
+
const high = b !== null ? DIGITS.indexOf(b[0]) : 62;
|
|
17
|
+
if (high - low > 1) return DIGITS[Math.round((low + high) / 2)];
|
|
18
|
+
if (b !== null && b.length > 1) return b.slice(0, 1);
|
|
19
|
+
return DIGITS[low] + midpoint(a.slice(1), null);
|
|
20
|
+
}
|
|
21
|
+
/** A key after `a` and before `b`; either may be missing. Needs `a < b`. */
|
|
22
|
+
function keyBetween(a, b) {
|
|
23
|
+
if (a !== null && b !== null && a >= b) throw new Error(`[@dou-ui/form-builder] bad order keys ${a} ${b}`);
|
|
24
|
+
return midpoint(a ?? "", b);
|
|
25
|
+
}
|
|
26
|
+
/** `n` keys in order between `a` and `b`, kept short by splitting the range in halves. */
|
|
27
|
+
function keysBetween(a, b, n) {
|
|
28
|
+
if (n <= 0) return [];
|
|
29
|
+
const middle = keyBetween(a, b);
|
|
30
|
+
const left = Math.floor((n - 1) / 2);
|
|
31
|
+
return [
|
|
32
|
+
...keysBetween(a, middle, left),
|
|
33
|
+
middle,
|
|
34
|
+
...keysBetween(middle, b, n - 1 - left)
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Which of `keys` (in their wanted order) can stay: the longest strictly
|
|
39
|
+
* increasing run. Missing keys (`null`) never stay. Returns the indexes.
|
|
40
|
+
*/
|
|
41
|
+
function keptIndexes(keys) {
|
|
42
|
+
const tails = [];
|
|
43
|
+
const previous = [];
|
|
44
|
+
keys.forEach((key, index) => {
|
|
45
|
+
if (key === null) return;
|
|
46
|
+
let low = 0;
|
|
47
|
+
let high = tails.length;
|
|
48
|
+
while (low < high) {
|
|
49
|
+
const mid = low + high >> 1;
|
|
50
|
+
if (keys[tails[mid]] < key) low = mid + 1;
|
|
51
|
+
else high = mid;
|
|
52
|
+
}
|
|
53
|
+
previous[index] = low > 0 ? tails[low - 1] : -1;
|
|
54
|
+
tails[low] = index;
|
|
55
|
+
});
|
|
56
|
+
const kept = /* @__PURE__ */ new Set();
|
|
57
|
+
let index = tails.length ? tails[tails.length - 1] : -1;
|
|
58
|
+
while (index >= 0) {
|
|
59
|
+
kept.add(index);
|
|
60
|
+
index = previous[index];
|
|
61
|
+
}
|
|
62
|
+
return kept;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Order keys for items in their wanted order, given each one's current key
|
|
66
|
+
* (`null` when it has none). The longest run already in order keeps its
|
|
67
|
+
* keys; the others get fresh keys between their kept neighbours.
|
|
68
|
+
*/
|
|
69
|
+
function orderKeys(keys) {
|
|
70
|
+
const kept = keptIndexes(keys);
|
|
71
|
+
const result = keys.slice();
|
|
72
|
+
let i = 0;
|
|
73
|
+
while (i < result.length) {
|
|
74
|
+
if (kept.has(i)) {
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
let j = i;
|
|
79
|
+
while (j < result.length && !kept.has(j)) j++;
|
|
80
|
+
const fresh = keysBetween(i > 0 ? result[i - 1] : null, j < result.length ? result[j] : null, j - i);
|
|
81
|
+
for (let k = i; k < j; k++) result[k] = fresh[k - i];
|
|
82
|
+
i = j;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
export { keptIndexes, keyBetween, keysBetween, orderKeys };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order.js","names":[],"sources":["../../src/collaboration/order.ts"],"sourcesContent":["/**\n * Order keys: strings that sort like the nodes they belong to. A key can\n * always be made between two others, so moving or inserting a node only\n * writes that node's key, never its neighbours'.\n */\n\n// ascending in code-unit order, so plain `<` compares keys\nconst DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\n\n/** A key between `a` and `b` (`b === null` means no upper bound). Keys never end in `0`. */\nfunction midpoint(a: string, b: string | null): string {\n if (b !== null) {\n let n = 0\n while ((a[n] ?? '0') === b[n]) n++\n if (n > 0) return b.slice(0, n) + midpoint(a.slice(n), b.slice(n))\n }\n const low = a ? DIGITS.indexOf(a[0]!) : 0\n const high = b !== null ? DIGITS.indexOf(b[0]!) : DIGITS.length\n if (high - low > 1) return DIGITS[Math.round((low + high) / 2)]!\n if (b !== null && b.length > 1) return b.slice(0, 1)\n return DIGITS[low]! + midpoint(a.slice(1), null)\n}\n\n/** A key after `a` and before `b`; either may be missing. Needs `a < b`. */\nexport function keyBetween(a: string | null, b: string | null): string {\n if (a !== null && b !== null && a >= b)\n throw new Error(`[@dou-ui/form-builder] bad order keys ${a} ${b}`)\n return midpoint(a ?? '', b)\n}\n\n/** `n` keys in order between `a` and `b`, kept short by splitting the range in halves. */\nexport function keysBetween(a: string | null, b: string | null, n: number): string[] {\n if (n <= 0) return []\n const middle = keyBetween(a, b)\n const left = Math.floor((n - 1) / 2)\n return [...keysBetween(a, middle, left), middle, ...keysBetween(middle, b, n - 1 - left)]\n}\n\n/**\n * Which of `keys` (in their wanted order) can stay: the longest strictly\n * increasing run. Missing keys (`null`) never stay. Returns the indexes.\n */\nexport function keptIndexes(keys: (string | null)[]): Set<number> {\n // patience sorting with back links\n const tails: number[] = []\n const previous: number[] = []\n keys.forEach((key, index) => {\n if (key === null) return\n let low = 0\n let high = tails.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (keys[tails[mid]!]! < key) low = mid + 1\n else high = mid\n }\n previous[index] = low > 0 ? tails[low - 1]! : -1\n tails[low] = index\n })\n const kept = new Set<number>()\n let index = tails.length ? tails[tails.length - 1]! : -1\n while (index >= 0) {\n kept.add(index)\n index = previous[index]!\n }\n return kept\n}\n\n/**\n * Order keys for items in their wanted order, given each one's current key\n * (`null` when it has none). The longest run already in order keeps its\n * keys; the others get fresh keys between their kept neighbours.\n */\nexport function orderKeys(keys: (string | null)[]): string[] {\n const kept = keptIndexes(keys)\n const result = keys.slice()\n let i = 0\n while (i < result.length) {\n if (kept.has(i)) {\n i++\n continue\n }\n let j = i\n while (j < result.length && !kept.has(j)) j++\n const fresh = keysBetween(\n i > 0 ? result[i - 1]! : null,\n j < result.length ? result[j]! : null,\n j - i,\n )\n for (let k = i; k < j; k++) result[k] = fresh[k - i]!\n i = j\n }\n return result as string[]\n}\n"],"mappings":";;;;;;AAOA,IAAM,SAAS;;AAGf,SAAS,SAAS,GAAW,GAA0B;CACrD,IAAI,MAAM,MAAM;EACd,IAAI,IAAI;EACR,QAAQ,EAAE,MAAM,SAAS,EAAE,IAAI;EAC/B,IAAI,IAAI,GAAG,OAAO,EAAE,MAAM,GAAG,CAAC,IAAI,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;CACnE;CACA,MAAM,MAAM,IAAI,OAAO,QAAQ,EAAE,EAAG,IAAI;CACxC,MAAM,OAAO,MAAM,OAAO,OAAO,QAAQ,EAAE,EAAG,IAAI;CAClD,IAAI,OAAO,MAAM,GAAG,OAAO,OAAO,KAAK,OAAO,MAAM,QAAQ,CAAC;CAC7D,IAAI,MAAM,QAAQ,EAAE,SAAS,GAAG,OAAO,EAAE,MAAM,GAAG,CAAC;CACnD,OAAO,OAAO,OAAQ,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI;AACjD;;AAGA,SAAgB,WAAW,GAAkB,GAA0B;CACrE,IAAI,MAAM,QAAQ,MAAM,QAAQ,KAAK,GACnC,MAAM,IAAI,MAAM,yCAAyC,EAAE,GAAG,GAAG;CACnE,OAAO,SAAS,KAAK,IAAI,CAAC;AAC5B;;AAGA,SAAgB,YAAY,GAAkB,GAAkB,GAAqB;CACnF,IAAI,KAAK,GAAG,OAAO,CAAC;CACpB,MAAM,SAAS,WAAW,GAAG,CAAC;CAC9B,MAAM,OAAO,KAAK,OAAO,IAAI,KAAK,CAAC;CACnC,OAAO;EAAC,GAAG,YAAY,GAAG,QAAQ,IAAI;EAAG;EAAQ,GAAG,YAAY,QAAQ,GAAG,IAAI,IAAI,IAAI;CAAC;AAC1F;;;;;AAMA,SAAgB,YAAY,MAAsC;CAEhE,MAAM,QAAkB,CAAC;CACzB,MAAM,WAAqB,CAAC;CAC5B,KAAK,SAAS,KAAK,UAAU;EAC3B,IAAI,QAAQ,MAAM;EAClB,IAAI,MAAM;EACV,IAAI,OAAO,MAAM;EACjB,OAAO,MAAM,MAAM;GACjB,MAAM,MAAO,MAAM,QAAS;GAC5B,IAAI,KAAK,MAAM,QAAU,KAAK,MAAM,MAAM;QACrC,OAAO;EACd;EACA,SAAS,SAAS,MAAM,IAAI,MAAM,MAAM,KAAM;EAC9C,MAAM,OAAO;CACf,CAAC;CACD,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI,QAAQ,MAAM,SAAS,MAAM,MAAM,SAAS,KAAM;CACtD,OAAO,SAAS,GAAG;EACjB,KAAK,IAAI,KAAK;EACd,QAAQ,SAAS;CACnB;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,UAAU,MAAmC;CAC3D,MAAM,OAAO,YAAY,IAAI;CAC7B,MAAM,SAAS,KAAK,MAAM;CAC1B,IAAI,IAAI;CACR,OAAO,IAAI,OAAO,QAAQ;EACxB,IAAI,KAAK,IAAI,CAAC,GAAG;GACf;GACA;EACF;EACA,IAAI,IAAI;EACR,OAAO,IAAI,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG;EAC1C,MAAM,QAAQ,YACZ,IAAI,IAAI,OAAO,IAAI,KAAM,MACzB,IAAI,OAAO,SAAS,OAAO,KAAM,MACjC,IAAI,CACN;EACA,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,OAAO,KAAK,MAAM,IAAI;EAClD,IAAI;CACN;CACA,OAAO;AACT"}
|