@abraca/nuxt 2.9.0 → 2.11.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/dist/module.d.mts +14 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/assets/editor.css +1 -1
- package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
- package/dist/runtime/components/AConnectionBadge.vue +79 -0
- package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
- package/dist/runtime/components/AEditor.d.vue.ts +2 -2
- package/dist/runtime/components/AEditor.vue +11 -1
- package/dist/runtime/components/AEditor.vue.d.ts +2 -2
- package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
- package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
- package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
- package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
- package/dist/runtime/components/AModalShell.vue +105 -0
- package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
- package/dist/runtime/components/ANodePanel.d.vue.ts +8 -6
- package/dist/runtime/components/ANodePanel.vue +25 -0
- package/dist/runtime/components/ANodePanel.vue.d.ts +8 -6
- package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
- package/dist/runtime/components/ANodePanelHeader.vue +17 -3
- package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
- package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
- package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
- package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
- package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
- package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
- package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
- package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +2 -2
- package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +2 -2
- package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
- package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
- package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
- package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
- package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
- package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
- package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
- package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
- package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
- package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
- package/dist/runtime/composables/useDocSnapshots.js +5 -0
- package/dist/runtime/composables/useEditor.d.ts +1 -1
- package/dist/runtime/composables/useEditor.js +120 -0
- package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
- package/dist/runtime/composables/useEditorToolbar.js +78 -56
- package/dist/runtime/composables/useNodeContextMenu.d.ts +10 -0
- package/dist/runtime/composables/useNodeContextMenu.js +41 -1
- package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
- package/dist/runtime/composables/useSwipeGesture.js +140 -0
- package/dist/runtime/extensions/document-header.js +16 -6
- package/dist/runtime/extensions/document-meta.js +344 -19
- package/dist/runtime/extensions/meta-field.js +42 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
- package/dist/runtime/extensions/views/FieldView.vue +51 -19
- package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
- package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
- package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
- package/dist/runtime/plugin-abracadabra.client.js +12 -2
- package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
- package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
- package/package.json +1 -4
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Node } from "@tiptap/core";
|
|
2
|
-
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
3
2
|
import { VueNodeViewRenderer } from "@tiptap/vue-3";
|
|
3
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
4
|
+
import { Fragment } from "@tiptap/pm/model";
|
|
5
|
+
import { DecorationSet, Decoration } from "@tiptap/pm/view";
|
|
6
|
+
import { isChangeOrigin } from "@tiptap/extension-collaboration";
|
|
4
7
|
import DocumentMetaView from "./views/DocumentMetaView.vue";
|
|
5
8
|
export const DocumentMeta = Node.create({
|
|
6
9
|
name: "documentMeta",
|
|
7
10
|
content: "inline*",
|
|
8
11
|
defining: true,
|
|
12
|
+
draggable: false,
|
|
9
13
|
parseHTML() {
|
|
10
14
|
return [{ tag: 'div[data-type="document-meta"]' }];
|
|
11
15
|
},
|
|
@@ -56,6 +60,263 @@ export const DocumentMeta = Node.create({
|
|
|
56
60
|
},
|
|
57
61
|
addProseMirrorPlugins() {
|
|
58
62
|
return [
|
|
63
|
+
// ── L2: comprehensive structure guard ────────────────────────────────
|
|
64
|
+
// Rejects local transactions that would violate the
|
|
65
|
+
// documentHeader@0 documentMeta@1 block+
|
|
66
|
+
// invariant, and reshapes any post-state (including y-sync deltas
|
|
67
|
+
// that slipped past the Y-level enforcer) so we never render a
|
|
68
|
+
// damaged structure. Also enforces L6: documentMeta content is
|
|
69
|
+
// limited to metaField inline atoms.
|
|
70
|
+
new Plugin({
|
|
71
|
+
key: new PluginKey("documentStructureGuard"),
|
|
72
|
+
filterTransaction(tr) {
|
|
73
|
+
if (!tr.docChanged) return true;
|
|
74
|
+
if (isChangeOrigin(tr)) return true;
|
|
75
|
+
if (tr.getMeta("shape-repair")) return true;
|
|
76
|
+
const doc = tr.doc;
|
|
77
|
+
let headerCount = 0;
|
|
78
|
+
let headerIdx = -1;
|
|
79
|
+
let metaCount = 0;
|
|
80
|
+
let metaIdx = -1;
|
|
81
|
+
doc.forEach((node, _offset, index) => {
|
|
82
|
+
if (node.type.name === "documentHeader") {
|
|
83
|
+
headerCount++;
|
|
84
|
+
if (headerIdx === -1) headerIdx = index;
|
|
85
|
+
} else if (node.type.name === "documentMeta") {
|
|
86
|
+
metaCount++;
|
|
87
|
+
if (metaIdx === -1) metaIdx = index;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (headerCount !== 1 || headerIdx !== 0) return false;
|
|
91
|
+
if (metaCount !== 1 || metaIdx !== 1) return false;
|
|
92
|
+
let strayMetaField = false;
|
|
93
|
+
doc.descendants((node, _pos, parent) => {
|
|
94
|
+
if (node.type.name === "metaField" && parent?.type.name !== "documentMeta") {
|
|
95
|
+
strayMetaField = true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
if (strayMetaField) return false;
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
// Repair damage that arrived via y-sync (or any tr the filter
|
|
104
|
+
// couldn't reject). Runs on every doc-changing tr so peers always
|
|
105
|
+
// see a healthy structure. We collapse duplicates by content, merge
|
|
106
|
+
// metaField children from extras into the canonical meta, and
|
|
107
|
+
// restore correct ordering.
|
|
108
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
109
|
+
if (!transactions.some((t) => t.docChanged)) return null;
|
|
110
|
+
const doc = newState.doc;
|
|
111
|
+
const schema = newState.schema;
|
|
112
|
+
const headerType = schema.nodes.documentHeader;
|
|
113
|
+
const metaType = schema.nodes.documentMeta;
|
|
114
|
+
const paragraphType = schema.nodes.paragraph;
|
|
115
|
+
if (!headerType || !metaType || !paragraphType) return null;
|
|
116
|
+
const headers = [];
|
|
117
|
+
const metas = [];
|
|
118
|
+
const bodyContent = [];
|
|
119
|
+
const strayMetaFields = [];
|
|
120
|
+
doc.forEach((node, _offset, index) => {
|
|
121
|
+
if (node.type.name === "documentHeader")
|
|
122
|
+
headers.push({ node, idx: index, weight: node.textContent.length });
|
|
123
|
+
else if (node.type.name === "documentMeta")
|
|
124
|
+
metas.push({ node, idx: index, weight: node.childCount });
|
|
125
|
+
else bodyContent.push(node);
|
|
126
|
+
});
|
|
127
|
+
doc.descendants((node, _pos, parent) => {
|
|
128
|
+
if (node.type.name === "metaField" && parent?.type.name !== "documentMeta") {
|
|
129
|
+
strayMetaFields.push(node);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
});
|
|
134
|
+
const pick = (arr) => {
|
|
135
|
+
if (arr.length === 0) return null;
|
|
136
|
+
let best = arr[0];
|
|
137
|
+
for (let i = 1; i < arr.length; i++) {
|
|
138
|
+
const c = arr[i];
|
|
139
|
+
if (c.weight > best.weight || c.weight === best.weight && c.idx < best.idx)
|
|
140
|
+
best = c;
|
|
141
|
+
}
|
|
142
|
+
return best;
|
|
143
|
+
};
|
|
144
|
+
const canonicalHeader = pick(headers);
|
|
145
|
+
const canonicalMeta = pick(metas);
|
|
146
|
+
const headerExtras = canonicalHeader ? headers.filter((h) => h !== canonicalHeader) : [];
|
|
147
|
+
const metaExtras = canonicalMeta ? metas.filter((m) => m !== canonicalMeta) : [];
|
|
148
|
+
const headerMissing = headers.length === 0;
|
|
149
|
+
const metaMissing = metas.length === 0;
|
|
150
|
+
const headerMisplaced = !headerMissing && (canonicalHeader?.idx ?? 0) !== 0;
|
|
151
|
+
const metaMisplaced = !metaMissing && (canonicalMeta?.idx ?? 0) !== 1;
|
|
152
|
+
const hasDuplicates = headerExtras.length > 0 || metaExtras.length > 0;
|
|
153
|
+
const hasStrayFields = strayMetaFields.length > 0;
|
|
154
|
+
if (!headerMissing && !metaMissing && !headerMisplaced && !metaMisplaced && !hasDuplicates && !hasStrayFields)
|
|
155
|
+
return null;
|
|
156
|
+
const repairedHeader = canonicalHeader?.node ?? headerType.create();
|
|
157
|
+
const identityOf = (mf) => {
|
|
158
|
+
const a = mf.attrs ?? {};
|
|
159
|
+
if (a.metaKey) return `mk:${a.metaKey}`;
|
|
160
|
+
const composite = `${a.startKey ?? ""}:${a.endKey ?? ""}:${a.latKey ?? ""}:${a.lngKey ?? ""}:${a.allDayKey ?? ""}`;
|
|
161
|
+
return composite === "::::" ? "" : `c:${composite}`;
|
|
162
|
+
};
|
|
163
|
+
const seen = /* @__PURE__ */ new Set();
|
|
164
|
+
const mergedFields = [];
|
|
165
|
+
const collectFields = (source) => {
|
|
166
|
+
source.forEach?.((child) => {
|
|
167
|
+
if (child.type.name !== "metaField") return;
|
|
168
|
+
const id = identityOf(child);
|
|
169
|
+
if (!id || seen.has(id)) return;
|
|
170
|
+
seen.add(id);
|
|
171
|
+
mergedFields.push(child);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
if (canonicalMeta?.node) collectFields(canonicalMeta.node);
|
|
175
|
+
for (const m of metaExtras) collectFields(m.node);
|
|
176
|
+
for (const sf of strayMetaFields) {
|
|
177
|
+
const id = identityOf(sf);
|
|
178
|
+
if (!id || seen.has(id)) continue;
|
|
179
|
+
seen.add(id);
|
|
180
|
+
mergedFields.push(sf);
|
|
181
|
+
}
|
|
182
|
+
const metaNeedsRebuild = metaExtras.length > 0 || strayMetaFields.length > 0;
|
|
183
|
+
const repairedMeta = !metaNeedsRebuild && canonicalMeta?.node ? canonicalMeta.node : metaType.create(
|
|
184
|
+
null,
|
|
185
|
+
mergedFields.length > 0 ? Fragment.fromArray(mergedFields) : Fragment.empty
|
|
186
|
+
);
|
|
187
|
+
const finalBody = bodyContent.length > 0 ? bodyContent : [paragraphType.create()];
|
|
188
|
+
const newContent = Fragment.fromArray([
|
|
189
|
+
repairedHeader,
|
|
190
|
+
repairedMeta,
|
|
191
|
+
...finalBody
|
|
192
|
+
]);
|
|
193
|
+
const tr = newState.tr;
|
|
194
|
+
tr.replaceWith(0, doc.content.size, newContent);
|
|
195
|
+
tr.setMeta("shape-repair", true);
|
|
196
|
+
try {
|
|
197
|
+
const mappedAnchor = tr.mapping.map(newState.selection.anchor);
|
|
198
|
+
const mappedHead = tr.mapping.map(newState.selection.head);
|
|
199
|
+
tr.setSelection(
|
|
200
|
+
TextSelection.create(
|
|
201
|
+
tr.doc,
|
|
202
|
+
Math.min(mappedAnchor, tr.doc.content.size),
|
|
203
|
+
Math.min(mappedHead, tr.doc.content.size)
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
} catch {
|
|
207
|
+
const firstBodyPos = repairedHeader.nodeSize + repairedMeta.nodeSize + 1;
|
|
208
|
+
tr.setSelection(
|
|
209
|
+
TextSelection.create(
|
|
210
|
+
tr.doc,
|
|
211
|
+
Math.min(firstBodyPos, tr.doc.content.size)
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return tr;
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
// ── L5: selection-aware placeholder + cursor-in-meta DOM signal ──────
|
|
219
|
+
// Marks the documentMeta DOM node with:
|
|
220
|
+
// data-cursor-in-meta="true" when the selection is inside it
|
|
221
|
+
// data-empty="true" when its content is empty
|
|
222
|
+
// CSS uses the combination to render the placeholder hint only when
|
|
223
|
+
// BOTH are true — so an empty meta with the cursor elsewhere stays
|
|
224
|
+
// visually empty (but still clickable, per Layer 4), and a populated
|
|
225
|
+
// meta with cursor inside doesn't get a stray hint behind the chips.
|
|
226
|
+
new Plugin({
|
|
227
|
+
key: new PluginKey("documentMetaCursorSignal"),
|
|
228
|
+
props: {
|
|
229
|
+
decorations(state) {
|
|
230
|
+
let metaPos = -1;
|
|
231
|
+
let metaNode = null;
|
|
232
|
+
state.doc.forEach((node, offset) => {
|
|
233
|
+
if (metaPos === -1 && node.type.name === "documentMeta") {
|
|
234
|
+
metaPos = offset;
|
|
235
|
+
metaNode = node;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
if (metaPos === -1 || !metaNode) return DecorationSet.empty;
|
|
239
|
+
const { $head } = state.selection;
|
|
240
|
+
let inMeta = false;
|
|
241
|
+
for (let d = $head.depth; d >= 0; d--) {
|
|
242
|
+
if ($head.node(d).type.name === "documentMeta") {
|
|
243
|
+
inMeta = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const isEmpty = metaNode.content.size === 0;
|
|
248
|
+
if (!inMeta && !isEmpty) return DecorationSet.empty;
|
|
249
|
+
const attrs = {};
|
|
250
|
+
if (inMeta) attrs["data-cursor-in-meta"] = "true";
|
|
251
|
+
if (isEmpty) attrs["data-empty"] = "true";
|
|
252
|
+
return DecorationSet.create(state.doc, [
|
|
253
|
+
Decoration.node(metaPos, metaPos + metaNode.nodeSize, attrs)
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
// ── L6: meta text-input lockout ──────────────────────────────────────
|
|
259
|
+
// Swallow all keystrokes inside documentMeta except '/' (which
|
|
260
|
+
// triggers the slash menu). Property chips remain insertable via
|
|
261
|
+
// the menu / programmatic commands. Combined with filterTransaction
|
|
262
|
+
// above, this guarantees nothing but metaField atoms can ever live
|
|
263
|
+
// in the meta header.
|
|
264
|
+
new Plugin({
|
|
265
|
+
key: new PluginKey("documentMetaInputLock"),
|
|
266
|
+
props: {
|
|
267
|
+
handleTextInput(view, _from, _to, text) {
|
|
268
|
+
const { $head } = view.state.selection;
|
|
269
|
+
let inMeta = false;
|
|
270
|
+
for (let d = $head.depth; d >= 0; d--) {
|
|
271
|
+
if ($head.node(d).type.name === "documentMeta") {
|
|
272
|
+
inMeta = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!inMeta) return false;
|
|
277
|
+
return text !== "/";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}),
|
|
281
|
+
// ── L4 helper: explicit empty-meta click fallback ───────────────────
|
|
282
|
+
// With NodeViewContent using display:contents (required so chips
|
|
283
|
+
// align as direct flex children of the wrapper), PM's posAtCoords
|
|
284
|
+
// can't always find an inline position in an empty meta row.
|
|
285
|
+
// Catch any click that lands inside the meta wrapper but not on a
|
|
286
|
+
// chip / button / dropdown and place the cursor at the end of meta
|
|
287
|
+
// ourselves.
|
|
288
|
+
new Plugin({
|
|
289
|
+
key: new PluginKey("documentMetaClickFallback"),
|
|
290
|
+
props: {
|
|
291
|
+
handleClick(view, _pos, event) {
|
|
292
|
+
const target = event.target;
|
|
293
|
+
if (!target) return false;
|
|
294
|
+
const metaEl = target.closest('[data-type="document-meta"]');
|
|
295
|
+
if (!metaEl) return false;
|
|
296
|
+
if (target.closest('[data-type="meta-field"]')) return false;
|
|
297
|
+
if (target.closest("button")) return false;
|
|
298
|
+
if (target.closest('[role="menu"]')) return false;
|
|
299
|
+
if (target.closest("[data-popper-content]")) return false;
|
|
300
|
+
let metaPos = -1;
|
|
301
|
+
let metaContentSize = 0;
|
|
302
|
+
view.state.doc.forEach((node, offset) => {
|
|
303
|
+
if (metaPos === -1 && node.type.name === "documentMeta") {
|
|
304
|
+
metaPos = offset;
|
|
305
|
+
metaContentSize = node.content.size;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
if (metaPos === -1) return false;
|
|
309
|
+
const targetPos = metaPos + 1 + metaContentSize;
|
|
310
|
+
view.dispatch(
|
|
311
|
+
view.state.tr.setSelection(
|
|
312
|
+
TextSelection.create(view.state.doc, targetPos)
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
view.focus();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}),
|
|
59
320
|
new Plugin({
|
|
60
321
|
key: new PluginKey("documentMetaGuard"),
|
|
61
322
|
props: {
|
|
@@ -89,26 +350,90 @@ export const DocumentMeta = Node.create({
|
|
|
89
350
|
return true;
|
|
90
351
|
}
|
|
91
352
|
return false;
|
|
353
|
+
},
|
|
354
|
+
// Block drops that target inside documentMeta or carry documentMeta content
|
|
355
|
+
handleDrop(view, event, slice, moved) {
|
|
356
|
+
if (event) {
|
|
357
|
+
const coords = { left: event.clientX, top: event.clientY };
|
|
358
|
+
const pos = view.posAtCoords(coords);
|
|
359
|
+
if (pos) {
|
|
360
|
+
const $pos = view.state.doc.resolve(pos.pos);
|
|
361
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
362
|
+
if ($pos.node(d).type.name === "documentMeta") return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (moved && slice) {
|
|
367
|
+
let hasMeta = false;
|
|
368
|
+
slice.content.forEach((node) => {
|
|
369
|
+
if (node.type.name === "documentMeta") hasMeta = true;
|
|
370
|
+
});
|
|
371
|
+
if (hasMeta) return true;
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
},
|
|
375
|
+
// Extract metaField nodes from pasted content and merge into documentMeta
|
|
376
|
+
handlePaste(view, _event, slice) {
|
|
377
|
+
const metaFieldNodes = [];
|
|
378
|
+
slice.content.forEach((node) => {
|
|
379
|
+
if (node.type.name === "metaField") {
|
|
380
|
+
metaFieldNodes.push(node);
|
|
381
|
+
} else if (node.type.name === "documentMeta") {
|
|
382
|
+
node.forEach((child) => {
|
|
383
|
+
if (child.type.name === "metaField") metaFieldNodes.push(child);
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
node.forEach((child) => {
|
|
387
|
+
if (child.type.name === "metaField") metaFieldNodes.push(child);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
if (metaFieldNodes.length === 0) return false;
|
|
392
|
+
let metaPos = -1;
|
|
393
|
+
let metaNode = null;
|
|
394
|
+
view.state.doc.forEach((node, offset) => {
|
|
395
|
+
if (node.type.name === "documentMeta") {
|
|
396
|
+
metaPos = offset;
|
|
397
|
+
metaNode = node;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
if (metaPos === -1 || !metaNode) return false;
|
|
401
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
402
|
+
metaNode.forEach((child) => {
|
|
403
|
+
if (child.type.name === "metaField" && child.attrs.metaKey) {
|
|
404
|
+
existingKeys.add(`${child.attrs.fieldType}:${child.attrs.metaKey}`);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const newFields = metaFieldNodes.filter((n) => {
|
|
408
|
+
if (!n.attrs.metaKey) return true;
|
|
409
|
+
const key = `${n.attrs.fieldType}:${n.attrs.metaKey}`;
|
|
410
|
+
if (existingKeys.has(key)) return false;
|
|
411
|
+
existingKeys.add(key);
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
414
|
+
if (newFields.length > 0) {
|
|
415
|
+
const insertPos = metaPos + 1 + metaNode.content.size;
|
|
416
|
+
const tr = view.state.tr;
|
|
417
|
+
tr.insert(insertPos, Fragment.from(newFields));
|
|
418
|
+
view.dispatch(tr);
|
|
419
|
+
}
|
|
420
|
+
let hasNonMetaContent = false;
|
|
421
|
+
slice.content.forEach((node) => {
|
|
422
|
+
if (node.type.name !== "metaField" && node.type.name !== "documentMeta") {
|
|
423
|
+
if (node.type.name === "paragraph" || node.type.name === "heading") {
|
|
424
|
+
let hasNonFieldChild = false;
|
|
425
|
+
node.forEach((child) => {
|
|
426
|
+
if (child.type.name !== "metaField") hasNonFieldChild = true;
|
|
427
|
+
});
|
|
428
|
+
if (hasNonFieldChild || node.content.size === 0) hasNonMetaContent = true;
|
|
429
|
+
} else {
|
|
430
|
+
hasNonMetaContent = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
return !hasNonMetaContent;
|
|
92
435
|
}
|
|
93
436
|
}
|
|
94
|
-
}),
|
|
95
|
-
new Plugin({
|
|
96
|
-
key: new PluginKey("documentMetaDedupe"),
|
|
97
|
-
appendTransaction(transactions, _old, newState) {
|
|
98
|
-
if (!transactions.some((tr2) => tr2.docChanged)) return null;
|
|
99
|
-
const metaPositions = [];
|
|
100
|
-
newState.doc.forEach((node, offset) => {
|
|
101
|
-
if (node.type.name === "documentMeta") metaPositions.push(offset);
|
|
102
|
-
});
|
|
103
|
-
if (metaPositions.length <= 1) return null;
|
|
104
|
-
const tr = newState.tr;
|
|
105
|
-
for (let i = metaPositions.length - 1; i >= 1; i--) {
|
|
106
|
-
const pos = metaPositions[i];
|
|
107
|
-
const node = newState.doc.nodeAt(pos);
|
|
108
|
-
if (node) tr.delete(pos, pos + node.nodeSize);
|
|
109
|
-
}
|
|
110
|
-
return tr;
|
|
111
|
-
}
|
|
112
437
|
})
|
|
113
438
|
];
|
|
114
439
|
}
|
|
@@ -66,6 +66,48 @@ export const MetaField = Node.create({
|
|
|
66
66
|
const newNode = metaFieldType.create(attrs);
|
|
67
67
|
if (dispatch) dispatch(tr.insert(insertPos, newNode));
|
|
68
68
|
return true;
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Move the metaField at `fromPos` to the index of the metaField currently
|
|
72
|
+
* at `toPos` (both are positions *inside* documentMeta — i.e. any position
|
|
73
|
+
* of the chip as reported by its NodeView's getPos()). No-op if the move
|
|
74
|
+
* would result in no change.
|
|
75
|
+
*
|
|
76
|
+
* MetaFieldView's drag-grip pointer handler calls this; without it the
|
|
77
|
+
* drag-reorder UI is rendered but inert.
|
|
78
|
+
*/
|
|
79
|
+
moveMetaField: (fromPos, toPos) => ({ state, dispatch }) => {
|
|
80
|
+
const { doc, tr } = state;
|
|
81
|
+
let metaStart = -1;
|
|
82
|
+
let metaEnd = -1;
|
|
83
|
+
doc.forEach((node, offset) => {
|
|
84
|
+
if (node.type.name === "documentMeta") {
|
|
85
|
+
metaStart = offset + 1;
|
|
86
|
+
metaEnd = offset + 1 + node.content.size;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (metaStart === -1) return false;
|
|
90
|
+
const fromNode = doc.nodeAt(fromPos);
|
|
91
|
+
if (!fromNode || fromNode.type.name !== "metaField") return false;
|
|
92
|
+
const fromFrom = fromPos;
|
|
93
|
+
const fromTo = fromPos + fromNode.nodeSize;
|
|
94
|
+
const target = Math.min(Math.max(toPos, metaStart), metaEnd);
|
|
95
|
+
let insertPos;
|
|
96
|
+
const toNode = doc.nodeAt(target);
|
|
97
|
+
if (toNode && toNode.type.name === "metaField") {
|
|
98
|
+
const targetStart = target;
|
|
99
|
+
const targetEnd = target + toNode.nodeSize;
|
|
100
|
+
const mid = (targetStart + targetEnd) / 2;
|
|
101
|
+
insertPos = toPos < mid ? targetStart : targetEnd;
|
|
102
|
+
} else {
|
|
103
|
+
insertPos = target;
|
|
104
|
+
}
|
|
105
|
+
if (insertPos === fromFrom || insertPos === fromTo) return false;
|
|
106
|
+
const newTr = tr.delete(fromFrom, fromTo);
|
|
107
|
+
const adjusted = insertPos > fromTo ? insertPos - fromNode.nodeSize : insertPos;
|
|
108
|
+
newTr.insert(adjusted, fromNode);
|
|
109
|
+
if (dispatch) dispatch(newTr);
|
|
110
|
+
return true;
|
|
69
111
|
}
|
|
70
112
|
};
|
|
71
113
|
}
|
|
@@ -19,19 +19,51 @@ const items = computed(() => {
|
|
|
19
19
|
void props.node.content.size;
|
|
20
20
|
return buildMetaMenuItems(props.editor);
|
|
21
21
|
});
|
|
22
|
+
function placeCursorInMeta(event) {
|
|
23
|
+
const target = event.target;
|
|
24
|
+
if (!target) return;
|
|
25
|
+
if (target.closest('[data-type="meta-field"]')) return;
|
|
26
|
+
if (target.closest("button")) return;
|
|
27
|
+
if (target.closest('[role="menu"]')) return;
|
|
28
|
+
if (target.closest("[data-popper-content]")) return;
|
|
29
|
+
const pos = props.getPos();
|
|
30
|
+
if (typeof pos !== "number") return;
|
|
31
|
+
const node = props.node;
|
|
32
|
+
const endPos = pos + 1 + node.content.size;
|
|
33
|
+
requestAnimationFrame(() => {
|
|
34
|
+
props.editor.chain().focus(endPos).run();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
22
37
|
</script>
|
|
23
38
|
|
|
24
39
|
<template>
|
|
40
|
+
<!--
|
|
41
|
+
L4: clicks anywhere on the row that aren't on a chip / + button /
|
|
42
|
+
menu are routed to placeCursorInMeta so the cursor lands inside an
|
|
43
|
+
empty meta even when PM's posAtCoords would have missed it. The
|
|
44
|
+
row's CSS min-height in editor.css guarantees the row stays tall
|
|
45
|
+
enough to receive clicks.
|
|
46
|
+
|
|
47
|
+
L5: placeholder text is rendered by a CSS ::after pseudo on the
|
|
48
|
+
wrapper, gated on the `data-cursor-in-meta="true"` +
|
|
49
|
+
`data-empty="true"` attributes set by the documentMetaCursorSignal
|
|
50
|
+
decoration plugin (see extensions/document-meta.ts). It flows
|
|
51
|
+
inline after the + button so the two never overlap.
|
|
52
|
+
-->
|
|
25
53
|
<NodeViewWrapper
|
|
26
54
|
as="div"
|
|
27
55
|
data-type="document-meta"
|
|
28
56
|
class="group/meta"
|
|
29
|
-
:class="{ 'meta-empty': node.content.size === 0 && !props.editor.isEditable }"
|
|
30
57
|
draggable="false"
|
|
58
|
+
@mousedown="placeCursorInMeta"
|
|
31
59
|
@dragstart.prevent.stop
|
|
32
60
|
>
|
|
61
|
+
<!-- display: contents keeps chips as direct flex children of the
|
|
62
|
+
wrapper so they align with `align-items: center` / `gap` and
|
|
63
|
+
the cursor caret sits on the same baseline. -->
|
|
33
64
|
<NodeViewContent
|
|
34
65
|
as="span"
|
|
66
|
+
class="meta-content"
|
|
35
67
|
style="display: contents"
|
|
36
68
|
/>
|
|
37
69
|
<span
|
|
@@ -53,11 +85,5 @@ const items = computed(() => {
|
|
|
53
85
|
/>
|
|
54
86
|
</UDropdownMenu>
|
|
55
87
|
</span>
|
|
56
|
-
<span
|
|
57
|
-
v-if="node.content.size === 0 && props.editor.isEditable"
|
|
58
|
-
class="text-sm text-(--ui-text-muted) pointer-events-none opacity-60"
|
|
59
|
-
>
|
|
60
|
-
Type '/' to add a property…
|
|
61
|
-
</span>
|
|
62
88
|
</NodeViewWrapper>
|
|
63
89
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { NodeViewWrapper, NodeViewContent } from "@tiptap/vue-3";
|
|
3
2
|
import { computed } from "vue";
|
|
3
|
+
import { NodeViewWrapper, NodeViewContent } from "@tiptap/vue-3";
|
|
4
|
+
import ANodeInlineLabel from "../../components/editor/ANodeInlineLabel.vue";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
decorations: { type: Array, required: true },
|
|
6
7
|
selected: { type: Boolean, required: true },
|
|
@@ -14,9 +15,26 @@ const props = defineProps({
|
|
|
14
15
|
extension: { type: Object, required: true },
|
|
15
16
|
HTMLAttributes: { type: Object, required: true }
|
|
16
17
|
});
|
|
17
|
-
const
|
|
18
|
+
const FIELD_TYPES = ["string", "number", "boolean", "object", "array", "any"];
|
|
19
|
+
const name = computed(() => props.node.attrs.name || "");
|
|
18
20
|
const fieldType = computed(() => props.node.attrs.type || "string");
|
|
19
|
-
const required = computed(() => props.node.attrs.required);
|
|
21
|
+
const required = computed(() => !!props.node.attrs.required);
|
|
22
|
+
function setName(v) {
|
|
23
|
+
props.updateAttributes({ name: v });
|
|
24
|
+
}
|
|
25
|
+
function setType(v) {
|
|
26
|
+
props.updateAttributes({ type: v });
|
|
27
|
+
}
|
|
28
|
+
function toggleRequired() {
|
|
29
|
+
props.updateAttributes({ required: !required.value });
|
|
30
|
+
}
|
|
31
|
+
const typeItems = computed(
|
|
32
|
+
() => FIELD_TYPES.map((t) => ({
|
|
33
|
+
label: t,
|
|
34
|
+
onSelect: () => setType(t),
|
|
35
|
+
...fieldType.value === t ? { icon: "i-lucide-check" } : {}
|
|
36
|
+
}))
|
|
37
|
+
);
|
|
20
38
|
</script>
|
|
21
39
|
|
|
22
40
|
<template>
|
|
@@ -28,22 +46,36 @@ const required = computed(() => props.node.attrs.required);
|
|
|
28
46
|
class="flex items-center gap-3 font-mono text-sm"
|
|
29
47
|
contenteditable="false"
|
|
30
48
|
>
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
<ANodeInlineLabel
|
|
50
|
+
:model-value="name"
|
|
51
|
+
placeholder="fieldName"
|
|
52
|
+
variant="name"
|
|
53
|
+
@update:model-value="setName"
|
|
54
|
+
/>
|
|
55
|
+
<div class="flex-1 flex items-center gap-1.5 text-xs">
|
|
56
|
+
<UDropdownMenu
|
|
57
|
+
:items="typeItems"
|
|
58
|
+
:content="{ align: 'start' }"
|
|
59
|
+
>
|
|
60
|
+
<button
|
|
61
|
+
class="rounded-sm bg-(--ui-bg-elevated) text-(--ui-text-toned) px-1.5 py-0.5 hover:bg-(--ui-bg-accented) inline-flex items-center gap-1"
|
|
62
|
+
title="Change type"
|
|
63
|
+
>
|
|
64
|
+
{{ fieldType }}
|
|
65
|
+
<UIcon
|
|
66
|
+
name="i-lucide-chevron-down"
|
|
67
|
+
class="size-3 opacity-60"
|
|
68
|
+
/>
|
|
69
|
+
</button>
|
|
70
|
+
</UDropdownMenu>
|
|
71
|
+
<button
|
|
72
|
+
class="rounded-sm px-1.5 py-0.5 transition-colors"
|
|
73
|
+
:class="required ? 'bg-red-500/10 text-(--ui-error) hover:bg-red-500/20' : 'text-(--ui-text-muted) border border-dashed border-(--ui-border) hover:border-(--ui-border-accented) hover:text-(--ui-text)'"
|
|
74
|
+
:title="required ? 'Mark optional' : 'Mark required'"
|
|
75
|
+
@click="toggleRequired"
|
|
76
|
+
>
|
|
77
|
+
{{ required ? "required" : "optional" }}
|
|
78
|
+
</button>
|
|
47
79
|
</div>
|
|
48
80
|
</div>
|
|
49
81
|
<div class="mt-3 text-(--ui-text-muted) text-sm [&_code]:text-xs/4">
|
|
@@ -75,6 +75,32 @@ function getObjArr(key) {
|
|
|
75
75
|
if (!v || !Array.isArray(v)) return [];
|
|
76
76
|
return v;
|
|
77
77
|
}
|
|
78
|
+
function resolveOptionLabel(raw, opts) {
|
|
79
|
+
if (raw == null || raw === "") return "";
|
|
80
|
+
if (typeof raw === "string") {
|
|
81
|
+
if (!opts.length) return raw;
|
|
82
|
+
if (opts.includes(raw)) return raw;
|
|
83
|
+
if (/^-?\d+$/.test(raw)) {
|
|
84
|
+
const idx = Number(raw);
|
|
85
|
+
if (idx >= 0 && idx < opts.length) return opts[idx];
|
|
86
|
+
}
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
90
|
+
if (raw >= 0 && raw < opts.length) return opts[raw];
|
|
91
|
+
return String(raw);
|
|
92
|
+
}
|
|
93
|
+
return String(raw);
|
|
94
|
+
}
|
|
95
|
+
const selectChipLabel = computed(
|
|
96
|
+
() => resolveOptionLabel(storage()?.pageMeta?.[metaKey.value], options.value)
|
|
97
|
+
);
|
|
98
|
+
const multiSelectChipValues = computed(() => {
|
|
99
|
+
const raw = storage()?.pageMeta?.[metaKey.value];
|
|
100
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
101
|
+
return raw.map((v) => resolveOptionLabel(v, options.value)).filter((s) => s.length > 0);
|
|
102
|
+
});
|
|
103
|
+
const multiSelectChipLabel = computed(() => multiSelectChipValues.value.join(", "));
|
|
78
104
|
function getNum(key, fallback = 0) {
|
|
79
105
|
return storage()?.pageMeta?.[key] ?? fallback;
|
|
80
106
|
}
|
|
@@ -837,8 +863,8 @@ function removeOption(opt) {
|
|
|
837
863
|
trailing
|
|
838
864
|
class="h-7 px-2.5 border border-(--ui-border) rounded-md hover:bg-(--ui-bg-elevated) data-[state=open]:bg-(--ui-bg-elevated) min-w-28"
|
|
839
865
|
>
|
|
840
|
-
<span :class="
|
|
841
|
-
{{
|
|
866
|
+
<span :class="selectChipLabel ? '' : 'text-(--ui-text-dimmed)'">
|
|
867
|
+
{{ selectChipLabel || fieldLabel || t.select }}
|
|
842
868
|
</span>
|
|
843
869
|
</UButton>
|
|
844
870
|
<template #content>
|
|
@@ -913,8 +939,8 @@ function removeOption(opt) {
|
|
|
913
939
|
trailing
|
|
914
940
|
class="h-7 px-2.5 border border-(--ui-border) rounded-md hover:bg-(--ui-bg-elevated) data-[state=open]:bg-(--ui-bg-elevated) min-w-28"
|
|
915
941
|
>
|
|
916
|
-
<span :class="
|
|
917
|
-
{{
|
|
942
|
+
<span :class="multiSelectChipValues.length ? '' : 'text-(--ui-text-dimmed)'">
|
|
943
|
+
{{ multiSelectChipValues.length ? multiSelectChipLabel : fieldLabel || t.select }}
|
|
918
944
|
</span>
|
|
919
945
|
</UButton>
|
|
920
946
|
<template #content>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("#app").RouteMiddleware;
|
|
2
2
|
export default _default;
|