@revisium/schema-toolkit-ui 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/index.cjs +105 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +40 -26
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +105 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -284,6 +284,54 @@ core.applyViewState(s) // restore view settings
|
|
|
284
284
|
core.dispose() // cleanup
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
+
## Hooks
|
|
288
|
+
|
|
289
|
+
### `useContentEditable`
|
|
290
|
+
|
|
291
|
+
A controlled `contenteditable` hook that manages DOM synchronization, cursor position, and keyboard shortcuts.
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { useContentEditable } from '@revisium/schema-toolkit-ui';
|
|
295
|
+
|
|
296
|
+
const editableProps = useContentEditable({
|
|
297
|
+
value,
|
|
298
|
+
onChange: (v) => setValue(v),
|
|
299
|
+
onBlur: () => { /* called after DOM is synced */ },
|
|
300
|
+
onFocus: () => {},
|
|
301
|
+
onEnter: () => { /* Enter key pressed with non-empty value */ },
|
|
302
|
+
onEscape: () => { /* Escape key pressed */ },
|
|
303
|
+
restrict: /^[a-z0-9-]$/, // optional — block disallowed keys
|
|
304
|
+
autoFocus: true, // optional — focus on mount
|
|
305
|
+
focusTrigger: n, // optional — increment to focus programmatically
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return <div data-testid="editable" {...editableProps} />;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### How it works
|
|
312
|
+
|
|
313
|
+
`dangerouslySetInnerHTML` sets the initial DOM content. After that React does not overwrite the DOM on re-renders (standard contenteditable contract). The hook handles synchronization explicitly:
|
|
314
|
+
|
|
315
|
+
| Event | Behaviour |
|
|
316
|
+
|-------|-----------|
|
|
317
|
+
| User types | `onChange(value)` called on every `input` event. Cursor position is preserved across re-renders. |
|
|
318
|
+
| `value` prop changes while **not focused** | DOM updated immediately via `textContent`. |
|
|
319
|
+
| `value` prop changes while **focused** | DOM is left untouched so the user can keep typing without interruption. |
|
|
320
|
+
| `blur` | If `value` diverged from DOM (e.g. external revert happened while focused), DOM is synced to `value` before `onBlur` is called. |
|
|
321
|
+
| `Enter` | `blur()` called then `onEnter()` (only when `value` is non-empty). |
|
|
322
|
+
| `Escape` | `blur()` called then `onEscape()`. |
|
|
323
|
+
|
|
324
|
+
#### Controlled revert pattern
|
|
325
|
+
|
|
326
|
+
Because DOM sync on blur is guaranteed, a revert triggered while the element is focused will correctly reset the displayed text when the user leaves the field:
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
// External state revert while user is typing:
|
|
330
|
+
// value prop → 'original'
|
|
331
|
+
// DOM → 'user typed...' (untouched during focus)
|
|
332
|
+
// on blur → DOM is reset to 'original' ✓
|
|
333
|
+
```
|
|
334
|
+
|
|
287
335
|
### Cleanup
|
|
288
336
|
|
|
289
337
|
Call `vm.dispose()` (or `core.dispose()` for `TableEditorCore`) when the editor is unmounted.
|
package/dist/index.cjs
CHANGED
|
@@ -2550,6 +2550,11 @@ function useContentEditable(options) {
|
|
|
2550
2550
|
const prevFocusTriggerRef = (0, react.useRef)(focusTrigger);
|
|
2551
2551
|
const optionsRef = (0, react.useRef)(options);
|
|
2552
2552
|
optionsRef.current = options;
|
|
2553
|
+
const isFocusedRef = (0, react.useRef)(false);
|
|
2554
|
+
(0, react.useEffect)(() => {
|
|
2555
|
+
const el = elementRef.current;
|
|
2556
|
+
if (el && !isFocusedRef.current && el.textContent !== value) el.textContent = value;
|
|
2557
|
+
}, [value]);
|
|
2553
2558
|
const ref = (0, react.useCallback)((node) => {
|
|
2554
2559
|
elementRef.current = node;
|
|
2555
2560
|
}, []);
|
|
@@ -2592,10 +2597,14 @@ function useContentEditable(options) {
|
|
|
2592
2597
|
optionsRef.current.onChange?.(val);
|
|
2593
2598
|
}, []);
|
|
2594
2599
|
const handleBlur = (0, react.useCallback)(() => {
|
|
2600
|
+
isFocusedRef.current = false;
|
|
2601
|
+
const el = elementRef.current;
|
|
2602
|
+
if (el && el.textContent !== optionsRef.current.value) el.textContent = optionsRef.current.value;
|
|
2595
2603
|
optionsRef.current.onBlur?.();
|
|
2596
2604
|
cursorPosition.current = null;
|
|
2597
2605
|
}, []);
|
|
2598
2606
|
const handleFocus = (0, react.useCallback)(() => {
|
|
2607
|
+
isFocusedRef.current = true;
|
|
2599
2608
|
optionsRef.current.onFocus?.();
|
|
2600
2609
|
}, []);
|
|
2601
2610
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
@@ -7078,7 +7087,7 @@ function resolveRefColumn(child, fieldPath) {
|
|
|
7078
7087
|
systemFieldId: systemDef.id,
|
|
7079
7088
|
isDeprecated,
|
|
7080
7089
|
hasFormula,
|
|
7081
|
-
isSortable: !isDeprecated
|
|
7090
|
+
isSortable: !isDeprecated
|
|
7082
7091
|
};
|
|
7083
7092
|
}
|
|
7084
7093
|
if (refValue === _revisium_schema_toolkit.SystemSchemaIds.File) return resolveFileRefColumns(child, fieldPath);
|
|
@@ -7104,7 +7113,8 @@ function stripDataPrefix(fieldPath) {
|
|
|
7104
7113
|
}
|
|
7105
7114
|
function createColumn(fieldPath, child, fieldType) {
|
|
7106
7115
|
const isDeprecated = child.metadata().deprecated ?? false;
|
|
7107
|
-
const
|
|
7116
|
+
const formula = child.formula();
|
|
7117
|
+
const hasFormula = formula !== void 0;
|
|
7108
7118
|
return {
|
|
7109
7119
|
field: fieldPath,
|
|
7110
7120
|
label: stripDataPrefix(fieldPath),
|
|
@@ -7112,7 +7122,8 @@ function createColumn(fieldPath, child, fieldType) {
|
|
|
7112
7122
|
isSystem: false,
|
|
7113
7123
|
isDeprecated,
|
|
7114
7124
|
hasFormula,
|
|
7115
|
-
|
|
7125
|
+
formulaExpression: formula?.expression(),
|
|
7126
|
+
isSortable: !isDeprecated && fieldType !== FilterFieldType.File
|
|
7116
7127
|
};
|
|
7117
7128
|
}
|
|
7118
7129
|
|
|
@@ -11115,6 +11126,9 @@ function clearPendingContextMenu() {
|
|
|
11115
11126
|
function setPendingContextMenu(value) {
|
|
11116
11127
|
pendingContextMenu = value;
|
|
11117
11128
|
}
|
|
11129
|
+
function hasPendingContextMenu() {
|
|
11130
|
+
return pendingContextMenu !== null;
|
|
11131
|
+
}
|
|
11118
11132
|
const cellMenuRegistry = /* @__PURE__ */ new WeakMap();
|
|
11119
11133
|
function useCellContextMenu(cell, cellRef, deferredEdit) {
|
|
11120
11134
|
const [menuAnchor, setMenuAnchor] = (0, react.useState)(null);
|
|
@@ -11371,7 +11385,7 @@ const CellWrapper = (0, mobx_react_lite.observer)(({ cell, children, onDoubleCli
|
|
|
11371
11385
|
const handleMouseDown = (0, react.useCallback)((e) => {
|
|
11372
11386
|
if (e.detail === 2 && state !== "readonly" && state !== "readonlyFocused") e.preventDefault();
|
|
11373
11387
|
if (e.button === 2) {
|
|
11374
|
-
openContextMenuAt(e.clientX, e.clientY);
|
|
11388
|
+
if (!hasPendingContextMenu()) openContextMenuAt(e.clientX, e.clientY);
|
|
11375
11389
|
return;
|
|
11376
11390
|
}
|
|
11377
11391
|
if (!e.shiftKey && e.button === 0 && state !== "editing") {
|
|
@@ -13673,6 +13687,35 @@ const RowActionsMenu = ({ rowId, onSelect, onDuplicate, onDelete }) => {
|
|
|
13673
13687
|
});
|
|
13674
13688
|
};
|
|
13675
13689
|
|
|
13690
|
+
//#endregion
|
|
13691
|
+
//#region src/table-editor/Status/model/CellInfoModel.ts
|
|
13692
|
+
var CellInfoModel = class {
|
|
13693
|
+
_cellFSM;
|
|
13694
|
+
_columnsModel;
|
|
13695
|
+
constructor(cellFSM, columnsModel) {
|
|
13696
|
+
this._cellFSM = cellFSM;
|
|
13697
|
+
this._columnsModel = columnsModel;
|
|
13698
|
+
(0, mobx.makeAutoObservable)(this, {}, { autoBind: true });
|
|
13699
|
+
}
|
|
13700
|
+
get _focusedColumn() {
|
|
13701
|
+
const focused = this._cellFSM.focusedCell;
|
|
13702
|
+
if (!focused) return null;
|
|
13703
|
+
return this._columnsModel.allColumns.find((c) => c.field === focused.field) ?? null;
|
|
13704
|
+
}
|
|
13705
|
+
get isVisible() {
|
|
13706
|
+
return this._focusedColumn !== null && !this._cellFSM.hasSelection;
|
|
13707
|
+
}
|
|
13708
|
+
get fieldLabel() {
|
|
13709
|
+
return this._focusedColumn?.label ?? "";
|
|
13710
|
+
}
|
|
13711
|
+
get formulaExpression() {
|
|
13712
|
+
return this._focusedColumn?.formulaExpression;
|
|
13713
|
+
}
|
|
13714
|
+
get foreignKeyTableId() {
|
|
13715
|
+
return this._focusedColumn?.foreignKeyTableId;
|
|
13716
|
+
}
|
|
13717
|
+
};
|
|
13718
|
+
|
|
13676
13719
|
//#endregion
|
|
13677
13720
|
//#region src/table-editor/Status/model/RowCountModel.ts
|
|
13678
13721
|
var RowCountModel = class {
|
|
@@ -13786,6 +13829,7 @@ var TableEditorCore = class {
|
|
|
13786
13829
|
cellFSM;
|
|
13787
13830
|
selection;
|
|
13788
13831
|
rowCount;
|
|
13832
|
+
cellInfo;
|
|
13789
13833
|
_dataSource;
|
|
13790
13834
|
_pageSize;
|
|
13791
13835
|
_breadcrumbs;
|
|
@@ -13814,6 +13858,7 @@ var TableEditorCore = class {
|
|
|
13814
13858
|
this.cellFSM = new CellFSM();
|
|
13815
13859
|
this.selection = new SelectionModel();
|
|
13816
13860
|
this.rowCount = new RowCountModel();
|
|
13861
|
+
this.cellInfo = new CellInfoModel(this.cellFSM, this.columns);
|
|
13817
13862
|
this.columns.setOnChange(() => this._handleColumnsChange());
|
|
13818
13863
|
this.filters.setOnChange(() => this._handleFilterChange());
|
|
13819
13864
|
this.filters.setOnApply((_where) => this._handleFilterApply());
|
|
@@ -14158,6 +14203,50 @@ var MockDataSource = class {
|
|
|
14158
14203
|
}
|
|
14159
14204
|
};
|
|
14160
14205
|
|
|
14206
|
+
//#endregion
|
|
14207
|
+
//#region src/table-editor/Status/ui/CellInfoWidget.tsx
|
|
14208
|
+
const CellInfoWidget = (0, mobx_react_lite.observer)(({ model }) => {
|
|
14209
|
+
if (!model.isVisible) return null;
|
|
14210
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_chakra_ui_react.Flex, {
|
|
14211
|
+
alignItems: "center",
|
|
14212
|
+
gap: 2,
|
|
14213
|
+
overflow: "hidden",
|
|
14214
|
+
flexShrink: 1,
|
|
14215
|
+
minW: 0,
|
|
14216
|
+
children: [
|
|
14217
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_chakra_ui_react.Text, {
|
|
14218
|
+
fontSize: "sm",
|
|
14219
|
+
color: "gray.500",
|
|
14220
|
+
overflow: "hidden",
|
|
14221
|
+
textOverflow: "ellipsis",
|
|
14222
|
+
whiteSpace: "nowrap",
|
|
14223
|
+
"data-testid": "cell-info-field",
|
|
14224
|
+
children: model.fieldLabel
|
|
14225
|
+
}),
|
|
14226
|
+
model.formulaExpression && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_chakra_ui_react.Text, {
|
|
14227
|
+
fontSize: "sm",
|
|
14228
|
+
color: "purple.400",
|
|
14229
|
+
fontFamily: "mono",
|
|
14230
|
+
overflow: "hidden",
|
|
14231
|
+
textOverflow: "ellipsis",
|
|
14232
|
+
whiteSpace: "nowrap",
|
|
14233
|
+
"data-testid": "cell-info-formula",
|
|
14234
|
+
children: ["= ", model.formulaExpression]
|
|
14235
|
+
}),
|
|
14236
|
+
model.foreignKeyTableId && !model.formulaExpression && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_chakra_ui_react.Text, {
|
|
14237
|
+
fontSize: "sm",
|
|
14238
|
+
color: "blue.400",
|
|
14239
|
+
fontFamily: "mono",
|
|
14240
|
+
overflow: "hidden",
|
|
14241
|
+
textOverflow: "ellipsis",
|
|
14242
|
+
whiteSpace: "nowrap",
|
|
14243
|
+
"data-testid": "cell-info-fk",
|
|
14244
|
+
children: ["→ ", model.foreignKeyTableId]
|
|
14245
|
+
})
|
|
14246
|
+
]
|
|
14247
|
+
});
|
|
14248
|
+
});
|
|
14249
|
+
|
|
14161
14250
|
//#endregion
|
|
14162
14251
|
//#region src/table-editor/Status/ui/RowCountWidget.tsx
|
|
14163
14252
|
const RowCountWidget = (0, mobx_react_lite.observer)(({ model }) => {
|
|
@@ -14340,14 +14429,24 @@ const TableEditor = (0, mobx_react_lite.observer)(({ viewModel, useWindowScroll
|
|
|
14340
14429
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_chakra_ui_react.Flex, {
|
|
14341
14430
|
px: 3,
|
|
14342
14431
|
py: 2,
|
|
14343
|
-
|
|
14432
|
+
alignItems: "center",
|
|
14433
|
+
overflow: "hidden",
|
|
14344
14434
|
...useWindowScroll && {
|
|
14345
14435
|
position: "sticky",
|
|
14346
14436
|
bottom: 0,
|
|
14347
14437
|
bg: "white",
|
|
14348
14438
|
zIndex: 3
|
|
14349
14439
|
},
|
|
14350
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCountWidget, { model: viewModel.rowCount }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
14440
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCountWidget, { model: viewModel.rowCount }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_chakra_ui_react.Flex, {
|
|
14441
|
+
alignItems: "center",
|
|
14442
|
+
gap: 3,
|
|
14443
|
+
overflow: "hidden",
|
|
14444
|
+
flexShrink: 1,
|
|
14445
|
+
minW: 0,
|
|
14446
|
+
flex: 1,
|
|
14447
|
+
justifyContent: "flex-end",
|
|
14448
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CellInfoWidget, { model: viewModel.cellInfo }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewSettingsBadge, { model: viewModel.viewBadge })]
|
|
14449
|
+
})]
|
|
14351
14450
|
})
|
|
14352
14451
|
]
|
|
14353
14452
|
});
|