anyjsonviewer 0.1.0__tar.gz
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.
- anyjsonviewer-0.1.0/.gitignore +13 -0
- anyjsonviewer-0.1.0/PKG-INFO +112 -0
- anyjsonviewer-0.1.0/README.md +103 -0
- anyjsonviewer-0.1.0/hatch_build.py +33 -0
- anyjsonviewer-0.1.0/js/index.jsx +196 -0
- anyjsonviewer-0.1.0/package-lock.json +1517 -0
- anyjsonviewer-0.1.0/package.json +18 -0
- anyjsonviewer-0.1.0/pyproject.toml +30 -0
- anyjsonviewer-0.1.0/src/anyjsonviewer/__init__.py +90 -0
- anyjsonviewer-0.1.0/src/anyjsonviewer/static/widget.js +160 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anyjsonviewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An anywidget wrapper for @textea/json-viewer
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: anywidget>=0.10.0
|
|
7
|
+
Requires-Dist: traitlets>=5.14.3
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# anyjsonviewer
|
|
11
|
+
|
|
12
|
+
An [anywidget](https://anywidget.dev) wrapper around
|
|
13
|
+
[`@textea/json-viewer`](https://github.com/TexteaInc/json-viewer), giving you an
|
|
14
|
+
interactive, collapsible JSON viewer inside Jupyter, JupyterLab, VS Code,
|
|
15
|
+
Marimo, or any other frontend that supports Jupyter widgets.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install anyjsonviewer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with uv:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv add anyjsonviewer
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from anyjsonviewer import JsonViewer
|
|
33
|
+
|
|
34
|
+
JsonViewer(
|
|
35
|
+
value={
|
|
36
|
+
"name": "anyjsonviewer",
|
|
37
|
+
"tags": ["json", "viewer", "anywidget"],
|
|
38
|
+
"nested": {"count": 3, "ok": True},
|
|
39
|
+
},
|
|
40
|
+
theme="dark",
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Traits
|
|
45
|
+
|
|
46
|
+
All props are synced traits, so you can update them from Python and the view
|
|
47
|
+
re-renders live:
|
|
48
|
+
|
|
49
|
+
| Trait | Type | Default | Description |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| `value` | any JSON-serializable | `None` | The data to display. |
|
|
52
|
+
| `root_name` | `str \| None` | `"root"` | Label for the root node. `None` hides it. |
|
|
53
|
+
| `theme` | `str` | `"light"` | `"light"`, `"dark"`, or `"auto"`. |
|
|
54
|
+
| `class_name` | `str \| None` | `None` | Custom CSS class on the root element. |
|
|
55
|
+
| `style` | `dict \| None` | `None` | Inline CSS styles for the root element. |
|
|
56
|
+
| `indent_width` | `int` | `3` | Indentation width for nested objects. |
|
|
57
|
+
| `default_inspect_depth` | `int` | `5` | Depth to auto-expand on first render. Set to `0` to fully collapse. |
|
|
58
|
+
| `max_display_length` | `int` | `30` | Hide items in arrays/objects past this count. |
|
|
59
|
+
| `group_arrays_after_length` | `int` | `100` | Group arrays into bracketed chunks after this length. |
|
|
60
|
+
| `collapse_strings_after_length` | `int` | `50` | Truncate long strings; click to expand. |
|
|
61
|
+
| `object_sort_keys` | `bool` | `False` | Sort object keys alphabetically. |
|
|
62
|
+
| `quotes_on_keys` | `bool` | `True` | Wrap keys in quotes. |
|
|
63
|
+
| `display_data_types` | `bool` | `True` | Show type annotations next to values. |
|
|
64
|
+
| `display_size` | `bool` | `True` | Show length of arrays/objects. |
|
|
65
|
+
| `display_comma` | `bool` | `True` | Show trailing commas. |
|
|
66
|
+
| `highlight_updates` | `bool` | `True` | Briefly highlight values when they change. |
|
|
67
|
+
| `enable_clipboard` | `bool` | `False` | Show copy-to-clipboard buttons. |
|
|
68
|
+
| `enable_add` | `bool` | `False` | Show "add" buttons on objects/arrays. |
|
|
69
|
+
| `enable_delete` | `bool` | `False` | Show "delete" buttons on items. |
|
|
70
|
+
| `editable` | `bool` | `False` | Allow inline editing of values. |
|
|
71
|
+
|
|
72
|
+
When `editable`, `enable_add`, or `enable_delete` are on, edits round-trip
|
|
73
|
+
through the `value` trait — change something in the UI and the new value
|
|
74
|
+
becomes visible from Python.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
w = JsonViewer(value={"a": 1})
|
|
78
|
+
w.theme = "dark" # live update
|
|
79
|
+
w.value = {"a": 1, "b": 2}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
The widget's JavaScript lives in `js/index.jsx` and is bundled with
|
|
85
|
+
[`esbuild`](https://esbuild.github.io/) into `src/anyjsonviewer/static/widget.js`.
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# Install Python deps
|
|
89
|
+
uv sync
|
|
90
|
+
|
|
91
|
+
# Install JS deps
|
|
92
|
+
npm install
|
|
93
|
+
|
|
94
|
+
# One-off build
|
|
95
|
+
npm run build
|
|
96
|
+
|
|
97
|
+
# Watch mode (pair with ANYWIDGET_HMR=1 in your notebook)
|
|
98
|
+
npm run dev
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Building the wheel automatically runs the JS build via a custom Hatch hook:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
uv build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To skip the JS build step (e.g. in CI where you've already built the bundle),
|
|
108
|
+
set `ANYJSONVIEWER_SKIP_JS_BUILD=1`.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# anyjsonviewer
|
|
2
|
+
|
|
3
|
+
An [anywidget](https://anywidget.dev) wrapper around
|
|
4
|
+
[`@textea/json-viewer`](https://github.com/TexteaInc/json-viewer), giving you an
|
|
5
|
+
interactive, collapsible JSON viewer inside Jupyter, JupyterLab, VS Code,
|
|
6
|
+
Marimo, or any other frontend that supports Jupyter widgets.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install anyjsonviewer
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or with uv:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
uv add anyjsonviewer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from anyjsonviewer import JsonViewer
|
|
24
|
+
|
|
25
|
+
JsonViewer(
|
|
26
|
+
value={
|
|
27
|
+
"name": "anyjsonviewer",
|
|
28
|
+
"tags": ["json", "viewer", "anywidget"],
|
|
29
|
+
"nested": {"count": 3, "ok": True},
|
|
30
|
+
},
|
|
31
|
+
theme="dark",
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Traits
|
|
36
|
+
|
|
37
|
+
All props are synced traits, so you can update them from Python and the view
|
|
38
|
+
re-renders live:
|
|
39
|
+
|
|
40
|
+
| Trait | Type | Default | Description |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `value` | any JSON-serializable | `None` | The data to display. |
|
|
43
|
+
| `root_name` | `str \| None` | `"root"` | Label for the root node. `None` hides it. |
|
|
44
|
+
| `theme` | `str` | `"light"` | `"light"`, `"dark"`, or `"auto"`. |
|
|
45
|
+
| `class_name` | `str \| None` | `None` | Custom CSS class on the root element. |
|
|
46
|
+
| `style` | `dict \| None` | `None` | Inline CSS styles for the root element. |
|
|
47
|
+
| `indent_width` | `int` | `3` | Indentation width for nested objects. |
|
|
48
|
+
| `default_inspect_depth` | `int` | `5` | Depth to auto-expand on first render. Set to `0` to fully collapse. |
|
|
49
|
+
| `max_display_length` | `int` | `30` | Hide items in arrays/objects past this count. |
|
|
50
|
+
| `group_arrays_after_length` | `int` | `100` | Group arrays into bracketed chunks after this length. |
|
|
51
|
+
| `collapse_strings_after_length` | `int` | `50` | Truncate long strings; click to expand. |
|
|
52
|
+
| `object_sort_keys` | `bool` | `False` | Sort object keys alphabetically. |
|
|
53
|
+
| `quotes_on_keys` | `bool` | `True` | Wrap keys in quotes. |
|
|
54
|
+
| `display_data_types` | `bool` | `True` | Show type annotations next to values. |
|
|
55
|
+
| `display_size` | `bool` | `True` | Show length of arrays/objects. |
|
|
56
|
+
| `display_comma` | `bool` | `True` | Show trailing commas. |
|
|
57
|
+
| `highlight_updates` | `bool` | `True` | Briefly highlight values when they change. |
|
|
58
|
+
| `enable_clipboard` | `bool` | `False` | Show copy-to-clipboard buttons. |
|
|
59
|
+
| `enable_add` | `bool` | `False` | Show "add" buttons on objects/arrays. |
|
|
60
|
+
| `enable_delete` | `bool` | `False` | Show "delete" buttons on items. |
|
|
61
|
+
| `editable` | `bool` | `False` | Allow inline editing of values. |
|
|
62
|
+
|
|
63
|
+
When `editable`, `enable_add`, or `enable_delete` are on, edits round-trip
|
|
64
|
+
through the `value` trait — change something in the UI and the new value
|
|
65
|
+
becomes visible from Python.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
w = JsonViewer(value={"a": 1})
|
|
69
|
+
w.theme = "dark" # live update
|
|
70
|
+
w.value = {"a": 1, "b": 2}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
The widget's JavaScript lives in `js/index.jsx` and is bundled with
|
|
76
|
+
[`esbuild`](https://esbuild.github.io/) into `src/anyjsonviewer/static/widget.js`.
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# Install Python deps
|
|
80
|
+
uv sync
|
|
81
|
+
|
|
82
|
+
# Install JS deps
|
|
83
|
+
npm install
|
|
84
|
+
|
|
85
|
+
# One-off build
|
|
86
|
+
npm run build
|
|
87
|
+
|
|
88
|
+
# Watch mode (pair with ANYWIDGET_HMR=1 in your notebook)
|
|
89
|
+
npm run dev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Building the wheel automatically runs the JS build via a custom Hatch hook:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
uv build
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To skip the JS build step (e.g. in CI where you've already built the bundle),
|
|
99
|
+
set `ANYJSONVIEWER_SKIP_JS_BUILD=1`.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).parent
|
|
9
|
+
BUNDLE = ROOT / "src" / "anyjsonviewer" / "static" / "widget.js"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsBuildHook(BuildHookInterface):
|
|
13
|
+
PLUGIN_NAME = "anyjsonviewer-js"
|
|
14
|
+
|
|
15
|
+
def initialize(self, version, build_data):
|
|
16
|
+
if os.environ.get("ANYJSONVIEWER_SKIP_JS_BUILD"):
|
|
17
|
+
return
|
|
18
|
+
if shutil.which("npm") is None:
|
|
19
|
+
if BUNDLE.exists():
|
|
20
|
+
self.app.display_warning(
|
|
21
|
+
"npm not found; using existing bundled widget.js"
|
|
22
|
+
)
|
|
23
|
+
return
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
"npm is required to build the JS bundle and no prebuilt "
|
|
26
|
+
"widget.js was found. Install Node.js or set "
|
|
27
|
+
"ANYJSONVIEWER_SKIP_JS_BUILD=1 if you know what you're doing."
|
|
28
|
+
)
|
|
29
|
+
if not (ROOT / "node_modules").exists():
|
|
30
|
+
subprocess.run(["npm", "install"], cwd=ROOT, check=True)
|
|
31
|
+
subprocess.run(["npm", "run", "build"], cwd=ROOT, check=True)
|
|
32
|
+
if not BUNDLE.exists():
|
|
33
|
+
raise RuntimeError(f"JS build did not produce {BUNDLE}")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { JsonViewer } from "@textea/json-viewer";
|
|
4
|
+
|
|
5
|
+
// Mirrors src/jsonview/__init__.py:_TAG. Tagged dicts coming from Python
|
|
6
|
+
// represent values that don't survive a JSON round-trip and need to be
|
|
7
|
+
// rehydrated into real JS objects so @textea/json-viewer's built-in type
|
|
8
|
+
// handlers (dateType, the Set renderer, etc.) can display them properly.
|
|
9
|
+
const TAG = "__jsonview_type__";
|
|
10
|
+
|
|
11
|
+
function encodeValue(node) {
|
|
12
|
+
if (node === null || node === undefined) return node;
|
|
13
|
+
if (node instanceof Date) {
|
|
14
|
+
return { [TAG]: "datetime", iso: node.toISOString() };
|
|
15
|
+
}
|
|
16
|
+
if (node instanceof Set) {
|
|
17
|
+
return { [TAG]: "set", items: [...node].map(encodeValue) };
|
|
18
|
+
}
|
|
19
|
+
if (node instanceof Uint8Array) {
|
|
20
|
+
let bin = "";
|
|
21
|
+
for (let i = 0; i < node.length; i++) bin += String.fromCharCode(node[i]);
|
|
22
|
+
return { [TAG]: "bytes", b64: btoa(bin) };
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(node)) return node.map(encodeValue);
|
|
25
|
+
if (typeof node === "object") {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const [k, v] of Object.entries(node)) out[k] = encodeValue(v);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decodeValue(node) {
|
|
34
|
+
if (node === null || typeof node !== "object") return node;
|
|
35
|
+
if (Array.isArray(node)) return node.map(decodeValue);
|
|
36
|
+
if (typeof node[TAG] === "string") {
|
|
37
|
+
switch (node[TAG]) {
|
|
38
|
+
case "datetime":
|
|
39
|
+
case "date":
|
|
40
|
+
return new Date(node.iso);
|
|
41
|
+
case "set":
|
|
42
|
+
return new Set(node.items.map(decodeValue));
|
|
43
|
+
case "bytes": {
|
|
44
|
+
const bin = atob(node.b64);
|
|
45
|
+
const arr = new Uint8Array(bin.length);
|
|
46
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
47
|
+
return arr;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [k, v] of Object.entries(node)) out[k] = decodeValue(v);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PROPS = [
|
|
57
|
+
"value",
|
|
58
|
+
"root_name",
|
|
59
|
+
"theme",
|
|
60
|
+
"class_name",
|
|
61
|
+
"style",
|
|
62
|
+
"indent_width",
|
|
63
|
+
"default_inspect_depth",
|
|
64
|
+
"max_display_length",
|
|
65
|
+
"group_arrays_after_length",
|
|
66
|
+
"collapse_strings_after_length",
|
|
67
|
+
"object_sort_keys",
|
|
68
|
+
"quotes_on_keys",
|
|
69
|
+
"display_data_types",
|
|
70
|
+
"display_size",
|
|
71
|
+
"display_comma",
|
|
72
|
+
"highlight_updates",
|
|
73
|
+
"enable_clipboard",
|
|
74
|
+
"enable_add",
|
|
75
|
+
"enable_delete",
|
|
76
|
+
"editable",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Apply an edit/add/delete operation to a JSON value at the given path.
|
|
80
|
+
function applyChange(root, path, newValue, op) {
|
|
81
|
+
// Clone along the path so React/traitlets sees a new object identity.
|
|
82
|
+
if (path.length === 0) return newValue;
|
|
83
|
+
const cloneStep = (node) => (Array.isArray(node) ? [...node] : { ...node });
|
|
84
|
+
const newRoot = cloneStep(root);
|
|
85
|
+
let parent = newRoot;
|
|
86
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
87
|
+
const key = path[i];
|
|
88
|
+
parent[key] = cloneStep(parent[key]);
|
|
89
|
+
parent = parent[key];
|
|
90
|
+
}
|
|
91
|
+
const lastKey = path[path.length - 1];
|
|
92
|
+
if (op === "delete") {
|
|
93
|
+
if (Array.isArray(parent)) parent.splice(lastKey, 1);
|
|
94
|
+
else delete parent[lastKey];
|
|
95
|
+
} else {
|
|
96
|
+
parent[lastKey] = newValue;
|
|
97
|
+
}
|
|
98
|
+
return newRoot;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Props that @textea/json-viewer bakes into its internal Zustand store on
|
|
102
|
+
// first mount and never re-reads (see createJsonViewerStore in
|
|
103
|
+
// node_modules/@textea/json-viewer/dist/index.mjs — the useMemo there has an
|
|
104
|
+
// empty deps array). Changing any of these requires remounting the component
|
|
105
|
+
// via a different React `key`. The remaining props (value, theme, class_name,
|
|
106
|
+
// style) are read directly from props on every render and update live.
|
|
107
|
+
const STORE_BAKED_PROPS = [
|
|
108
|
+
"root_name",
|
|
109
|
+
"indent_width",
|
|
110
|
+
"default_inspect_depth",
|
|
111
|
+
"max_display_length",
|
|
112
|
+
"group_arrays_after_length",
|
|
113
|
+
"collapse_strings_after_length",
|
|
114
|
+
"object_sort_keys",
|
|
115
|
+
"quotes_on_keys",
|
|
116
|
+
"display_data_types",
|
|
117
|
+
"display_size",
|
|
118
|
+
"display_comma",
|
|
119
|
+
"highlight_updates",
|
|
120
|
+
"enable_clipboard",
|
|
121
|
+
"enable_add",
|
|
122
|
+
"enable_delete",
|
|
123
|
+
"editable",
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
function Widget({ model }) {
|
|
127
|
+
const [, force] = React.useReducer((x) => x + 1, 0);
|
|
128
|
+
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
for (const p of PROPS) model.on(`change:${p}`, force);
|
|
131
|
+
return () => {
|
|
132
|
+
for (const p of PROPS) model.off(`change:${p}`, force);
|
|
133
|
+
};
|
|
134
|
+
}, [model]);
|
|
135
|
+
|
|
136
|
+
const remountKey = STORE_BAKED_PROPS.map((p) => JSON.stringify(model.get(p))).join("|");
|
|
137
|
+
const decodedValue = React.useMemo(() => decodeValue(model.get("value")), [model.get("value")]);
|
|
138
|
+
|
|
139
|
+
const handleChange = React.useCallback(
|
|
140
|
+
(path, _oldVal, newVal) => {
|
|
141
|
+
const current = decodeValue(model.get("value"));
|
|
142
|
+
const next = applyChange(current, path, newVal, "set");
|
|
143
|
+
model.set("value", encodeValue(next));
|
|
144
|
+
model.save_changes();
|
|
145
|
+
},
|
|
146
|
+
[model],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const handleDelete = React.useCallback(
|
|
150
|
+
(path) => {
|
|
151
|
+
const current = decodeValue(model.get("value"));
|
|
152
|
+
const next = applyChange(current, path, undefined, "delete");
|
|
153
|
+
model.set("value", encodeValue(next));
|
|
154
|
+
model.save_changes();
|
|
155
|
+
},
|
|
156
|
+
[model],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<JsonViewer
|
|
161
|
+
key={remountKey}
|
|
162
|
+
value={decodedValue}
|
|
163
|
+
rootName={model.get("root_name") ?? false}
|
|
164
|
+
theme={model.get("theme")}
|
|
165
|
+
className={model.get("class_name") ?? undefined}
|
|
166
|
+
style={model.get("style") ?? undefined}
|
|
167
|
+
indentWidth={model.get("indent_width")}
|
|
168
|
+
defaultInspectDepth={model.get("default_inspect_depth")}
|
|
169
|
+
maxDisplayLength={model.get("max_display_length")}
|
|
170
|
+
groupArraysAfterLength={model.get("group_arrays_after_length")}
|
|
171
|
+
collapseStringsAfterLength={model.get("collapse_strings_after_length")}
|
|
172
|
+
objectSortKeys={model.get("object_sort_keys")}
|
|
173
|
+
quotesOnKeys={model.get("quotes_on_keys")}
|
|
174
|
+
displayDataTypes={model.get("display_data_types")}
|
|
175
|
+
displaySize={model.get("display_size")}
|
|
176
|
+
displayComma={model.get("display_comma")}
|
|
177
|
+
highlightUpdates={model.get("highlight_updates")}
|
|
178
|
+
enableClipboard={model.get("enable_clipboard")}
|
|
179
|
+
enableAdd={model.get("enable_add")}
|
|
180
|
+
enableDelete={model.get("enable_delete")}
|
|
181
|
+
editable={model.get("editable")}
|
|
182
|
+
onChange={handleChange}
|
|
183
|
+
onDelete={handleDelete}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function render({ model, el }) {
|
|
189
|
+
const container = document.createElement("div");
|
|
190
|
+
el.appendChild(container);
|
|
191
|
+
const root = createRoot(container);
|
|
192
|
+
root.render(<Widget model={model} />);
|
|
193
|
+
return () => root.unmount();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default { render };
|