@script-it/json-views-react 0.1.0-beta.1 → 2026.10.27
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 +24 -3
- package/dist/index.d.ts +1 -0
- package/dist/json/html-view/format-value.d.ts +5 -0
- package/dist/json/html-view/format-value.js +84 -0
- package/dist/json/html-view/native-control.d.ts +3 -8
- package/dist/json/html-view/native-control.js +3 -1
- package/dist/json/html-view/template.js +5 -0
- package/dist/json/html-view.js +50 -8
- package/dist/json/json-content.d.ts +3 -0
- package/dist/json/json-content.js +22 -15
- package/dist/json/json-view.d.ts +1 -1
- package/dist/json/json-view.js +34 -32
- package/dist/json/schema-value.js +11 -3
- package/dist/json/table-view.js +27 -14
- package/dist/json/view-model.d.ts +1 -1
- package/dist/json/view-model.js +3 -1
- package/dist/json/view-settings.js +42 -23
- package/dist/json/view-types.d.ts +3 -0
- package/dist/json/viewers/general/json-node-view.js +6 -2
- package/dist/lib/enter-key.d.ts +29 -0
- package/dist/lib/enter-key.js +27 -0
- package/dist/primitives/choice-control.js +2 -1
- package/dist/structured-data/atomic-value-editor.d.ts +4 -1
- package/dist/structured-data/atomic-value-editor.js +17 -9
- package/dist/structured-data/table-cell-options.d.ts +20 -0
- package/dist/structured-data/table-cell-options.js +11 -0
- package/dist/structured-data/tabular-data-cell.d.ts +19 -0
- package/dist/structured-data/tabular-data-cell.js +55 -0
- package/dist/styles.css +1 -1
- package/dist/tabular-data-view.js +23 -3
- package/dist/widget-registry.d.ts +5 -0
- package/dist/widget-registry.js +13 -7
- package/dist/widgets/date-picker-widget.js +11 -0
- package/dist/widgets/textarea-widget.js +4 -1
- package/package.json +4 -4
package/dist/json/json-view.js
CHANGED
|
@@ -18,6 +18,7 @@ import { GeneralRecordView, RecordView } from './record-view.js';
|
|
|
18
18
|
import { ProjectedTableView } from './table-view.js';
|
|
19
19
|
import { KanbanView } from './kanban-view.js';
|
|
20
20
|
import { useViewerState } from '../viewer-state.js';
|
|
21
|
+
import { TableCellOptionsContext } from '../structured-data/table-cell-options.js';
|
|
21
22
|
const VIEW_TAB_CLASS = 'h-7 shrink-0 gap-1.5 rounded-md px-2 py-0 text-xs font-medium text-muted-foreground shadow-none hover:bg-accent hover:text-foreground data-[state=active]:bg-accent data-[state=active]:text-foreground data-[state=active]:shadow-none [&>svg]:shrink-0';
|
|
22
23
|
function slashPath(view) {
|
|
23
24
|
const segments = view.path.segments.map((segment) => {
|
|
@@ -98,7 +99,7 @@ function ConfiguredView(props) {
|
|
|
98
99
|
}
|
|
99
100
|
return _jsx(AdaptiveView, { ...props });
|
|
100
101
|
}
|
|
101
|
-
export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight = false, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, uiSize, viewsEnabled = true, tabularRoot = false, }) {
|
|
102
|
+
export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight = false, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, tableCells, uiSize, viewsEnabled = true, tabularRoot = false, }) {
|
|
102
103
|
const { types } = useJsonViewsRegistries();
|
|
103
104
|
const viewerState = useViewerState();
|
|
104
105
|
const firstViewTab = viewsEnabled && compiled.views[0] ? `view:${compiled.views[0].id}` : 'root';
|
|
@@ -146,11 +147,12 @@ export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight
|
|
|
146
147
|
}, [compiled.root, currentPath, defaultKey, rawViews]);
|
|
147
148
|
const settingsRawView = activeRawView ?? rootDraftRawView;
|
|
148
149
|
const settingsDeclarationIndex = declaredView?.declarationIndex ?? rawViews.length;
|
|
150
|
+
// Only the compiled view is needed here, so the per-value validation pass is skipped.
|
|
149
151
|
const settingsView = useMemo(() => {
|
|
150
152
|
if (declaredView)
|
|
151
153
|
return declaredView;
|
|
152
154
|
const candidate = compileJsonViewMetadata(compiled.root, types, {
|
|
153
|
-
metadata: metadataWithViews(compiled, [...rawViews, rootDraftRawView]),
|
|
155
|
+
metadata: metadataWithViews(compiled, [...rawViews, rootDraftRawView]), validateValues: false,
|
|
154
156
|
});
|
|
155
157
|
return candidate.views.find((view) => view.declarationIndex === rawViews.length);
|
|
156
158
|
}, [compiled, declaredView, rawViews, rootDraftRawView, types, types.version]);
|
|
@@ -169,7 +171,7 @@ export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight
|
|
|
169
171
|
views[declaredView.declarationIndex] = previewRawView;
|
|
170
172
|
else
|
|
171
173
|
views.push(previewRawView);
|
|
172
|
-
const candidate = compileJsonViewMetadata(compiled.root, types, { metadata: metadataWithViews(compiled, views) });
|
|
174
|
+
const candidate = compileJsonViewMetadata(compiled.root, types, { metadata: metadataWithViews(compiled, views), validateValues: false });
|
|
173
175
|
return candidate.views.find((view) => view.declarationIndex === settingsDeclarationIndex);
|
|
174
176
|
}, [compiled, declaredView, previewRawView, rawViews, settingsDeclarationIndex, types, types.version]);
|
|
175
177
|
const previewSettings = useCallback((next) => {
|
|
@@ -215,7 +217,7 @@ export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight
|
|
|
215
217
|
if (!editing)
|
|
216
218
|
throw new Error('This JSON is read-only');
|
|
217
219
|
if (requiredIndex !== undefined) {
|
|
218
|
-
const candidate = compileJsonViewMetadata(compiled.root, types, { metadata: metadataWithViews(compiled, views) });
|
|
220
|
+
const candidate = compileJsonViewMetadata(compiled.root, types, { metadata: metadataWithViews(compiled, views), validateValues: false });
|
|
219
221
|
const valid = candidate.views.some((view) => view.declarationIndex === requiredIndex);
|
|
220
222
|
const issue = candidate.diagnostics.find((item) => item.scope === 'view' && item.metadataPath[2] === requiredIndex);
|
|
221
223
|
if (!valid)
|
|
@@ -291,32 +293,32 @@ export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight
|
|
|
291
293
|
if (!query)
|
|
292
294
|
setSearchOpenTab(undefined);
|
|
293
295
|
};
|
|
294
|
-
return (_jsx(Tabs, { value: selectedTab, onValueChange: (value) => { if (value !== 'source')
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
296
|
+
return (_jsx(TableCellOptionsContext.Provider, { value: tableCells, children: _jsx(Tabs, { value: selectedTab, onValueChange: (value) => { if (value !== 'source')
|
|
297
|
+
setActiveTab(value); onSourceVisibleChange?.(value === 'source'); }, className: cn('flex min-h-0 flex-1 flex-col bg-background text-foreground', fillHeight && 'h-full'), children: _jsxs("div", { "data-id": "json-viewer-content", className: cn('flex min-h-0 w-full max-w-none flex-1 flex-col px-3 py-4 sm:px-6 sm:py-5', fillHeight && 'h-full'), children: [hasToolbar && (_jsxs("div", { "data-id": "json-viewer-toolbar", className: "relative flex min-h-8 shrink-0 flex-nowrap items-center gap-x-2", children: [_jsxs("div", { "data-id": "jsonView-toolbar-actions", className: "ml-auto flex h-8 shrink-0 items-center gap-1", children: [selectedTab !== 'source' && searchable && (searchOpen ? (_jsxs("label", { "data-id": "jsonView-search-control", className: "flex h-7 w-44 items-center overflow-hidden rounded-md border border-transparent bg-transparent transition-[width,background-color] focus-within:border-border focus-within:bg-card sm:w-52", children: [_jsx(Search, { className: "mx-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("input", { ref: searchInput, "aria-label": "Search rows", className: "h-full min-w-0 flex-1 bg-transparent pr-2 text-xs outline-none placeholder:text-muted-foreground", value: query, onBlur: closeSearch, onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
|
|
298
|
+
if (event.key === 'Escape') {
|
|
299
|
+
setQuery('');
|
|
300
|
+
setSearchOpenTab(undefined);
|
|
301
|
+
}
|
|
302
|
+
}, placeholder: "Type to search\u2026" })] })) : (_jsx("button", { "data-id": "jsonView-search-button", type: "button", "aria-label": "Search rows", title: "Search", className: "grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", onClick: () => setSearchOpenTab(selectedTab), children: _jsx(Search, { className: "h-3.5 w-3.5" }) }))), editing && settingsRawView && (_jsx(JsonViewViewSettings, { hasUnsavedChanges: previewRawView !== undefined, view: previewView ?? settingsView ?? activeView, rawView: activeView.display === 'html' ? activeRawView ?? settingsDraftRawView : settingsDraftRawView, schema: compiled.schema, disabled: editing.saving, saveMode: viewSaveMode, saveUnavailableReason: canPersistViews ? undefined : editing.viewCreationUnavailableReason, unavailableReason: settingsUnavailableReason, onPreview: previewSettings, onSave: saveSettingsView, onDuplicate: declaredView && activeRawView && canPersistViews ? async () => {
|
|
303
|
+
const name = uniqueViewName(`${declaredView.name} copy`, rawViews);
|
|
304
|
+
const id = uniqueViewId(name, rawViews);
|
|
305
|
+
const duplicate = { ...activeRawView, id, name };
|
|
306
|
+
const nextViews = [...rawViews];
|
|
307
|
+
const duplicateIndex = declaredView.declarationIndex + 1;
|
|
308
|
+
nextViews.splice(duplicateIndex, 0, duplicate);
|
|
309
|
+
await persistViews(nextViews, duplicateIndex);
|
|
310
|
+
setActiveTab(`view:${id}`);
|
|
311
|
+
} : undefined, onDelete: declaredView && canPersistViews ? async () => {
|
|
312
|
+
const nextViews = [...rawViews];
|
|
313
|
+
nextViews.splice(declaredView.declarationIndex, 1);
|
|
314
|
+
await persistViews(nextViews);
|
|
315
|
+
setActiveTab('root');
|
|
316
|
+
} : undefined }, typeof settingsDraftRawView.id === 'string' ? settingsDraftRawView.id : '__view_settings__')), sourceControl, _jsx(Diagnostics, { compiled: compiled })] }), (hasTabs || canAddViews || sourceContent) && (_jsxs("div", { "data-id": "jsonView-view-controls", className: "flex min-w-0 flex-1 items-center", children: [canAddViews && _jsx(JsonViewAddView, { align: "start", root: compiled.root, schema: compiled.schema, currentPath: jsonPathForValuePath(currentPath), disabled: editing.saving, onAdd: addView, unavailableReason: addViewUnavailableReason }, "add-view"), (hasTabs || sourceContent) && (_jsxs(TabsList, { "data-id": "jsonView-json-tabs", className: "h-8 min-w-0 flex-initial justify-start gap-0.5 overflow-x-auto overscroll-x-contain rounded-none bg-transparent p-0", children: [compiled.views.map((view) => {
|
|
317
|
+
const Icon = view.display === 'html' ? LayoutGrid : view.display === 'kanban'
|
|
318
|
+
? Columns3
|
|
319
|
+
: isInferredSingleRecord(view) || (view.value !== null && typeof view.value === 'object' && !Array.isArray(view.value) && !isRecordCollection(view.value))
|
|
320
|
+
? FileText
|
|
321
|
+
: Table2;
|
|
322
|
+
return _jsxs(ViewTab, { id: `jsonView-json-tab-${view.id}`, value: `view:${view.id}`, path: slashPath(view), children: [_jsx(Icon, { className: "h-3.5 w-3.5" }), view.name] }, view.id);
|
|
323
|
+
}), _jsxs(ViewTab, { id: "jsonView-json-tab-root", value: "root", path: isInferredSingleRecord(rootView) && !forceTable ? 'Root record' : 'Root table', children: [isInferredSingleRecord(rootView) && !forceTable ? _jsx(FileText, { className: "h-3.5 w-3.5" }) : _jsx(Table2, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "sr-only", children: isInferredSingleRecord(rootView) && !forceTable ? 'Record' : 'Table' })] }), sourceContent && _jsxs(ViewTab, { id: "jsonView-json-tab-source", value: "source", path: "Source", children: [_jsx(Braces, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "sr-only", children: "Source" })] })] }, "view-tabs"))] }))] })), _jsx(TabsContent, { value: selectedTab, className: cn('mt-2 min-w-0', fillHeight && 'flex min-h-0 flex-1 flex-col'), children: selectedTab === 'source' ? sourceContent : _jsx(ConfiguredView, { forceTable: forceTable, compiled: compiled, copyFileName: defaultKey === 'root' ? 'document.json' : defaultKey, editing: editing, filePath: filePath, fillHeight: fillHeight, onCurrentPathChange: rememberCurrentPath, onSaveView: saveColumnView, query: query, rawView: columnRawView, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, uiSize: uiSize, view: renderedView }, selectedTab) })] }) }) }));
|
|
322
324
|
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { AlertTriangle, Check, ExternalLink } from 'lucide-react';
|
|
4
4
|
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { submitsOnEnter, useEnterKeyBehavior } from '../lib/enter-key.js';
|
|
5
6
|
import { OptionPill, optionColorsForValues } from '../primitives/option-pill.js';
|
|
6
7
|
import { EditValueAction, StructuredValueCellFrame } from '../structured-data/atomic-value-editor.js';
|
|
7
8
|
import { StructuredCellFillContext } from '../structured-data/structured-cell-fill-context.js';
|
|
@@ -106,6 +107,7 @@ export function SchemaEditor({ descriptor, disabled, label, minHeight, onCancel,
|
|
|
106
107
|
const baseChanged = useEditBaseChanged();
|
|
107
108
|
const fillCell = useContext(StructuredCellFillContext);
|
|
108
109
|
const format = descriptor.type;
|
|
110
|
+
const enterKey = useEnterKeyBehavior(widgets.getDefinition(format)?.enterKey);
|
|
109
111
|
const stringDraft = typeof draft === 'string' || typeof draft === 'number' || typeof draft === 'boolean'
|
|
110
112
|
? String(draft)
|
|
111
113
|
: Array.isArray(draft) ? JSON.stringify(draft) : '';
|
|
@@ -208,10 +210,16 @@ export function SchemaEditor({ descriptor, disabled, label, minHeight, onCancel,
|
|
|
208
210
|
: (format === 'body' || format === 'markdown' || format === 'html')
|
|
209
211
|
? 'leading-normal [&_textarea]:[font:inherit]'
|
|
210
212
|
: '[&_input]:[font:inherit] [&_textarea]:[font:inherit]'), "aria-label": `Edit ${label}`, onKeyDown: (event) => {
|
|
211
|
-
|
|
213
|
+
// React delivers keys from a widget's portaled popup (a date picker,
|
|
214
|
+
// a choice menu) here too; that popup owns them. A widget that
|
|
215
|
+
// handled Enter itself, such as a code editor, keeps it.
|
|
216
|
+
const target = event.target;
|
|
217
|
+
if (event.defaultPrevented || !(target instanceof Element) || !event.currentTarget.contains(target))
|
|
212
218
|
return;
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
if (target instanceof HTMLButtonElement)
|
|
220
|
+
return;
|
|
221
|
+
const multiline = target instanceof HTMLTextAreaElement || (target instanceof HTMLElement && target.isContentEditable);
|
|
222
|
+
if (!submitsOnEnter(event, { multiline, enterKey }))
|
|
215
223
|
return;
|
|
216
224
|
event.preventDefault();
|
|
217
225
|
void commit();
|
package/dist/json/table-view.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isLongText } from './long-text.js';
|
|
|
3
3
|
import { useJsonSourceLiterals } from './source-literals.js';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import { useJsonViewsRegistries } from '../widget-registry.js';
|
|
6
|
-
import { AtomicValueEditor } from '../structured-data/atomic-value-editor.js';
|
|
6
|
+
import { AtomicValueEditor, EditValueAction, StructuredValueCellFrame } from '../structured-data/atomic-value-editor.js';
|
|
7
7
|
import { isEditableAtomicValue } from '../structured-data/atomic-value.js';
|
|
8
8
|
import { TabularObjectArrayView, ValueCell, ValueCellContent } from '../tabular-data-view.js';
|
|
9
9
|
import {} from '@script-it/json-views-core';
|
|
@@ -46,8 +46,21 @@ export function ProjectedTableView({ compiled, editing, filePath, copyFileName,
|
|
|
46
46
|
const [editPath, setEditPath] = useState();
|
|
47
47
|
const [openRowPath, setOpenRowPath] = useState();
|
|
48
48
|
const [openCell, setOpenCell] = useState();
|
|
49
|
-
const unfilteredProjection = useMemo(() => projectJsonViewCollection(compiled.root, { ...view, filter: undefined }, compiled.schema, types, sourceLiteral), [compiled.root, compiled.schema, view, types, types.version, sourceLiteral]);
|
|
50
49
|
const completeProjection = useMemo(() => projectJsonViewCollection(compiled.root, view, compiled.schema, types, sourceLiteral), [compiled.root, compiled.schema, view, types, types.version, sourceLiteral]);
|
|
50
|
+
// Without a filter, the complete projection already is the unfiltered one.
|
|
51
|
+
const unfilteredProjection = useMemo(() => view.filter
|
|
52
|
+
? projectJsonViewCollection(compiled.root, { ...view, filter: undefined }, compiled.schema, types, sourceLiteral)
|
|
53
|
+
: completeProjection, [compiled.root, compiled.schema, completeProjection, view, types, types.version, sourceLiteral]);
|
|
54
|
+
// The first record with a schema match decides each column's descriptor. This
|
|
55
|
+
// does not depend on the search query, unlike the rest of the table model.
|
|
56
|
+
const columnDescriptors = useMemo(() => Object.fromEntries(completeProjection.columns.map((column) => {
|
|
57
|
+
for (const row of unfilteredProjection.rows) {
|
|
58
|
+
const schema = schemaForJsonViewPath(compiled.schema, resolveJsonViewRowPath(compiled.root, row, column.path).sourcePath);
|
|
59
|
+
if (schema)
|
|
60
|
+
return [column.id, schema.descriptor];
|
|
61
|
+
}
|
|
62
|
+
return [column.id, compiled.schema.find((schema) => sameJsonViewPath(schema.path, column.path))?.descriptor];
|
|
63
|
+
})), [compiled.root, compiled.schema, completeProjection.columns, unfilteredProjection]);
|
|
51
64
|
const projected = useMemo(() => {
|
|
52
65
|
const indices = completeProjection.rows.flatMap((row, index) => matchesSearch(row.value, query) ? [index] : []);
|
|
53
66
|
return {
|
|
@@ -65,16 +78,8 @@ export function ProjectedTableView({ compiled, editing, filePath, copyFileName,
|
|
|
65
78
|
fieldOccurrences.set(field, occurrence + 1);
|
|
66
79
|
return [column.id, JSON.stringify([field, occurrence])];
|
|
67
80
|
}));
|
|
68
|
-
const descriptors = Object.fromEntries(visibleColumns.map((column) => {
|
|
69
|
-
const descriptor = unfilteredProjection.rows.flatMap((row) => {
|
|
70
|
-
const path = resolveJsonViewRowPath(compiled.root, row, column.path).sourcePath;
|
|
71
|
-
const schema = schemaForJsonViewPath(compiled.schema, path);
|
|
72
|
-
return schema ? [schema.descriptor] : [];
|
|
73
|
-
})[0] ?? compiled.schema.find((schema) => sameJsonViewPath(schema.path, column.path))?.descriptor;
|
|
74
|
-
return [column.id, descriptor];
|
|
75
|
-
}));
|
|
76
81
|
const columnFilterOperators = Object.fromEntries(visibleColumns.map((column) => {
|
|
77
|
-
const descriptor =
|
|
82
|
+
const descriptor = columnDescriptors[column.id];
|
|
78
83
|
const defaults = descriptor?.type === 'date' ? DATE_FILTER_OPERATORS : FILTER_OPERATORS;
|
|
79
84
|
const declared = descriptor ? types.filterOperators(descriptor) : undefined;
|
|
80
85
|
return [column.id, declared ? [
|
|
@@ -94,7 +99,7 @@ export function ProjectedTableView({ compiled, editing, filePath, copyFileName,
|
|
|
94
99
|
])),
|
|
95
100
|
titleColumn: visibleColumns[0],
|
|
96
101
|
};
|
|
97
|
-
}, [
|
|
102
|
+
}, [columnDescriptors, completeProjection, projected.records, types, unfilteredProjection]);
|
|
98
103
|
const declaredColumnActions = rawView && onSaveView ? {
|
|
99
104
|
activeFilters: view.filter?.rules.flatMap((rule) => {
|
|
100
105
|
const column = tableModel.visibleColumns.find((candidate) => sameJsonViewPath(candidate.path, rule.path));
|
|
@@ -281,8 +286,16 @@ export function ProjectedTableView({ compiled, editing, filePath, copyFileName,
|
|
|
281
286
|
const schema = schemaForJsonViewPath(compiled.schema, resolved.sourcePath);
|
|
282
287
|
const descriptor = schema?.descriptor;
|
|
283
288
|
const coloredDescriptor = descriptor && withColumnOptionColors(descriptor, columnFilterValues[column] ?? [value], visibleColumns.findIndex((candidate) => candidate.id === column));
|
|
284
|
-
if (isLongText(value, descriptor))
|
|
285
|
-
|
|
289
|
+
if (isLongText(value, descriptor)) {
|
|
290
|
+
const openRecordEditor = () => { setEditPath(resolved.sourcePath); setOpenRowPath(row.sourcePath); };
|
|
291
|
+
// Render before applying the plain-text shortcut. A group preserves
|
|
292
|
+
// native link interactions without nesting links inside a button.
|
|
293
|
+
if (renderMarkdown && (descriptor?.type === 'markdown' || descriptor?.type === 'body'))
|
|
294
|
+
return (_jsx(StructuredValueCellFrame, { activationLabel: `Open ${displayColumn.label} in record`, activationRole: "group", onActivate: openRecordEditor, actions: editing && editing.canReplace?.(resolved.sourcePath) !== false
|
|
295
|
+
? _jsx(EditValueAction, { label: displayColumn.label, onClick: openRecordEditor })
|
|
296
|
+
: undefined, children: _jsx("div", { className: "min-w-0 flex-1 break-words pr-2", children: _jsx(JsonViewSchemaValueDisplay, { sourcePath: resolved.sourcePath, descriptor: descriptor, value: value, onOpen: open, renderMarkdown: renderMarkdown }) }) }));
|
|
297
|
+
return _jsx("button", { type: "button", className: "block h-full w-full min-w-0 truncate px-1.5 py-1 text-left hover:bg-accent focus-visible:ring-1 focus-visible:ring-ring", "aria-label": `Open ${displayColumn.label} in record`, onClick: openRecordEditor, children: value || '—' });
|
|
298
|
+
}
|
|
286
299
|
if (coloredDescriptor) {
|
|
287
300
|
return (_jsx(JsonViewSchemaValueCell, { sourcePath: resolved.sourcePath, descriptor: coloredDescriptor, suppressRequiredValidation: editing?.isDraftRow?.(row.sourcePath) === true, onOptionColorChange: editing?.setOptionColor && schema ? (optionValue, color) => editing.setOptionColor(schema.declaration, optionValue, color) : undefined, inputKey: String(resolved.sourcePath[resolved.sourcePath.length - 1] ?? displayColumn.label), label: descriptor.title ?? displayColumn.label, value: value, onOpen: open, onClear: editing && resolved.value !== JSON_VIEW_PATH_MISSING ? () => editing.clear(resolved.sourcePath) : undefined, onCommit: editing && editing.canReplace?.(resolved.sourcePath) !== false && resolved.value !== JSON_VIEW_PATH_MISSING ? (next) => editing.replace(resolved.sourcePath, next) : undefined, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: editing?.saving }));
|
|
288
301
|
}
|
|
@@ -3,7 +3,7 @@ import { type CompiledJsonViewMetadata, type CompiledJsonViewView, type JsonView
|
|
|
3
3
|
export declare function isRecordCollection(value: unknown): boolean;
|
|
4
4
|
export declare function presentsRecordCollection(view: CompiledJsonViewView, forceTable?: boolean): boolean;
|
|
5
5
|
export declare function matchesSearch(value: unknown, query: string): boolean;
|
|
6
|
-
export declare function rawViewsFor(compiled: CompiledJsonViewMetadata): unknown[];
|
|
6
|
+
export declare function rawViewsFor(compiled: CompiledJsonViewMetadata): readonly unknown[];
|
|
7
7
|
export declare function rawViewFor(compiled: CompiledJsonViewMetadata, view: CompiledJsonViewView): Record<string, unknown> | undefined;
|
|
8
8
|
export declare function uniqueViewName(label: string, views: readonly unknown[]): string;
|
|
9
9
|
export declare function uniqueViewId(name: string, views: readonly unknown[]): string;
|
package/dist/json/view-model.js
CHANGED
|
@@ -34,6 +34,8 @@ export function matchesSearch(value, query) {
|
|
|
34
34
|
return String(value).toLocaleLowerCase().includes(normalized);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
// A stable empty result keeps memos that depend on the raw views from recomputing every render.
|
|
38
|
+
const NO_RAW_VIEWS = Object.freeze([]);
|
|
37
39
|
export function rawViewsFor(compiled) {
|
|
38
40
|
const metadata = compiled.metadata;
|
|
39
41
|
if (metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata)) {
|
|
@@ -41,7 +43,7 @@ export function rawViewsFor(compiled) {
|
|
|
41
43
|
if (Array.isArray(views))
|
|
42
44
|
return views;
|
|
43
45
|
}
|
|
44
|
-
return compiled.inference?.views ??
|
|
46
|
+
return compiled.inference?.views ?? NO_RAW_VIEWS;
|
|
45
47
|
}
|
|
46
48
|
export function rawViewFor(compiled, view) {
|
|
47
49
|
const raw = rawViewsFor(compiled)[view.declarationIndex];
|
|
@@ -516,28 +516,48 @@ function isRecordCollectionValue(value) {
|
|
|
516
516
|
return value.every(isRecord);
|
|
517
517
|
return isRecord(value) && Object.values(value).every(isRecord);
|
|
518
518
|
}
|
|
519
|
-
|
|
519
|
+
/** The option for one location, as a walk of the whole document would list it:
|
|
520
|
+
* own properties only, nothing under `$jsonviews`, and the canonical path
|
|
521
|
+
* spelling. Scalars are options too; they just are not record collections.
|
|
522
|
+
* Without a path, the root is the option.
|
|
523
|
+
*/
|
|
524
|
+
function resolveViewPathOption(root, currentPath) {
|
|
520
525
|
if (!isRecord(root) && !Array.isArray(root))
|
|
521
|
-
return
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
529
|
-
if (Array.isArray(value)) {
|
|
530
|
-
value.forEach((child, index) => visit(child, [...path, index]));
|
|
531
|
-
return;
|
|
526
|
+
return undefined;
|
|
527
|
+
const path = [];
|
|
528
|
+
let value = root;
|
|
529
|
+
if (currentPath !== undefined) {
|
|
530
|
+
let segments;
|
|
531
|
+
try {
|
|
532
|
+
segments = parseJsonViewPath(currentPath, { root: '$' }).segments;
|
|
532
533
|
}
|
|
533
|
-
|
|
534
|
-
return;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
534
|
+
catch {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
for (const segment of segments) {
|
|
538
|
+
if (segment.kind === 'property') {
|
|
539
|
+
if (segment.key === '$jsonviews' || !isRecord(value) || !Object.prototype.hasOwnProperty.call(value, segment.key))
|
|
540
|
+
return undefined;
|
|
541
|
+
value = value[segment.key];
|
|
542
|
+
path.push(segment.key);
|
|
543
|
+
}
|
|
544
|
+
else if (segment.kind === 'index') {
|
|
545
|
+
if (!Array.isArray(value) || !Object.prototype.hasOwnProperty.call(value, segment.index))
|
|
546
|
+
return undefined;
|
|
547
|
+
value = value[segment.index];
|
|
548
|
+
path.push(segment.index);
|
|
549
|
+
}
|
|
550
|
+
else
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (collectionPath(path) !== currentPath)
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
path: collectionPath(path),
|
|
558
|
+
label: path.length === 0 ? (Array.isArray(root) ? 'Data' : 'Root') : path.map((segment) => typeof segment === 'number' ? String(segment + 1) : friendlyLabel(segment)).join(' › '),
|
|
559
|
+
recordCollection: isRecordCollectionValue(value),
|
|
538
560
|
};
|
|
539
|
-
visit(root, []);
|
|
540
|
-
return options;
|
|
541
561
|
}
|
|
542
562
|
function collectionFields(root, option, schema) {
|
|
543
563
|
if (!option)
|
|
@@ -577,12 +597,11 @@ export function JsonViewAddView({ align = 'start', currentPath, disabled = false
|
|
|
577
597
|
const popoverRef = useRef(null);
|
|
578
598
|
const portalContainer = useJsonViewsPortalContainer();
|
|
579
599
|
const position = useAnchoredPosition(open, triggerRef, popoverRef, 'bottom', align);
|
|
580
|
-
const
|
|
581
|
-
const selectedOption = options.find((option) => option.path === currentPath)
|
|
582
|
-
?? (currentPath === undefined ? options[0] : undefined);
|
|
600
|
+
const selectedOption = useMemo(() => resolveViewPathOption(root, currentPath), [currentPath, root]);
|
|
583
601
|
const unavailableReason = unavailableReasonOverride
|
|
584
602
|
?? (selectedOption ? undefined : 'This location is not document data that a view can target.');
|
|
585
|
-
|
|
603
|
+
// Field discovery reads every record; only the open popover uses it.
|
|
604
|
+
const fields = useMemo(() => open ? collectionFields(root, selectedOption, schema) : [], [open, root, schema, selectedOption]);
|
|
586
605
|
const groupFields = fields.filter((field) => field.kind !== 'array' && field.kind !== 'object');
|
|
587
606
|
const preferredGroup = groupFields.find((field) => field.type === 'select') ?? groupFields[0];
|
|
588
607
|
const reset = () => {
|
|
@@ -4,6 +4,7 @@ import { type ValuePath } from '@script-it/json-views-core';
|
|
|
4
4
|
import { type CompiledJsonViewMetadata } from '@script-it/json-views-core';
|
|
5
5
|
import { type JsonViewSchemaExternalWidgetRenderer } from './schema-value.js';
|
|
6
6
|
import type { JsonValueReplacement } from '@script-it/json-views-core';
|
|
7
|
+
import type { JsonViewsTableCellOptions } from '../structured-data/table-cell-options.js';
|
|
7
8
|
export interface JsonViewJsonEditing {
|
|
8
9
|
canEditViews?: boolean;
|
|
9
10
|
/** Why view creation is visible but unavailable. */
|
|
@@ -40,6 +41,8 @@ export interface JsonViewerProps {
|
|
|
40
41
|
sourceVisible?: boolean;
|
|
41
42
|
onSourceVisibleChange?: (visible: boolean) => void;
|
|
42
43
|
sourceControl?: ReactNode;
|
|
44
|
+
/** Clamps tall table cells and reveals one on selection (default), or lets rows grow to fit. */
|
|
45
|
+
tableCells?: JsonViewsTableCellOptions;
|
|
43
46
|
uiSize?: UiSize;
|
|
44
47
|
viewsEnabled?: boolean;
|
|
45
48
|
/** CSV is already tabular, irrespective of JSON prediction confidence. */
|
|
@@ -60,7 +60,11 @@ function NodeView({ node, navigateTo, filePath, currentPath, currentJsonPath, cu
|
|
|
60
60
|
}
|
|
61
61
|
if (typeof node === 'string') {
|
|
62
62
|
const content = (_jsx(ExpandedStringValue, { value: node, renderAsMarkdown: renderAsMarkdown, renderMarkdown: renderMarkdown }));
|
|
63
|
-
return atomicEditing ? (_jsx(AtomicValueEditor
|
|
63
|
+
return atomicEditing ? (_jsx(AtomicValueEditor
|
|
64
|
+
// Shown as Markdown, the string is a document: Enter breaks the line.
|
|
65
|
+
, {
|
|
66
|
+
// Shown as Markdown, the string is a document: Enter breaks the line.
|
|
67
|
+
enterKey: renderAsMarkdown && renderMarkdown ? 'newline' : undefined, label: currentLabel, multiline: node.includes('\n'), onCommit: (nextValue) => atomicEditing.commit(currentJsonPath, nextValue), saving: atomicEditing.saving, value: node, children: content })) : content;
|
|
64
68
|
}
|
|
65
69
|
const content = (_jsx("div", { className: "whitespace-pre-wrap text-sm text-foreground", style: { overflowWrap: 'anywhere' }, children: sourceLiteral !== undefined ? _jsx(JsonSourceNumber, { literal: sourceLiteral }) : String(node) }));
|
|
66
70
|
return atomicEditing && isEditableAtomicValue(node) ? (_jsx(AtomicValueEditor, { label: currentLabel, onCommit: (nextValue) => atomicEditing.commit(currentJsonPath, nextValue), saving: atomicEditing.saving, value: node, children: content })) : content;
|
|
@@ -210,6 +214,6 @@ function ObjectView({ obj, navigateTo, currentJsonPath, atomicEditing, renderMar
|
|
|
210
214
|
if (!atomicEditing || atomicEditing.canReplace?.(jsonPath) === false) {
|
|
211
215
|
return (_jsx(StructuredValueCellFrame, { actions: expandAction, children: content }));
|
|
212
216
|
}
|
|
213
|
-
return (_jsx(AtomicValueEditor, { actions: expandAction, label: k, multiline: true, onCommit: (nextValue) => atomicEditing.commit(jsonPath, nextValue), saving: atomicEditing.saving, value: v, children: content }));
|
|
217
|
+
return (_jsx(AtomicValueEditor, { actions: expandAction, enterKey: "newline", label: k, multiline: true, onCommit: (nextValue) => atomicEditing.commit(jsonPath, nextValue), saving: atomicEditing.saving, value: v, children: content }));
|
|
214
218
|
})() : (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, label: k, navigateTo: () => navigateTo(k, [k]), path: [...currentJsonPath, k], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: v })) })] }, k))) }) }) }) }));
|
|
215
219
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** What a plain Enter does in a multiline control: save the value, or break the line. */
|
|
2
|
+
export type EnterKeyBehavior = 'submit' | 'newline';
|
|
3
|
+
/** Structural, so React's synthetic events and native events both fit. */
|
|
4
|
+
export interface EnterKeyEvent {
|
|
5
|
+
key: string;
|
|
6
|
+
ctrlKey: boolean;
|
|
7
|
+
metaKey: boolean;
|
|
8
|
+
shiftKey: boolean;
|
|
9
|
+
isComposing?: boolean;
|
|
10
|
+
keyCode?: number;
|
|
11
|
+
nativeEvent?: {
|
|
12
|
+
isComposing?: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** An IME is confirming composed text, so the Enter belongs to the IME.
|
|
16
|
+
* Safari reports that keydown with keyCode 229 instead of `isComposing`. */
|
|
17
|
+
export declare function isComposing(event: EnterKeyEvent): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a keydown saves the editor it happened in. A single-line control
|
|
20
|
+
* saves on Enter. In a multiline control Cmd/Ctrl+Enter always saves,
|
|
21
|
+
* Shift+Enter never does (the browser inserts the line break), and a plain
|
|
22
|
+
* Enter saves only when the editor's behavior is `'submit'`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function submitsOnEnter(event: EnterKeyEvent, { multiline, enterKey }: {
|
|
25
|
+
multiline: boolean;
|
|
26
|
+
enterKey: EnterKeyBehavior;
|
|
27
|
+
}): boolean;
|
|
28
|
+
/** Soft keyboards have no Shift+Enter, so on mobile Return always breaks the line. */
|
|
29
|
+
export declare function useEnterKeyBehavior(preferred?: EnterKeyBehavior): EnterKeyBehavior;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useJsonViewsDevice } from '../browser-device.js';
|
|
2
|
+
/** An IME is confirming composed text, so the Enter belongs to the IME.
|
|
3
|
+
* Safari reports that keydown with keyCode 229 instead of `isComposing`. */
|
|
4
|
+
export function isComposing(event) {
|
|
5
|
+
return event.isComposing === true || event.nativeEvent?.isComposing === true || event.keyCode === 229;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether a keydown saves the editor it happened in. A single-line control
|
|
9
|
+
* saves on Enter. In a multiline control Cmd/Ctrl+Enter always saves,
|
|
10
|
+
* Shift+Enter never does (the browser inserts the line break), and a plain
|
|
11
|
+
* Enter saves only when the editor's behavior is `'submit'`.
|
|
12
|
+
*/
|
|
13
|
+
export function submitsOnEnter(event, { multiline, enterKey }) {
|
|
14
|
+
if (event.key !== 'Enter' || isComposing(event))
|
|
15
|
+
return false;
|
|
16
|
+
if (!multiline)
|
|
17
|
+
return true;
|
|
18
|
+
if (event.metaKey || event.ctrlKey)
|
|
19
|
+
return true;
|
|
20
|
+
if (event.shiftKey)
|
|
21
|
+
return false;
|
|
22
|
+
return enterKey === 'submit';
|
|
23
|
+
}
|
|
24
|
+
/** Soft keyboards have no Shift+Enter, so on mobile Return always breaks the line. */
|
|
25
|
+
export function useEnterKeyBehavior(preferred = 'submit') {
|
|
26
|
+
return useJsonViewsDevice() === 'mobile' ? 'newline' : preferred;
|
|
27
|
+
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { Check, ChevronDown, Palette } from 'lucide-react';
|
|
4
4
|
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { isComposing } from '../lib/enter-key.js';
|
|
5
6
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from './dropdown-menu.js';
|
|
6
7
|
import { OptionPill, optionColorsForValues } from './option-pill.js';
|
|
7
8
|
const OPTION_COLORS = [
|
|
@@ -59,7 +60,7 @@ export const ChoiceControl = React.forwardRef(({ mode, value, onValueChange, opt
|
|
|
59
60
|
choose(nextValue);
|
|
60
61
|
};
|
|
61
62
|
const handleQueryKeyDown = (event) => {
|
|
62
|
-
if (event.key === 'Enter' && canCreate) {
|
|
63
|
+
if (event.key === 'Enter' && canCreate && !isComposing(event)) {
|
|
63
64
|
event.preventDefault();
|
|
64
65
|
createOption();
|
|
65
66
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { type EnterKeyBehavior } from '../lib/enter-key.js';
|
|
2
3
|
import type { AtomicValue } from './atomic-value.js';
|
|
3
4
|
interface AtomicValueEditorProps {
|
|
4
5
|
actions?: ReactNode;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
className?: string;
|
|
7
8
|
editorClassName?: string;
|
|
9
|
+
/** Long-form text keeps Enter for line breaks and saves on Cmd/Ctrl+Enter. */
|
|
10
|
+
enterKey?: EnterKeyBehavior;
|
|
8
11
|
label: string;
|
|
9
12
|
multiline?: boolean;
|
|
10
13
|
onCommit: (value: AtomicValue) => Promise<void>;
|
|
@@ -17,7 +20,7 @@ export declare function EditValueAction({ disabled, label, onClick, }: {
|
|
|
17
20
|
label: string;
|
|
18
21
|
onClick: () => void;
|
|
19
22
|
}): import("react").JSX.Element;
|
|
20
|
-
export declare function AtomicValueEditor({ actions, children, className, editorClassName, label, multiline, onCommit, preserveLayout, saving, value, }: AtomicValueEditorProps): import("react").JSX.Element;
|
|
23
|
+
export declare function AtomicValueEditor({ actions, children, className, editorClassName, enterKey, label, multiline, onCommit, preserveLayout, saving, value, }: AtomicValueEditorProps): import("react").JSX.Element;
|
|
21
24
|
export declare function StructuredValueCellFrame({ activationDisabled, activationLabel, activationRole, actions, children, leading, className, onActivate, overlayActions, }: {
|
|
22
25
|
activationDisabled?: boolean;
|
|
23
26
|
activationLabel?: string;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
3
3
|
import { Pencil } from 'lucide-react';
|
|
4
4
|
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { submitsOnEnter, useEnterKeyBehavior } from '../lib/enter-key.js';
|
|
5
6
|
import { StructuredCellFillContext } from './structured-cell-fill-context.js';
|
|
6
7
|
import { useEditBaseChanged } from './edit-base.js';
|
|
7
8
|
const CONTROL_CLASS = 'min-h-8 w-full min-w-0 rounded-md border border-border bg-background px-2 [font:inherit] text-foreground outline-none transition-colors placeholder:text-muted-foreground hover:border-input focus:border-ring focus:ring-2 focus:ring-inset focus:ring-ring/20';
|
|
@@ -11,7 +12,7 @@ export function EditValueAction({ disabled = false, label, onClick, }) {
|
|
|
11
12
|
onClick();
|
|
12
13
|
}, children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }));
|
|
13
14
|
}
|
|
14
|
-
export function AtomicValueEditor({ actions, children, className, editorClassName, label, multiline = false, onCommit, preserveLayout = false, saving = false, value, }) {
|
|
15
|
+
export function AtomicValueEditor({ actions, children, className, editorClassName, enterKey, label, multiline = false, onCommit, preserveLayout = false, saving = false, value, }) {
|
|
15
16
|
const [editing, setEditing] = useState(false);
|
|
16
17
|
const fillCell = useContext(StructuredCellFillContext);
|
|
17
18
|
const frameRef = useRef(null);
|
|
@@ -29,7 +30,7 @@ export function AtomicValueEditor({ actions, children, className, editorClassNam
|
|
|
29
30
|
setEditing(false);
|
|
30
31
|
};
|
|
31
32
|
if (editing) {
|
|
32
|
-
const editor = (_jsx(AtomicValueDirectEditor, { controlClassName: editorClassName, label: label, disabled: saving, minHeight: editorMinHeight, multiline: multiline, onCancel: stopEditing, onCommit: async (nextValue) => {
|
|
33
|
+
const editor = (_jsx(AtomicValueDirectEditor, { controlClassName: editorClassName, enterKey: enterKey, label: label, disabled: saving, minHeight: editorMinHeight, multiline: multiline, onCancel: stopEditing, onCommit: async (nextValue) => {
|
|
33
34
|
await onCommit(nextValue);
|
|
34
35
|
stopEditing();
|
|
35
36
|
}, value: value }));
|
|
@@ -65,16 +66,21 @@ export function StructuredValueCellFrame({ activationDisabled = false, activatio
|
|
|
65
66
|
// boundary of its column unmistakable.
|
|
66
67
|
fillCell ? 'h-full items-center rounded-none py-2 pl-3 pr-0' : 'rounded-md px-1.5', onActivate && !activationDisabled
|
|
67
68
|
? fillCell
|
|
68
|
-
? 'before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-primary/10 dark:hover:before:bg-primary/25'
|
|
69
|
+
? 'before:pointer-events-none before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-primary/10 dark:hover:before:bg-primary/25'
|
|
69
70
|
: 'hover:bg-primary/10 dark:hover:bg-primary/25'
|
|
70
71
|
: fillCell
|
|
71
|
-
? 'before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-accent/70'
|
|
72
|
+
? 'before:pointer-events-none before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-accent/70'
|
|
72
73
|
: 'hover:bg-accent/70', fillCell && onActivate && !activationDisabled && 'cursor-text', className), onClick: (event) => {
|
|
73
74
|
// The table cell's padding belongs to the same action as its value.
|
|
74
75
|
// Child actions keep their own handlers and must not activate twice.
|
|
75
76
|
if (fillCell && event.target === event.currentTarget && !activationDisabled)
|
|
76
77
|
onActivate?.();
|
|
77
|
-
}, children: [leading && _jsx("span", { className: "mr-1.5 flex min-h-7 shrink-0 items-center", children: leading }), _jsx("div", { "data-id": onActivate ? 'atomic-edit-value' : undefined, "aria-disabled": onActivate ? activationDisabled : undefined, "aria-label": onActivate ? activationLabel : undefined, role: onActivate ? activationRole : undefined, tabIndex: onActivate && !activationDisabled ? 0 : undefined, title: onActivate ? activationLabel : undefined, className: cn('flex min-h-7 min-w-0 flex-1 items-center',
|
|
78
|
+
}, children: [leading && _jsx("span", { className: "mr-1.5 flex min-h-7 shrink-0 items-center", children: leading }), _jsx("div", { "data-id": onActivate ? 'atomic-edit-value' : undefined, "aria-disabled": onActivate ? activationDisabled : undefined, "aria-label": onActivate ? activationLabel : undefined, role: onActivate ? activationRole : undefined, tabIndex: onActivate && !activationDisabled ? 0 : undefined, title: onActivate ? activationLabel : undefined, className: cn('flex min-h-7 min-w-0 flex-1 items-center',
|
|
79
|
+
// Auto margins centre a value like `items-center` does, but drop
|
|
80
|
+
// to zero when a clamped table cell is shorter than the value, so
|
|
81
|
+
// it starts at the top instead of losing its first lines above
|
|
82
|
+
// the clip.
|
|
83
|
+
fillCell && 'self-stretch *:my-auto',
|
|
78
84
|
// The actions overlay the trailing edge, so the value ends before
|
|
79
85
|
// their lane: truncation stops short of the chip rather than being
|
|
80
86
|
// covered by it. Reserved unconditionally — sizing it on hover
|
|
@@ -113,7 +119,7 @@ export function StructuredValueCellFrame({ activationDisabled = false, activatio
|
|
|
113
119
|
// chip belongs beside its first line.
|
|
114
120
|
overlayActions ? 'top-2 right-2 rounded-md bg-background/90 shadow-sm' : fillCell ? 'top-1/2 -translate-y-1/2' : 'top-0.5'), children: actions }))] }));
|
|
115
121
|
}
|
|
116
|
-
function AtomicValueDirectEditor({ controlClassName, disabled = false, label, minHeight, multiline, onCancel, onCommit, value, }) {
|
|
122
|
+
function AtomicValueDirectEditor({ controlClassName, disabled = false, enterKey: preferredEnterKey, label, minHeight, multiline, onCancel, onCommit, value, }) {
|
|
117
123
|
const [draft, setDraft] = useState(typeof value === 'boolean' ? value : String(value));
|
|
118
124
|
const [error, setError] = useState();
|
|
119
125
|
const containerRef = useRef(null);
|
|
@@ -122,6 +128,7 @@ function AtomicValueDirectEditor({ controlClassName, disabled = false, label, mi
|
|
|
122
128
|
const originalValueRef = useRef(value);
|
|
123
129
|
const attemptedCommitRef = useRef(false);
|
|
124
130
|
const baseChanged = useEditBaseChanged();
|
|
131
|
+
const enterKey = useEnterKeyBehavior(preferredEnterKey);
|
|
125
132
|
useEffect(() => {
|
|
126
133
|
containerRef.current?.querySelector('input, textarea')?.focus({ preventScroll: multiline });
|
|
127
134
|
}, [multiline]);
|
|
@@ -130,7 +137,9 @@ function AtomicValueDirectEditor({ controlClassName, disabled = false, label, mi
|
|
|
130
137
|
return;
|
|
131
138
|
const textarea = textareaRef.current;
|
|
132
139
|
textarea.style.height = 'auto';
|
|
133
|
-
|
|
140
|
+
// A border-box height also holds the borders `scrollHeight` leaves out.
|
|
141
|
+
const borders = textarea.offsetHeight - textarea.clientHeight;
|
|
142
|
+
textarea.style.height = `${Math.max(minHeight ?? 0, textarea.scrollHeight + borders)}px`;
|
|
134
143
|
}, [draft, minHeight, multiline]);
|
|
135
144
|
const commit = async (nextBoolean) => {
|
|
136
145
|
if (committingRef.current)
|
|
@@ -182,8 +191,7 @@ function AtomicValueDirectEditor({ controlClassName, disabled = false, label, mi
|
|
|
182
191
|
onCancel();
|
|
183
192
|
return;
|
|
184
193
|
}
|
|
185
|
-
|
|
186
|
-
if (event.key === 'Enter' && (!isTextarea || event.metaKey || event.ctrlKey)) {
|
|
194
|
+
if (submitsOnEnter(event, { multiline: event.target instanceof HTMLTextAreaElement, enterKey })) {
|
|
187
195
|
event.preventDefault();
|
|
188
196
|
void commit();
|
|
189
197
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** How table cells treat content taller than the cell height limit. */
|
|
2
|
+
export interface JsonViewsTableCellOptions {
|
|
3
|
+
/** `'clamp'` clips each cell at the maximum height; `'grow'` lets rows fit
|
|
4
|
+
* their tallest value. @default 'clamp' */
|
|
5
|
+
overflow?: 'clamp' | 'grow';
|
|
6
|
+
/** Pixels, or any CSS length. Sets `--json-views-table-cell-max-height`
|
|
7
|
+
* on the table, overriding host CSS for this viewer. @default '9rem' */
|
|
8
|
+
maxHeight?: number | string;
|
|
9
|
+
/** How a clamped cell shows the rest of its content: `'select'` floats it
|
|
10
|
+
* over the rows below while the cell is selected; `'none'` keeps it
|
|
11
|
+
* clipped. @default 'select' */
|
|
12
|
+
reveal?: 'select' | 'none';
|
|
13
|
+
}
|
|
14
|
+
/** Provided by `JsonViewer`, so every table it renders shares one policy. */
|
|
15
|
+
export declare const TableCellOptionsContext: import("react").Context<JsonViewsTableCellOptions | undefined>;
|
|
16
|
+
export declare function resolveTableCellOptions(options: JsonViewsTableCellOptions | undefined): {
|
|
17
|
+
clamp: boolean;
|
|
18
|
+
maxHeight: string | undefined;
|
|
19
|
+
reveal: boolean;
|
|
20
|
+
};
|