@abraca/nuxt 1.6.0 → 1.8.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 +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -2
- package/dist/runtime/assets/sources.css +1 -0
- package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
- package/dist/runtime/components/ADocumentTree.vue +13 -6
- package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
- package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
- package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
- package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
- package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
- package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
- package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
- package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
- package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
- package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
- package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
- package/dist/runtime/composables/useAwareness.js +14 -3
- package/dist/runtime/composables/useBackgroundSync.js +19 -1
- package/dist/runtime/composables/useFileIndex.js +38 -17
- package/dist/runtime/composables/useSearchIndex.js +41 -16
- package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
- package/dist/runtime/composables/useSlidesNavigation.js +185 -0
- package/dist/runtime/composables/useYDoc.d.ts +1 -1
- package/dist/runtime/composables/useYDoc.js +47 -9
- package/dist/runtime/locale.d.ts +38 -0
- package/dist/runtime/locale.js +41 -3
- package/dist/runtime/utils/docTypes.js +17 -0
- package/package.json +3 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { useTableView } from '../../../composables/useTableView.js';
|
|
2
|
+
import type { AbracadabraLocale } from '../../../locale.js';
|
|
3
|
+
type TableLabels = AbracadabraLocale['renderers']['table'];
|
|
4
|
+
type __VLS_Props = {
|
|
5
|
+
tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
|
|
6
|
+
tableView: ReturnType<typeof useTableView>;
|
|
7
|
+
states: Array<Record<string, any>>;
|
|
8
|
+
myClientId: number;
|
|
9
|
+
setLocalState: (state: Record<string, any>) => void;
|
|
10
|
+
editable?: boolean;
|
|
11
|
+
labels?: Partial<TableLabels>;
|
|
12
|
+
};
|
|
13
|
+
declare function addColumn(): void;
|
|
14
|
+
declare function addRow(): void;
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
16
|
+
addColumn: typeof addColumn;
|
|
17
|
+
addRow: typeof addRow;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
openNode: (id: string, label: string) => any;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
|
+
onOpenNode?: ((id: string, label: string) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
editable: boolean;
|
|
24
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const _default: typeof __VLS_export;
|
|
26
|
+
export default _default;
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onBeforeUnmount } from "vue";
|
|
3
|
+
import { useEventListener } from "@vueuse/core";
|
|
4
|
+
import { useTouchDrag } from "../../../composables/useTouchDrag";
|
|
5
|
+
import { DEFAULT_LOCALE } from "../../../locale";
|
|
6
|
+
import ATableColumnHeader from "./ATableColumnHeader.vue";
|
|
7
|
+
import ATableCell from "./cells/ATableCell.vue";
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
tree: { type: null, required: true },
|
|
10
|
+
tableView: { type: null, required: true },
|
|
11
|
+
states: { type: Array, required: true },
|
|
12
|
+
myClientId: { type: Number, required: true },
|
|
13
|
+
setLocalState: { type: Function, required: true },
|
|
14
|
+
editable: { type: Boolean, required: false, default: true },
|
|
15
|
+
labels: { type: Object, required: false }
|
|
16
|
+
});
|
|
17
|
+
const emit = defineEmits(["openNode"]);
|
|
18
|
+
const labels = computed(() => ({
|
|
19
|
+
...DEFAULT_LOCALE.renderers.table,
|
|
20
|
+
...props.labels ?? {}
|
|
21
|
+
}));
|
|
22
|
+
const INTERNAL_META_KEYS = /* @__PURE__ */ new Set([
|
|
23
|
+
"_metaFields",
|
|
24
|
+
"_metaInitialized",
|
|
25
|
+
"coverUploadId",
|
|
26
|
+
"coverDocId",
|
|
27
|
+
"coverMimeType",
|
|
28
|
+
"geoType",
|
|
29
|
+
"geoLat",
|
|
30
|
+
"geoLng",
|
|
31
|
+
"icon",
|
|
32
|
+
"color",
|
|
33
|
+
"geoDescription"
|
|
34
|
+
]);
|
|
35
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
36
|
+
function camelToTitle(s) {
|
|
37
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
|
|
38
|
+
}
|
|
39
|
+
const columns = computed(
|
|
40
|
+
() => props.tree.childrenOf(null).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
41
|
+
);
|
|
42
|
+
const orderedColumns = computed(() => {
|
|
43
|
+
const order = props.tableView.columnOrder.value;
|
|
44
|
+
if (!order.length) return columns.value;
|
|
45
|
+
const colMap = new Map(columns.value.map((c) => [c.id, c]));
|
|
46
|
+
const ordered = [];
|
|
47
|
+
for (const id of order) {
|
|
48
|
+
const col = colMap.get(id);
|
|
49
|
+
if (col) {
|
|
50
|
+
ordered.push(col);
|
|
51
|
+
colMap.delete(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const col of colMap.values()) ordered.push(col);
|
|
55
|
+
return ordered;
|
|
56
|
+
});
|
|
57
|
+
const colChildren = computed(() => {
|
|
58
|
+
const map = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const col of columns.value) {
|
|
60
|
+
map.set(col.id, props.tree.childrenOf(col.id).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)));
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
});
|
|
64
|
+
const rowCount = computed(
|
|
65
|
+
() => Math.max(0, ...columns.value.map((c) => colChildren.value.get(c.id)?.length ?? 0))
|
|
66
|
+
);
|
|
67
|
+
function getCellEntry(colId, rowIdx) {
|
|
68
|
+
return colChildren.value.get(colId)?.[rowIdx];
|
|
69
|
+
}
|
|
70
|
+
function getRowRep(rowIdx) {
|
|
71
|
+
for (const col of columns.value) {
|
|
72
|
+
const entry = getCellEntry(col.id, rowIdx);
|
|
73
|
+
if (entry) return entry;
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
const allCellEntries = computed(() => {
|
|
78
|
+
const result = [];
|
|
79
|
+
for (const children of colChildren.value.values()) result.push(...children);
|
|
80
|
+
return result;
|
|
81
|
+
});
|
|
82
|
+
const metaColumns = computed(() => {
|
|
83
|
+
const colIds = new Set(columns.value.map((c) => c.id));
|
|
84
|
+
const keys = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const col of columns.value) {
|
|
86
|
+
const children = colChildren.value.get(col.id) ?? [];
|
|
87
|
+
for (const cell of children) {
|
|
88
|
+
if (!cell.meta) continue;
|
|
89
|
+
for (const key of Object.keys(cell.meta)) {
|
|
90
|
+
if (INTERNAL_META_KEYS.has(key)) continue;
|
|
91
|
+
if (UUID_RE.test(key)) continue;
|
|
92
|
+
if (colIds.has(key)) continue;
|
|
93
|
+
keys.add(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...keys].sort().map((key) => ({
|
|
98
|
+
id: `__meta_${key}`,
|
|
99
|
+
label: camelToTitle(key),
|
|
100
|
+
metaKey: key,
|
|
101
|
+
metaType: props.tableView.inferColumnType(allCellEntries.value, key)
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
const TYPE_MIN_WIDTHS = {
|
|
105
|
+
text: 180,
|
|
106
|
+
number: 100,
|
|
107
|
+
date: 150,
|
|
108
|
+
datetime: 170,
|
|
109
|
+
select: 130,
|
|
110
|
+
multiselect: 160,
|
|
111
|
+
tags: 160,
|
|
112
|
+
toggle: 80,
|
|
113
|
+
rating: 140,
|
|
114
|
+
url: 180,
|
|
115
|
+
color: 100,
|
|
116
|
+
colorPicker: 100,
|
|
117
|
+
slider: 150,
|
|
118
|
+
members: 140,
|
|
119
|
+
priority: 130,
|
|
120
|
+
status: 130
|
|
121
|
+
};
|
|
122
|
+
function getDocColumnWidth(colId) {
|
|
123
|
+
return props.tableView.columnWidths.value[colId] ?? 180;
|
|
124
|
+
}
|
|
125
|
+
function getMetaColumnWidth(mc) {
|
|
126
|
+
return props.tableView.columnWidths.value[mc.id] ?? TYPE_MIN_WIDTHS[mc.metaType] ?? 140;
|
|
127
|
+
}
|
|
128
|
+
function getCellValue(colId, rowIdx) {
|
|
129
|
+
const entry = getCellEntry(colId, rowIdx);
|
|
130
|
+
return entry?.label && entry.label !== "Untitled" ? entry.label : "";
|
|
131
|
+
}
|
|
132
|
+
function getMetaCellValue(rowIdx, metaKey) {
|
|
133
|
+
for (const col of columns.value) {
|
|
134
|
+
const entry = getCellEntry(col.id, rowIdx);
|
|
135
|
+
if (entry?.meta?.[metaKey] !== void 0) return entry.meta[metaKey];
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function setCellValue(colId, rowIdx, value) {
|
|
140
|
+
if (!props.editable) return;
|
|
141
|
+
const str = typeof value === "string" ? value.trim() : String(value ?? "");
|
|
142
|
+
const entry = getCellEntry(colId, rowIdx);
|
|
143
|
+
if (entry) {
|
|
144
|
+
props.tree.renameEntry(entry.id, str || "Untitled");
|
|
145
|
+
} else {
|
|
146
|
+
props.tree.createChild(colId, str || "Untitled");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function setMetaCellValue(rowIdx, metaKey, value) {
|
|
150
|
+
if (!props.editable) return;
|
|
151
|
+
const rep = getRowRep(rowIdx);
|
|
152
|
+
if (rep) props.tree.updateMeta(rep.id, { [metaKey]: value });
|
|
153
|
+
}
|
|
154
|
+
function addColumn() {
|
|
155
|
+
if (!props.editable) return;
|
|
156
|
+
props.tree.createChild(null, labels.value.column || "New Column");
|
|
157
|
+
}
|
|
158
|
+
function deleteColumn(col) {
|
|
159
|
+
if (!props.editable) return;
|
|
160
|
+
props.tree.deleteEntry(col.id);
|
|
161
|
+
}
|
|
162
|
+
function deleteMetaColumn(metaKey) {
|
|
163
|
+
if (!props.editable) return;
|
|
164
|
+
for (const col of columns.value) {
|
|
165
|
+
const children = colChildren.value.get(col.id) ?? [];
|
|
166
|
+
for (const cell of children) {
|
|
167
|
+
if (cell.meta?.[metaKey] !== void 0) {
|
|
168
|
+
const newMeta = { ...cell.meta };
|
|
169
|
+
delete newMeta[metaKey];
|
|
170
|
+
props.tree.treeMap.set(cell.id, { ...props.tree.treeMap.get(cell.id), meta: newMeta });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function addRow() {
|
|
176
|
+
if (!props.editable) return;
|
|
177
|
+
if (!columns.value[0]) return;
|
|
178
|
+
props.tree.createChild(columns.value[0].id, "");
|
|
179
|
+
}
|
|
180
|
+
function deleteRow(rowIdx) {
|
|
181
|
+
if (!props.editable) return;
|
|
182
|
+
for (const col of columns.value) {
|
|
183
|
+
const entry = getCellEntry(col.id, rowIdx);
|
|
184
|
+
if (entry) props.tree.deleteEntry(entry.id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const editingCell = ref(null);
|
|
188
|
+
function startEditDocCol(rowIdx, col) {
|
|
189
|
+
if (!props.editable) return;
|
|
190
|
+
editingCell.value = { rowIdx, colId: col.id };
|
|
191
|
+
props.setLocalState({ "table:editing": { rowIdx, fieldId: col.id } });
|
|
192
|
+
}
|
|
193
|
+
function startEditMetaCol(rowIdx, mc) {
|
|
194
|
+
if (!props.editable) return;
|
|
195
|
+
editingCell.value = { rowIdx, colId: mc.id, metaKey: mc.metaKey };
|
|
196
|
+
props.setLocalState({ "table:editing": { rowIdx, fieldId: mc.id } });
|
|
197
|
+
}
|
|
198
|
+
function stopEdit() {
|
|
199
|
+
editingCell.value = null;
|
|
200
|
+
props.setLocalState({ "table:editing": null });
|
|
201
|
+
}
|
|
202
|
+
function onDocCellUpdate(colId, rowIdx, value) {
|
|
203
|
+
setCellValue(colId, rowIdx, value);
|
|
204
|
+
}
|
|
205
|
+
function onMetaCellUpdate(rowIdx, metaKey, value) {
|
|
206
|
+
setMetaCellValue(rowIdx, metaKey, value);
|
|
207
|
+
}
|
|
208
|
+
function renameColumn(colId, label) {
|
|
209
|
+
if (!props.editable) return;
|
|
210
|
+
props.tree.renameEntry(colId, label);
|
|
211
|
+
}
|
|
212
|
+
function onResizeStart(e, colId) {
|
|
213
|
+
if (!props.editable) return;
|
|
214
|
+
const startX = e.clientX;
|
|
215
|
+
const startWidth = props.tableView.columnWidths.value[colId] ?? 120;
|
|
216
|
+
props.setLocalState({ "table:resizing": colId });
|
|
217
|
+
function onMove(ev) {
|
|
218
|
+
const newWidth = Math.max(80, startWidth + (ev.clientX - startX));
|
|
219
|
+
props.tableView.setColumnWidth(colId, newWidth);
|
|
220
|
+
}
|
|
221
|
+
function onUp() {
|
|
222
|
+
document.removeEventListener("pointermove", onMove);
|
|
223
|
+
document.removeEventListener("pointerup", onUp);
|
|
224
|
+
document.body.style.userSelect = "";
|
|
225
|
+
document.body.style.cursor = "";
|
|
226
|
+
props.setLocalState({ "table:resizing": null });
|
|
227
|
+
}
|
|
228
|
+
document.addEventListener("pointermove", onMove);
|
|
229
|
+
document.addEventListener("pointerup", onUp);
|
|
230
|
+
document.body.style.userSelect = "none";
|
|
231
|
+
document.body.style.cursor = "col-resize";
|
|
232
|
+
}
|
|
233
|
+
function rowIdxFromDragId(dragId) {
|
|
234
|
+
return Number.parseInt(dragId.replace("row-", ""), 10);
|
|
235
|
+
}
|
|
236
|
+
const {
|
|
237
|
+
dragId: rowDragId,
|
|
238
|
+
dragOverId: rowDragOverId,
|
|
239
|
+
handlePointerDown: handleRowPointerDown
|
|
240
|
+
} = useTouchDrag({
|
|
241
|
+
idAttr: "data-drag-id",
|
|
242
|
+
onDragStart: (dragId) => {
|
|
243
|
+
if (!props.editable) return;
|
|
244
|
+
props.setLocalState({ "table:dragging": rowIdxFromDragId(dragId) });
|
|
245
|
+
},
|
|
246
|
+
onDragEnd: () => {
|
|
247
|
+
props.setLocalState({ "table:dragging": null });
|
|
248
|
+
},
|
|
249
|
+
onDrop: (srcDragId, targetDragId) => {
|
|
250
|
+
if (!props.editable) return;
|
|
251
|
+
const srcIdx = rowIdxFromDragId(srcDragId);
|
|
252
|
+
const targetIdx = rowIdxFromDragId(targetDragId);
|
|
253
|
+
if (srcIdx === targetIdx) return;
|
|
254
|
+
for (const col of columns.value) {
|
|
255
|
+
const children = colChildren.value.get(col.id) ?? [];
|
|
256
|
+
const srcEntry = children[srcIdx];
|
|
257
|
+
if (!srcEntry) continue;
|
|
258
|
+
const prev = children[targetIdx - (targetIdx > srcIdx ? 0 : 1)];
|
|
259
|
+
const next = children[targetIdx + (targetIdx > srcIdx ? 1 : 0)];
|
|
260
|
+
let newOrder;
|
|
261
|
+
if (!prev && !next) newOrder = Date.now();
|
|
262
|
+
else if (!prev) newOrder = next.order - 1e3;
|
|
263
|
+
else if (!next) newOrder = prev.order + 1e3;
|
|
264
|
+
else newOrder = (prev.order + next.order) / 2;
|
|
265
|
+
props.tree.moveEntry(srcEntry.id, col.id, newOrder);
|
|
266
|
+
}
|
|
267
|
+
props.setLocalState({ "table:dragging": null });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
const {
|
|
271
|
+
dragId: colDragId,
|
|
272
|
+
dragOverId: colDragOverId,
|
|
273
|
+
handlePointerDown: handleColPointerDown
|
|
274
|
+
} = useTouchDrag({
|
|
275
|
+
idAttr: "data-col-drag-id",
|
|
276
|
+
onDrop: (srcId, targetId) => {
|
|
277
|
+
if (!props.editable) return;
|
|
278
|
+
if (srcId === targetId) return;
|
|
279
|
+
const cols = orderedColumns.value;
|
|
280
|
+
const srcIdx = cols.findIndex((c) => c.id === srcId);
|
|
281
|
+
const targetIdx = cols.findIndex((c) => c.id === targetId);
|
|
282
|
+
if (srcIdx === -1 || targetIdx === -1) return;
|
|
283
|
+
const reordered = [...cols];
|
|
284
|
+
const [moved] = reordered.splice(srcIdx, 1);
|
|
285
|
+
reordered.splice(targetIdx, 0, moved);
|
|
286
|
+
props.tableView.setColumnOrder(reordered.map((c) => c.id));
|
|
287
|
+
const prev = reordered[targetIdx - 1];
|
|
288
|
+
const next = reordered[targetIdx + 1];
|
|
289
|
+
let newOrder;
|
|
290
|
+
if (!prev && !next) newOrder = Date.now();
|
|
291
|
+
else if (!prev) newOrder = next.order - 1e3;
|
|
292
|
+
else if (!next) newOrder = prev.order + 1e3;
|
|
293
|
+
else newOrder = (prev.order + next.order) / 2;
|
|
294
|
+
props.tree.moveEntry(srcId, null, newOrder);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
function handleSort(key, dir) {
|
|
298
|
+
if (dir === "clear") props.tableView.setSort(void 0);
|
|
299
|
+
else props.tableView.setSort(key, dir);
|
|
300
|
+
}
|
|
301
|
+
const ctxCell = ref(null);
|
|
302
|
+
const ctxPos = ref({ x: 0, y: 0 });
|
|
303
|
+
const showCtx = ref(false);
|
|
304
|
+
function onCellContextMenu(e, entry) {
|
|
305
|
+
if (!entry) return;
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
ctxCell.value = entry;
|
|
308
|
+
ctxPos.value = { x: e.clientX, y: e.clientY };
|
|
309
|
+
showCtx.value = true;
|
|
310
|
+
}
|
|
311
|
+
useEventListener(window, "pointerdown", () => {
|
|
312
|
+
showCtx.value = false;
|
|
313
|
+
});
|
|
314
|
+
let longPressTimer = null;
|
|
315
|
+
function startLongPress(e, rowIdx) {
|
|
316
|
+
const entry = getRowRep(rowIdx);
|
|
317
|
+
if (!entry) return;
|
|
318
|
+
longPressTimer = setTimeout(() => {
|
|
319
|
+
const touch = e.touches[0];
|
|
320
|
+
if (touch) {
|
|
321
|
+
ctxCell.value = entry;
|
|
322
|
+
ctxPos.value = { x: touch.clientX, y: touch.clientY };
|
|
323
|
+
showCtx.value = true;
|
|
324
|
+
}
|
|
325
|
+
longPressTimer = null;
|
|
326
|
+
}, 500);
|
|
327
|
+
}
|
|
328
|
+
function cancelLongPress() {
|
|
329
|
+
if (longPressTimer) {
|
|
330
|
+
clearTimeout(longPressTimer);
|
|
331
|
+
longPressTimer = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const ctxItems = computed(() => {
|
|
335
|
+
if (!ctxCell.value) return [];
|
|
336
|
+
const cell = ctxCell.value;
|
|
337
|
+
return [
|
|
338
|
+
[
|
|
339
|
+
{ label: labels.value.open, icon: "i-lucide-external-link", onSelect: () => emit("openNode", cell.id, cell.label) },
|
|
340
|
+
{ label: labels.value.rename, icon: "i-lucide-pencil", onSelect: () => {
|
|
341
|
+
for (const col of columns.value) {
|
|
342
|
+
const children = colChildren.value.get(col.id) ?? [];
|
|
343
|
+
const rowIdx = children.findIndex((c) => c.id === cell.id);
|
|
344
|
+
if (rowIdx !== -1) {
|
|
345
|
+
startEditDocCol(rowIdx, col);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} },
|
|
350
|
+
{ label: labels.value.duplicate, icon: "i-lucide-copy", onSelect: () => props.tree.duplicateEntry(cell.id) }
|
|
351
|
+
],
|
|
352
|
+
[
|
|
353
|
+
{ label: labels.value.delete, icon: "i-lucide-trash-2", color: "error", onSelect: () => props.tree.deleteEntry(cell.id) }
|
|
354
|
+
]
|
|
355
|
+
];
|
|
356
|
+
});
|
|
357
|
+
function cellEditor(rowIdx, fieldId) {
|
|
358
|
+
return props.states.filter((s) => s.clientId !== props.myClientId).find(
|
|
359
|
+
(s) => s["table:editing"]?.rowIdx === rowIdx && s["table:editing"]?.fieldId === fieldId
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function onRowPointerEnter(rowIdx) {
|
|
363
|
+
if (rowDragId.value) return;
|
|
364
|
+
props.setLocalState({ "table:hovering": rowIdx });
|
|
365
|
+
}
|
|
366
|
+
function onRowPointerLeave() {
|
|
367
|
+
if (rowDragId.value) return;
|
|
368
|
+
props.setLocalState({ "table:hovering": null });
|
|
369
|
+
}
|
|
370
|
+
function rowHoverers(rowIdx) {
|
|
371
|
+
const result = [];
|
|
372
|
+
for (const s of props.states) {
|
|
373
|
+
if (s.clientId === props.myClientId) continue;
|
|
374
|
+
if (s["table:hovering"] === rowIdx && s.user) {
|
|
375
|
+
result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
function rowEditors(rowIdx) {
|
|
381
|
+
const result = [];
|
|
382
|
+
for (const s of props.states) {
|
|
383
|
+
if (s.clientId === props.myClientId) continue;
|
|
384
|
+
if (s["table:editing"]?.rowIdx === rowIdx && s.user) {
|
|
385
|
+
result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
function remoteResizeColor(colId) {
|
|
391
|
+
for (const s of props.states) {
|
|
392
|
+
if (s.clientId === props.myClientId) continue;
|
|
393
|
+
if (s["table:resizing"] === colId && s.user) {
|
|
394
|
+
return s.user.color ?? null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
onBeforeUnmount(() => {
|
|
400
|
+
props.setLocalState({
|
|
401
|
+
"table:editing": null,
|
|
402
|
+
"table:hovering": null,
|
|
403
|
+
"table:dragging": null,
|
|
404
|
+
"table:resizing": null
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
defineExpose({ addColumn, addRow });
|
|
408
|
+
</script>
|
|
409
|
+
|
|
410
|
+
<template>
|
|
411
|
+
<div class="flex-1 overflow-auto">
|
|
412
|
+
<!-- Empty state -->
|
|
413
|
+
<div
|
|
414
|
+
v-if="columns.length === 0"
|
|
415
|
+
class="flex flex-col items-center justify-center h-full gap-3 text-center py-12"
|
|
416
|
+
>
|
|
417
|
+
<UIcon
|
|
418
|
+
name="i-lucide-table"
|
|
419
|
+
class="size-10 text-(--ui-text-dimmed)"
|
|
420
|
+
/>
|
|
421
|
+
<p class="text-sm text-(--ui-text-muted)">
|
|
422
|
+
{{ labels.noRows }}
|
|
423
|
+
</p>
|
|
424
|
+
<UButton
|
|
425
|
+
v-if="editable"
|
|
426
|
+
icon="i-lucide-plus"
|
|
427
|
+
:label="labels.addColumn"
|
|
428
|
+
size="sm"
|
|
429
|
+
@click="addColumn"
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<template v-else>
|
|
434
|
+
<table
|
|
435
|
+
class="text-sm border-collapse"
|
|
436
|
+
style="table-layout: fixed; min-width: 100%;"
|
|
437
|
+
>
|
|
438
|
+
<thead class="sticky top-0 bg-(--ui-bg) z-10">
|
|
439
|
+
<tr class="border-b border-(--ui-border)">
|
|
440
|
+
<th class="w-0 p-0" />
|
|
441
|
+
<th class="w-6 px-1">
|
|
442
|
+
<!-- grip -->
|
|
443
|
+
</th>
|
|
444
|
+
<th class="text-left px-2 py-2 text-xs font-medium text-(--ui-text-muted) w-20">
|
|
445
|
+
#
|
|
446
|
+
</th>
|
|
447
|
+
<!-- Document columns -->
|
|
448
|
+
<ATableColumnHeader
|
|
449
|
+
v-for="col in orderedColumns"
|
|
450
|
+
:key="col.id"
|
|
451
|
+
:column-id="col.id"
|
|
452
|
+
:label="col.label"
|
|
453
|
+
:width="getDocColumnWidth(col.id)"
|
|
454
|
+
:sort-key="tableView.sortKey.value"
|
|
455
|
+
:sort-dir="tableView.sortDir.value"
|
|
456
|
+
:can-rename="editable !== false"
|
|
457
|
+
:can-delete="editable !== false"
|
|
458
|
+
:can-drag="editable !== false"
|
|
459
|
+
:data-col-drag-id="col.id"
|
|
460
|
+
:class="[
|
|
461
|
+
colDragOverId === col.id ? 'border-l-2 border-(--ui-primary)' : '',
|
|
462
|
+
colDragId === col.id ? 'opacity-30' : ''
|
|
463
|
+
]"
|
|
464
|
+
:style="remoteResizeColor(col.id) ? { backgroundColor: remoteResizeColor(col.id) + '20' } : {}"
|
|
465
|
+
@rename="renameColumn(col.id, $event)"
|
|
466
|
+
@delete="deleteColumn(col)"
|
|
467
|
+
@sort="handleSort(col.id, $event)"
|
|
468
|
+
@resize-start="onResizeStart($event, col.id)"
|
|
469
|
+
@drag-pointer-down="handleColPointerDown($event, col.id)"
|
|
470
|
+
/>
|
|
471
|
+
<!-- Auto-detected meta columns -->
|
|
472
|
+
<ATableColumnHeader
|
|
473
|
+
v-for="mc in metaColumns"
|
|
474
|
+
:key="mc.id"
|
|
475
|
+
:column-id="mc.id"
|
|
476
|
+
:label="mc.label"
|
|
477
|
+
:width="getMetaColumnWidth(mc)"
|
|
478
|
+
:sort-key="tableView.sortKey.value"
|
|
479
|
+
:sort-dir="tableView.sortDir.value"
|
|
480
|
+
:can-rename="false"
|
|
481
|
+
:can-delete="editable !== false"
|
|
482
|
+
:can-drag="false"
|
|
483
|
+
@delete="deleteMetaColumn(mc.metaKey)"
|
|
484
|
+
@sort="handleSort(mc.id, $event)"
|
|
485
|
+
/>
|
|
486
|
+
</tr>
|
|
487
|
+
</thead>
|
|
488
|
+
<TransitionGroup
|
|
489
|
+
name="trow"
|
|
490
|
+
tag="tbody"
|
|
491
|
+
>
|
|
492
|
+
<tr
|
|
493
|
+
v-for="idx in rowCount"
|
|
494
|
+
:key="getRowRep(idx - 1)?.id ?? `row-${idx}`"
|
|
495
|
+
:data-drag-id="`row-${idx - 1}`"
|
|
496
|
+
class="group hover:bg-(--ui-bg-elevated)"
|
|
497
|
+
:class="[
|
|
498
|
+
rowDragOverId === `row-${idx - 1}` ? 'border-t-2 border-(--ui-primary)' : 'border-b border-(--ui-border)',
|
|
499
|
+
rowDragId === `row-${idx - 1}` ? 'opacity-30' : ''
|
|
500
|
+
]"
|
|
501
|
+
@contextmenu="editable !== false && onCellContextMenu($event, getRowRep(idx - 1))"
|
|
502
|
+
@touchstart.passive="editable !== false && startLongPress($event, idx - 1)"
|
|
503
|
+
@touchend="cancelLongPress"
|
|
504
|
+
@touchmove="cancelLongPress"
|
|
505
|
+
@pointerenter="onRowPointerEnter(idx - 1)"
|
|
506
|
+
@pointerleave="onRowPointerLeave"
|
|
507
|
+
>
|
|
508
|
+
<td class="w-0 p-0 relative overflow-visible">
|
|
509
|
+
<div
|
|
510
|
+
v-if="rowEditors(idx - 1).length"
|
|
511
|
+
class="absolute left-0 top-0 bottom-0 w-0.5 rounded-full z-10"
|
|
512
|
+
:style="{ background: rowEditors(idx - 1)[0].color }"
|
|
513
|
+
/>
|
|
514
|
+
<span
|
|
515
|
+
v-if="rowHoverers(idx - 1).length"
|
|
516
|
+
class="absolute -top-2.5 left-6 text-[10px] px-1 rounded text-white leading-tight z-10 whitespace-nowrap pointer-events-none"
|
|
517
|
+
:style="{ backgroundColor: rowHoverers(idx - 1)[0].color }"
|
|
518
|
+
>
|
|
519
|
+
{{ rowHoverers(idx - 1).map((h) => h.name).join(", ") }}
|
|
520
|
+
</span>
|
|
521
|
+
</td>
|
|
522
|
+
<td
|
|
523
|
+
class="px-1 py-1.5 text-(--ui-text-dimmed) opacity-0 group-hover:opacity-60 cursor-grab w-6 touch-none"
|
|
524
|
+
@pointerdown="handleRowPointerDown($event, `row-${idx - 1}`)"
|
|
525
|
+
>
|
|
526
|
+
<div class="flex items-center justify-center">
|
|
527
|
+
<UIcon
|
|
528
|
+
name="i-lucide-grip-vertical"
|
|
529
|
+
class="size-4"
|
|
530
|
+
/>
|
|
531
|
+
</div>
|
|
532
|
+
</td>
|
|
533
|
+
<td class="px-2 py-1.5 text-xs text-(--ui-text-dimmed) select-none w-20">
|
|
534
|
+
<div class="flex items-center gap-0.5">
|
|
535
|
+
<span class="w-4 text-center">{{ idx }}</span>
|
|
536
|
+
<UTooltip
|
|
537
|
+
v-if="getRowRep(idx - 1)"
|
|
538
|
+
:text="labels.openAsSlideover"
|
|
539
|
+
:content="{ side: 'bottom' }"
|
|
540
|
+
>
|
|
541
|
+
<UButton
|
|
542
|
+
icon="i-lucide-external-link"
|
|
543
|
+
size="xs"
|
|
544
|
+
variant="ghost"
|
|
545
|
+
color="neutral"
|
|
546
|
+
class="opacity-0 group-hover:opacity-100"
|
|
547
|
+
@click="emit('openNode', getRowRep(idx - 1).id, getRowRep(idx - 1).label)"
|
|
548
|
+
/>
|
|
549
|
+
</UTooltip>
|
|
550
|
+
<UTooltip
|
|
551
|
+
v-if="editable !== false"
|
|
552
|
+
:text="labels.deleteRow"
|
|
553
|
+
:content="{ side: 'bottom' }"
|
|
554
|
+
>
|
|
555
|
+
<UButton
|
|
556
|
+
icon="i-lucide-trash-2"
|
|
557
|
+
size="xs"
|
|
558
|
+
variant="ghost"
|
|
559
|
+
color="error"
|
|
560
|
+
class="opacity-0 group-hover:opacity-100"
|
|
561
|
+
@click="deleteRow(idx - 1)"
|
|
562
|
+
/>
|
|
563
|
+
</UTooltip>
|
|
564
|
+
</div>
|
|
565
|
+
</td>
|
|
566
|
+
<!-- Document column cells -->
|
|
567
|
+
<td
|
|
568
|
+
v-for="col in orderedColumns"
|
|
569
|
+
:key="col.id"
|
|
570
|
+
class="px-1 py-0.5"
|
|
571
|
+
:style="
|
|
572
|
+
cellEditor(idx - 1, col.id) ? { outline: `2px solid ${cellEditor(idx - 1, col.id).user?.color}` } : {}
|
|
573
|
+
"
|
|
574
|
+
>
|
|
575
|
+
<ATableCell
|
|
576
|
+
:value="getCellValue(col.id, idx - 1)"
|
|
577
|
+
meta-type="text"
|
|
578
|
+
:editing="editingCell?.rowIdx === idx - 1 && editingCell?.colId === col.id"
|
|
579
|
+
:column="{ id: col.id, type: 'label', label: col.label }"
|
|
580
|
+
@update:model-value="onDocCellUpdate(col.id, idx - 1, $event)"
|
|
581
|
+
@start-edit="startEditDocCol(idx - 1, col)"
|
|
582
|
+
@stop-edit="stopEdit"
|
|
583
|
+
/>
|
|
584
|
+
</td>
|
|
585
|
+
<!-- Auto-detected meta column cells -->
|
|
586
|
+
<td
|
|
587
|
+
v-for="mc in metaColumns"
|
|
588
|
+
:key="mc.id"
|
|
589
|
+
class="px-1 py-0.5"
|
|
590
|
+
:style="
|
|
591
|
+
cellEditor(idx - 1, mc.id) ? { outline: `2px solid ${cellEditor(idx - 1, mc.id).user?.color}` } : {}
|
|
592
|
+
"
|
|
593
|
+
>
|
|
594
|
+
<ATableCell
|
|
595
|
+
:value="getMetaCellValue(idx - 1, mc.metaKey)"
|
|
596
|
+
:meta-type="mc.metaType"
|
|
597
|
+
:editing="editingCell?.rowIdx === idx - 1 && editingCell?.colId === mc.id"
|
|
598
|
+
:column="{ id: mc.id, type: 'meta', metaKey: mc.metaKey, metaType: mc.metaType, label: mc.label }"
|
|
599
|
+
@update:model-value="onMetaCellUpdate(idx - 1, mc.metaKey, $event)"
|
|
600
|
+
@start-edit="startEditMetaCol(idx - 1, mc)"
|
|
601
|
+
@stop-edit="stopEdit"
|
|
602
|
+
/>
|
|
603
|
+
</td>
|
|
604
|
+
</tr>
|
|
605
|
+
</TransitionGroup>
|
|
606
|
+
<tfoot v-if="editable !== false">
|
|
607
|
+
<tr>
|
|
608
|
+
<td :colspan="orderedColumns.length + metaColumns.length + 3">
|
|
609
|
+
<button
|
|
610
|
+
class="w-full text-left px-3 py-2 text-xs text-(--ui-text-dimmed) hover:bg-(--ui-bg-elevated) border-b border-(--ui-border) transition-colors"
|
|
611
|
+
@click="addRow"
|
|
612
|
+
>
|
|
613
|
+
+ {{ labels.addRow }}
|
|
614
|
+
</button>
|
|
615
|
+
</td>
|
|
616
|
+
</tr>
|
|
617
|
+
</tfoot>
|
|
618
|
+
</table>
|
|
619
|
+
</template>
|
|
620
|
+
|
|
621
|
+
<!-- Cell context menu -->
|
|
622
|
+
<Teleport
|
|
623
|
+
v-if="editable !== false"
|
|
624
|
+
to="body"
|
|
625
|
+
>
|
|
626
|
+
<div
|
|
627
|
+
v-if="showCtx && ctxItems.length"
|
|
628
|
+
class="fixed z-50 min-w-40 bg-(--ui-bg) border border-(--ui-border) rounded-lg shadow-xl py-1 text-sm"
|
|
629
|
+
:style="{ left: ctxPos.x + 'px', top: ctxPos.y + 'px' }"
|
|
630
|
+
>
|
|
631
|
+
<template
|
|
632
|
+
v-for="(group, gi) in ctxItems"
|
|
633
|
+
:key="gi"
|
|
634
|
+
>
|
|
635
|
+
<div
|
|
636
|
+
v-if="gi > 0"
|
|
637
|
+
class="my-1 border-t border-(--ui-border)"
|
|
638
|
+
/>
|
|
639
|
+
<button
|
|
640
|
+
v-for="item in group"
|
|
641
|
+
:key="item.label"
|
|
642
|
+
class="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-(--ui-bg-elevated) text-left cursor-default"
|
|
643
|
+
:class="item.color === 'error' ? 'text-(--ui-color-error-500)' : 'text-(--ui-text)'"
|
|
644
|
+
@pointerdown.stop
|
|
645
|
+
@click="item.onSelect();
|
|
646
|
+
showCtx = false"
|
|
647
|
+
>
|
|
648
|
+
<UIcon
|
|
649
|
+
:name="item.icon"
|
|
650
|
+
class="size-3.5 shrink-0 opacity-70"
|
|
651
|
+
/>
|
|
652
|
+
{{ item.label }}
|
|
653
|
+
</button>
|
|
654
|
+
</template>
|
|
655
|
+
</div>
|
|
656
|
+
</Teleport>
|
|
657
|
+
</div>
|
|
658
|
+
</template>
|
|
659
|
+
|
|
660
|
+
<style scoped>
|
|
661
|
+
.trow-move{transition:transform .25s ease}.trow-enter-active{transition:opacity .18s ease,transform .18s ease}.trow-enter-from{opacity:0;transform:translateY(-6px) scale(.97)}.trow-leave-active{transition:opacity .15s ease}.trow-leave-to{opacity:0}
|
|
662
|
+
</style>
|