@abraca/nuxt 0.1.0 → 0.2.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/README.md +216 -56
- package/dist/module.json +1 -1
- package/dist/runtime/components/ADocumentTree.d.vue.ts +8 -43
- package/dist/runtime/components/ADocumentTree.vue +1239 -274
- package/dist/runtime/components/ADocumentTree.vue.d.ts +8 -43
- package/dist/runtime/components/AEditor.d.vue.ts +5 -0
- package/dist/runtime/components/AEditor.vue +85 -29
- package/dist/runtime/components/AEditor.vue.d.ts +5 -0
- package/dist/runtime/components/AFloatingWindow.vue +1 -1
- package/dist/runtime/components/ANodePanel.vue +1 -1
- package/dist/runtime/components/AWindowLayer.vue +1 -1
- package/dist/runtime/composables/useConnectionStatus.d.ts +5 -1
- package/dist/runtime/composables/useConnectionStatus.js +36 -11
- package/dist/runtime/composables/useEditorSuggestions.js +10 -0
- package/dist/runtime/extensions/meta-field.d.ts +16 -0
- package/dist/runtime/extensions/meta-field.js +110 -0
- package/dist/runtime/extensions/views/MetaFieldView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue +489 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue.d.ts +4 -0
- package/dist/runtime/plugin-abracadabra.client.js +55 -4
- package/dist/runtime/plugins/core.plugin.js +7 -3
- package/dist/runtime/server/plugins/abracadabra-service.d.ts +1 -1
- package/dist/runtime/server/plugins/abracadabra-service.js +3 -0
- package/dist/runtime/utils/docDragDrop.d.ts +13 -0
- package/dist/runtime/utils/docDragDrop.js +26 -0
- package/package.json +14 -12
|
@@ -1,53 +1,18 @@
|
|
|
1
|
-
import type { AbracadabraLocale } from '../locale.js';
|
|
2
1
|
type __VLS_Props = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
draggable?: boolean;
|
|
2
|
+
collapsed?: boolean;
|
|
3
|
+
editable?: boolean;
|
|
6
4
|
selectedId?: string | null;
|
|
7
|
-
labels?: Partial<AbracadabraLocale['documentTree']>;
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
meta?: Record<string, any>;
|
|
14
|
-
depth: number;
|
|
15
|
-
parentId: string | null;
|
|
16
|
-
order: number;
|
|
17
|
-
isExpanded: boolean;
|
|
18
|
-
hasChildren: boolean;
|
|
19
|
-
}
|
|
20
|
-
declare var __VLS_1: {}, __VLS_20: {
|
|
21
|
-
entry: FlatItem;
|
|
22
|
-
depth: number;
|
|
23
|
-
isExpanded: boolean;
|
|
24
|
-
}, __VLS_42: {}, __VLS_85: {};
|
|
25
|
-
type __VLS_Slots = {} & {
|
|
26
|
-
header?: (props: typeof __VLS_1) => any;
|
|
27
|
-
} & {
|
|
28
|
-
item?: (props: typeof __VLS_20) => any;
|
|
29
|
-
} & {
|
|
30
|
-
empty?: (props: typeof __VLS_42) => any;
|
|
31
|
-
} & {
|
|
32
|
-
footer?: (props: typeof __VLS_85) => any;
|
|
33
|
-
};
|
|
34
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
35
|
-
select: (docId: string) => any;
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
7
|
+
handleExternalDrop: (e: DragEvent, parentId?: string | null) => any;
|
|
8
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
navigate: (id: string) => any;
|
|
36
10
|
create: (parentId: string | null) => any;
|
|
37
11
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
38
|
-
|
|
12
|
+
onNavigate?: ((id: string) => any) | undefined;
|
|
39
13
|
onCreate?: ((parentId: string | null) => any) | undefined;
|
|
40
14
|
}>, {
|
|
41
|
-
|
|
42
|
-
showTrash: boolean;
|
|
43
|
-
allowFileDrop: boolean;
|
|
44
|
-
selectedId: string | null;
|
|
15
|
+
editable: boolean;
|
|
45
16
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
46
|
-
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
47
17
|
declare const _default: typeof __VLS_export;
|
|
48
18
|
export default _default;
|
|
49
|
-
type __VLS_WithSlots<T, S> = T & {
|
|
50
|
-
new (): {
|
|
51
|
-
$slots: S;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DocPageMeta } from '../types.js';
|
|
1
2
|
type __VLS_Props = {
|
|
2
3
|
/** Y.Doc ID to load as a child document */
|
|
3
4
|
docId: string;
|
|
@@ -15,6 +16,8 @@ type __VLS_Props = {
|
|
|
15
16
|
showSuggestionMenu?: boolean;
|
|
16
17
|
/** Whether to show the drag handle (plus + grip buttons). Set false to use slot. */
|
|
17
18
|
showDragHandle?: boolean;
|
|
19
|
+
/** Current document metadata from the doc-tree — written into MetaField storage */
|
|
20
|
+
docMeta?: DocPageMeta;
|
|
18
21
|
};
|
|
19
22
|
type __VLS_ModelProps = {
|
|
20
23
|
modelValue?: any;
|
|
@@ -38,11 +41,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
38
41
|
rename: (label: string) => any;
|
|
39
42
|
ready: () => any;
|
|
40
43
|
update: (content: any) => any;
|
|
44
|
+
updateMeta: (patch: Partial<DocPageMeta>) => any;
|
|
41
45
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
42
46
|
"onUpdate:modelValue"?: ((value: any) => any) | undefined;
|
|
43
47
|
onRename?: ((label: string) => any) | undefined;
|
|
44
48
|
onReady?: (() => any) | undefined;
|
|
45
49
|
onUpdate?: ((content: any) => any) | undefined;
|
|
50
|
+
onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
|
|
46
51
|
}>, {
|
|
47
52
|
contentType: "json" | "html" | "markdown";
|
|
48
53
|
editable: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { shallowRef, ref, watch, computed, nextTick } from "vue";
|
|
2
|
+
import { shallowRef, ref, watch, watchEffect, computed, nextTick } from "vue";
|
|
3
3
|
import { useEditor } from "../composables/useEditor";
|
|
4
4
|
import { useEditorToolbar } from "../composables/useEditorToolbar";
|
|
5
5
|
import { useEditorSuggestions } from "../composables/useEditorSuggestions";
|
|
@@ -12,11 +12,13 @@ const props = defineProps({
|
|
|
12
12
|
placeholder: { type: String, required: false },
|
|
13
13
|
showToolbar: { type: Boolean, required: false, default: true },
|
|
14
14
|
showSuggestionMenu: { type: Boolean, required: false, default: true },
|
|
15
|
-
showDragHandle: { type: Boolean, required: false, default: true }
|
|
15
|
+
showDragHandle: { type: Boolean, required: false, default: true },
|
|
16
|
+
docMeta: { type: Object, required: false }
|
|
16
17
|
});
|
|
17
|
-
const emit = defineEmits(["ready", "update", "rename"]);
|
|
18
|
+
const emit = defineEmits(["ready", "update", "rename", "updateMeta"]);
|
|
18
19
|
const model = defineModel({ type: null });
|
|
19
|
-
const { provider, registry } = useNuxtApp().$abracadabra;
|
|
20
|
+
const { doc, provider, registry } = useNuxtApp().$abracadabra;
|
|
21
|
+
const _treeMap = useSyncedMap(doc, "doc-tree");
|
|
20
22
|
const childProvider = shallowRef(null);
|
|
21
23
|
watch(provider, async (prov) => {
|
|
22
24
|
if (!prov || !props.docId) return;
|
|
@@ -34,43 +36,96 @@ watch(ready, (val) => {
|
|
|
34
36
|
if (val) emit("ready");
|
|
35
37
|
});
|
|
36
38
|
const editorRef = ref(null);
|
|
37
|
-
const currentEditor = shallowRef(null);
|
|
38
39
|
const { items: toolbarItems } = useEditorToolbar({ docId: props.docId });
|
|
39
40
|
const { items: suggestionItems } = useEditorSuggestions({ docId: props.docId });
|
|
40
41
|
const dragHandle = useEditorDragHandle();
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
_lastEmittedHeader.value = text;
|
|
51
|
-
emit("rename", text || "Untitled");
|
|
52
|
-
}
|
|
42
|
+
watchEffect(() => {
|
|
43
|
+
const isReady = ready.value;
|
|
44
|
+
const editorInstance = editorRef.value;
|
|
45
|
+
const docMeta = props.docMeta;
|
|
46
|
+
if (!isReady || !editorInstance) return;
|
|
47
|
+
const storage = editorInstance.editor?.storage?.metaField;
|
|
48
|
+
if (!storage) return;
|
|
49
|
+
storage.pageMeta = docMeta ?? null;
|
|
50
|
+
storage.updateMeta = (patch) => emit("updateMeta", patch);
|
|
53
51
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
let syncingFromEditor = false;
|
|
53
|
+
const _lastEmittedHeader = ref("");
|
|
54
|
+
function getHeaderText(ed) {
|
|
55
|
+
const first = ed.state.doc.firstChild;
|
|
56
|
+
if (!first || first.type.name !== "documentHeader") return "";
|
|
57
|
+
return first.textContent;
|
|
58
|
+
}
|
|
59
|
+
function setHeaderText(ed, text) {
|
|
60
|
+
const first = ed.state.doc.firstChild;
|
|
59
61
|
if (!first || first.type.name !== "documentHeader") return;
|
|
60
|
-
if (first.textContent ===
|
|
61
|
-
|
|
62
|
-
const { tr, schema } = editor.state;
|
|
62
|
+
if (first.textContent === text) return;
|
|
63
|
+
const { tr, schema } = ed.state;
|
|
63
64
|
const to = 1 + first.content.size;
|
|
64
|
-
if (
|
|
65
|
-
tr.replaceWith(1, to, schema.text(
|
|
65
|
+
if (text) {
|
|
66
|
+
tr.replaceWith(1, to, schema.text(text));
|
|
66
67
|
} else {
|
|
67
68
|
tr.delete(1, to);
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
ed.view.dispatch(tr);
|
|
71
|
+
}
|
|
72
|
+
function syncHeaderToTree(ed) {
|
|
73
|
+
const text = getHeaderText(ed);
|
|
74
|
+
if (text === _lastEmittedHeader.value) return;
|
|
75
|
+
_lastEmittedHeader.value = text;
|
|
76
|
+
syncingFromEditor = true;
|
|
77
|
+
emit("rename", text || "Untitled");
|
|
70
78
|
nextTick(() => {
|
|
71
|
-
|
|
79
|
+
syncingFromEditor = false;
|
|
72
80
|
});
|
|
81
|
+
}
|
|
82
|
+
watchEffect((onCleanup) => {
|
|
83
|
+
const ed = editorRef.value?.editor;
|
|
84
|
+
if (!ed || !ready.value) return;
|
|
85
|
+
let initialSyncDone = false;
|
|
86
|
+
function doInitialSync() {
|
|
87
|
+
if (initialSyncDone) return;
|
|
88
|
+
initialSyncDone = true;
|
|
89
|
+
const headerText = getHeaderText(ed);
|
|
90
|
+
const treeLabel = props.docLabel ?? "";
|
|
91
|
+
if (headerText !== treeLabel) {
|
|
92
|
+
const treeMeansEmpty = !treeLabel || treeLabel === "Untitled";
|
|
93
|
+
const headerMeansEmpty = !headerText;
|
|
94
|
+
if (treeMeansEmpty && headerMeansEmpty) return;
|
|
95
|
+
if (!treeMeansEmpty && headerMeansEmpty) {
|
|
96
|
+
setHeaderText(ed, treeLabel);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
_lastEmittedHeader.value = headerText;
|
|
100
|
+
syncingFromEditor = true;
|
|
101
|
+
emit("rename", headerText);
|
|
102
|
+
nextTick(() => {
|
|
103
|
+
syncingFromEditor = false;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function onUpdate() {
|
|
108
|
+
syncHeaderToTree(ed);
|
|
109
|
+
}
|
|
110
|
+
nextTick(() => doInitialSync());
|
|
111
|
+
ed.on("update", onUpdate);
|
|
112
|
+
onCleanup(() => ed.off("update", onUpdate));
|
|
73
113
|
});
|
|
114
|
+
watch(() => props.docLabel, (newLabel) => {
|
|
115
|
+
if (syncingFromEditor) return;
|
|
116
|
+
if (newLabel === _lastEmittedHeader.value) return;
|
|
117
|
+
if (!_treeMap.lastUpdateLocal.value) return;
|
|
118
|
+
const ed = editorRef.value?.editor;
|
|
119
|
+
if (!ed || !ready.value) return;
|
|
120
|
+
setHeaderText(ed, newLabel || "");
|
|
121
|
+
});
|
|
122
|
+
const editorHandlers = {
|
|
123
|
+
insertMetaField: {
|
|
124
|
+
canExecute: () => true,
|
|
125
|
+
execute: (editor, item) => editor.chain().focus().insertMetaField(item.attrs),
|
|
126
|
+
isActive: () => false
|
|
127
|
+
}
|
|
128
|
+
};
|
|
74
129
|
const mentionItems = computed(
|
|
75
130
|
() => registry.getAllMentionProviders().flatMap((p) => p.label ? [p] : [])
|
|
76
131
|
);
|
|
@@ -101,6 +156,7 @@ function onPlusClick(e, onClick) {
|
|
|
101
156
|
:editable="editable"
|
|
102
157
|
:extensions="extensions"
|
|
103
158
|
:starter-kit="{ undoRedo: false, codeBlock: false, document: false }"
|
|
159
|
+
:handlers="editorHandlers"
|
|
104
160
|
:placeholder="placeholder"
|
|
105
161
|
v-slot="{ editor }"
|
|
106
162
|
@update:model-value="emit('update', $event)"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DocPageMeta } from '../types.js';
|
|
1
2
|
type __VLS_Props = {
|
|
2
3
|
/** Y.Doc ID to load as a child document */
|
|
3
4
|
docId: string;
|
|
@@ -15,6 +16,8 @@ type __VLS_Props = {
|
|
|
15
16
|
showSuggestionMenu?: boolean;
|
|
16
17
|
/** Whether to show the drag handle (plus + grip buttons). Set false to use slot. */
|
|
17
18
|
showDragHandle?: boolean;
|
|
19
|
+
/** Current document metadata from the doc-tree — written into MetaField storage */
|
|
20
|
+
docMeta?: DocPageMeta;
|
|
18
21
|
};
|
|
19
22
|
type __VLS_ModelProps = {
|
|
20
23
|
modelValue?: any;
|
|
@@ -38,11 +41,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
38
41
|
rename: (label: string) => any;
|
|
39
42
|
ready: () => any;
|
|
40
43
|
update: (content: any) => any;
|
|
44
|
+
updateMeta: (patch: Partial<DocPageMeta>) => any;
|
|
41
45
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
42
46
|
"onUpdate:modelValue"?: ((value: any) => any) | undefined;
|
|
43
47
|
onRename?: ((label: string) => any) | undefined;
|
|
44
48
|
onReady?: (() => any) | undefined;
|
|
45
49
|
onUpdate?: ((content: any) => any) | undefined;
|
|
50
|
+
onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
|
|
46
51
|
}>, {
|
|
47
52
|
contentType: "json" | "html" | "markdown";
|
|
48
53
|
editable: boolean;
|
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
moveWindow,
|
|
20
20
|
resizeWindow
|
|
21
21
|
} = useWindowManager();
|
|
22
|
-
const win = computed(() => windows.
|
|
22
|
+
const win = computed(() => windows.get(props.windowId));
|
|
23
23
|
const isAnimatingOut = ref(false);
|
|
24
24
|
const animStyle = ref({});
|
|
25
25
|
watch(() => win.value?.minimized, (minimized, wasMinimized) => {
|
|
@@ -3,6 +3,10 @@ type ConnectionStatusValue = 'disconnected' | 'connecting' | 'connected' | 'offl
|
|
|
3
3
|
/**
|
|
4
4
|
* Maps connection status + synced state to UI-ready label, color, and icon.
|
|
5
5
|
*
|
|
6
|
+
* Includes a stabilization delay: when the status flickers between connecting
|
|
7
|
+
* and disconnected rapidly (server not reachable), waits 4 seconds before
|
|
8
|
+
* downgrading to "Offline" to avoid UI flashing.
|
|
9
|
+
*
|
|
6
10
|
* If `status` and `synced` are not provided, reads from `useAbracadabra()` globals.
|
|
7
11
|
*
|
|
8
12
|
* Usage:
|
|
@@ -12,6 +16,6 @@ type ConnectionStatusValue = 'disconnected' | 'connecting' | 'connected' | 'offl
|
|
|
12
16
|
export declare function useConnectionStatus(status?: MaybeRef<ConnectionStatusValue | string | undefined>, synced?: MaybeRef<boolean | undefined>): {
|
|
13
17
|
label: import("vue").ComputedRef<string>;
|
|
14
18
|
color: import("vue").ComputedRef<"success" | "warning" | "error">;
|
|
15
|
-
icon: import("vue").ComputedRef<"i-lucide-cloud-check" | "i-lucide-cloud
|
|
19
|
+
icon: import("vue").ComputedRef<"i-lucide-cloud-check" | "i-lucide-cloud" | "i-lucide-cloud-off">;
|
|
16
20
|
};
|
|
17
21
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, toValue } from "vue";
|
|
1
|
+
import { computed, ref, toValue, watch } from "vue";
|
|
2
2
|
import { useAbraLocale } from "./useAbraLocale.js";
|
|
3
3
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
4
4
|
export function useConnectionStatus(status, synced) {
|
|
@@ -10,26 +10,51 @@ export function useConnectionStatus(status, synced) {
|
|
|
10
10
|
});
|
|
11
11
|
const resolvedSynced = computed(() => {
|
|
12
12
|
if (synced !== void 0) return toValue(synced) ?? false;
|
|
13
|
-
|
|
13
|
+
const isSynced = abra.synced?.value ?? false;
|
|
14
|
+
if (isSynced) return true;
|
|
15
|
+
const isReady = abra.isReady?.value ?? false;
|
|
16
|
+
const isConnected = abra.status?.value === "connected";
|
|
17
|
+
return isReady && isConnected;
|
|
14
18
|
});
|
|
19
|
+
const stableStatus = ref(resolvedStatus.value);
|
|
20
|
+
let offlineTimer = null;
|
|
21
|
+
watch(resolvedStatus, (next, prev) => {
|
|
22
|
+
if (offlineTimer) {
|
|
23
|
+
clearTimeout(offlineTimer);
|
|
24
|
+
offlineTimer = null;
|
|
25
|
+
}
|
|
26
|
+
if (next === "connected") {
|
|
27
|
+
stableStatus.value = "connected";
|
|
28
|
+
} else if (next === "connecting") {
|
|
29
|
+
stableStatus.value = "connecting";
|
|
30
|
+
} else if (next === "disconnected" || next === "offline") {
|
|
31
|
+
if (prev === "connecting" || prev === "connected") {
|
|
32
|
+
offlineTimer = setTimeout(() => {
|
|
33
|
+
stableStatus.value = resolvedStatus.value;
|
|
34
|
+
offlineTimer = null;
|
|
35
|
+
}, 4e3);
|
|
36
|
+
} else {
|
|
37
|
+
stableStatus.value = next;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
stableStatus.value = next;
|
|
41
|
+
}
|
|
42
|
+
}, { immediate: true });
|
|
15
43
|
const label = computed(() => {
|
|
16
|
-
const s =
|
|
17
|
-
if (s === "connected"
|
|
18
|
-
if (s === "connected") return locale.syncing;
|
|
44
|
+
const s = stableStatus.value;
|
|
45
|
+
if (s === "connected") return locale.connected;
|
|
19
46
|
if (s === "connecting") return locale.connecting;
|
|
20
47
|
return locale.offline;
|
|
21
48
|
});
|
|
22
49
|
const color = computed(() => {
|
|
23
|
-
const s =
|
|
24
|
-
if (s === "connected"
|
|
25
|
-
if (s === "connected") return "warning";
|
|
50
|
+
const s = stableStatus.value;
|
|
51
|
+
if (s === "connected") return "success";
|
|
26
52
|
if (s === "connecting") return "warning";
|
|
27
53
|
return "error";
|
|
28
54
|
});
|
|
29
55
|
const icon = computed(() => {
|
|
30
|
-
const s =
|
|
31
|
-
if (s === "connected"
|
|
32
|
-
if (s === "connected") return "i-lucide-cloud-upload";
|
|
56
|
+
const s = stableStatus.value;
|
|
57
|
+
if (s === "connected") return "i-lucide-cloud-check";
|
|
33
58
|
if (s === "connecting") return "i-lucide-cloud";
|
|
34
59
|
return "i-lucide-cloud-off";
|
|
35
60
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { META_FIELD_DEFINITIONS } from "../utils/metaFieldDefinitions.js";
|
|
1
2
|
export function useEditorSuggestions(options = {}) {
|
|
2
3
|
const registry = usePluginRegistry();
|
|
3
4
|
const items = computed(() => {
|
|
@@ -21,6 +22,15 @@ export function useEditorSuggestions(options = {}) {
|
|
|
21
22
|
{ kind: "table", label: "Table", icon: "i-lucide-table" },
|
|
22
23
|
{ kind: "horizontalRule", label: "Divider", icon: "i-lucide-minus" }
|
|
23
24
|
],
|
|
25
|
+
[
|
|
26
|
+
{ type: "label", label: "Properties" },
|
|
27
|
+
...META_FIELD_DEFINITIONS.map((def) => ({
|
|
28
|
+
label: def.label,
|
|
29
|
+
icon: def.icon,
|
|
30
|
+
kind: "insertMetaField",
|
|
31
|
+
attrs: def.buildAttrs()
|
|
32
|
+
}))
|
|
33
|
+
],
|
|
24
34
|
[
|
|
25
35
|
{ type: "label", label: "Components" },
|
|
26
36
|
{ kind: "callout", label: "Callout", icon: "i-lucide-info" },
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
/**
|
|
3
|
+
* MetaField — an inline atomic chip node that lives inside documentMeta.
|
|
4
|
+
*
|
|
5
|
+
* - inline: true so documentMeta (content: 'inline*') can contain it and the
|
|
6
|
+
* cursor can be placed before/after chips, enabling the slash menu.
|
|
7
|
+
* - atom: true so it renders as a single NodeView unit.
|
|
8
|
+
* - stopEvent: true so TipTap never intercepts mouse/drag events inside the
|
|
9
|
+
* chip (prevents drag-handle hijacking USlider drags etc.).
|
|
10
|
+
*
|
|
11
|
+
* Values live in tree meta (Y.Map). The storage.metaField reactive object is
|
|
12
|
+
* the bridge: AEditor writes to it via watchEffect, MetaFieldView reads it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MetaField: Node<any, any>;
|
|
15
|
+
export declare function schemaFieldToAttrs(field: any): Record<string, unknown>;
|
|
16
|
+
export declare function userFieldToAttrs(f: any): Record<string, unknown>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { VueNodeViewRenderer } from "@tiptap/vue-3";
|
|
3
|
+
import { reactive } from "vue";
|
|
4
|
+
import MetaFieldView from "./views/MetaFieldView.vue";
|
|
5
|
+
export const MetaField = Node.create({
|
|
6
|
+
name: "metaField",
|
|
7
|
+
inline: true,
|
|
8
|
+
group: "inline",
|
|
9
|
+
atom: true,
|
|
10
|
+
selectable: true,
|
|
11
|
+
addStorage() {
|
|
12
|
+
return reactive({
|
|
13
|
+
pageMeta: null,
|
|
14
|
+
updateMeta: null
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
addAttributes() {
|
|
18
|
+
return {
|
|
19
|
+
fieldType: { default: "colorPreset" },
|
|
20
|
+
fieldLabel: { default: "" },
|
|
21
|
+
userDefined: { default: false },
|
|
22
|
+
metaKey: { default: "" },
|
|
23
|
+
startKey: { default: "" },
|
|
24
|
+
endKey: { default: "" },
|
|
25
|
+
allDayKey: { default: "" },
|
|
26
|
+
latKey: { default: "" },
|
|
27
|
+
lngKey: { default: "" },
|
|
28
|
+
presets: { default: "[]" },
|
|
29
|
+
iconOptions: { default: "[]" },
|
|
30
|
+
options: { default: "[]" },
|
|
31
|
+
sliderMin: { default: 0 },
|
|
32
|
+
sliderMax: { default: 100 },
|
|
33
|
+
sliderStep: { default: 1 },
|
|
34
|
+
unit: { default: "" }
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
parseHTML() {
|
|
38
|
+
return [{ tag: 'span[data-type="meta-field"]' }];
|
|
39
|
+
},
|
|
40
|
+
renderHTML({ HTMLAttributes }) {
|
|
41
|
+
return ["span", mergeAttributes(HTMLAttributes, { "data-type": "meta-field" })];
|
|
42
|
+
},
|
|
43
|
+
addNodeView() {
|
|
44
|
+
return VueNodeViewRenderer(MetaFieldView, {
|
|
45
|
+
// Block ALL events from reaching TipTap — popovers, sliders etc.
|
|
46
|
+
// handle their own interactions; TipTap must not intercept them.
|
|
47
|
+
stopEvent: () => true
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
addCommands() {
|
|
51
|
+
return {
|
|
52
|
+
insertMetaField: (attrs) => ({ state, dispatch }) => {
|
|
53
|
+
const { doc, tr, schema } = state;
|
|
54
|
+
const metaFieldType = schema.nodes.metaField;
|
|
55
|
+
if (!metaFieldType) return false;
|
|
56
|
+
let metaPos = -1;
|
|
57
|
+
let metaNode = null;
|
|
58
|
+
doc.forEach((node, offset) => {
|
|
59
|
+
if (node.type.name === "documentMeta") {
|
|
60
|
+
metaPos = offset;
|
|
61
|
+
metaNode = node;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (metaPos === -1) return false;
|
|
65
|
+
const insertPos = metaPos + 1 + (metaNode?.content.size ?? 0);
|
|
66
|
+
const newNode = metaFieldType.create(attrs);
|
|
67
|
+
if (dispatch) dispatch(tr.insert(insertPos, newNode));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
export function schemaFieldToAttrs(field) {
|
|
74
|
+
const t = field.type;
|
|
75
|
+
if (t === "datetimerange") return { fieldType: "datetimerange", fieldLabel: field.label ?? "", userDefined: false, startKey: String(field.startKey), endKey: String(field.endKey), allDayKey: String(field.allDayKey ?? "") };
|
|
76
|
+
if (t === "daterange") return { fieldType: "daterange", fieldLabel: field.label ?? "", userDefined: false, startKey: String(field.startKey), endKey: String(field.endKey) };
|
|
77
|
+
if (t === "timerange") return { fieldType: "timerange", fieldLabel: field.label ?? "", userDefined: false, startKey: String(field.startKey), endKey: String(field.endKey) };
|
|
78
|
+
if (t === "datetime") return { fieldType: "datetime", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
79
|
+
if (t === "date") return { fieldType: "date", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
80
|
+
if (t === "time") return { fieldType: "time", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
81
|
+
if (t === "slider") return { fieldType: "slider", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), sliderMin: field.min ?? 0, sliderMax: field.max ?? 100 };
|
|
82
|
+
if (t === "number") return { fieldType: "number", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), sliderMin: field.min ?? 0, sliderMax: field.max ?? 999999, sliderStep: field.step ?? 1, unit: field.unit ?? "" };
|
|
83
|
+
if (t === "toggle") return { fieldType: "toggle", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
84
|
+
if (t === "select") return { fieldType: "select", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), options: JSON.stringify(field.options ?? []) };
|
|
85
|
+
if (t === "multiselect") return { fieldType: "multiselect", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), options: JSON.stringify(field.options ?? []) };
|
|
86
|
+
if (t === "url") return { fieldType: "url", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
87
|
+
if (t === "rating") return { fieldType: "rating", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), sliderMax: field.max ?? 5 };
|
|
88
|
+
if (t === "tags") return { fieldType: "tags", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), options: JSON.stringify(field.options ?? []) };
|
|
89
|
+
if (t === "textarea") return { fieldType: "textarea", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
90
|
+
if (t === "colorPreset") return { fieldType: "colorPreset", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), presets: JSON.stringify(field.presets ?? []) };
|
|
91
|
+
if (t === "colorPicker") return { fieldType: "colorPicker", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key) };
|
|
92
|
+
if (t === "location") return { fieldType: "location", fieldLabel: field.label ?? "", userDefined: false, latKey: String(field.latKey), lngKey: String(field.lngKey) };
|
|
93
|
+
if (t === "icon") return { fieldType: "icon", fieldLabel: field.label ?? "", userDefined: false, metaKey: String(field.key), iconOptions: JSON.stringify(field.options ?? []) };
|
|
94
|
+
return { fieldType: "colorPreset", fieldLabel: "" };
|
|
95
|
+
}
|
|
96
|
+
export function userFieldToAttrs(f) {
|
|
97
|
+
const base = { fieldType: f.type, fieldLabel: f.label ?? "", userDefined: true };
|
|
98
|
+
if (f.type === "datetimerange") return { ...base, startKey: f.startKey ?? "", endKey: f.endKey ?? "", allDayKey: f.allDayKey ?? "" };
|
|
99
|
+
if (f.type === "daterange") return { ...base, startKey: f.startKey ?? "", endKey: f.endKey ?? "" };
|
|
100
|
+
if (f.type === "timerange") return { ...base, startKey: f.startKey ?? "", endKey: f.endKey ?? "" };
|
|
101
|
+
if (["datetime", "date", "time", "toggle", "url", "textarea", "colorPicker"].includes(f.type)) return { ...base, metaKey: f.key ?? "" };
|
|
102
|
+
if (f.type === "slider") return { ...base, metaKey: f.key ?? "", sliderMin: f.min ?? 0, sliderMax: f.max ?? 100 };
|
|
103
|
+
if (f.type === "number") return { ...base, metaKey: f.key ?? "", sliderMin: f.min ?? 0, sliderMax: f.max ?? 999999, sliderStep: f.step ?? 1, unit: f.unit ?? "" };
|
|
104
|
+
if (f.type === "rating") return { ...base, metaKey: f.key ?? "", sliderMax: f.max ?? 5 };
|
|
105
|
+
if (f.type === "colorPreset") return { ...base, metaKey: f.key ?? "", presets: JSON.stringify(f.presets ?? []) };
|
|
106
|
+
if (["select", "multiselect", "tags"].includes(f.type)) return { ...base, metaKey: f.key ?? "", options: JSON.stringify(f.options ?? []) };
|
|
107
|
+
if (f.type === "location") return { ...base, latKey: f.latKey ?? "", lngKey: f.lngKey ?? "" };
|
|
108
|
+
if (f.type === "icon") return { ...base, metaKey: f.key ?? "", iconOptions: JSON.stringify(f.options ?? []) };
|
|
109
|
+
return base;
|
|
110
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NodeViewProps } from '@tiptap/vue-3';
|
|
2
|
+
declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
3
|
+
declare const _default: typeof __VLS_export;
|
|
4
|
+
export default _default;
|