@keenaioc/data-view 0.0.0-poc.1 → 0.2.0-alpha.10

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DATA-PRIVACY.md +20 -0
  3. package/INTEGRATION.md +218 -0
  4. package/KNOWN-LIMITATIONS.md +49 -0
  5. package/LICENSE +41 -0
  6. package/README.md +69 -5
  7. package/dist/contracts.d.ts +33 -0
  8. package/dist/contracts.d.ts.map +1 -0
  9. package/dist/contracts.js +83 -0
  10. package/dist/contracts.js.map +1 -0
  11. package/dist/data-view.d.ts +3 -0
  12. package/dist/data-view.d.ts.map +1 -0
  13. package/dist/data-view.js +844 -0
  14. package/dist/data-view.js.map +1 -0
  15. package/dist/engine.d.ts +34 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +307 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/field-order.d.ts +8 -0
  20. package/dist/field-order.d.ts.map +1 -0
  21. package/dist/field-order.js +28 -0
  22. package/dist/field-order.js.map +1 -0
  23. package/dist/gantt-bar.d.ts +17 -0
  24. package/dist/gantt-bar.d.ts.map +1 -0
  25. package/dist/gantt-bar.js +142 -0
  26. package/dist/gantt-bar.js.map +1 -0
  27. package/dist/gantt-dependencies.d.ts +27 -0
  28. package/dist/gantt-dependencies.d.ts.map +1 -0
  29. package/dist/gantt-dependencies.js +218 -0
  30. package/dist/gantt-dependencies.js.map +1 -0
  31. package/dist/gantt-dependency-ui.d.ts +37 -0
  32. package/dist/gantt-dependency-ui.d.ts.map +1 -0
  33. package/dist/gantt-dependency-ui.js +79 -0
  34. package/dist/gantt-dependency-ui.js.map +1 -0
  35. package/dist/gantt-grid.d.ts +27 -0
  36. package/dist/gantt-grid.d.ts.map +1 -0
  37. package/dist/gantt-grid.js +72 -0
  38. package/dist/gantt-grid.js.map +1 -0
  39. package/dist/gantt-operations.d.ts +14 -0
  40. package/dist/gantt-operations.d.ts.map +1 -0
  41. package/dist/gantt-operations.js +103 -0
  42. package/dist/gantt-operations.js.map +1 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +17 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/operations.d.ts +11 -0
  48. package/dist/operations.d.ts.map +1 -0
  49. package/dist/operations.js +117 -0
  50. package/dist/operations.js.map +1 -0
  51. package/dist/registry.d.ts +17 -0
  52. package/dist/registry.d.ts.map +1 -0
  53. package/dist/registry.js +482 -0
  54. package/dist/registry.js.map +1 -0
  55. package/dist/remote-data.d.ts +19 -0
  56. package/dist/remote-data.d.ts.map +1 -0
  57. package/dist/remote-data.js +67 -0
  58. package/dist/remote-data.js.map +1 -0
  59. package/dist/row-move.d.ts +7 -0
  60. package/dist/row-move.d.ts.map +1 -0
  61. package/dist/row-move.js +27 -0
  62. package/dist/row-move.js.map +1 -0
  63. package/dist/schedule-view.d.ts +27 -0
  64. package/dist/schedule-view.d.ts.map +1 -0
  65. package/dist/schedule-view.js +122 -0
  66. package/dist/schedule-view.js.map +1 -0
  67. package/dist/schedule.d.ts +28 -0
  68. package/dist/schedule.d.ts.map +1 -0
  69. package/dist/schedule.js +97 -0
  70. package/dist/schedule.js.map +1 -0
  71. package/dist/style.css +1003 -0
  72. package/dist/timeline.d.ts +25 -0
  73. package/dist/timeline.d.ts.map +1 -0
  74. package/dist/timeline.js +58 -0
  75. package/dist/timeline.js.map +1 -0
  76. package/dist/toolbar.d.ts +10 -0
  77. package/dist/toolbar.d.ts.map +1 -0
  78. package/dist/toolbar.js +31 -0
  79. package/dist/toolbar.js.map +1 -0
  80. package/dist/types.d.ts +472 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +22 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/use-remote-data.d.ts +18 -0
  85. package/dist/use-remote-data.d.ts.map +1 -0
  86. package/dist/use-remote-data.js +12 -0
  87. package/dist/use-remote-data.js.map +1 -0
  88. package/dist/use-row-move.d.ts +12 -0
  89. package/dist/use-row-move.d.ts.map +1 -0
  90. package/dist/use-row-move.js +151 -0
  91. package/dist/use-row-move.js.map +1 -0
  92. package/dist/v2-panels.d.ts +37 -0
  93. package/dist/v2-panels.d.ts.map +1 -0
  94. package/dist/v2-panels.js +119 -0
  95. package/dist/v2-panels.js.map +1 -0
  96. package/dist/view-state.d.ts +30 -0
  97. package/dist/view-state.d.ts.map +1 -0
  98. package/dist/view-state.js +94 -0
  99. package/dist/view-state.js.map +1 -0
  100. package/dist/view-workspace.d.ts +3 -0
  101. package/dist/view-workspace.d.ts.map +1 -0
  102. package/dist/view-workspace.js +95 -0
  103. package/dist/view-workspace.js.map +1 -0
  104. package/dist/virtual-list.d.ts +12 -0
  105. package/dist/virtual-list.d.ts.map +1 -0
  106. package/dist/virtual-list.js +46 -0
  107. package/dist/virtual-list.js.map +1 -0
  108. package/package.json +19 -16
  109. package/index.d.ts +0 -2
  110. package/index.js +0 -5
  111. package/react.d.ts +0 -2
  112. package/react.js +0 -8
  113. package/style.css +0 -1
@@ -0,0 +1,844 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Fragment, Children, useEffect, useMemo, useRef, useState, } from "react";
4
+ import { columnMovePeers, moveColumnSetting } from "./field-order.js";
5
+ import { DataViewToolbar } from "./toolbar.js";
6
+ import { buildGroupTree, DataViewConflictError, filterRows, getFieldValue, getPinnedOffsets, getRightPinnedOffsets, MAX_GROUP_LEVELS, moveColumn, normalizeColumnWidth, normalizeViewConfig, retainViewReferences, searchRows, sortRows, validateValue, visibleFields, } from "./engine.js";
7
+ import { FILTER_OPERATOR_LABELS, RelationSelect, getFieldTypeDefinition, renderFieldValue, formatClipboardValue, formatReadableValue, formatReadableRow } from "./registry.js";
8
+ import { canWriteField, createPasteChanges, encodeTSV, newOperationId, validateChanges, validateFieldChange } from "./operations.js";
9
+ import { createDataViewQuery, dataViewQueryKey, resolveMetrics, mergeViewState, splitViewState, createWriteQueue, virtualWindow } from "./contracts.js";
10
+ import { VirtualList } from "./virtual-list.js";
11
+ import { useRowMove } from "./use-row-move.js";
12
+ import { createSnapshotWriter } from "./view-state.js";
13
+ import { ScheduleView } from "./schedule-view.js";
14
+ import { applyGanttDependencyChange, ganttHistoryGuard, validateGanttScheduleGuard } from "./gantt-dependencies.js";
15
+ import { BatchEditor, ColorSettings, DataViewDialog, FieldCreator, FieldManager, ViewSettings } from "./v2-panels.js";
16
+ export function DataView(props) {
17
+ return _jsx(DataViewInner, { ...props }, props.identityKey ?? "default");
18
+ }
19
+ function DataViewInner(props) {
20
+ const { fields, rows, getRowId, title = "資料表格", loading = false, error = null, access = "granted", emptyMessage = "目前沒有資料", onViewChange, onCellCommit, onBatchCommit, onFieldChange, ganttDependencies, onGanttDependencyChange, onRequestAttachment, permissions, selectedRowIds, onSelectionChange, searchPlaceholder = "搜尋目前資料", } = props;
21
+ const [internalView, setInternalView] = useState(() => mergeViewState(normalizeViewConfig(fields, props.view ?? props.initialView), props.runtimeState));
22
+ const activeView = internalView;
23
+ const search = activeView.location?.search ?? "";
24
+ const [panel, setPanel] = useState(null);
25
+ const [draggedColumn, setDraggedColumn] = useState(null);
26
+ const [editing, setEditing] = useState(null);
27
+ const [viewSaveState, setViewSaveState] = useState(onViewChange ? "idle" : "readonly");
28
+ const [cellStatuses, setCellStatuses] = useState({});
29
+ const [localSelected, setLocalSelected] = useState([]);
30
+ const viewRef = useRef(activeView);
31
+ const layoutRef = useRef(normalizeViewConfig(fields, props.view ?? props.initialView));
32
+ const saveCallback = useRef(onViewChange);
33
+ useEffect(() => { saveCallback.current = onViewChange; }, [onViewChange]);
34
+ const writer = useRef(null);
35
+ const latestSave = useRef(0);
36
+ const pendingViewWrite = useRef(false);
37
+ const rootRef = useRef(null);
38
+ const scrollRef = useRef(null);
39
+ const mobileRef = useRef(null);
40
+ useEffect(() => { setEditing(null); }, [props.presentation]);
41
+ const [scrollTop, setScrollTop] = useState(0);
42
+ const [focus, setFocus] = useState(null);
43
+ const [anchor, setAnchor] = useState(null);
44
+ const [detailId, setDetailId] = useState(null);
45
+ const [dialog, setDialog] = useState(null);
46
+ const [pending, setPending] = useState(null);
47
+ const [operation, setOperation] = useState({ state: "idle" });
48
+ const [undoStack, setUndoStack] = useState([]);
49
+ const [redoStack, setRedoStack] = useState([]);
50
+ const busyRef = useRef(false);
51
+ const pendingCells = useRef(new Set());
52
+ const writes = useRef(createWriteQueue());
53
+ useEffect(() => { const queue = createWriteQueue(); writes.current = queue; return () => queue.dispose(); }, []);
54
+ const [quick, setQuick] = useState(null);
55
+ const bottomRef = useRef(null);
56
+ const [viewportHeight, setViewportHeight] = useState(560);
57
+ const [viewportWidth, setViewportWidth] = useState(0);
58
+ const metrics = resolveMetrics(activeView.density, props.metrics);
59
+ const runtimeCallback = useRef(props.onRuntimeChange);
60
+ runtimeCallback.current = props.onRuntimeChange;
61
+ const detailCallback = useRef(props.onViewChangeDetail);
62
+ detailCallback.current = props.onViewChangeDetail;
63
+ const runtimeWriter = useRef(createSnapshotWriter((v) => runtimeCallback.current?.(v)));
64
+ const detailWriter = useRef(createSnapshotWriter((v) => detailCallback.current?.(v)));
65
+ const features = props.features ?? {};
66
+ const availableFields = useMemo(() => fields.filter((f) => !f.internal), [fields]);
67
+ const latest = useRef({ rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt });
68
+ useEffect(() => { latest.current = { rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt }; }, [rows, fields, permissions, access, ganttDependencies, activeView.gantt]);
69
+ useEffect(() => {
70
+ if (props.view)
71
+ layoutRef.current = normalizeViewConfig(fields, props.view);
72
+ setInternalView((current) => mergeViewState(normalizeViewConfig(fields, !pendingViewWrite.current && props.view ? props.view : current), props.runtimeState));
73
+ }, [fields, props.view, props.runtimeState]);
74
+ useEffect(() => { viewRef.current = activeView; }, [activeView]);
75
+ useEffect(() => {
76
+ if (mobileRef.current)
77
+ mobileRef.current.scrollTop = viewRef.current.location?.mobileTop ?? 0;
78
+ const el = scrollRef.current;
79
+ if (!el)
80
+ return;
81
+ el.scrollTop = viewRef.current.location?.tableTop ?? 0;
82
+ el.scrollLeft = viewRef.current.location?.tableLeft ?? 0;
83
+ setScrollTop(el.scrollTop);
84
+ }, [activeView.viewType]);
85
+ useEffect(() => { const leave = (e) => { if (pendingViewWrite.current) {
86
+ e.preventDefault();
87
+ e.returnValue = "";
88
+ } }; window.addEventListener("beforeunload", leave); return () => window.removeEventListener("beforeunload", leave); }, []);
89
+ const selected = selectedRowIds ?? localSelected;
90
+ const selectable = permissions?.canSelectRows !== false;
91
+ const controlColumn = selectable || !!props.rowMove || features.rowOpen === true;
92
+ const controlWidth = (selectable ? 32 : 0) + (props.rowMove ? 24 : 0) + (features.rowOpen === true ? 28 : 0);
93
+ const move = useRowMove(props, activeView, rootRef, (id, action) => writes.current.run([id], action));
94
+ const configurable = permissions?.canConfigureView !== false;
95
+ const canManageFields = Boolean(onFieldChange && permissions?.canManageFields === true && access === "granted");
96
+ const addColumnWidth = canManageFields ? 56 : 0;
97
+ const busy = operation.state === "saving";
98
+ const selectedSet = useMemo(() => new Set(selected), [selected]);
99
+ const selectedRows = useMemo(() => rows.filter((row) => selectedSet.has(getRowId(row))), [rows, selectedSet, getRowId]);
100
+ const detailRow = detailId ? rows.find((row) => getRowId(row) === detailId) : undefined;
101
+ function setView(next, scope = "layout") {
102
+ if (scope === "layout" && !configurable)
103
+ return;
104
+ const normalized = retainViewReferences(viewRef.current, normalizeViewConfig(fields, next));
105
+ viewRef.current = normalized;
106
+ setInternalView(normalized);
107
+ const runtime = { viewType: normalized.viewType, location: normalized.location, collapsedGroups: normalized.collapsedGroups, ...(props.runtimeState?.filters || scope === "query" ? { filters: normalized.filters, sorts: normalized.sorts } : {}) };
108
+ const callbacks = [];
109
+ if (detailCallback.current)
110
+ callbacks.push(detailWriter.current({ scope, view: normalized, runtime }));
111
+ if (scope !== "layout" && runtimeCallback.current)
112
+ callbacks.push(runtimeWriter.current(runtime));
113
+ else if (saveCallback.current) {
114
+ if (!writer.current)
115
+ writer.current = createSnapshotWriter((value) => saveCallback.current?.(value));
116
+ const savedLayout = runtimeCallback.current ? { ...splitViewState(normalized).layout, filters: layoutRef.current.filters, sorts: layoutRef.current.sorts } : normalized;
117
+ layoutRef.current = savedLayout;
118
+ callbacks.push(writer.current(savedLayout));
119
+ }
120
+ if (!callbacks.length)
121
+ return;
122
+ setViewSaveState("saving");
123
+ pendingViewWrite.current = true;
124
+ const saveNumber = ++latestSave.current;
125
+ void Promise.all(callbacks).then(() => {
126
+ if (latestSave.current === saveNumber) {
127
+ setViewSaveState("saved");
128
+ pendingViewWrite.current = false;
129
+ }
130
+ }, (e) => { if (latestSave.current === saveNumber)
131
+ setViewSaveState(e instanceof DataViewConflictError ? "conflict" : "failed"); });
132
+ }
133
+ function patchView(patch, scope = "layout") { setView({ ...viewRef.current, ...patch }, scope); }
134
+ function patchLocation(patch) { patchView({ location: { ...viewRef.current.location, ...patch } }, "navigation"); }
135
+ const query = useMemo(() => createDataViewQuery(activeView), [activeView.filters, activeView.sorts, activeView.groups, search]);
136
+ const queryKey = dataViewQueryKey(query);
137
+ const remoteStale = !!props.remote && props.remote.queryKey !== queryKey;
138
+ const queryCallback = useRef(props.onQueryChange);
139
+ queryCallback.current = props.onQueryChange;
140
+ const previousQuery = useRef(queryKey);
141
+ useEffect(() => { if (access === "granted" && !loading && !error)
142
+ queryCallback.current?.(query); }, [queryKey, props.onQueryChange, access, loading, error]);
143
+ useEffect(() => { if (previousQuery.current === queryKey)
144
+ return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); }, [queryKey]);
145
+ const canFilter = permissions?.canFilter ?? configurable;
146
+ const canSort = permissions?.canSort ?? configurable;
147
+ const processedRows = useMemo(() => {
148
+ if (props.remote)
149
+ return remoteStale ? [] : rows;
150
+ const searched = searchRows(rows, fields, search);
151
+ const filtered = filterRows(searched, fields, activeView.filters.rules, activeView.filters.conjunction);
152
+ return sortRows(filtered, fields, activeView.sorts);
153
+ }, [activeView.filters, activeView.sorts, fields, rows, search, Boolean(props.remote), remoteStale]);
154
+ const fullGroups = useMemo(() => buildGroupTree(processedRows, fields, activeView.groups), [processedRows, fields, activeView.groups]);
155
+ const groups = fullGroups;
156
+ const visible = useMemo(() => visibleFields(fields, activeView), [activeView, fields]);
157
+ const tableColumnCount = visible.length + (controlColumn ? 1 : 0) + (canManageFields ? 1 : 0);
158
+ const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns, selectable]);
159
+ const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44), [activeView.columns]);
160
+ const ganttRightOffsets = useMemo(() => getRightPinnedOffsets(activeView.columns), [activeView.columns]);
161
+ const tableWidth = visible.reduce((total, item) => total + item.column.width, (controlWidth) + addColumnWidth);
162
+ const collapsed = useMemo(() => new Set(activeView.collapsedGroups), [activeView.collapsedGroups]);
163
+ const allGroupKeys = useMemo(() => collectGroupKeys(fullGroups), [fullGroups]);
164
+ const rightOffsets = useMemo(() => new Map([...getRightPinnedOffsets(activeView.columns)].map(([id, offset]) => [id, offset + addColumnWidth])), [activeView.columns, addColumnWidth]);
165
+ const virtual = activeView.virtualize !== false || processedRows.length > 200;
166
+ const rowHeight = metrics.rowHeight;
167
+ const flatEntries = useMemo(() => flattenGroups(fullGroups, collapsed), [fullGroups, collapsed]);
168
+ const desktopRows = useMemo(() => flatEntries.flatMap((entry) => entry.kind === "row" ? [entry.row] : []), [flatEntries]);
169
+ const entryHeights = useMemo(() => flatEntries.map((e) => e.kind === "group" ? metrics.groupHeight + (e.node.depth ? metrics.nestedGroupGap : metrics.groupGap) : rowHeight), [flatEntries, rowHeight, metrics.groupHeight, metrics.groupGap, metrics.nestedGroupGap]);
170
+ const windowed = useMemo(() => virtualWindow(entryHeights, scrollTop, viewportHeight), [entryHeights, scrollTop, viewportHeight]);
171
+ const virtualStart = virtual ? windowed.start : 0;
172
+ const virtualEnd = virtual ? windowed.end : flatEntries.length;
173
+ useEffect(() => {
174
+ const el = scrollRef.current;
175
+ if (!el)
176
+ return;
177
+ const observer = new ResizeObserver(() => { setViewportHeight(el.clientHeight); setViewportWidth(el.clientWidth); });
178
+ observer.observe(el);
179
+ return () => observer.disconnect();
180
+ }, [activeView.viewType, loading, error, access, processedRows.length === 0]);
181
+ const mobileFields = useMemo(() => activeView.mobile ? activeView.mobile.fieldIds.flatMap((id) => { const field = availableFields.find((f) => f.id === id); return field ? [field] : []; }) : visible.slice(0, 5).map((v) => v.field), [activeView.mobile, availableFields, visible]);
182
+ function readableLabel(row) {
183
+ const id = activeView.viewType === "gantt" ? activeView.gantt?.titleFieldId : activeView.viewType === "calendar" ? activeView.calendar?.titleFieldId : activeView.mobile?.summaryFieldId;
184
+ const field = fields.find((f) => f.id === id) ?? availableFields.find((f) => !f.renderAction && canRead(row, f));
185
+ if (field && (field.internal || !canRead(row, field)))
186
+ return "無權限";
187
+ return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row) : "") || "未命名工作";
188
+ }
189
+ function openRow(row) { if (features.rowOpen === false)
190
+ return; if (props.onRowOpen)
191
+ props.onRowOpen(row);
192
+ else
193
+ setDetailId(getRowId(row)); }
194
+ function groupLabel(node) {
195
+ const field = fields.find((f) => f.id === node.fieldId);
196
+ return _jsxs(_Fragment, { children: [_jsx("strong", { className: "hy-dv-group-label", style: { backgroundColor: node.color, color: node.textColor }, children: node.valueKey === "__restricted__" ? "無權限" : field?.renderGroup?.(node) ?? node.label }), _jsxs("span", { children: [props.remote?.groupCounts?.[node.key] ?? node.rows.length, props.remote && !props.remote.complete && props.remote.groupCounts?.[node.key] === undefined ? " 已載入" : ""] })] });
197
+ }
198
+ function detailContext(row) { return { row, fields: availableFields.filter((f) => canRead(row, f)), close: () => { setDetailId(null); setEditing(null); }, renderCell: (field) => renderCell(row, field, "detail") }; }
199
+ function loadNearEnd(el) { if (el.scrollHeight - el.scrollTop - el.clientHeight < 200 && props.remote && !remoteStale && !props.remote.complete && !props.remote.loading && !props.remote.error)
200
+ props.remote.onLoadMore?.(); }
201
+ const colorMap = useMemo(() => {
202
+ const result = new Map();
203
+ for (const row of rows) {
204
+ const colors = new Map();
205
+ for (const rule of activeView.colorRules ?? []) {
206
+ const field = fields.find((f) => f.id === rule.condition.fieldId);
207
+ if (!field || field.canRead?.({ row, field }) === false || colors.has(rule.target))
208
+ continue;
209
+ if (getFieldTypeDefinition(field.type).filter(getFieldValue(row, field), rule.condition, field))
210
+ colors.set(rule.target, rule.color);
211
+ }
212
+ if (colors.size)
213
+ result.set(getRowId(row), colors);
214
+ }
215
+ return result;
216
+ }, [rows, fields, getRowId, activeView.colorRules]);
217
+ function changeSelection(next) {
218
+ const unique = [...new Set(next)];
219
+ if (!selectedRowIds)
220
+ setLocalSelected(unique);
221
+ onSelectionChange?.(unique);
222
+ }
223
+ function toggleRow(rowId) {
224
+ changeSelection(selected.includes(rowId) ? selected.filter((id) => id !== rowId) : [...selected, rowId]);
225
+ }
226
+ const pageIds = processedRows.map(getRowId);
227
+ const allPageSelected = pageIds.length > 0 && pageIds.every((id) => selected.includes(id));
228
+ function togglePageSelection() {
229
+ if (allPageSelected)
230
+ changeSelection(selected.filter((id) => !pageIds.includes(id)));
231
+ else
232
+ changeSelection([...selected, ...pageIds]);
233
+ }
234
+ function toggleGroup(key) {
235
+ if (permissions?.canCollapseGroups === false)
236
+ return;
237
+ const next = new Set(activeView.collapsedGroups);
238
+ if (next.has(key))
239
+ next.delete(key);
240
+ else
241
+ next.add(key);
242
+ patchView({ collapsedGroups: [...next] }, "navigation");
243
+ }
244
+ function setAllGroups(close) {
245
+ if (permissions?.canCollapseGroups !== false)
246
+ patchView({ collapsedGroups: close ? allGroupKeys : [] }, "navigation");
247
+ }
248
+ function resizeColumn(fieldId, startWidth, event) {
249
+ event.preventDefault();
250
+ const startX = event.clientX;
251
+ function move(pointer) {
252
+ const width = normalizeColumnWidth(startWidth + pointer.clientX - startX);
253
+ setView({
254
+ ...viewRef.current,
255
+ columns: viewRef.current.columns.map((column) => column.fieldId === fieldId ? { ...column, width } : column),
256
+ });
257
+ }
258
+ function up() {
259
+ window.removeEventListener("pointermove", move);
260
+ window.removeEventListener("pointerup", up);
261
+ window.removeEventListener("pointercancel", up);
262
+ }
263
+ window.addEventListener("pointermove", move);
264
+ window.addEventListener("pointerup", up);
265
+ window.addEventListener("pointercancel", up);
266
+ }
267
+ function dropColumn(targetId) {
268
+ if (!draggedColumn)
269
+ return;
270
+ patchView({ columns: moveColumn(activeView.columns, draggedColumn, targetId) });
271
+ setDraggedColumn(null);
272
+ }
273
+ function canRead(row, field) {
274
+ return field.canRead?.({ row, field }) !== false;
275
+ }
276
+ function canEdit(row, field) {
277
+ const spec = getFieldTypeDefinition(field.type);
278
+ return Boolean(!busyRef.current && !pendingCells.current.has(`${getRowId(row)}:${field.id}`) && access === "granted" && !remoteStale && (field.Editor || spec.Editor) && canWriteField(row, field, permissions) &&
279
+ (field.type === "attachment" ? onRequestAttachment : onBatchCommit ?? onCellCommit));
280
+ }
281
+ async function commitCell(row, field, nextValue) {
282
+ if (!canEdit(row, field))
283
+ return;
284
+ const rowId = getRowId(row);
285
+ const key = `${rowId}:${field.id}`;
286
+ const previousValue = getFieldValue(row, field);
287
+ if (JSON.stringify(previousValue) === JSON.stringify(nextValue)) {
288
+ setEditing(null);
289
+ return;
290
+ }
291
+ const validation = validateValue(nextValue, field, row);
292
+ if (validation) {
293
+ setEditing(null);
294
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: validation } }));
295
+ return;
296
+ }
297
+ setEditing(null);
298
+ if (onBatchCommit && !onCellCommit) {
299
+ await execute({ id: newOperationId(), reason: "edit", changes: [{ row, rowId, field, previousValue, nextValue }] });
300
+ return;
301
+ }
302
+ pendingCells.current.add(key);
303
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
304
+ try {
305
+ await writes.current.run([rowId], async () => {
306
+ const fresh = latest.current.rows.find((r) => getRowId(r) === rowId);
307
+ const definition = latest.current.fields.find((f) => f.id === field.id);
308
+ if (!fresh || !definition || latest.current.access !== "granted" || !canWriteField(fresh, definition, latest.current.permissions))
309
+ throw new Error("資料不存在或無修改權限");
310
+ if (JSON.stringify(getFieldValue(fresh, definition)) !== JSON.stringify(previousValue))
311
+ throw new DataViewConflictError();
312
+ await onCellCommit?.({ row: fresh, rowId, field: definition, previousValue, nextValue });
313
+ });
314
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
315
+ setTimeout(() => setCellStatuses((current) => {
316
+ if (current[key]?.state !== "saved")
317
+ return current;
318
+ const next = { ...current };
319
+ delete next[key];
320
+ return next;
321
+ }), 1400);
322
+ }
323
+ catch (commitError) {
324
+ setCellStatuses((current) => ({
325
+ ...current,
326
+ [key]: {
327
+ state: commitError instanceof DataViewConflictError ? "conflict" : "failed",
328
+ message: commitError instanceof Error ? commitError.message : "儲存失敗",
329
+ },
330
+ }));
331
+ }
332
+ finally {
333
+ pendingCells.current.delete(key);
334
+ }
335
+ }
336
+ function renderCell(row, field, surface) {
337
+ if (field.internal)
338
+ return null;
339
+ if (!canRead(row, field))
340
+ return _jsx("span", { className: "hy-dv-no-access", children: "\u7121\u6B0A\u9650" });
341
+ const rowId = getRowId(row);
342
+ const key = `${rowId}:${field.id}`;
343
+ const value = getFieldValue(row, field);
344
+ const status = cellStatuses[key];
345
+ const isEditing = editing?.rowId === rowId && editing.fieldId === field.id && editing.surface === surface;
346
+ const spec = getFieldTypeDefinition(field.type);
347
+ if (field.renderAction)
348
+ return _jsxs("fieldset", { className: "hy-dv-action-cell", disabled: field.canAct?.(row) === false || status?.state === "saving" || access !== "granted", children: [field.renderAction({ row, field, value, close: () => setEditing(null), openRow: () => openRow(row), disabled: field.canAct?.(row) === false || status?.state === "saving", pending: status?.state === "saving", run: async (action) => { if (field.canAct?.(row) === false || pendingCells.current.has(key))
349
+ return; pendingCells.current.add(key); setCellStatuses((s) => ({ ...s, [key]: { state: "saving" } })); try {
350
+ await writes.current.run([rowId], () => { const fresh = latest.current.rows.find((r) => getRowId(r) === rowId); const f = latest.current.fields.find((f) => f.id === field.id); if (!fresh || !f || latest.current.access !== "granted" || f.canRead?.({ row: fresh, field: f }) === false || f.canAct?.(fresh) === false)
351
+ throw new Error("無操作權限"); return action(); });
352
+ setCellStatuses((s) => ({ ...s, [key]: { state: "saved" } }));
353
+ }
354
+ catch (e) {
355
+ setCellStatuses((s) => ({ ...s, [key]: { state: e instanceof DataViewConflictError ? "conflict" : "failed", message: e instanceof Error ? e.message : "操作失敗" } }));
356
+ }
357
+ finally {
358
+ pendingCells.current.delete(key);
359
+ } } }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
360
+ if (isEditing && !spec.toggleValue && (field.Editor || spec.Editor) && canEdit(row, field)) {
361
+ const Editor = field.Editor ?? spec.Editor;
362
+ return _jsx("div", { className: "hy-dv-editor-wrap", children: _jsx(Editor, { value: value, row: row, field: field, onCommit: (next) => void commitCell(row, field, next), onCancel: () => setEditing(null), onRequestAttachment: onRequestAttachment ? async (files) => {
363
+ if (pendingCells.current.has(key))
364
+ return;
365
+ pendingCells.current.add(key);
366
+ setEditing(null);
367
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
368
+ try {
369
+ await writes.current.run([rowId], () => onRequestAttachment({ row, rowId, field, files }));
370
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
371
+ }
372
+ catch (uploadError) {
373
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: uploadError instanceof Error ? uploadError.message : "附件儲存失敗" } }));
374
+ }
375
+ finally {
376
+ pendingCells.current.delete(key);
377
+ }
378
+ } : undefined }) });
379
+ }
380
+ if (field.type === "relation" && !field.Editor && !field.renderQuickView && !field.render && canWriteField(row, field, permissions) && (onBatchCommit || onCellCommit))
381
+ return _jsxs("div", { className: "hy-dv-relation-cell", children: [_jsx(RelationSelect, { value: value, row: row, field: field, disabled: !canEdit(row, field), onCommit: (next) => void commitCell(row, field, next), onCancel: () => setEditing(null) }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
382
+ if (field.type === "attachment")
383
+ return _jsxs("div", { className: "hy-dv-attachment-cell", children: [(!spec.isEmpty(value) || !canEdit(row, field) || field.render) && renderFieldValue(value, field, row), canEdit(row, field) && _jsx("button", { className: "hy-dv-attachment-add", type: "button", "aria-label": "\u65B0\u589E\u9644\u4EF6", title: "\u65B0\u589E\u9644\u4EF6", onClick: () => setEditing({ rowId, fieldId: field.id, surface }), children: _jsx("svg", { "aria-hidden": "true", focusable: "false", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" }) }) }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
384
+ return _jsxs("button", { type: "button", className: `hy-dv-cell-button ${canEdit(row, field) ? "is-editable" : ""} ${spec.toggleValue ? "is-toggle" : ""}`, role: spec.toggleValue ? "checkbox" : undefined, "aria-checked": spec.toggleValue ? Boolean(value) : undefined, "aria-label": spec.toggleValue ? `${rowId} ${field.label}` : undefined, "aria-busy": status?.state === "saving", tabIndex: surface === "desktop" ? -1 : undefined, onClick: (event) => {
385
+ if (event.shiftKey && surface === "desktop")
386
+ return;
387
+ setFocus({ rowId, fieldId: field.id });
388
+ setAnchor(null);
389
+ if (spec.toggleValue) {
390
+ void commitCell(row, field, spec.toggleValue(value));
391
+ return;
392
+ }
393
+ if (field.renderQuickView) {
394
+ setQuick({ rowId, fieldId: field.id });
395
+ return;
396
+ }
397
+ if (!canEdit(row, field)) {
398
+ openRow(row);
399
+ return;
400
+ }
401
+ if (surface !== "detail" && (field.Editor || ["multiLineText", "multiSelect", "attachment", "relation", "person"].includes(field.type))) {
402
+ setDetailId(rowId);
403
+ setEditing({ rowId, fieldId: field.id, surface: "detail" });
404
+ return;
405
+ }
406
+ if (canEdit(row, field))
407
+ setEditing({ rowId, fieldId: field.id, surface });
408
+ }, disabled: status?.state === "saving" || (!!spec.toggleValue && !canEdit(row, field)), title: status?.message, children: [_jsx("span", { className: "hy-dv-cell-value", title: formatReadableValue(value, field, row), children: renderFieldValue(value, field, row) }), field.appearance?.arrow === true && !spec.toggleValue && _jsx("span", { className: "hy-dv-cell-arrow", "aria-hidden": true, children: "\u2304" }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
409
+ }
410
+ function reportFailure(error) {
411
+ setOperation({ state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : String(error) });
412
+ }
413
+ async function execute(request) {
414
+ if (!onBatchCommit || busyRef.current || latest.current.access !== "granted" || remoteStale)
415
+ return false;
416
+ const keys = request.changes.map((c) => `${c.rowId}:${c.field.id}`);
417
+ keys.forEach((k) => pendingCells.current.add(k));
418
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saving" }])) }));
419
+ return writes.current.run(request.changes.map((c) => c.rowId), async () => {
420
+ const currentRows = new Map(latest.current.rows.map((row) => [getRowId(row), row]));
421
+ try {
422
+ if (request.scheduleGuard) {
423
+ const guard = request.scheduleGuard;
424
+ const mapping = latest.current.mapping;
425
+ if (!mapping || mapping.startFieldId !== guard.startFieldId || mapping.endFieldId !== guard.endFieldId || (mapping.workdaysOnly === true) !== guard.workdaysOnly)
426
+ throw new DataViewConflictError("日期設定已變更,請重新預覽");
427
+ validateGanttScheduleGuard(guard, latest.current.rows, latest.current.fields, getRowId, latest.current.ganttDependencies);
428
+ for (const c of request.changes) {
429
+ const row = currentRows.get(c.rowId);
430
+ if (row && latest.current.ganttDependencies?.isDateLocked?.(row))
431
+ throw new DataViewConflictError("排程已確認或鎖定,請重新協調日期");
432
+ }
433
+ }
434
+ const changes = request.changes.map((change) => {
435
+ const row = currentRows.get(change.rowId);
436
+ const field = latest.current.fields.find((f) => f.id === change.field.id);
437
+ if (!row || !field)
438
+ throw new DataViewConflictError("資料列或欄位已不存在,請重新選取");
439
+ if (JSON.stringify(getFieldValue(row, field)) !== JSON.stringify(change.previousValue))
440
+ throw new DataViewConflictError("預覽後資料已更新,請重新檢查修改內容");
441
+ return { ...change, row, field };
442
+ });
443
+ const validation = validateChanges(changes, latest.current.permissions);
444
+ if (validation)
445
+ throw new Error(validation);
446
+ setOperation({ state: "saving", message: `正在儲存 ${changes.length} 格` });
447
+ await onBatchCommit({ ...request, changes });
448
+ if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo") {
449
+ setUndoStack((stack) => [...stack.slice(-19), { changes: changes.map((c) => ({ ...c, previousValue: structuredClone(c.previousValue), nextValue: structuredClone(c.nextValue) })), scheduleGuard: request.scheduleGuard ? structuredClone(request.scheduleGuard) : undefined }]);
450
+ setRedoStack([]);
451
+ }
452
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saved" }])) }));
453
+ setOperation({ state: "saved", message: `已儲存 ${changes.length} 格` });
454
+ return true;
455
+ }
456
+ catch (error) {
457
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : "儲存失敗" }])) }));
458
+ reportFailure(error);
459
+ return false;
460
+ }
461
+ finally {
462
+ keys.forEach((k) => pendingCells.current.delete(k));
463
+ }
464
+ });
465
+ }
466
+ function reviewChanges(changes, reason) {
467
+ const validation = validateChanges(changes, permissions);
468
+ if (validation) {
469
+ reportFailure(new Error(validation));
470
+ return;
471
+ }
472
+ setPending({ id: newOperationId(), reason, changes });
473
+ setDialog("review");
474
+ setEditing(null);
475
+ }
476
+ async function applyFieldChange(change) {
477
+ if (!onFieldChange || busyRef.current || latest.current.access !== "granted")
478
+ return false;
479
+ const validation = validateFieldChange(latest.current.fields, change, latest.current.permissions);
480
+ if (validation) {
481
+ reportFailure(new Error(validation));
482
+ return false;
483
+ }
484
+ busyRef.current = true;
485
+ setOperation({ state: "saving", message: "正在儲存欄位結構" });
486
+ try {
487
+ await writes.current.run(["*"], () => onFieldChange(change));
488
+ setUndoStack([]);
489
+ setRedoStack([]);
490
+ setFocus(null);
491
+ setAnchor(null);
492
+ setEditing(null);
493
+ setOperation({ state: "saved", message: "欄位結構已儲存" });
494
+ return true;
495
+ }
496
+ catch (error) {
497
+ reportFailure(error);
498
+ return false;
499
+ }
500
+ finally {
501
+ busyRef.current = false;
502
+ }
503
+ }
504
+ async function changeDependency(change) {
505
+ const current = latest.current;
506
+ if (!onGanttDependencyChange || !current.ganttDependencies || busyRef.current || current.access !== "granted" || current.permissions?.canManageDependencies !== true)
507
+ return false;
508
+ try {
509
+ applyGanttDependencyChange(current.ganttDependencies, change);
510
+ const link = change.action === "add" ? change.link : current.ganttDependencies.links.find((l) => l.id === change.linkId);
511
+ if (!link || ![link.predecessorId, link.successorId].every((id) => {
512
+ const row = current.rows.find((r) => getRowId(r) === id);
513
+ return row && [current.mapping?.titleFieldId, current.mapping?.startFieldId, current.mapping?.endFieldId].every((fieldId) => { const field = current.fields.find((f) => f.id === fieldId); return field && field.canRead?.({ row, field }) !== false; });
514
+ }))
515
+ throw new Error("相關資料不存在或無權限");
516
+ busyRef.current = true;
517
+ setOperation({ state: "saving", message: "正在儲存依賴" });
518
+ await writes.current.run(["*"], () => onGanttDependencyChange(change));
519
+ setUndoStack([]);
520
+ setRedoStack([]);
521
+ setOperation({ state: "saved", message: "依賴已儲存,日期保持原值" });
522
+ return true;
523
+ }
524
+ catch (error) {
525
+ reportFailure(error);
526
+ return false;
527
+ }
528
+ finally {
529
+ busyRef.current = false;
530
+ }
531
+ }
532
+ async function historyAction(direction) {
533
+ if (features[direction] === false)
534
+ return;
535
+ const stack = direction === "undo" ? undoStack : redoStack;
536
+ const entry = stack.at(-1);
537
+ if (!entry)
538
+ return;
539
+ const changes = direction === "undo" ? entry.changes.map((c) => ({ ...c, previousValue: c.nextValue, nextValue: c.previousValue })) : entry.changes;
540
+ if (await execute({ id: newOperationId(), reason: direction, changes, scheduleGuard: ganttHistoryGuard(entry.scheduleGuard, entry.changes, direction === "undo") })) {
541
+ if (direction === "undo") {
542
+ setUndoStack((s) => s.slice(0, -1));
543
+ setRedoStack((s) => [...s, entry]);
544
+ }
545
+ else {
546
+ setRedoStack((s) => s.slice(0, -1));
547
+ setUndoStack((s) => [...s, entry]);
548
+ }
549
+ }
550
+ }
551
+ function gridRange() {
552
+ if (!focus)
553
+ return null;
554
+ const row = desktopRows.findIndex((r) => getRowId(r) === focus.rowId);
555
+ const col = visible.findIndex((v) => v.field.id === focus.fieldId);
556
+ if (row < 0 || col < 0)
557
+ return null;
558
+ const anchorRow = anchor ? desktopRows.findIndex((r) => getRowId(r) === anchor.rowId) : row;
559
+ const anchorCol = anchor ? visible.findIndex((v) => v.field.id === anchor.fieldId) : col;
560
+ return { row, col, fromRow: Math.min(row, anchorRow < 0 ? row : anchorRow), toRow: Math.max(row, anchorRow), fromCol: Math.min(col, anchorCol < 0 ? col : anchorCol), toCol: Math.max(col, anchorCol) };
561
+ }
562
+ function inputTarget(target) { return target instanceof HTMLElement && (target.matches("input,textarea,select,a") || !!target.closest(".hy-dv-action-cell") || target.isContentEditable); }
563
+ function handleCopy(event) {
564
+ if (features.copy === false || inputTarget(event.target) || !event.target || !isGridTarget(event.target))
565
+ return;
566
+ const range = gridRange();
567
+ if (!range)
568
+ return;
569
+ const values = desktopRows.slice(range.fromRow, range.toRow + 1).map((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => !field.internal && !field.renderAction && canRead(row, field) ? formatClipboardValue(getFieldValue(row, field), field) : ""));
570
+ event.preventDefault();
571
+ event.clipboardData.setData("text/plain", encodeTSV(values));
572
+ setOperation({ state: "saved", message: `已複製 ${values.length} 列(未讀取無權限值)` });
573
+ }
574
+ function handlePaste(event) {
575
+ if (features.paste === false || inputTarget(event.target) || !isGridTarget(event.target) || !onBatchCommit || busy)
576
+ return;
577
+ const range = gridRange();
578
+ if (!range)
579
+ return;
580
+ event.preventDefault();
581
+ try {
582
+ reviewChanges(createPasteChanges(event.clipboardData.getData("text/plain"), desktopRows, visible.map((v) => v.field), getRowId, range.fromRow, range.fromCol), "paste");
583
+ }
584
+ catch (error) {
585
+ reportFailure(error);
586
+ }
587
+ }
588
+ function isGridTarget(target) { return target instanceof Element && !!target.closest("td[data-hy-cell]"); }
589
+ function handleKey(event) {
590
+ if (inputTarget(event.target) || !isGridTarget(event.target) || editing)
591
+ return;
592
+ const modifier = event.ctrlKey || event.metaKey;
593
+ if (modifier && (event.key.toLowerCase() === "z" || event.key.toLowerCase() === "y")) {
594
+ if (onBatchCommit) {
595
+ event.preventDefault();
596
+ void historyAction(event.shiftKey || event.key.toLowerCase() === "y" ? "redo" : "undo");
597
+ }
598
+ return;
599
+ }
600
+ const range = gridRange();
601
+ if (!range)
602
+ return;
603
+ if (event.key === "Escape") {
604
+ setAnchor(null);
605
+ setEditing(null);
606
+ return;
607
+ }
608
+ if (event.key === " " && modifier && selectable) {
609
+ event.preventDefault();
610
+ toggleRow(getRowId(desktopRows[range.row]));
611
+ return;
612
+ }
613
+ if (event.key === "Enter" || event.key === "F2" || (event.key === " " && !modifier && !event.shiftKey && getFieldTypeDefinition(visible[range.col].field.type).toggleValue)) {
614
+ event.preventDefault();
615
+ const row = desktopRows[range.row];
616
+ const field = visible[range.col].field;
617
+ const relationSelect = event.target.closest("td")?.querySelector(".hy-dv-relation-select");
618
+ if (relationSelect && !relationSelect.disabled) {
619
+ relationSelect.focus();
620
+ try {
621
+ relationSelect.showPicker?.();
622
+ }
623
+ catch { /* Focus still permits native keyboard selection. */ }
624
+ return;
625
+ }
626
+ const toggleValue = getFieldTypeDefinition(field.type).toggleValue;
627
+ if (toggleValue)
628
+ void commitCell(row, field, toggleValue(getFieldValue(row, field)));
629
+ else if (!canEdit(row, field))
630
+ openRow(row);
631
+ else if (field.Editor || ["person", "relation", "multiLineText", "multiSelect"].includes(field.type)) {
632
+ setDetailId(getRowId(row));
633
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" });
634
+ }
635
+ else if (canEdit(row, field))
636
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "desktop" });
637
+ return;
638
+ }
639
+ if ((event.key === "Delete" || event.key === "Backspace") && onBatchCommit && features.clear !== false) {
640
+ event.preventDefault();
641
+ reviewChanges(desktopRows.slice(range.fromRow, range.toRow + 1).flatMap((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => ({ row, rowId: getRowId(row), field, previousValue: getFieldValue(row, field), nextValue: field.type === "checkbox" ? false : null }))), "clear");
642
+ return;
643
+ }
644
+ let row = range.row;
645
+ let col = range.col;
646
+ if (event.key === "ArrowDown")
647
+ row++;
648
+ else if (event.key === "ArrowUp")
649
+ row--;
650
+ else if (event.key === "PageDown")
651
+ row += Math.max(1, Math.floor(viewportHeight / rowHeight));
652
+ else if (event.key === "PageUp")
653
+ row -= Math.max(1, Math.floor(viewportHeight / rowHeight));
654
+ else if (event.key === "ArrowLeft")
655
+ col--;
656
+ else if (event.key === "ArrowRight")
657
+ col++;
658
+ else if (event.key === "Home") {
659
+ col = 0;
660
+ if (modifier)
661
+ row = 0;
662
+ }
663
+ else if (event.key === "End") {
664
+ col = visible.length - 1;
665
+ if (modifier)
666
+ row = desktopRows.length - 1;
667
+ }
668
+ else if (event.key === "Tab") {
669
+ const next = row * visible.length + col + (event.shiftKey ? -1 : 1);
670
+ if (next < 0 || next >= desktopRows.length * visible.length)
671
+ return;
672
+ row = Math.floor(next / visible.length);
673
+ col = next % visible.length;
674
+ }
675
+ else
676
+ return;
677
+ event.preventDefault();
678
+ row = Math.max(0, Math.min(desktopRows.length - 1, row));
679
+ col = Math.max(0, Math.min(visible.length - 1, col));
680
+ const next = { rowId: getRowId(desktopRows[row]), fieldId: visible[col].field.id };
681
+ setAnchor(event.shiftKey && event.key !== "Tab" ? anchor ?? focus : null);
682
+ setFocus(next);
683
+ if (virtual) {
684
+ const index = flatEntries.findIndex((e) => e.kind === "row" && getRowId(e.row) === next.rowId);
685
+ if (index < virtualStart || index >= virtualEnd) {
686
+ if (scrollRef.current)
687
+ scrollRef.current.scrollTop = windowed.offsets[index];
688
+ setScrollTop(windowed.offsets[index]);
689
+ }
690
+ }
691
+ requestAnimationFrame(() => {
692
+ const cell = rootRef.current?.querySelector(`td[data-row-id="${CSS.escape(next.rowId)}"][data-field-id="${CSS.escape(next.fieldId)}"]`);
693
+ cell?.focus({ preventScroll: true });
694
+ cell?.scrollIntoView({ block: "nearest", inline: "nearest" });
695
+ });
696
+ }
697
+ if (access === "denied" || loading || error) {
698
+ const state = access === "denied" ? _jsx(StateCard, { title: "\u7121\u6B0A\u9650", message: "\u4F60\u6C92\u6709\u67E5\u770B\u9019\u4EFD\u8CC7\u6599\u7684\u6B0A\u9650\u3002", tone: "warning" }) : loading ? _jsx(LoadingState, {}) : _jsx(StateCard, { title: "\u8F09\u5165\u5931\u6557", message: error, tone: "error" });
699
+ return props.height === undefined ? state : _jsx("section", { className: "hy-dv", style: { height: props.height, overflow: "auto" }, "aria-label": title, children: state });
700
+ }
701
+ const range = gridRange();
702
+ const desktopRowIndices = new Map(desktopRows.map((row, index) => [getRowId(row), index]));
703
+ return _jsxs("section", { ref: rootRef, className: `hy-dv density-${activeView.density ?? "standard"} presentation-${props.presentation ?? "auto"} chrome-${props.chrome ?? "standard"} ${props.height !== undefined ? "has-total-height" : ""}`, style: { height: props.height, "--hy-row-height": `${rowHeight}px`, "--hy-header-height": `${metrics.headerHeight}px`, "--hy-group-height": `${metrics.groupHeight}px`, "--hy-cell-font": `${metrics.cellFontSize}px`, "--hy-cell-line": `${metrics.cellLineHeight}px`, "--hy-header-font": `${metrics.headerFontSize}px`, "--hy-cell-padding": `${metrics.cellPadding}px`, "--hy-header-left": `${metrics.headerPaddingLeft}px`, "--hy-header-right": `${metrics.headerPaddingRight}px`, "--hy-control-height": `${metrics.controlHeight}px`, "--hy-action-height": `${metrics.actionHeight}px`, "--hy-badge-height": `${metrics.badgeHeight}px`, "--hy-badge-font": `${metrics.badgeFontSize}px`, "--hy-badge-padding": `${metrics.badgePadding}px`, "--hy-viewport-height": typeof props.viewportHeight === "number" ? `${props.viewportHeight}px` : props.viewportHeight ?? "560px", "--hy-scrollbar-height": `${metrics.scrollbarHeight}px`, "--hy-scrollbar-thumb": `${metrics.scrollbarThumb}px` }, "aria-label": title, onKeyDown: handleKey, onCopy: handleCopy, onPaste: handlePaste, children: [_jsxs("div", { className: "hy-dv-heading", children: [_jsxs("header", { className: "hy-dv-titlebar", children: [props.headerStart, _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("span", { children: props.remote ? `符合 ${remoteStale ? "…" : props.remote.total ?? "…"} 筆,已載入 ${processedRows.length} 筆${props.remote.complete && !remoteStale ? "(完整)" : "(尚未完整)"}` : `${processedRows.length} / ${rows.length} 筆` })] }), _jsxs("div", { className: "hy-dv-title-status", children: [selected.length > 0 && _jsxs("span", { children: ["\u5DF2\u9078 ", selected.length, " \u7B46"] }), !props.hideViewSaveStatus && _jsx(SaveBadge, { state: viewSaveState, prefix: "\u6AA2\u8996" }), !props.hideViewSaveStatus && (viewSaveState === "failed" || viewSaveState === "conflict") && _jsx("button", { onClick: () => setView(viewRef.current), children: "\u91CD\u8A66\u5132\u5B58" })] }), props.headerEnd] }), features.viewTabs !== false && _jsxs("nav", { className: "hy-dv-view-tabs", "aria-label": "\u8CC7\u6599\u8996\u5716", children: [[["table", "資料表格"], ["gantt", "甘特圖"], ["calendar", "行事曆"]].map(([type, label]) => _jsx("button", { "aria-pressed": activeView.viewType === type, disabled: permissions?.canSwitchView === false, onClick: () => { patchView({ viewType: type }, configurable ? "layout" : "navigation"); setEditing(null); setPanel(null); }, children: label }, type)), configurable && features.viewSettings !== false && _jsx("button", { onClick: () => setPanel(panel === "settings" ? null : "settings"), children: activeView.viewType === "gantt" ? "甘特圖設定" : activeView.viewType === "calendar" ? "行事曆設定" : "視圖設定" })] })] }), _jsxs(DataViewToolbar, { compact: props.chrome === "compact", start: props.toolbarStart, end: props.toolbarEnd, secondary: _jsxs(_Fragment, { children: [configurable && features.columns !== false && _jsx(ToolbarButton, { active: panel === "columns", onClick: () => setPanel(panel === "columns" ? null : "columns"), children: "\u96FB\u8166\u8868\u683C\u6B04\u4F4D" }), configurable && features.colors !== false && _jsx(ToolbarButton, { active: panel === "colors", onClick: () => setPanel(panel === "colors" ? null : "colors"), children: "\u586B\u8272" }), canManageFields && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B \u65B0\u589E\u6B04\u4F4D" }), _jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("fields"); }, children: "\u7BA1\u7406\u6B04\u4F4D" })] }), onBatchCommit && _jsxs(_Fragment, { children: [features.batch !== false && _jsxs("button", { disabled: !selectedRows.length || busy, onClick: () => { setPanel(null); setDialog("batch"); }, children: ["\u6279\u6B21\u4FEE\u6539 ", selectedRows.length || ""] }), features.undo !== false && _jsx("button", { disabled: !undoStack.length || busy, onClick: () => void historyAction("undo"), children: "\u5FA9\u539F" }), features.redo !== false && _jsx("button", { disabled: !redoStack.length || busy, onClick: () => void historyAction("redo"), children: "\u91CD\u505A" })] }), selected.length > 0 && _jsx(ToolbarButton, { onClick: () => changeSelection([]), children: "\u6E05\u9664\u9078\u53D6" }), activeView.groups.length > 0 && permissions?.canCollapseGroups !== false && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => setAllGroups(true), children: "\u5168\u90E8\u6536\u5408" }), _jsx(ToolbarButton, { onClick: () => setAllGroups(false), children: "\u5168\u90E8\u5C55\u958B" })] })] }), children: [features.search !== false && permissions?.canSearch !== false && _jsxs("label", { className: "hy-dv-search", children: [_jsx("span", { "aria-hidden": true, children: "\u2315" }), _jsx("input", { "aria-label": searchPlaceholder, value: search, onChange: (event) => patchLocation({ search: event.target.value, tableTop: 0, ganttTop: 0 }), placeholder: searchPlaceholder })] }), canFilter && features.filter !== false && _jsxs(ToolbarButton, { active: panel === "filter" || activeView.filters.rules.length > 0, onClick: () => setPanel(panel === "filter" ? null : "filter"), children: ["\u7BE9\u9078 ", activeView.filters.rules.length || ""] }), canSort && features.sort !== false && _jsxs(ToolbarButton, { active: panel === "sort" || activeView.sorts.length > 0, onClick: () => setPanel(panel === "sort" ? null : "sort"), children: ["\u6392\u5E8F ", activeView.sorts.length || ""] }), configurable && features.group !== false && _jsxs(ToolbarButton, { active: panel === "group" || activeView.groups.length > 0, onClick: () => setPanel(panel === "group" ? null : "group"), children: ["\u5206\u7D44 ", activeView.groups.length || ""] }), props.onPresentationChange && _jsxs("select", { "aria-label": "\u5448\u73FE\u65B9\u5F0F", value: props.presentation ?? "auto", onChange: (e) => props.onPresentationChange?.(e.target.value), children: [_jsx("option", { value: "auto", children: "\u81EA\u52D5" }), _jsx("option", { value: "table", children: "\u8868\u683C" }), _jsx("option", { value: "cards", children: "\u5361\u7247" })] })] }), move.message && _jsx("p", { role: "status", children: move.message }), operation.state !== "idle" && _jsxs("div", { className: `hy-dv-operation is-${operation.state}`, role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: [_jsx(SaveBadge, { state: operation.state }), _jsx("span", { children: operation.message })] }), panel && (configurable || panel === "filter" && canFilter || panel === "sort" && canSort) && _jsxs("div", { className: "hy-dv-panel-wrap", children: [panel === "filter" && _jsx(FilterPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "sort" && _jsx(SortPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "group" && _jsx(GroupPanel, { fields: fields, view: activeView, onChange: setView, onClose: () => setPanel(null) }), panel === "columns" && _jsx(ColumnPanel, { fields: availableFields, view: { ...activeView, columns: activeView.columns.filter((c) => availableFields.some((f) => f.id === c.fieldId)) }, onChange: setView, onClose: () => setPanel(null), dragged: draggedColumn, setDragged: setDraggedColumn, drop: dropColumn }), panel === "settings" && _jsx(PanelShell, { title: "\u8996\u5716\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(ViewSettings, { fields: availableFields, view: activeView, onChange: setView }) }), panel === "colors" && _jsx(PanelShell, { title: "\u586B\u8272\u898F\u5247", onClose: () => setPanel(null), children: _jsx(ColorSettings, { fields: availableFields, view: activeView, onChange: setView }) })] }), _jsx("div", { className: "hy-dv-body", children: remoteStale || props.remote?.loading && !processedRows.length ? _jsx(LoadingState, {}) : processedRows.length === 0 && props.remote?.error ? _jsx(StateCard, { title: "\u67E5\u8A62\u5931\u6557", message: props.remote.error, tone: "error" }) : processedRows.length === 0 ? _jsx(StateCard, { title: "\u6C92\u6709\u7B26\u5408\u689D\u4EF6\u7684\u8CC7\u6599", message: rows.length === 0 ? emptyMessage : "請調整搜尋或篩選條件。" }) : activeView.viewType !== "table" ? _jsx(ScheduleView, { metrics: metrics, getRowLabel: readableLabel, groupLabel: groupLabel, onLoadNearEnd: loadNearEnd, allRows: rows, dependencies: ganttDependencies, onDependencyChange: onGanttDependencyChange && permissions?.canManageDependencies === true ? changeDependency : undefined, onScheduleCommit: onBatchCommit ? execute : undefined, rows: processedRows, renderMobileRow: (row) => _jsx("dl", { className: "hy-dv-schedule-fields", children: mobileFields.filter((field) => canRead(row, field)).map((field) => _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { children: renderCell(row, field, "mobile") })] }, field.id)) }), onLocationChange: patchLocation, onToggleGroup: toggleGroup, renderLeftHeader: () => _jsxs("tr", { children: [_jsx("th", { className: "hy-dv-open-header is-pinned", style: { left: 0 }, children: "\u8A73\u60C5" }), visible.map((v) => renderColumnHeader(v.field, v.column, true))] }), renderLeftRow: (row) => _jsxs("tr", { children: [_jsx("td", { className: "is-pinned", style: { left: 0 }, children: _jsx("button", { "aria-label": `開啟 ${getRowId(row)} 詳情`, onClick: () => openRow(row), children: "\u2197" }) }), visible.map(({ field }) => _jsx("td", { className: ganttLeftOffsets.has(field.id) || ganttRightOffsets.has(field.id) ? "is-pinned" : "", style: { left: ganttLeftOffsets.get(field.id), right: ganttRightOffsets.get(field.id), backgroundColor: colorMap.get(getRowId(row))?.get(field.id) ?? colorMap.get(getRowId(row))?.get("row") }, children: _jsx("div", { className: "hy-dv-gantt-cell", children: renderCell(row, field, "gantt") }) }, field.id))] }, getRowId(row)), fields: fields, getRowId: getRowId, view: activeView, permissions: permissions, busy: busy, onCommit: onBatchCommit ? (changes) => execute({ id: newOperationId(), reason: "edit", changes }) : undefined, onOpen: openRow }, activeView.viewType) : _jsxs(_Fragment, { children: [_jsx("div", { className: "hy-dv-desktop", children: _jsx("div", { ref: scrollRef, className: `hy-dv-table-scroll ${virtual ? "is-virtual" : ""}`, onScroll: (event) => { setScrollTop(event.currentTarget.scrollTop); loadNearEnd(event.currentTarget); if (bottomRef.current)
704
+ bottomRef.current.scrollLeft = event.currentTarget.scrollLeft; patchLocation({ tableTop: event.currentTarget.scrollTop, tableLeft: event.currentTarget.scrollLeft }); }, children: _jsxs("table", { className: "hy-dv-table", style: { width: tableWidth }, "aria-label": "\u8CC7\u6599\u5132\u5B58\u683C", "aria-rowcount": desktopRows.length + 1, children: [_jsxs("colgroup", { children: [controlColumn && _jsx("col", { style: { width: controlWidth } }), visible.map(({ field, column }) => _jsx("col", { style: { width: column.width } }, field.id)), canManageFields && _jsx("col", { style: { width: addColumnWidth } })] }), _jsx("thead", { children: _jsxs("tr", { children: [controlColumn && _jsx("th", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: selectable && _jsx("input", { type: "checkbox", checked: allPageSelected, onChange: togglePageSelection, "aria-label": props.remote && !props.remote.complete ? "選取目前已載入資料" : "選取全部符合條件資料" }) }), visible.map(({ field, column }) => renderColumnHeader(field, column)), canManageFields && _jsx("th", { className: "hy-dv-add-column is-pinned", style: { right: 0 }, children: _jsx("button", { type: "button", disabled: busy, "aria-label": "\u65B0\u589E\u6B04\u4F4D", title: "\u65B0\u589E\u6B04\u4F4D", onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B" }) })] }) }), _jsxs("tbody", { children: [virtual && virtualStart > 0 && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.before, padding: 0, border: 0 } }) }), flatEntries.slice(virtualStart, virtualEnd).map((entry) => entry.kind === "row" ? renderDesktopRow(entry.row) : _jsx("tr", { "data-group-key": entry.node.key, className: `hy-dv-group depth-${entry.node.depth} ${move.target?.position === "groupEnd" && move.target.targetGroupPath.at(-1)?.key === entry.node.key ? "is-drop-after" : ""}`, style: { height: entryHeights[flatEntries.indexOf(entry)] }, children: _jsx("td", { colSpan: tableColumnCount, children: _jsxs("button", { type: "button", onClick: () => toggleGroup(entry.node.key), "aria-expanded": !collapsed.has(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) }) }, `group:${entry.node.key}`)), virtual && virtualEnd < flatEntries.length && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.after, padding: 0, border: 0 } }) })] })] }) }) }), _jsx(VirtualList, { items: flatEntries, itemKey: (e) => e.kind === "group" ? `g:${e.node.key}` : getRowId(e.row), estimate: (e) => e.kind === "group" ? 40 : activeView.mobile?.presentation === "compact" ? 80 + mobileFields.length * 28 : 100 + mobileFields.length * 36, className: `hy-dv-mobile mobile-${activeView.mobile?.presentation ?? "cards"}`, initialTop: activeView.location?.mobileTop, onScroll: (el) => { loadNearEnd(el); patchLocation({ mobileTop: el.scrollTop }); }, renderItem: (entry) => entry.kind === "group" ? _jsxs("button", { "data-group-key": entry.node.key, className: "hy-dv-mobile-group-title", "aria-expanded": !collapsed.has(entry.node.key), onClick: () => toggleGroup(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) : renderMobileCard(entry.row) }), _jsxs("p", { className: "hy-dv-keyboard-help", children: ["\u65B9\u5411\u9375\u79FB\u52D5 \u00B7 Shift\uFF0B\u65B9\u5411\u9375\u9078\u7BC4\u570D \u00B7 Enter \u7DE8\u8F2F \u00B7 Ctrl/Cmd\uFF0BC\uFF0FV \u8907\u88FD\u8CBC\u4E0A \u00B7 Delete \u6E05\u7A7A\u9810\u89BD", virtual ? ` · 連續捲動,共 ${desktopRows.length} 筆` : ""] }), props.bottomScrollbar !== false && tableWidth > viewportWidth && _jsx("div", { ref: bottomRef, className: "hy-dv-bottom-scroll", "aria-label": "\u8868\u683C\u6C34\u5E73\u6372\u52D5", onScroll: (e) => { if (scrollRef.current)
705
+ scrollRef.current.scrollLeft = e.currentTarget.scrollLeft; }, children: _jsx("div", { style: { width: tableWidth, height: 1 } }) })] }) }), props.remote && _jsxs("div", { className: "hy-dv-load-status", role: props.remote.error ? "alert" : "status", children: [props.remote.error || (props.remote.loading || remoteStale ? "正在載入符合條件的資料…" : props.remote.complete ? `已完整載入 ${processedRows.length} 筆` : `尚未完整:已載入 ${processedRows.length}/${props.remote.total ?? "…"} 筆`), !props.remote.complete && !props.remote.loading && !remoteStale && _jsx("button", { onClick: props.remote.error ? props.remote.onRetry : props.remote.onLoadMore, children: props.remote.error ? "重新查詢" : "繼續載入" })] }), quick && (() => { const row = rows.find((r) => getRowId(r) === quick.rowId); const field = fields.find((f) => f.id === quick.fieldId); return row && field && !field.internal && canRead(row, field) && _jsxs(DataViewDialog, { title: field.label, onClose: () => setQuick(null), children: [field.renderQuickView?.({ row, field, value: getFieldValue(row, field), close: () => setQuick(null), openRow: () => { setQuick(null); openRow(row); } }), canEdit(row, field) && _jsx("button", { onClick: () => { setQuick(null); setDetailId(getRowId(row)); setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" }); }, children: "\u7DE8\u8F2F" })] }); })(), detailRow && _jsxs(DataViewDialog, { title: readableLabel(detailRow), busy: busy, onClose: () => { setDetailId(null); setEditing(null); }, children: [props.renderDetail ? props.renderDetail(detailContext(detailRow)) : _jsx("div", { className: "hy-dv-detail", children: availableFields.filter((field) => canRead(detailRow, field)).map((field) => _jsxs("label", { children: [_jsx("span", { children: field.label }), renderCell(detailRow, field, "detail")] }, field.id)) }), features.copy !== false && _jsx("button", { onClick: async () => { try {
706
+ await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
707
+ setOperation({ state: "saved", message: "已複製可讀內容" });
708
+ }
709
+ catch {
710
+ reportFailure(new Error("無法使用剪貼簿,請確認瀏覽器權限"));
711
+ } }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }), dialog && _jsxs(DataViewDialog, { title: dialog === "add-field" ? "新增欄位" : dialog === "fields" ? "管理欄位" : dialog === "batch" ? "批次修改" : "確認資料修改", busy: busy, onClose: () => { setDialog(null); setPending(null); }, children: [dialog === "add-field" && _jsx(FieldCreator, { fields: fields, permissions: permissions, onApply: async (change) => { const saved = await applyFieldChange(change); if (saved)
712
+ setDialog(null); return saved; }, busy: busy, onCancel: () => setDialog(null) }), dialog === "fields" && _jsx(FieldManager, { fields: availableFields, permissions: permissions, onApply: applyFieldChange, busy: busy }), dialog === "batch" && _jsx(BatchEditor, { fields: availableFields, rows: selectedRows, getRowId: getRowId, permissions: permissions, onReview: reviewChanges }), dialog === "review" && pending && _jsxs("div", { className: "hy-dv-form", children: [_jsxs("p", { children: ["\u5171 ", pending.changes.length, " \u683C\u3002\u6240\u6709\u6B04\u4F4D\u9A57\u8B49\u901A\u904E\u5F8C\uFF0C\u7531\u5C08\u6848\u4E00\u6B21\u5BEB\u5165\uFF1B\u5931\u6557\u6216\u885D\u7A81\u6642\u4FDD\u7559\u539F\u8CC7\u6599\u3002"] }), _jsx("div", { className: "hy-dv-review-list", children: pending.changes.slice(0, 30).map((change) => _jsxs("div", { children: [_jsxs("strong", { children: [change.rowId, "\uFF0F", change.field.label] }), _jsxs("span", { children: [formatClipboardValue(change.previousValue, change.field) || "空白", " \u2192 ", formatClipboardValue(change.nextValue, change.field) || "空白"] })] }, `${change.rowId}:${change.field.id}`)) }), pending.changes.length > 30 && _jsxs("p", { children: ["\u9810\u89BD\u524D 30 \u683C\uFF0C\u5168\u90E8 ", pending.changes.length, " \u683C\u90FD\u6703\u9A57\u8B49\u3002"] }), _jsxs("button", { disabled: busy, onClick: async () => { if (await execute(pending)) {
713
+ setDialog(null);
714
+ setPending(null);
715
+ } }, children: ["\u78BA\u8A8D\u5957\u7528 ", pending.changes.length, " \u683C"] })] }), operation.state !== "idle" && _jsx("p", { role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: operation.message })] })] });
716
+ function renderDesktopRow(row) {
717
+ const rowId = getRowId(row);
718
+ const index = desktopRowIndices.get(rowId) ?? 0;
719
+ const colors = colorMap.get(rowId);
720
+ return _jsxs("tr", { "data-move-row": rowId, "aria-rowindex": index + 2, className: `${selectedSet.has(rowId) ? "is-selected" : ""} ${move.sourceId === rowId ? "is-moving" : ""} ${move.target?.targetRowId === rowId ? `is-drop-${move.target.position}` : ""}`, style: { height: rowHeight }, children: [controlColumn && _jsx("td", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: _jsxs("div", { className: "hy-dv-row-controls", children: [props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), selectable && _jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId), "aria-label": `選取資料 ${readableLabel(row)}` }), features.rowOpen === true && _jsx("button", { "aria-label": `開啟 ${readableLabel(row)}`, onClick: () => openRow(row), children: "\u2197" })] }) }), visible.map(({ field }, col) => {
721
+ const left = pinnedOffsets.get(field.id);
722
+ const right = rightOffsets.get(field.id);
723
+ const inRange = range && index >= range.fromRow && index <= range.toRow && col >= range.fromCol && col <= range.toCol;
724
+ return _jsx("td", { "data-hy-cell": true, "data-row-id": rowId, "data-field-id": field.id, tabIndex: range && focus ? focus.rowId === rowId && focus.fieldId === field.id ? 0 : -1 : index === 0 && col === 0 ? 0 : -1, "aria-label": `${rowId} ${field.label}`, onFocus: () => setFocus({ rowId, fieldId: field.id }), onClick: (event) => { if (event.shiftKey) {
725
+ setAnchor(anchor ?? focus);
726
+ setFocus({ rowId, fieldId: field.id });
727
+ } }, className: `${left === undefined && right === undefined ? "" : "is-pinned"} ${inRange ? "is-range" : ""}`, style: { left, right, backgroundColor: colors?.get(field.id) ?? colors?.get("row") }, children: _jsx("div", { className: "hy-dv-cell-content", children: renderCell(row, field, "desktop") }) }, field.id);
728
+ }), canManageFields && _jsx("td", { className: "hy-dv-add-column is-pinned", style: { right: 0 } })] }, rowId);
729
+ }
730
+ function renderColumnHeader(field, column, gantt = false) {
731
+ const left = (gantt ? ganttLeftOffsets : pinnedOffsets).get(field.id);
732
+ const right = (gantt ? ganttRightOffsets : rightOffsets).get(field.id);
733
+ return _jsxs("th", { draggable: configurable, onDragStart: () => setDraggedColumn(field.id), onDragOver: (event) => event.preventDefault(), onDrop: () => dropColumn(field.id), className: left === undefined && right === undefined ? "" : "is-pinned", style: { left, right }, children: [_jsx("span", { children: field.label }), activeView.sorts.findIndex((rule) => rule.fieldId === field.id) >= 0 && _jsxs("small", { children: [activeView.sorts.findIndex((rule) => rule.fieldId === field.id) + 1, activeView.sorts.find((rule) => rule.fieldId === field.id)?.direction === "asc" ? "↑" : "↓"] }), configurable && _jsx("span", { className: "hy-dv-resize", onPointerDown: (event) => resizeColumn(field.id, column.width, event), "aria-hidden": true })] }, field.id);
734
+ }
735
+ function renderMobileCard(row) {
736
+ const rowId = getRowId(row);
737
+ return _jsxs("article", { "data-move-row": rowId, className: `hy-dv-card ${selected.includes(rowId) ? "is-selected" : ""}`, style: { backgroundColor: colorMap.get(rowId)?.get("row") }, children: [selectable && _jsxs("label", { className: "hy-dv-card-select", children: [_jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId) }), " \u9078\u53D6"] }), activeView.mobile?.summaryFieldId && _jsx("button", { className: "hy-dv-mobile-summary", onClick: () => openRow(row), children: readableLabel(row) }), props.renderMobileRow ? props.renderMobileRow(detailContext(row)) : _jsx("dl", { children: mobileFields.map((field) => canRead(row, field) && _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { style: { backgroundColor: colorMap.get(rowId)?.get(field.id) }, children: renderCell(row, field, "mobile") })] }, field.id)) }), props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), _jsx("button", { className: "hy-dv-detail-button", onClick: () => openRow(row), children: "\u67E5\u770B\u8A73\u60C5" })] }, rowId);
738
+ }
739
+ }
740
+ function ToolbarButton({ children, active = false, onClick }) {
741
+ const label = Children.toArray(children).filter((v) => typeof v === "string" || typeof v === "number").join("").trim();
742
+ const icon = label.startsWith("篩選") ? "▽" : label.startsWith("排序") ? "↕" : label.startsWith("分組") ? "▤" : null;
743
+ const count = label.match(/\d+$/)?.[0];
744
+ return _jsxs("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, className: `${active ? "is-active" : ""} ${icon ? "hy-dv-tool" : ""}`, onClick: onClick, children: [icon && _jsxs("span", { className: "hy-dv-tool-icon", "aria-hidden": true, children: [icon, count && _jsx("small", { children: count })] }), _jsx("span", { className: "hy-dv-tool-label", children: children })] });
745
+ }
746
+ function PanelShell({ title, children, onClose }) {
747
+ return _jsxs("aside", { className: "hy-dv-panel", children: [_jsxs("header", { children: [_jsx("h3", { children: title }), _jsx("button", { type: "button", onClick: onClose, "aria-label": `關閉${title}`, children: "\u00D7" })] }), children] });
748
+ }
749
+ function SortPanel({ fields, view, onChange, onClose }) {
750
+ function update(index, patch) {
751
+ onChange({ ...view, sorts: view.sorts.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule) });
752
+ }
753
+ return _jsxs(PanelShell, { title: "\u591A\u6B04\u6392\u5E8F", onClose: onClose, children: [_jsx("p", { children: "\u7531\u4E0A\u5230\u4E0B\u5957\u7528\uFF0C\u7B2C\u4E00\u5217\u512A\u5148\u3002" }), _jsxs("div", { className: "hy-dv-rules", children: [view.sorts.map((rule, index) => _jsxs("div", { className: "hy-dv-rule", children: [_jsx("span", { children: index + 1 }), _jsx("select", { value: rule.fieldId, onChange: (event) => update(index, { fieldId: event.target.value }), children: fields.map((field) => _jsx("option", { value: field.id, disabled: view.sorts.some((item, i) => i !== index && item.fieldId === field.id), children: field.label }, field.id)) }), _jsxs("select", { value: rule.direction, onChange: (event) => update(index, { direction: event.target.value }), children: [_jsx("option", { value: "asc", children: "\u5C0F \u2192 \u5927" }), _jsx("option", { value: "desc", children: "\u5927 \u2192 \u5C0F" })] }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, sorts: view.sorts.filter((_, i) => i !== index) }), children: "\u79FB\u9664" })] }, `${rule.fieldId}-${index}`)), _jsx("button", { type: "button", className: "hy-dv-add", disabled: view.sorts.length >= fields.length, onClick: () => {
754
+ const field = fields.find((item) => !view.sorts.some((rule) => rule.fieldId === item.id));
755
+ if (field)
756
+ onChange({ ...view, sorts: [...view.sorts, { fieldId: field.id, direction: "asc" }] });
757
+ }, children: "\uFF0B \u589E\u52A0\u6392\u5E8F" })] })] });
758
+ }
759
+ function FilterPanel({ fields, view, onChange, onClose }) {
760
+ function update(index, patch) {
761
+ onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.map((rule, i) => i === index ? { ...rule, ...patch } : rule) } });
762
+ }
763
+ return _jsxs(PanelShell, { title: "\u7BE9\u9078\u689D\u4EF6", onClose: onClose, children: [_jsxs("div", { className: "hy-dv-conjunction", children: [_jsx("span", { children: "\u689D\u4EF6\u95DC\u4FC2" }), _jsxs("select", { value: view.filters.conjunction, onChange: (event) => onChange({ ...view, filters: { ...view.filters, conjunction: event.target.value } }), children: [_jsx("option", { value: "and", children: "\u5168\u90E8\u7B26\u5408\uFF08AND\uFF09" }), _jsx("option", { value: "or", children: "\u4EFB\u4E00\u7B26\u5408\uFF08OR\uFF09" })] })] }), _jsxs("div", { className: "hy-dv-rules", children: [view.filters.rules.map((rule, index) => {
764
+ const field = fields.find((item) => item.id === rule.fieldId) ?? fields[0];
765
+ const operators = getFieldTypeDefinition(field.type).operators;
766
+ return _jsxs("div", { className: "hy-dv-rule hy-dv-filter-rule", children: [_jsx("select", { value: rule.fieldId, onChange: (event) => {
767
+ const nextField = fields.find((item) => item.id === event.target.value);
768
+ update(index, { fieldId: nextField.id, operator: getFieldTypeDefinition(nextField.type).operators[0], value: undefined });
769
+ }, children: fields.map((item) => _jsx("option", { value: item.id, children: item.label }, item.id)) }), _jsx("select", { value: rule.operator, onChange: (event) => update(index, { operator: event.target.value }), children: operators.map((operator) => _jsx("option", { value: operator, children: FILTER_OPERATOR_LABELS[operator] }, operator)) }), _jsx(FilterValueInput, { field: field, rule: rule, onChange: (value) => update(index, { value }) }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.filter((_, i) => i !== index) } }), children: "\u79FB\u9664" })] }, rule.id);
770
+ }), _jsx("button", { type: "button", className: "hy-dv-add", disabled: fields.length === 0 || view.filters.rules.length >= 20, onClick: () => {
771
+ const field = fields[0];
772
+ if (!field)
773
+ return;
774
+ onChange({ ...view, filters: { ...view.filters, rules: [...view.filters.rules, { id: createRuleId(), fieldId: field.id, operator: getFieldTypeDefinition(field.type).operators[0] }] } });
775
+ }, children: "\uFF0B \u589E\u52A0\u689D\u4EF6" })] })] });
776
+ }
777
+ function FilterValueInput({ field, rule, onChange }) {
778
+ if (["isEmpty", "isNotEmpty", "isTrue", "isFalse"].includes(rule.operator))
779
+ return _jsx("span", { className: "hy-dv-no-value", children: "\u4E0D\u9700\u586B\u503C" });
780
+ if (field.options?.length)
781
+ return _jsxs("select", { value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value), children: [_jsx("option", { value: "", children: "\u8ACB\u9078\u64C7" }), field.options.map((option) => _jsx("option", { value: option.value, children: option.label }, option.value))] });
782
+ if (["number", "currency", "percentage", "autoNumber"].includes(field.type))
783
+ return _jsx("input", { type: "number", value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value === "" ? undefined : Number(event.target.value)) });
784
+ if (["date", "dateTime", "createdAt", "updatedAt"].includes(field.type))
785
+ return _jsx("input", { type: "date", value: String(rule.value ?? "").slice(0, 10), onChange: (event) => onChange(event.target.value) });
786
+ return _jsx("input", { value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value), placeholder: "\u8F38\u5165\u689D\u4EF6" });
787
+ }
788
+ function GroupPanel({ fields, view, onChange, onClose }) {
789
+ return _jsxs(PanelShell, { title: "\u5206\u7D44\u8A2D\u5B9A", onClose: onClose, children: [_jsx("p", { children: "\u6700\u591A\u4E09\u5C64\uFF1B\u7531\u4E0A\u5230\u4E0B\u5957\u7528\u3002" }), _jsxs("div", { className: "hy-dv-rules", children: [view.groups.map((rule, index) => _jsxs("div", { className: "hy-dv-rule", children: [_jsx("span", { children: index + 1 }), _jsx("select", { value: rule.fieldId, onChange: (event) => onChange({ ...view, groups: view.groups.map((item, i) => i === index ? { fieldId: event.target.value } : item) }), children: fields.map((field) => _jsx("option", { value: field.id, disabled: view.groups.some((item, i) => i !== index && item.fieldId === field.id), children: field.label }, field.id)) }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, groups: view.groups.filter((_, i) => i !== index) }), children: "\u79FB\u9664" })] }, `${rule.fieldId}-${index}`)), _jsx("button", { type: "button", className: "hy-dv-add", disabled: view.groups.length >= Math.min(MAX_GROUP_LEVELS, fields.length), onClick: () => {
790
+ const field = fields.find((item) => !view.groups.some((rule) => rule.fieldId === item.id));
791
+ if (field)
792
+ onChange({ ...view, groups: [...view.groups, { fieldId: field.id }] });
793
+ }, children: "\uFF0B \u589E\u52A0\u5206\u7D44" })] })] });
794
+ }
795
+ function ColumnPanel({ fields, view, onChange, onClose, dragged, setDragged, drop }) {
796
+ const definitions = new Map(fields.map((field) => [field.id, field]));
797
+ const visibleCount = view.columns.filter((column) => column.visible).length;
798
+ function update(fieldId, patch) { onChange({ ...view, columns: view.columns.map((column) => column.fieldId === fieldId ? { ...column, ...patch } : column) }); }
799
+ function move(fieldId, direction) {
800
+ const columns = moveColumnSetting(view.columns, fieldId, direction);
801
+ if (columns !== view.columns)
802
+ onChange({ ...view, columns });
803
+ }
804
+ return _jsxs(PanelShell, { title: "\u96FB\u8166\u8868\u683C\u6B04\u4F4D", onClose: onClose, children: [_jsx("p", { children: "\u8ABF\u6574\u672C\u8996\u5716\u96FB\u8166\u8868\u683C\u8207\u7518\u7279\u5DE6\u8868\u7684\u6B04\u4F4D\uFF1B\u4E0D\u6703\u8986\u84CB\u624B\u6A5F\u986F\u793A\u6B04\u4F4D\u3002\u53EF\u62D6\u66F3\u6216\u7528\u6309\u9215\u6392\u5E8F\uFF1B\u56FA\u5B9A\u6B04\u4F4D\u7DAD\u6301\u5DE6\u53F3\u5340\u57DF\uFF0C\u79FB\u52D5\u9650\u540C\u4E00\u56FA\u5B9A\u5340\u57DF\u3002\u5BEC\u5EA6 56\u2013600px\u3002" }), _jsx("div", { className: "hy-dv-column-list", children: view.columns.map((column) => {
805
+ const label = definitions.get(column.fieldId)?.label ?? "未命名欄位";
806
+ const peers = columnMovePeers(view.columns, column.fieldId);
807
+ const index = peers.findIndex((peer) => peer.fieldId === column.fieldId);
808
+ return _jsxs("div", { draggable: true, onDragStart: () => setDragged(column.fieldId), onDragEnd: () => setDragged(null), onDragOver: (event) => event.preventDefault(), onDrop: () => drop(column.fieldId), className: dragged === column.fieldId ? "is-dragging" : "", children: [_jsx("span", { className: "hy-dv-drag", "aria-hidden": true, children: "\u283F" }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: column.visible, disabled: column.visible && visibleCount === 1, onChange: (event) => update(column.fieldId, { visible: event.target.checked }) }), _jsx("span", { children: label })] }), _jsxs("select", { "aria-label": `${label}固定位置`, value: column.pinned ?? "none", onChange: (event) => update(column.fieldId, { pinned: event.target.value === "none" ? null : event.target.value }), children: [_jsx("option", { value: "none", children: "\u4E0D\u56FA\u5B9A" }), _jsx("option", { value: "left", children: "\u56FA\u5B9A\u5DE6\u5074" }), _jsx("option", { value: "right", children: "\u56FA\u5B9A\u53F3\u5074" })] }), _jsx("input", { "aria-label": `${label}欄寬`, type: "number", min: 56, max: 600, value: column.width, onChange: (event) => update(column.fieldId, { width: normalizeColumnWidth(Number(event.target.value)) }) }), _jsxs("div", { className: "hy-dv-field-order-actions", children: [_jsx("button", { type: "button", title: "\u79FB\u5230\u6700\u4E0A", "aria-label": `移到最上 ${label}`, disabled: index <= 0, onClick: () => move(column.fieldId, "first"), children: "\u21C8" }), _jsx("button", { type: "button", title: "\u4E0A\u79FB", "aria-label": `上移 ${label}`, disabled: index <= 0, onClick: () => move(column.fieldId, "previous"), children: "\u2191" }), _jsx("button", { type: "button", title: "\u4E0B\u79FB", "aria-label": `下移 ${label}`, disabled: index < 0 || index === peers.length - 1, onClick: () => move(column.fieldId, "next"), children: "\u2193" }), _jsx("button", { type: "button", title: "\u79FB\u5230\u6700\u4E0B", "aria-label": `移到最下 ${label}`, disabled: index < 0 || index === peers.length - 1, onClick: () => move(column.fieldId, "last"), children: "\u21CA" })] })] }, column.fieldId);
809
+ }) })] });
810
+ }
811
+ function SaveBadge({ state, prefix, message }) {
812
+ if (state === "idle")
813
+ return null;
814
+ const labels = { saving: "儲存中…", saved: "已儲存", failed: "儲存失敗", conflict: "版本衝突", readonly: "唯讀" };
815
+ return _jsxs("small", { className: `hy-dv-save is-${state}`, role: "status", title: message, children: [prefix ? `${prefix}:` : "", labels[state]] });
816
+ }
817
+ function StateCard({ title, message, tone = "normal" }) {
818
+ return _jsxs("div", { className: `hy-dv-state is-${tone}`, role: tone === "error" ? "alert" : "status", children: [_jsx("strong", { children: title }), _jsx("p", { children: message })] });
819
+ }
820
+ function LoadingState() {
821
+ return _jsxs("div", { className: "hy-dv-loading", "aria-label": "\u8F09\u5165\u4E2D", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("span", {})] });
822
+ }
823
+ function collectGroupKeys(nodes) {
824
+ return nodes.flatMap((node) => node.fieldId ? [node.key, ...collectGroupKeys(node.children)] : collectGroupKeys(node.children));
825
+ }
826
+ function createRuleId() {
827
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `filter-${Date.now()}-${Math.random().toString(36).slice(2)}`;
828
+ }
829
+ function flattenGroups(nodes, collapsed) {
830
+ const result = [];
831
+ for (const node of nodes) {
832
+ if (node.fieldId)
833
+ result.push({ kind: "group", node });
834
+ if (collapsed.has(node.key))
835
+ continue;
836
+ if (node.children.length)
837
+ result.push(...flattenGroups(node.children, collapsed));
838
+ else
839
+ for (const row of node.rows)
840
+ result.push({ kind: "row", row });
841
+ }
842
+ return result;
843
+ }
844
+ //# sourceMappingURL=data-view.js.map