@script-it/json-views-react 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +301 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/browser-device.d.ts +21 -0
- package/dist/browser-device.js +22 -0
- package/dist/file-content-edit-state.d.ts +10 -0
- package/dist/file-content-edit-state.js +1 -0
- package/dist/file-content-view.d.ts +8 -0
- package/dist/file-content-view.js +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/json/copy-json.d.ts +15 -0
- package/dist/json/copy-json.js +52 -0
- package/dist/json/html-preview.d.ts +5 -0
- package/dist/json/html-preview.js +12 -0
- package/dist/json/html-view/native-control.d.ts +44 -0
- package/dist/json/html-view/native-control.js +96 -0
- package/dist/json/html-view/settings.d.ts +2 -0
- package/dist/json/html-view/settings.js +58 -0
- package/dist/json/html-view/template.d.ts +12 -0
- package/dist/json/html-view/template.js +55 -0
- package/dist/json/html-view.d.ts +9 -0
- package/dist/json/html-view.js +163 -0
- package/dist/json/json-content.d.ts +61 -0
- package/dist/json/json-content.js +433 -0
- package/dist/json/json-source-editor.d.ts +36 -0
- package/dist/json/json-source-editor.js +203 -0
- package/dist/json/json-view.d.ts +3 -0
- package/dist/json/json-view.js +322 -0
- package/dist/json/kanban-view.d.ts +20 -0
- package/dist/json/kanban-view.js +328 -0
- package/dist/json/long-text.d.ts +3 -0
- package/dist/json/long-text.js +6 -0
- package/dist/json/record-view.d.ts +38 -0
- package/dist/json/record-view.js +85 -0
- package/dist/json/row-mutations.d.ts +7 -0
- package/dist/json/row-mutations.js +93 -0
- package/dist/json/schema-value.d.ts +57 -0
- package/dist/json/schema-value.js +294 -0
- package/dist/json/source-default.d.ts +1 -0
- package/dist/json/source-default.js +1 -0
- package/dist/json/source-edit-capabilities.d.ts +8 -0
- package/dist/json/source-edit-capabilities.js +22 -0
- package/dist/json/source-literals.d.ts +15 -0
- package/dist/json/source-literals.js +24 -0
- package/dist/json/source-recovery.d.ts +32 -0
- package/dist/json/source-recovery.js +38 -0
- package/dist/json/source-selection.d.ts +3 -0
- package/dist/json/source-selection.js +16 -0
- package/dist/json/table-view.d.ts +20 -0
- package/dist/json/table-view.js +300 -0
- package/dist/json/use-kanban-card-drag.d.ts +13 -0
- package/dist/json/use-kanban-card-drag.js +255 -0
- package/dist/json/use-kanban-column-drag.d.ts +14 -0
- package/dist/json/use-kanban-column-drag.js +179 -0
- package/dist/json/view-diagnostics.d.ts +4 -0
- package/dist/json/view-diagnostics.js +26 -0
- package/dist/json/view-model.d.ts +27 -0
- package/dist/json/view-model.js +180 -0
- package/dist/json/view-save-status.d.ts +12 -0
- package/dist/json/view-save-status.js +38 -0
- package/dist/json/view-settings.d.ts +36 -0
- package/dist/json/view-settings.js +661 -0
- package/dist/json/view-types.d.ts +47 -0
- package/dist/json/view-types.js +4 -0
- package/dist/json/viewers/general/general-json-view.d.ts +46 -0
- package/dist/json/viewers/general/general-json-view.js +163 -0
- package/dist/json/viewers/general/json-flatten.d.ts +33 -0
- package/dist/json/viewers/general/json-flatten.js +51 -0
- package/dist/json/viewers/general/json-node-view.d.ts +43 -0
- package/dist/json/viewers/general/json-node-view.js +215 -0
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +24 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +5 -0
- package/dist/lib/file-data.d.ts +7 -0
- package/dist/lib/file-data.js +5 -0
- package/dist/lib/floating-portal-selectors.d.ts +4 -0
- package/dist/lib/floating-portal-selectors.js +4 -0
- package/dist/lib/time.d.ts +24 -0
- package/dist/lib/time.js +60 -0
- package/dist/lib/ui-size.d.ts +28 -0
- package/dist/lib/ui-size.js +39 -0
- package/dist/lib/use-anchored-position.d.ts +5 -0
- package/dist/lib/use-anchored-position.js +65 -0
- package/dist/lib/use-dismiss.d.ts +31 -0
- package/dist/lib/use-dismiss.js +65 -0
- package/dist/primitives/boolean-toggle.d.ts +10 -0
- package/dist/primitives/boolean-toggle.js +11 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.js +34 -0
- package/dist/primitives/checkbox.d.ts +4 -0
- package/dist/primitives/checkbox.js +8 -0
- package/dist/primitives/choice-control.d.ts +29 -0
- package/dist/primitives/choice-control.js +131 -0
- package/dist/primitives/dialog.d.ts +29 -0
- package/dist/primitives/dialog.js +25 -0
- package/dist/primitives/dropdown-menu.d.ts +37 -0
- package/dist/primitives/dropdown-menu.js +39 -0
- package/dist/primitives/inline-feedback-action.d.ts +13 -0
- package/dist/primitives/inline-feedback-action.js +6 -0
- package/dist/primitives/menu-select.d.ts +12 -0
- package/dist/primitives/menu-select.js +7 -0
- package/dist/primitives/multi-select.d.ts +18 -0
- package/dist/primitives/multi-select.js +7 -0
- package/dist/primitives/number-input.d.ts +10 -0
- package/dist/primitives/number-input.js +8 -0
- package/dist/primitives/option-pill.d.ts +13 -0
- package/dist/primitives/option-pill.js +50 -0
- package/dist/primitives/tabs.d.ts +7 -0
- package/dist/primitives/tabs.js +12 -0
- package/dist/primitives/textarea.d.ts +4 -0
- package/dist/primitives/textarea.js +8 -0
- package/dist/primitives/tooltip.d.ts +31 -0
- package/dist/primitives/tooltip.js +37 -0
- package/dist/standalone.css +113 -0
- package/dist/structured-data/atomic-value-editor.d.ts +33 -0
- package/dist/structured-data/atomic-value-editor.js +195 -0
- package/dist/structured-data/atomic-value.d.ts +3 -0
- package/dist/structured-data/atomic-value.js +6 -0
- package/dist/structured-data/column-controls.d.ts +21 -0
- package/dist/structured-data/column-controls.js +180 -0
- package/dist/structured-data/copy-json-action.d.ts +10 -0
- package/dist/structured-data/copy-json-action.js +87 -0
- package/dist/structured-data/delete-row-action.d.ts +10 -0
- package/dist/structured-data/delete-row-action.js +37 -0
- package/dist/structured-data/edit-base.d.ts +3 -0
- package/dist/structured-data/edit-base.js +8 -0
- package/dist/structured-data/filter-value.d.ts +2 -0
- package/dist/structured-data/filter-value.js +28 -0
- package/dist/structured-data/row-selection-icon.d.ts +9 -0
- package/dist/structured-data/row-selection-icon.js +6 -0
- package/dist/structured-data/structured-cell-fill-context.d.ts +6 -0
- package/dist/structured-data/structured-cell-fill-context.js +7 -0
- package/dist/structured-data/structured-data-content.d.ts +1 -0
- package/dist/structured-data/structured-data-content.js +1 -0
- package/dist/structured-data/table-types.d.ts +108 -0
- package/dist/structured-data/table-types.js +1 -0
- package/dist/structured-data/use-metadata-persistence.d.ts +15 -0
- package/dist/structured-data/use-metadata-persistence.js +18 -0
- package/dist/structured-data/use-optimistic-text-document.d.ts +22 -0
- package/dist/structured-data/use-optimistic-text-document.js +30 -0
- package/dist/structured-data/value-cell.d.ts +15 -0
- package/dist/structured-data/value-cell.js +81 -0
- package/dist/styles.css +2 -0
- package/dist/surface.d.ts +14 -0
- package/dist/surface.js +73 -0
- package/dist/tabular-data-view.d.ts +5 -0
- package/dist/tabular-data-view.js +493 -0
- package/dist/viewer-state.d.ts +72 -0
- package/dist/viewer-state.js +79 -0
- package/dist/widget-registry.d.ts +60 -0
- package/dist/widget-registry.js +126 -0
- package/dist/widgets/boolean-widget.d.ts +2 -0
- package/dist/widgets/boolean-widget.js +15 -0
- package/dist/widgets/date-picker-widget.d.ts +18 -0
- package/dist/widgets/date-picker-widget.js +265 -0
- package/dist/widgets/dropdown-widget.d.ts +10 -0
- package/dist/widgets/dropdown-widget.js +19 -0
- package/dist/widgets/multi-select-widget.d.ts +11 -0
- package/dist/widgets/multi-select-widget.js +46 -0
- package/dist/widgets/number-widget.d.ts +2 -0
- package/dist/widgets/number-widget.js +19 -0
- package/dist/widgets/read-only-field.d.ts +36 -0
- package/dist/widgets/read-only-field.js +39 -0
- package/dist/widgets/text-widget.d.ts +2 -0
- package/dist/widgets/text-widget.js +19 -0
- package/dist/widgets/textarea-widget.d.ts +2 -0
- package/dist/widgets/textarea-widget.js +34 -0
- package/dist/widgets/types.d.ts +53 -0
- package/dist/widgets/types.js +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type JsonViewsDevice = 'mobile' | 'desktop';
|
|
3
|
+
type BrowserIdentity = {
|
|
4
|
+
userAgent: string;
|
|
5
|
+
userAgentData?: {
|
|
6
|
+
mobile: boolean;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Use browser identity rather than width or touch support so resizing an embed
|
|
11
|
+
* doesn't switch interaction models. iPad desktop mode intentionally stays desktop;
|
|
12
|
+
* hosts that need a different policy can override it with JsonViewsDeviceProvider.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectBrowserDevice(browser?: BrowserIdentity): JsonViewsDevice;
|
|
15
|
+
/** Optional host override, useful for previews; all viewer surfaces and portals share it. */
|
|
16
|
+
export declare function JsonViewsDeviceProvider({ device, children }: {
|
|
17
|
+
device?: JsonViewsDevice;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
export declare function useJsonViewsDevice(): JsonViewsDevice;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Use browser identity rather than width or touch support so resizing an embed
|
|
5
|
+
* doesn't switch interaction models. iPad desktop mode intentionally stays desktop;
|
|
6
|
+
* hosts that need a different policy can override it with JsonViewsDeviceProvider.
|
|
7
|
+
*/
|
|
8
|
+
export function detectBrowserDevice(browser) {
|
|
9
|
+
if (!browser)
|
|
10
|
+
return 'desktop';
|
|
11
|
+
const mobile = browser.userAgentData?.mobile
|
|
12
|
+
?? /Android|iPhone|iPad|iPod|Mobile/i.test(browser.userAgent);
|
|
13
|
+
return mobile ? 'mobile' : 'desktop';
|
|
14
|
+
}
|
|
15
|
+
const DeviceContext = createContext(undefined);
|
|
16
|
+
/** Optional host override, useful for previews; all viewer surfaces and portals share it. */
|
|
17
|
+
export function JsonViewsDeviceProvider({ device, children }) {
|
|
18
|
+
return _jsx(DeviceContext.Provider, { value: device, children: children });
|
|
19
|
+
}
|
|
20
|
+
export function useJsonViewsDevice() {
|
|
21
|
+
return useContext(DeviceContext) ?? detectBrowserDevice(typeof navigator === 'undefined' ? undefined : navigator);
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonDocumentSave } from '@script-it/json-views-core';
|
|
2
|
+
/** Capabilities and controlled state supplied by an editable file host. */
|
|
3
|
+
export interface FileContentEditState {
|
|
4
|
+
isEditing: boolean;
|
|
5
|
+
editContent: string;
|
|
6
|
+
onEditChange: (content: string) => void;
|
|
7
|
+
isSaving?: boolean;
|
|
8
|
+
/** Persists a renderer-generated version of the complete file. */
|
|
9
|
+
onCommitContent?: JsonDocumentSave;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FileContentData } from './lib/file-data.js';
|
|
2
|
+
import type { FileContentEditState } from './file-content-edit-state.js';
|
|
3
|
+
export declare function FileContentView({ data, edit, fillHeight, path, }: {
|
|
4
|
+
data: FileContentData;
|
|
5
|
+
edit?: FileContentEditState;
|
|
6
|
+
fillHeight?: boolean;
|
|
7
|
+
path: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CSVContent, JSONContent } from './json/json-content.js';
|
|
3
|
+
import { fileDataToText } from './lib/file-data.js';
|
|
4
|
+
export function FileContentView({ data, edit, fillHeight, path, }) {
|
|
5
|
+
const props = { content: fileDataToText(data), edit, fillHeight, path };
|
|
6
|
+
return /\.csv$/i.test(path) || data.mimeType === 'text/csv' ? _jsx(CSVContent, { ...props }) : _jsx(JSONContent, { ...props });
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { CSVContent, JSONContent, StructuredDataContent, type CSVContentProps, type JSONContentProps, type JsonViewClearBehavior, type MetadataPersistence, type MetadataPersistenceRequest, type ObjectRootConversionRequest, type StructuredDataContentProps, } from './json/json-content.js';
|
|
2
|
+
export { JsonViewer, type JsonViewJsonEditing } from './json/json-view.js';
|
|
3
|
+
export { JsonViewsProvider, JsonViewWidgetRegistry, createDefaultWidgetRegistry, useJsonViewsRegistries, type JsonViewEditWidget, type JsonViewEditWidgetProps, type JsonViewDisplayWidget, type JsonViewDisplayWidgetProps, type JsonViewWidgetDefinition, type JsonViewWidgetRegistration, type JsonViewsRegistryValue, } from './widget-registry.js';
|
|
4
|
+
export { DateValueDisplay, DateValueEditor } from './widgets/date-picker-widget.js';
|
|
5
|
+
export { InlineFeedbackAction } from './primitives/inline-feedback-action.js';
|
|
6
|
+
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './primitives/dropdown-menu.js';
|
|
7
|
+
export type { FileContentEditState } from './file-content-edit-state.js';
|
|
8
|
+
export type { JsonViewSchemaExternalWidgetProps, JsonViewSchemaExternalWidgetRenderer, } from './json/schema-value.js';
|
|
9
|
+
export { JsonViewsSurface, type JsonViewsTheme } from './surface.js';
|
|
10
|
+
export type { JsonViewsGeneralPresentationState, JsonViewsNavigationPresentationState, JsonViewsPresentationState, JsonViewsTablePresentationState, } from './viewer-state.js';
|
|
11
|
+
export { JsonViewsDeviceProvider, useJsonViewsDevice, type JsonViewsDevice } from './browser-device.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CSVContent, JSONContent, StructuredDataContent, } from './json/json-content.js';
|
|
2
|
+
export { JsonViewer } from './json/json-view.js';
|
|
3
|
+
export { JsonViewsProvider, JsonViewWidgetRegistry, createDefaultWidgetRegistry, useJsonViewsRegistries, } from './widget-registry.js';
|
|
4
|
+
export { DateValueDisplay, DateValueEditor } from './widgets/date-picker-widget.js';
|
|
5
|
+
export { InlineFeedbackAction } from './primitives/inline-feedback-action.js';
|
|
6
|
+
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './primitives/dropdown-menu.js';
|
|
7
|
+
export { JsonViewsSurface } from './surface.js';
|
|
8
|
+
export { JsonViewsDeviceProvider, useJsonViewsDevice } from './browser-device.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValuePath } from '@script-it/json-views-core';
|
|
2
|
+
type SourceLiteralLookup = (path?: ValuePath | null) => string | undefined;
|
|
3
|
+
/** A filename-qualified URI reference whose fragment is an RFC 6901 JSON Pointer. */
|
|
4
|
+
export declare function jsonPathReference(fileName: string, path: ValuePath): string;
|
|
5
|
+
export declare function jsonRowsReferenceText(fileName: string, rows: readonly {
|
|
6
|
+
sourcePath: ValuePath;
|
|
7
|
+
}[]): string;
|
|
8
|
+
/** Pretty JSON for one record while retaining unsafe number tokens from source. */
|
|
9
|
+
export declare function jsonRecordCopyText(value: unknown, sourcePath: ValuePath, sourceLiteral: SourceLiteralLookup): string;
|
|
10
|
+
/** Selected rows are always copied as an array, in their visible table order. */
|
|
11
|
+
export declare function jsonRowsCopyText(rows: readonly {
|
|
12
|
+
value: unknown;
|
|
13
|
+
sourcePath: ValuePath;
|
|
14
|
+
}[], sourceLiteral: SourceLiteralLookup): string;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function jsonPointerToken(segment) {
|
|
2
|
+
return encodeURIComponent(String(segment).replace(/~/g, '~0').replace(/\//g, '~1'));
|
|
3
|
+
}
|
|
4
|
+
/** A filename-qualified URI reference whose fragment is an RFC 6901 JSON Pointer. */
|
|
5
|
+
export function jsonPathReference(fileName, path) {
|
|
6
|
+
const safeFileName = encodeURIComponent(fileName || 'document.json');
|
|
7
|
+
return `${safeFileName}#${path.map((segment) => `/${jsonPointerToken(segment)}`).join('')}`;
|
|
8
|
+
}
|
|
9
|
+
export function jsonRowsReferenceText(fileName, rows) {
|
|
10
|
+
return JSON.stringify(rows.map((row) => jsonPathReference(fileName, row.sourcePath)));
|
|
11
|
+
}
|
|
12
|
+
function serializeJsonValue(value, sourcePath, sourceLiteral, depth) {
|
|
13
|
+
if (value === null)
|
|
14
|
+
return 'null';
|
|
15
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
if (typeof value === 'number') {
|
|
18
|
+
const literal = sourceLiteral(sourcePath);
|
|
19
|
+
if (literal !== undefined)
|
|
20
|
+
return literal;
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
throw new TypeError('This record contains a number that cannot be copied as JSON');
|
|
23
|
+
return Object.is(value, -0) ? '-0' : String(value);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (value.length === 0)
|
|
27
|
+
return '[]';
|
|
28
|
+
const childIndent = ' '.repeat(depth + 1);
|
|
29
|
+
const closingIndent = ' '.repeat(depth);
|
|
30
|
+
return `[\n${value.map((item, index) => `${childIndent}${serializeJsonValue(item, [...sourcePath, index], sourceLiteral, depth + 1)}`).join(',\n')}\n${closingIndent}]`;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'object') {
|
|
33
|
+
const entries = Object.entries(value)
|
|
34
|
+
.filter(([, item]) => item !== undefined);
|
|
35
|
+
if (entries.length === 0)
|
|
36
|
+
return '{}';
|
|
37
|
+
const childIndent = ' '.repeat(depth + 1);
|
|
38
|
+
const closingIndent = ' '.repeat(depth);
|
|
39
|
+
return `{\n${entries.map(([key, item]) => `${childIndent}${JSON.stringify(key)}: ${serializeJsonValue(item, [...sourcePath, key], sourceLiteral, depth + 1)}`).join(',\n')}\n${closingIndent}}`;
|
|
40
|
+
}
|
|
41
|
+
throw new TypeError(`Cannot copy a ${typeof value} value as JSON`);
|
|
42
|
+
}
|
|
43
|
+
/** Pretty JSON for one record while retaining unsafe number tokens from source. */
|
|
44
|
+
export function jsonRecordCopyText(value, sourcePath, sourceLiteral) {
|
|
45
|
+
return serializeJsonValue(value, sourcePath, sourceLiteral, 0);
|
|
46
|
+
}
|
|
47
|
+
/** Selected rows are always copied as an array, in their visible table order. */
|
|
48
|
+
export function jsonRowsCopyText(rows, sourceLiteral) {
|
|
49
|
+
if (rows.length === 0)
|
|
50
|
+
return '[]';
|
|
51
|
+
return `[\n${rows.map((row) => ` ${serializeJsonValue(row.value, row.sourcePath, sourceLiteral, 1)}`).join(',\n')}\n]`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
const PREVIEW_CSP = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'">`;
|
|
3
|
+
function isolatedHtml(source) {
|
|
4
|
+
if (/<head\b[^>]*>/i.test(source))
|
|
5
|
+
return source.replace(/<head\b[^>]*>/i, (head) => `${head}${PREVIEW_CSP}`);
|
|
6
|
+
if (/<html\b[^>]*>/i.test(source))
|
|
7
|
+
return source.replace(/<html\b[^>]*>/i, (html) => `${html}<head>${PREVIEW_CSP}</head>`);
|
|
8
|
+
return `${PREVIEW_CSP}${source}`;
|
|
9
|
+
}
|
|
10
|
+
export function HtmlPreview({ source, title }) {
|
|
11
|
+
return _jsx("iframe", { "data-id": "jsonView-html-preview", title: `${title} HTML preview`, sandbox: "", referrerPolicy: "no-referrer", srcDoc: isolatedHtml(source), className: "h-[min(65vh,720px)] min-h-80 w-full rounded-md border border-border bg-white" });
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ValuePath } from '@script-it/json-views-core';
|
|
2
|
+
import type { JsonViewJsonEditing } from '../view-types.js';
|
|
3
|
+
export declare function NativeHtmlControl({ tag, props, path, value, editing, report }: {
|
|
4
|
+
tag: string;
|
|
5
|
+
props: Record<string, unknown>;
|
|
6
|
+
path: ValuePath;
|
|
7
|
+
value: unknown;
|
|
8
|
+
editing?: JsonViewJsonEditing;
|
|
9
|
+
report: (message: string) => void;
|
|
10
|
+
}): import("react").ReactElement<{
|
|
11
|
+
onFocus: () => void;
|
|
12
|
+
onPointerDown: () => void;
|
|
13
|
+
onChange: (event: {
|
|
14
|
+
target: HTMLInputElement;
|
|
15
|
+
}) => void;
|
|
16
|
+
onBlur: () => void;
|
|
17
|
+
onPointerUp: () => void;
|
|
18
|
+
onKeyUp: () => void;
|
|
19
|
+
onKeyDown: (event: {
|
|
20
|
+
key: string;
|
|
21
|
+
ctrlKey: boolean;
|
|
22
|
+
metaKey: boolean;
|
|
23
|
+
preventDefault: () => void;
|
|
24
|
+
}) => void;
|
|
25
|
+
checked: boolean;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
} | {
|
|
28
|
+
onFocus: () => void;
|
|
29
|
+
onPointerDown: () => void;
|
|
30
|
+
onChange: (event: {
|
|
31
|
+
target: HTMLInputElement;
|
|
32
|
+
}) => void;
|
|
33
|
+
onBlur: () => void;
|
|
34
|
+
onPointerUp: () => void;
|
|
35
|
+
onKeyUp: () => void;
|
|
36
|
+
onKeyDown: (event: {
|
|
37
|
+
key: string;
|
|
38
|
+
ctrlKey: boolean;
|
|
39
|
+
metaKey: boolean;
|
|
40
|
+
preventDefault: () => void;
|
|
41
|
+
}) => void;
|
|
42
|
+
value: string;
|
|
43
|
+
disabled: boolean;
|
|
44
|
+
}, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createElement, useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { EditBaseContext } from '../../structured-data/edit-base.js';
|
|
3
|
+
export function NativeHtmlControl({ tag, props, path, value, editing, report }) {
|
|
4
|
+
const base = useContext(EditBaseContext);
|
|
5
|
+
const [draft, setDraft] = useState(value == null ? '' : String(value));
|
|
6
|
+
const [active, setActive] = useState(false);
|
|
7
|
+
const [savingThisControl, setSavingThisControl] = useState(false);
|
|
8
|
+
const original = useRef({ base, path });
|
|
9
|
+
const pending = useRef(false), changed = useRef(false), draftRef = useRef(draft);
|
|
10
|
+
const type = props.type ?? 'text';
|
|
11
|
+
useEffect(() => { if (!active) {
|
|
12
|
+
const next = value == null ? '' : String(value);
|
|
13
|
+
setDraft(next);
|
|
14
|
+
draftRef.current = next;
|
|
15
|
+
} }, [value, active]);
|
|
16
|
+
const start = () => { if (!active) {
|
|
17
|
+
original.current = { base, path: [...path] };
|
|
18
|
+
setActive(true);
|
|
19
|
+
} };
|
|
20
|
+
const cancel = () => { changed.current = false; setActive(false); draftRef.current = value == null ? '' : String(value); setDraft(draftRef.current); };
|
|
21
|
+
const commit = async (candidate) => {
|
|
22
|
+
if (!editing || pending.current || (!changed.current && candidate === undefined))
|
|
23
|
+
return;
|
|
24
|
+
if (editing.saving) {
|
|
25
|
+
report('Wait for the current save to finish');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (original.current.base !== base || JSON.stringify(original.current.path) !== JSON.stringify(path)) {
|
|
29
|
+
report('This document changed outside this editor. Cancel and reopen it before saving.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let next = candidate ?? draftRef.current;
|
|
33
|
+
if (type === 'number' || type === 'range') {
|
|
34
|
+
if (String(next).trim() === '' || !Number.isFinite(Number(next))) {
|
|
35
|
+
report('Enter a finite number');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
next = Number(next);
|
|
39
|
+
if (props.min !== undefined && Number(next) < Number(props.min) || props.max !== undefined && Number(next) > Number(props.max)) {
|
|
40
|
+
report('Value is outside the control limits');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (type === 'checkbox' && typeof value !== 'boolean' || (type === 'number' || type === 'range') && typeof value !== 'number' || (type === 'text' || tag === 'textarea') && typeof value !== 'string') {
|
|
45
|
+
report('Control type does not match the JSON value');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
pending.current = true;
|
|
49
|
+
setSavingThisControl(true);
|
|
50
|
+
try {
|
|
51
|
+
await editing.replace(original.current.path, next);
|
|
52
|
+
changed.current = false;
|
|
53
|
+
setActive(false);
|
|
54
|
+
report('');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
report(error instanceof Error ? error.message : 'Could not save');
|
|
58
|
+
changed.current = true;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
pending.current = false;
|
|
62
|
+
setSavingThisControl(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return createElement(tag, {
|
|
66
|
+
...props, disabled: !editing || savingThisControl || editing.canReplace?.(path) === false,
|
|
67
|
+
...(type === 'checkbox' ? { checked: value === true } : { value: draft }),
|
|
68
|
+
onFocus: start, onPointerDown: start,
|
|
69
|
+
onChange: (event) => {
|
|
70
|
+
if (type === 'checkbox') {
|
|
71
|
+
original.current = { base, path: [...path] };
|
|
72
|
+
void commit(event.target.checked);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
changed.current = true;
|
|
76
|
+
draftRef.current = event.target.value;
|
|
77
|
+
setDraft(event.target.value);
|
|
78
|
+
},
|
|
79
|
+
onBlur: () => { void commit(); },
|
|
80
|
+
onPointerUp: () => { if (type === 'range')
|
|
81
|
+
void commit(); },
|
|
82
|
+
onKeyUp: () => { if (type === 'range')
|
|
83
|
+
void commit(); },
|
|
84
|
+
onKeyDown: (event) => {
|
|
85
|
+
if (event.key === 'Escape') {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
cancel();
|
|
88
|
+
report('');
|
|
89
|
+
}
|
|
90
|
+
if (event.key === 'Enter' && (tag !== 'textarea' || event.ctrlKey || event.metaKey)) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
void commit();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { ChevronDown, Copy, Settings2, Trash2 } from 'lucide-react';
|
|
5
|
+
import { useJsonViewsPortalContainer } from '../../surface.js';
|
|
6
|
+
import { useAnchoredPosition } from '../../lib/use-anchored-position.js';
|
|
7
|
+
import { useDismiss } from '../../lib/use-dismiss.js';
|
|
8
|
+
const ICON_BUTTON_CLASS = '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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97]';
|
|
9
|
+
const TEXTAREA_CLASS = 'h-32 w-full resize-y rounded-md border border-input bg-background p-2 font-mono text-xs text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring';
|
|
10
|
+
function EditorSection({ children, summary, title }) {
|
|
11
|
+
return _jsxs("details", { className: "group border-t border-border", children: [_jsxs("summary", { className: "flex min-h-10 cursor-pointer list-none items-center gap-2 rounded-md px-1 text-xs hover:bg-accent", children: [_jsx("span", { className: "w-20 shrink-0 font-medium text-foreground", children: title }), _jsx("span", { className: "min-w-0 flex-1 truncate text-right text-muted-foreground", children: summary }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-150 group-open:rotate-180" })] }), _jsx("div", { className: "px-1 pb-3 pt-1", children: children })] });
|
|
12
|
+
}
|
|
13
|
+
export function HtmlViewSettings({ rawView, onSave, onPreview, onDelete, onDuplicate, disabled, saveUnavailableReason }) {
|
|
14
|
+
const [open, setOpen] = useState(false), [draft, setDraft] = useState(rawView), [base, setBase] = useState(JSON.stringify(rawView)), [saving, setSaving] = useState(false), [error, setError] = useState(''), [confirmDelete, setConfirmDelete] = useState(false);
|
|
15
|
+
const anchor = useRef(null), content = useRef(null), portal = useJsonViewsPortalContainer();
|
|
16
|
+
const position = useAnchoredPosition(open, anchor, content, 'bottom', 'end');
|
|
17
|
+
const close = () => { onPreview?.(undefined); setOpen(false); setConfirmDelete(false); };
|
|
18
|
+
useDismiss({ enabled: open && !saving, onDismiss: close, insideRefs: [anchor, content] });
|
|
19
|
+
const apply = async () => {
|
|
20
|
+
if (base !== JSON.stringify(rawView)) {
|
|
21
|
+
setError('View settings changed outside this editor. Cancel and reopen before applying.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!String(draft.name ?? '').trim() || !String(draft.html ?? '').trim()) {
|
|
25
|
+
setError('Name and HTML are required');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setSaving(true);
|
|
29
|
+
setError('');
|
|
30
|
+
try {
|
|
31
|
+
await onSave(draft);
|
|
32
|
+
close();
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
setError(cause instanceof Error ? cause.message : 'Could not save');
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setSaving(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const runAction = async (action) => {
|
|
42
|
+
if (!action)
|
|
43
|
+
return;
|
|
44
|
+
setSaving(true);
|
|
45
|
+
setError('');
|
|
46
|
+
try {
|
|
47
|
+
await action();
|
|
48
|
+
close();
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
setError(cause instanceof Error ? cause.message : 'Could not update this view');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
setSaving(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return _jsxs(_Fragment, { children: [_jsx("button", { ref: anchor, type: "button", "aria-label": "View options", disabled: disabled, className: ICON_BUTTON_CLASS, onClick: () => { setDraft(rawView); setBase(JSON.stringify(rawView)); setError(''); setConfirmDelete(false); setOpen(true); }, children: _jsx(Settings2, { className: "h-3.5 w-3.5" }) }), open && portal && createPortal(_jsxs("section", { ref: content, "aria-label": "HTML view options", "data-id": "view-settings", "data-anchored-popup": true, style: position, className: "z-[100] w-[min(26rem,calc(100vw-2rem))] overflow-y-auto rounded-lg border border-border bg-card p-2 text-xs text-foreground shadow-lg [transform-origin:top_right]", children: [_jsx("header", { className: "flex min-h-10 items-center gap-2 px-1 pb-2", children: _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-medium", children: "View options" }), _jsxs("div", { className: "mt-1 flex min-w-0 items-center gap-0.5", children: [_jsx("input", { "aria-label": "View name", className: "h-7 min-w-0 flex-1 rounded-sm bg-transparent px-1 text-xs text-muted-foreground outline-none hover:bg-accent focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-ring", value: String(draft.name ?? ''), onChange: event => setDraft({ ...draft, name: event.target.value }) }), onDuplicate && _jsx("button", { type: "button", "aria-label": "Duplicate view", title: "Duplicate view", disabled: saving, className: ICON_BUTTON_CLASS, onClick: () => { void runAction(onDuplicate); }, children: _jsx(Copy, { className: "h-3.5 w-3.5" }) }), onDelete && _jsx("button", { type: "button", "aria-label": "Delete view", title: "Delete view", disabled: saving, className: `${ICON_BUTTON_CLASS} text-destructive hover:bg-destructive/10 hover:text-destructive`, onClick: () => setConfirmDelete(true), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] })] }) }), confirmDelete && onDelete && _jsxs("div", { className: "mb-2 flex items-center gap-2 rounded-md bg-destructive/10 px-2 py-2 text-destructive", children: [_jsx("span", { className: "min-w-0 flex-1", children: "Delete this view?" }), _jsx("button", { type: "button", className: "rounded-md px-2 py-1 hover:bg-accent", onClick: () => setConfirmDelete(false), children: "Cancel" }), _jsx("button", { type: "button", "data-id": "jsonView-confirm-delete-view", disabled: saving, className: "rounded-md bg-destructive px-2 py-1 text-destructive-foreground disabled:opacity-50", onClick: () => { void runAction(onDelete); }, children: "Delete" })] }), _jsx(EditorSection, { title: "HTML", summary: "Template", children: _jsx("textarea", { "aria-label": "View HTML", spellCheck: false, className: TEXTAREA_CLASS, value: String(draft.html ?? ''), onChange: event => setDraft({ ...draft, html: event.target.value }) }) }), _jsx(EditorSection, { title: "CSS", summary: String(draft.css ?? '').trim() ? 'Custom styles' : 'None', children: _jsx("textarea", { "aria-label": "View CSS", spellCheck: false, className: TEXTAREA_CLASS, value: String(draft.css ?? ''), onChange: event => setDraft({ ...draft, css: event.target.value }) }) }), (error || saveUnavailableReason) && _jsx("p", { role: "alert", className: "px-1 py-2 text-destructive", children: error || saveUnavailableReason }), _jsxs("footer", { className: "flex justify-end gap-1 border-t border-border px-1 pt-2", children: [_jsx("button", { type: "button", className: "h-8 rounded-md px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: () => onPreview?.(draft), disabled: saving, children: "Preview" }), _jsx("button", { type: "button", className: "h-8 rounded-md px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: close, disabled: saving, children: "Cancel" }), _jsx("button", { type: "button", "data-id": "jsonView-save-view", className: "h-8 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50", onClick: () => { void apply(); }, disabled: saving || disabled || Boolean(saveUnavailableReason), children: saving ? 'Saving…' : 'Save view' })] })] }), portal)] });
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface HtmlTemplateNode {
|
|
2
|
+
tag?: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
attrs: Record<string, string>;
|
|
5
|
+
children: HtmlTemplateNode[];
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const boundAttributes: Set<string>;
|
|
9
|
+
export declare function parseHtmlTemplate(doc: Document, html: string): {
|
|
10
|
+
nodes: HtmlTemplateNode[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const tags = new Set('main article section header footer nav aside div span h1 h2 h3 h4 h5 h6 p strong em b i small label ul ol li dl dt dd table caption thead tbody tfoot tr td th br hr input textarea progress meter button svg g path circle ellipse rect line polyline polygon text tspan title desc img jv-value jv-field jv-repeat'.split(' '));
|
|
2
|
+
const dangerous = new Set('script iframe object embed base meta link form foreignobject animate animatemotion animatetransform set style'.split(' '));
|
|
3
|
+
export const boundAttributes = new Set('title aria-label aria-valuenow aria-valuetext x y cx cy x1 x2 y1 y2 width height r rx ry fill stroke opacity'.split(' '));
|
|
4
|
+
const attributes = new Set('id class title role type min max step placeholder rows cols maxlength disabled readonly tabindex for value checked max low high optimum width height viewBox preserveAspectRatio d x y cx cy x1 x2 y1 y2 r rx ry points fill stroke stroke-width opacity text-anchor font-size font-family alt bind source as key'.toLowerCase().split(' '));
|
|
5
|
+
export function parseHtmlTemplate(doc, html) {
|
|
6
|
+
const template = doc.createElement('template');
|
|
7
|
+
// Template contents are inert; parsed only in the CSP-restricted frame document.
|
|
8
|
+
template.innerHTML = html;
|
|
9
|
+
const warnings = [];
|
|
10
|
+
let count = 0;
|
|
11
|
+
const visit = (node, depth) => {
|
|
12
|
+
if (++count > 5000 || depth > 64)
|
|
13
|
+
throw new Error('HTML template exceeds 5,000 nodes or 64 nesting levels');
|
|
14
|
+
const id = String(count);
|
|
15
|
+
if (node.nodeType === 3)
|
|
16
|
+
return [{ text: node.textContent ?? '', attrs: {}, children: [], id }];
|
|
17
|
+
if (node.nodeType !== 1)
|
|
18
|
+
return [];
|
|
19
|
+
const element = node, tag = element.localName.toLowerCase();
|
|
20
|
+
if (dangerous.has(tag)) {
|
|
21
|
+
warnings.push(`Removed unsafe element: ${tag}`);
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const children = () => Array.from(node.childNodes).flatMap(child => visit(child, depth + 1));
|
|
25
|
+
if (!tags.has(tag)) {
|
|
26
|
+
warnings.push(`Unsupported element: ${tag}`);
|
|
27
|
+
return children();
|
|
28
|
+
}
|
|
29
|
+
const attrs = {};
|
|
30
|
+
for (const attr of Array.from(element.attributes)) {
|
|
31
|
+
const name = attr.name.toLowerCase(), value = attr.value;
|
|
32
|
+
if (name === 'src' && tag === 'img' && /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+$/.test(value) && value.length <= 256 * 1024) {
|
|
33
|
+
attrs.src = value;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (name.startsWith('jv-attr-') && boundAttributes.has(name.slice(8)) || name === 'jv-bind' || name === 'jv-value' || attributes.has(name) || /^aria-[a-z-]+$/.test(name)) {
|
|
37
|
+
if ((name === 'fill' || name === 'stroke') && /url\s*\(/i.test(value)) {
|
|
38
|
+
warnings.push(`Removed URL attribute: ${name}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
attrs[name] = value;
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
warnings.push(`Removed unsupported attribute: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
if (tag === 'input' && !['text', 'number', 'range', 'checkbox'].includes(attrs.type ?? 'text')) {
|
|
47
|
+
warnings.push('Supported inputs: text, number, range, checkbox');
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (/\<jv-(field|value|repeat)\b[^>]*\/\s*>/i.test(html))
|
|
51
|
+
throw new Error('Use explicit closing tags for jv-field, jv-value and jv-repeat');
|
|
52
|
+
return [{ tag, attrs, children: children(), id }];
|
|
53
|
+
};
|
|
54
|
+
return { nodes: Array.from(template.content.childNodes).flatMap(node => visit(node, 0)), warnings: [...new Set(warnings)] };
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CompiledJsonViewMetadata, type CompiledJsonViewView, type ValuePath } from '@script-it/json-views-core';
|
|
2
|
+
import type { JsonViewJsonEditing } from './view-types.js';
|
|
3
|
+
export declare function HtmlView({ compiled, view, editing, fillHeight, onCurrentPathChange }: {
|
|
4
|
+
compiled: CompiledJsonViewMetadata;
|
|
5
|
+
view: CompiledJsonViewView;
|
|
6
|
+
editing?: JsonViewJsonEditing;
|
|
7
|
+
fillHeight?: boolean;
|
|
8
|
+
onCurrentPathChange?: (path: ValuePath) => void;
|
|
9
|
+
}): import("react").JSX.Element;
|