@revisium/schema-toolkit-ui 0.6.8 → 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 +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +26 -26
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +13 -1
- 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) => {
|
|
@@ -11117,6 +11126,9 @@ function clearPendingContextMenu() {
|
|
|
11117
11126
|
function setPendingContextMenu(value) {
|
|
11118
11127
|
pendingContextMenu = value;
|
|
11119
11128
|
}
|
|
11129
|
+
function hasPendingContextMenu() {
|
|
11130
|
+
return pendingContextMenu !== null;
|
|
11131
|
+
}
|
|
11120
11132
|
const cellMenuRegistry = /* @__PURE__ */ new WeakMap();
|
|
11121
11133
|
function useCellContextMenu(cell, cellRef, deferredEdit) {
|
|
11122
11134
|
const [menuAnchor, setMenuAnchor] = (0, react.useState)(null);
|
|
@@ -11373,7 +11385,7 @@ const CellWrapper = (0, mobx_react_lite.observer)(({ cell, children, onDoubleCli
|
|
|
11373
11385
|
const handleMouseDown = (0, react.useCallback)((e) => {
|
|
11374
11386
|
if (e.detail === 2 && state !== "readonly" && state !== "readonlyFocused") e.preventDefault();
|
|
11375
11387
|
if (e.button === 2) {
|
|
11376
|
-
openContextMenuAt(e.clientX, e.clientY);
|
|
11388
|
+
if (!hasPendingContextMenu()) openContextMenuAt(e.clientX, e.clientY);
|
|
11377
11389
|
return;
|
|
11378
11390
|
}
|
|
11379
11391
|
if (!e.shiftKey && e.button === 0 && state !== "editing") {
|