tosijs-ui 1.6.21 → 1.7.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/CHANGELOG.md +132 -0
- package/bin/make-icon-data.js +0 -0
- package/dist/code-editor-cm.js +41 -17
- package/dist/code-editor.d.ts +32 -2
- package/dist/code-editor.js +192 -25
- package/dist/doc-system/site/bundle-guard.d.ts +50 -0
- package/dist/doc-system/site/bundle-guard.js +111 -0
- package/dist/doc-system/site/check-examples.js +11 -3
- package/dist/doc-system/site/dev-server.js +97 -7
- package/dist/doc-system/site/epub-cli.d.ts +1 -0
- package/dist/doc-system/site/epub-cli.js +22 -0
- package/dist/doc-system/site/epub.js +34 -9
- package/dist/doc-system/site/make-llms-txt.js +1 -1
- package/dist/doc-system/site/orchestrator.js +139 -53
- package/dist/doc-system/site/site-config.d.ts +10 -0
- package/dist/iife.js +78 -71
- package/dist/iife.js.map +8 -8
- package/dist/live-example/code-transform.d.ts +39 -0
- package/dist/live-example/code-transform.js +305 -4
- package/dist/live-example/component.d.ts +10 -0
- package/dist/live-example/component.js +105 -21
- package/dist/live-example/execution.d.ts +16 -0
- package/dist/live-example/execution.js +29 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0-beta.1
|
|
4
|
+
|
|
5
|
+
**A beta, published under npm's `beta` tag** — `latest` stays on 1.6.x. A prerelease is not
|
|
6
|
+
matched by `^1.6.x` (or by `^1.7.0`), so nobody is auto-upgraded into the editor swap; you get
|
|
7
|
+
this only by asking for it:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i tosijs-ui@beta # or tosijs-ui@1.7.0-beta.1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The code editor moved from **ACE to [CodeMirror 6](https://codemirror.net/)**, `tjs`
|
|
14
|
+
became a first-class editing mode with runtime-value autocomplete, and the doc-site
|
|
15
|
+
builder gained the hooks that unblock the tosijs 2.0 TJS port.
|
|
16
|
+
|
|
17
|
+
**Known cost, deliberate for now.** `dist/iife.js` carries CodeMirror (bun's IIFE format cannot
|
|
18
|
+
code-split), and generated doc pages hide the body until hydration — so on a cheap phone the
|
|
19
|
+
blank-screen window is ~4.5s vs ~3.7s without the editor. The fix is to pre-render the page
|
|
20
|
+
chrome so hydration is additive and the gate can go (then the bundle gates *editing*, not
|
|
21
|
+
*reading*); that's the next doc-system release, not this one. Measurements in `TODO.md`.
|
|
22
|
+
|
|
23
|
+
### Breaking
|
|
24
|
+
|
|
25
|
+
`<tosi-code>` (ACE → CodeMirror 6). Each removed member now **warns once and no-ops**
|
|
26
|
+
rather than failing silently, but they are gone:
|
|
27
|
+
|
|
28
|
+
| removed | replacement |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `theme` attribute | style the editor with `--code-bg` / `--text-color` |
|
|
31
|
+
| `options` property (ACE-shaped) | configure via `editor` (a CodeMirror `EditorView`) |
|
|
32
|
+
| `ace` getter | there is no ACE global; use `editor` |
|
|
33
|
+
| `editor.session.getUndoManager()` | `undo()` / `redo()` / `canUndo()` / `canRedo()` |
|
|
34
|
+
|
|
35
|
+
**`editor` changed type in place** — it was an ACE `Editor`, it is now a CodeMirror
|
|
36
|
+
[`EditorView`](https://codemirror.net/docs/ref/#view.EditorView). A grep for the
|
|
37
|
+
removed names will not catch this; any code reaching into `editor` needs revisiting.
|
|
38
|
+
|
|
39
|
+
Unchanged: `value`, `original` / `showDiff()`, `mode`, `disabled`.
|
|
40
|
+
|
|
41
|
+
**Semver stance (deliberate, not an oversight).** This library breaks in minors before 2.0 —
|
|
42
|
+
**`2.0` is reserved for the tjs-native rewrite**, not for this. So `^1.6.x` resolves `1.7.0` and
|
|
43
|
+
existing consumers pick this up on their next install.
|
|
44
|
+
|
|
45
|
+
**If you use `<tosi-code>`, pin `~1.6` and upgrade deliberately.** Everything else in the library
|
|
46
|
+
is untouched, so a consumer of (say) `<tosi-rating>` can take 1.7 without changes — but note it
|
|
47
|
+
now installs 12 `@codemirror/*` runtime dependencies where the library previously had none.
|
|
48
|
+
|
|
49
|
+
The trap to know about: **`editor` changed type in place** and no shim can catch it. The removed
|
|
50
|
+
`theme` / `options` / `ace` members warn once and no-op, but `el.editor` still exists — it is
|
|
51
|
+
simply a CodeMirror `EditorView` now, so `el.editor.session.*` is a runtime `TypeError`. A grep
|
|
52
|
+
for the removed names will not find it.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `<tosi-code>` gained a `change` event (`event.detail.value`), `undo()` / `redo()` /
|
|
57
|
+
`canUndo()` / `canRedo()`, and `tjs` / `ajs` modes.
|
|
58
|
+
- **Runtime-value autocomplete** in `tjs` mode: set `tjsAutocomplete` and completion
|
|
59
|
+
suggests the *real members of live values* — including tosijs proxy members that no
|
|
60
|
+
static analysis can see. Live examples wire their own executed scope into it, so
|
|
61
|
+
`const { app } = tosi(…)` gives real `app.` / `app.items.` completions.
|
|
62
|
+
- Inline **WebAssembly** live examples (tjs `wasm {}` blocks).
|
|
63
|
+
- `buildSite`: `libraryBuild` and `generateCssPreload` hooks, for projects whose
|
|
64
|
+
library sources are native `.tjs` (this is what the tosijs 2.0 TJS port needs).
|
|
65
|
+
**These actually shipped in 1.6.21** — they are listed here only because they landed on this
|
|
66
|
+
branch first. Nothing needs 1.7 to use them.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **`dist/iife.js` is ~384KB gzipped, up from ~120KB.** Bun's IIFE format cannot
|
|
71
|
+
code-split, so CodeMirror is inlined there. (Under a bundler/ESM it stays a lazy
|
|
72
|
+
chunk — a page with no `<tosi-code>` doesn't load it.) This is deliberate: the
|
|
73
|
+
in-page editor and its save-to-source flow are the point of the doc-system, so the
|
|
74
|
+
IIFE carries the editor.
|
|
75
|
+
- The library now has runtime `dependencies` (12 `@codemirror/*` packages) where it
|
|
76
|
+
previously had none. They can't be optional peers: the tjs CodeMirror extension must
|
|
77
|
+
share this exact `@codemirror/state` instance or it silently no-ops.
|
|
78
|
+
- `tjs-lang` peer: `^0.8.7` → `^0.9.1`.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- `showDiff()` rendered nothing. Giving `<tosi-code>` its own `content` displaced the
|
|
83
|
+
default `<slot>` that used to project the light-DOM diff overlay, so the overlay was
|
|
84
|
+
invisible (0×0) while `showingDiff` still reported `true`. It now renders inside the
|
|
85
|
+
shadow root. This is the review step of the doc-system's edit-and-save-to-source
|
|
86
|
+
flow, so a blank diff meant saving changes you never saw.
|
|
87
|
+
|
|
88
|
+
## 1.6.22
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **The dev server no longer leaks memory on every rebuild — update as a priority if
|
|
93
|
+
you use `tosijs-ui/site`.** `buildSite()` called `Bun.build()` in-process, and Bun's
|
|
94
|
+
bundler never returns its native arena: RSS grows monotonically per call with no
|
|
95
|
+
plateau (40 sequential builds of one entry = **+367MB**, still climbing ~5MB/build at
|
|
96
|
+
the end), while the JS heap stays flat — so it is invisible to `Bun.gc()` and to any
|
|
97
|
+
heap profiler. `devServer()` calls it once per rebuild in a process that runs for
|
|
98
|
+
days, so it compounds: a ~2-day watch session reached **136GB RSS** and took the
|
|
99
|
+
machine down with it. Filed upstream as
|
|
100
|
+
[oven-sh/bun#34053](https://github.com/oven-sh/bun/issues/34053).
|
|
101
|
+
|
|
102
|
+
The bundle now builds in a **child process** (`bun build`), whose memory the OS
|
|
103
|
+
reclaims on exit — the same 15 bundles leave the parent **+0.5MB** instead of +192MB.
|
|
104
|
+
`buildEpub()` moved to a child too (happy-dom + `@resvg/resvg-js` are both native and
|
|
105
|
+
retaining, and it runs on every rebuild), with a 120s timeout kill so a hung or failed
|
|
106
|
+
ePub warns instead of wedging the rebuild. Measured on this repo's own dev server:
|
|
107
|
+
**baseline RSS 503MB → 150MB, and per-rebuild growth 26–59MB → ~2.7MB.**
|
|
108
|
+
|
|
109
|
+
Bundle output is byte-for-byte identical; this is purely where the work runs.
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- **Dev-server memory watchdog.** `devServer()` now samples RSS after each rebuild and,
|
|
114
|
+
past a ceiling (`memoryLimitMb` in the site config, or `DEV_MEMORY_LIMIT_MB`; default
|
|
115
|
+
4096), prints the growth-per-rebuild and exits rather than let a leak thrash the
|
|
116
|
+
machine. It distinguishes real growth (a leak — report it) from a build whose baseline
|
|
117
|
+
simply exceeds the ceiling (raise the ceiling).
|
|
118
|
+
|
|
119
|
+
## 1.6.21
|
|
120
|
+
|
|
121
|
+
### Fixed
|
|
122
|
+
|
|
123
|
+
- Doc-system nav-toggle in `routing: 'memory'` mode drove the *outer* doc-browser
|
|
124
|
+
instance instead of its own (now scoped per-instance).
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
|
|
128
|
+
- `buildSite` `libraryBuild` + `generateCssPreload` hooks (also in 1.7.0 above), so a
|
|
129
|
+
project whose library source is native `.tjs` can build through the doc-site pipeline.
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- `tjs-lang` peer: `^0.8.7` → `^0.9.0`.
|
|
134
|
+
|
|
3
135
|
## 1.6.20
|
|
4
136
|
|
|
5
137
|
### Fixed
|
package/bin/make-icon-data.js
CHANGED
|
File without changes
|
package/dist/code-editor-cm.js
CHANGED
|
@@ -10,11 +10,11 @@ Languages here are the always-available set (js/ts/css/html/markdown). First-cla
|
|
|
10
10
|
tjs (highlighting + runtime-value autocomplete via tjs-lang's editors/codemirror) is
|
|
11
11
|
layered in separately and only when tjs-lang is installed.
|
|
12
12
|
*/
|
|
13
|
-
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from '@codemirror/view';
|
|
13
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, } from '@codemirror/view';
|
|
14
14
|
import { EditorState, Compartment } from '@codemirror/state';
|
|
15
|
-
import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
|
16
|
-
import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language';
|
|
17
|
-
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
|
15
|
+
import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth, } from '@codemirror/commands';
|
|
16
|
+
import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, } from '@codemirror/language';
|
|
17
|
+
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap, } from '@codemirror/autocomplete';
|
|
18
18
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
|
19
19
|
import { lintKeymap } from '@codemirror/lint';
|
|
20
20
|
import { javascript } from '@codemirror/lang-javascript';
|
|
@@ -32,7 +32,6 @@ const isDarkMode = () => typeof document !== 'undefined' &&
|
|
|
32
32
|
const highlightFor = (dark) => dark
|
|
33
33
|
? syntaxHighlighting(oneDarkHighlightStyle)
|
|
34
34
|
: syntaxHighlighting(defaultHighlightStyle, { fallback: true });
|
|
35
|
-
const isTjsMode = (mode) => mode === 'tjs' || mode === 'ajs';
|
|
36
35
|
/** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
|
|
37
36
|
export function languageForMode(mode) {
|
|
38
37
|
switch (mode) {
|
|
@@ -45,6 +44,7 @@ export function languageForMode(mode) {
|
|
|
45
44
|
// tjs-lang language + autocomplete — is layered on asynchronously via
|
|
46
45
|
// `loadTjsExtension()` + `setLanguageExtension()`, since it lazy-loads the
|
|
47
46
|
// (optional) tjs-lang editor build; this keeps `mode="tjs"` readable meanwhile.
|
|
47
|
+
// falls through
|
|
48
48
|
case 'tjs':
|
|
49
49
|
case 'ajs':
|
|
50
50
|
return javascript({ typescript: true });
|
|
@@ -147,6 +147,31 @@ function baseExtensions() {
|
|
|
147
147
|
]),
|
|
148
148
|
];
|
|
149
149
|
}
|
|
150
|
+
const darkListeners = new Set();
|
|
151
|
+
let darkObserver;
|
|
152
|
+
function watchDarkMode(listener) {
|
|
153
|
+
darkListeners.add(listener);
|
|
154
|
+
if (darkObserver === undefined &&
|
|
155
|
+
typeof MutationObserver !== 'undefined' &&
|
|
156
|
+
typeof document !== 'undefined' &&
|
|
157
|
+
document.body) {
|
|
158
|
+
darkObserver = new MutationObserver(() => {
|
|
159
|
+
for (const l of darkListeners)
|
|
160
|
+
l();
|
|
161
|
+
});
|
|
162
|
+
darkObserver.observe(document.body, {
|
|
163
|
+
attributes: true,
|
|
164
|
+
attributeFilter: ['class'],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return () => {
|
|
168
|
+
darkListeners.delete(listener);
|
|
169
|
+
if (darkListeners.size === 0) {
|
|
170
|
+
darkObserver?.disconnect();
|
|
171
|
+
darkObserver = undefined;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
150
175
|
/** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
|
|
151
176
|
export function createCmEditor(parent, opts = {}) {
|
|
152
177
|
const language = new Compartment();
|
|
@@ -173,16 +198,13 @@ export function createCmEditor(parent, opts = {}) {
|
|
|
173
198
|
// Re-apply the right highlight palette when the doc-system toggles dark mode
|
|
174
199
|
// (it flips the `darkmode` class on <body>).
|
|
175
200
|
let dark = isDarkMode();
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
})
|
|
184
|
-
: null;
|
|
185
|
-
darkObserver?.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
201
|
+
const unwatchDark = watchDarkMode(() => {
|
|
202
|
+
const nowDark = isDarkMode();
|
|
203
|
+
if (nowDark !== dark) {
|
|
204
|
+
dark = nowDark;
|
|
205
|
+
view.dispatch({ effects: highlight.reconfigure(highlightFor(dark)) });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
186
208
|
const handle = {
|
|
187
209
|
view,
|
|
188
210
|
getValue: () => view.state.doc.toString(),
|
|
@@ -194,7 +216,9 @@ export function createCmEditor(parent, opts = {}) {
|
|
|
194
216
|
});
|
|
195
217
|
},
|
|
196
218
|
setMode(mode) {
|
|
197
|
-
view.dispatch({
|
|
219
|
+
view.dispatch({
|
|
220
|
+
effects: language.reconfigure(standardLanguageBundle(mode)),
|
|
221
|
+
});
|
|
198
222
|
},
|
|
199
223
|
setLanguageExtension(ext) {
|
|
200
224
|
view.dispatch({ effects: language.reconfigure(ext) });
|
|
@@ -211,7 +235,7 @@ export function createCmEditor(parent, opts = {}) {
|
|
|
211
235
|
focus: () => view.focus(),
|
|
212
236
|
refresh: () => view.requestMeasure(),
|
|
213
237
|
destroy: () => {
|
|
214
|
-
|
|
238
|
+
unwatchDark();
|
|
215
239
|
view.destroy();
|
|
216
240
|
},
|
|
217
241
|
};
|
package/dist/code-editor.d.ts
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { Component as WebComponent, ElementCreator, PartsMap } from 'tosijs';
|
|
2
|
-
import type { CmHandle } from './code-editor-cm';
|
|
2
|
+
import type { CmHandle, TjsAutocompleteConfig } from './code-editor-cm';
|
|
3
|
+
export type { TjsAutocompleteConfig } from './code-editor-cm';
|
|
3
4
|
interface CodeEditorParts extends PartsMap {
|
|
4
5
|
host: HTMLDivElement;
|
|
6
|
+
diffHost: HTMLDivElement;
|
|
5
7
|
}
|
|
6
8
|
export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
7
9
|
static preferredTagName: string;
|
|
8
10
|
private source;
|
|
9
11
|
private _handle;
|
|
10
12
|
private _loadPromise;
|
|
13
|
+
private _loadGeneration;
|
|
11
14
|
private _appliedMode;
|
|
12
15
|
private _appliedDisabled;
|
|
16
|
+
private _tjsAutocomplete;
|
|
17
|
+
/**
|
|
18
|
+
* Runtime-introspection hooks for tjs autocomplete (`getLiveBindings` /
|
|
19
|
+
* `getMembers`) — lets completion suggest the REAL members of live values (e.g.
|
|
20
|
+
* a tosijs proxy or a DOM element) that static analysis can't see. Only used in
|
|
21
|
+
* tjs mode; setting it re-applies the tjs extension so it takes effect live.
|
|
22
|
+
*/
|
|
23
|
+
get tjsAutocomplete(): TjsAutocompleteConfig | undefined;
|
|
24
|
+
set tjsAutocomplete(config: TjsAutocompleteConfig | undefined);
|
|
13
25
|
get value(): string;
|
|
14
26
|
set value(text: string);
|
|
15
27
|
private _original;
|
|
16
28
|
get original(): string;
|
|
17
29
|
set original(text: string);
|
|
18
30
|
private diffOverlay;
|
|
31
|
+
private _partsHydrated;
|
|
32
|
+
private _pendingDiff;
|
|
33
|
+
private get partsReady();
|
|
19
34
|
get showingDiff(): boolean;
|
|
20
35
|
showDiff(on: boolean): void;
|
|
21
36
|
static initAttributes: {
|
|
@@ -25,6 +40,14 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
25
40
|
role: string;
|
|
26
41
|
/** The underlying CodeMirror EditorView (undefined until loaded). */
|
|
27
42
|
get editor(): CmHandle['view'] | undefined;
|
|
43
|
+
/** @deprecated Removed in 1.7 — CodeMirror themes via `--code-bg`/`--text-color`. */
|
|
44
|
+
get theme(): string;
|
|
45
|
+
set theme(_: string);
|
|
46
|
+
/** @deprecated Removed in 1.7 — ACE-shaped options have no CodeMirror equivalent. */
|
|
47
|
+
get options(): Record<string, unknown>;
|
|
48
|
+
set options(_: Record<string, unknown>);
|
|
49
|
+
/** @deprecated Removed in 1.7 — there is no ACE global; use `editor` (an EditorView). */
|
|
50
|
+
get ace(): undefined;
|
|
28
51
|
undo(): void;
|
|
29
52
|
redo(): void;
|
|
30
53
|
canUndo(): boolean;
|
|
@@ -40,6 +63,13 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
40
63
|
'[part="host"]': {
|
|
41
64
|
height: string;
|
|
42
65
|
};
|
|
66
|
+
'[part="diffHost"]': {
|
|
67
|
+
position: string;
|
|
68
|
+
inset: string;
|
|
69
|
+
zIndex: string;
|
|
70
|
+
overflow: string;
|
|
71
|
+
background: string;
|
|
72
|
+
};
|
|
43
73
|
'.cm-editor': {
|
|
44
74
|
height: string;
|
|
45
75
|
};
|
|
@@ -50,6 +80,7 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
50
80
|
};
|
|
51
81
|
onResize(): void;
|
|
52
82
|
connectedCallback(): void;
|
|
83
|
+
disconnectedCallback(): void;
|
|
53
84
|
private isTjsMode;
|
|
54
85
|
/**
|
|
55
86
|
* When in tjs mode, lazily upgrade the editor to tjs-lang's CodeMirror language +
|
|
@@ -60,4 +91,3 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
60
91
|
render(): void;
|
|
61
92
|
}
|
|
62
93
|
export declare const codeEditor: ElementCreator<CodeEditor>;
|
|
63
|
-
export {};
|
package/dist/code-editor.js
CHANGED
|
@@ -16,24 +16,92 @@ body {
|
|
|
16
16
|
</tosi-code>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
[`EditorView`](https://codemirror.net/docs/ref/#view.EditorView), and
|
|
21
|
-
`undo()` / `redo()` / `canUndo()` / `canRedo()` methods for history control.
|
|
19
|
+
## Properties & events
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
| member | what it does |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `value` | the code in the editor (get/set) |
|
|
24
|
+
| `mode` | `javascript`, `typescript`, `tjs`, `ajs`, `css`, `html`, `markdown` |
|
|
25
|
+
| `disabled` | makes the editor read-only |
|
|
26
|
+
| `original` + `showDiff(on)` | diff the current `value` against a baseline, as an overlay |
|
|
27
|
+
| `editor` | the underlying CodeMirror [`EditorView`](https://codemirror.net/docs/ref/#view.EditorView) (`undefined` until loaded) |
|
|
28
|
+
| `undo()` / `redo()` / `canUndo()` / `canRedo()` | history control |
|
|
29
|
+
| `tjsAutocomplete` | runtime-value autocomplete hooks (tjs mode) — see below |
|
|
30
|
+
| `change` event | fires when the text changes; `event.detail.value` is the new text |
|
|
31
|
+
|
|
32
|
+
In `tjs`/`ajs` mode the editor loads tjs-lang's CodeMirror language and completion
|
|
33
|
+
source (if `tjs-lang` is installed — it's an optional peer). Set `tjsAutocomplete`
|
|
34
|
+
to a `TjsAutocompleteConfig` and completion will suggest the **real members of live
|
|
35
|
+
runtime values** — including proxy members no static analysis can see:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
codeEl.tjsAutocomplete = { getLiveBindings: () => ({ app, elements }) }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Bundling
|
|
42
|
+
|
|
43
|
+
CodeMirror is a **lazy chunk**: with a bundler (ESM), a page that never uses
|
|
44
|
+
`<tosi-code>` doesn't load it. **This is not true of the IIFE** (`dist/iife.js`) —
|
|
45
|
+
bun's IIFE format cannot code-split, so CodeMirror is inlined there. That is a
|
|
46
|
+
deliberate trade: the doc-system's editor (and its save-to-source flow) is the
|
|
47
|
+
point of the IIFE, so it carries the editor. It costs ~376KB gzipped, up from
|
|
48
|
+
~118KB in 1.6.x.
|
|
49
|
+
|
|
50
|
+
## Migrating from the ACE editor (pre-1.7)
|
|
51
|
+
|
|
52
|
+
1.7 replaced ACE with CodeMirror 6. `value`, `original`/`showDiff()`, `mode` and
|
|
53
|
+
`disabled` are unchanged. Removed (each warns once, then no-ops):
|
|
54
|
+
|
|
55
|
+
| removed | replacement |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `theme` | style with `--code-bg` / `--text-color` |
|
|
58
|
+
| `options` (ACE-shaped) | configure via `editor` (an `EditorView`) |
|
|
59
|
+
| `ace` | there is no ACE global; use `editor` |
|
|
60
|
+
| `editor.session.getUndoManager()` | `undo()` / `redo()` / `canUndo()` / `canRedo()` |
|
|
61
|
+
|
|
62
|
+
`editor` **changed type in place** — it was an ACE `Editor`, it is now a CodeMirror
|
|
63
|
+
`EditorView`. Code that reached into it needs revisiting; a grep for removed names
|
|
64
|
+
won't catch this one.
|
|
25
65
|
*/
|
|
26
66
|
/*{ "parent": "Components" }*/
|
|
27
|
-
import { Component as WebComponent, elements } from 'tosijs';
|
|
67
|
+
import { Component as WebComponent, elements, varDefault, } from 'tosijs';
|
|
28
68
|
import { tosiDiff } from './diff';
|
|
29
69
|
const { div } = elements;
|
|
70
|
+
// Warn once per removed member, not once per access — a live-example page holds
|
|
71
|
+
// dozens of editors and a render-loop read would otherwise flood the console.
|
|
72
|
+
const warned = new Set();
|
|
73
|
+
const warnRemoved = (member, advice) => {
|
|
74
|
+
if (warned.has(member))
|
|
75
|
+
return;
|
|
76
|
+
warned.add(member);
|
|
77
|
+
console.warn(`<tosi-code>.${member} was removed in tosijs-ui 1.7 (the editor is now CodeMirror 6, not ACE) — ${advice}. See CHANGELOG 1.7.0.`);
|
|
78
|
+
};
|
|
30
79
|
export class CodeEditor extends WebComponent {
|
|
31
80
|
static preferredTagName = 'tosi-code';
|
|
32
81
|
source = '';
|
|
33
82
|
_handle;
|
|
34
83
|
_loadPromise;
|
|
84
|
+
// Bumped on every connect. The lazy chunk resolves asynchronously, so by the time it
|
|
85
|
+
// does, this element may have been removed — or removed and re-added, starting a newer
|
|
86
|
+
// load. Mounting a stale load would build an EditorView nothing ever destroys.
|
|
87
|
+
_loadGeneration = 0;
|
|
35
88
|
_appliedMode = '';
|
|
36
89
|
_appliedDisabled;
|
|
90
|
+
_tjsAutocomplete;
|
|
91
|
+
/**
|
|
92
|
+
* Runtime-introspection hooks for tjs autocomplete (`getLiveBindings` /
|
|
93
|
+
* `getMembers`) — lets completion suggest the REAL members of live values (e.g.
|
|
94
|
+
* a tosijs proxy or a DOM element) that static analysis can't see. Only used in
|
|
95
|
+
* tjs mode; setting it re-applies the tjs extension so it takes effect live.
|
|
96
|
+
*/
|
|
97
|
+
get tjsAutocomplete() {
|
|
98
|
+
return this._tjsAutocomplete;
|
|
99
|
+
}
|
|
100
|
+
set tjsAutocomplete(config) {
|
|
101
|
+
this._tjsAutocomplete = config;
|
|
102
|
+
if (this._handle && this.isTjsMode())
|
|
103
|
+
this.applyTjsExtension();
|
|
104
|
+
}
|
|
37
105
|
get value() {
|
|
38
106
|
return this._handle ? this._handle.getValue() : this.source;
|
|
39
107
|
}
|
|
@@ -55,33 +123,53 @@ export class CodeEditor extends WebComponent {
|
|
|
55
123
|
this._original = text;
|
|
56
124
|
}
|
|
57
125
|
// Diff overlay — built only on the editor's public surface (`value` + `original`)
|
|
58
|
-
// and the tosi-diff component, never the underlying editor's API, so it
|
|
59
|
-
// editor-agnostic
|
|
126
|
+
// and the tosi-diff component, never the underlying editor's API, so it stays
|
|
127
|
+
// editor-agnostic.
|
|
128
|
+
//
|
|
129
|
+
// It lives in the SHADOW root (the `diffHost` part), not the light DOM. Under the
|
|
130
|
+
// old Ace editor this component had no `content`, so tosijs's default `slot()`
|
|
131
|
+
// filled the shadow root and Ace mounted into the light DOM — a light-DOM overlay
|
|
132
|
+
// projected through that slot. CodeMirror mounts into `[part=host]` inside the
|
|
133
|
+
// shadow root, and this component now declares its own `content`, so there is no
|
|
134
|
+
// slot: `this.append(overlay)` would put it in the light DOM where nothing renders
|
|
135
|
+
// it. (Re-adding a slot is NOT the fix — it would also project the element's
|
|
136
|
+
// textContent, i.e. the initial code, and double-render it under the editor.)
|
|
60
137
|
diffOverlay;
|
|
138
|
+
// Set once hydration has actually happened (end of connectedCallback).
|
|
139
|
+
//
|
|
140
|
+
// This CANNOT be a `try { this.parts.x } catch` probe, which is what it used to be.
|
|
141
|
+
// tosijs's `parts` proxy caches its query root on FIRST access — so touching it before
|
|
142
|
+
// hydration permanently roots the proxy at the light-DOM element, and every later
|
|
143
|
+
// `this.parts.*` throws, including the `this.parts.host` that mounts CodeMirror. The
|
|
144
|
+
// editor then never mounts at all. Merely reading `el.showingDiff` on a constructed,
|
|
145
|
+
// not-yet-inserted element was enough to brick it — silently, and unrecoverably.
|
|
146
|
+
_partsHydrated = false;
|
|
147
|
+
// A showDiff() call made before hydration, replayed once we're ready (rather than
|
|
148
|
+
// silently dropped, which is what used to happen).
|
|
149
|
+
_pendingDiff;
|
|
150
|
+
get partsReady() {
|
|
151
|
+
return this._partsHydrated;
|
|
152
|
+
}
|
|
61
153
|
get showingDiff() {
|
|
62
|
-
|
|
154
|
+
if (!this.partsReady)
|
|
155
|
+
return this._pendingDiff ?? false;
|
|
156
|
+
return !this.parts.diffHost.hidden;
|
|
63
157
|
}
|
|
64
158
|
showDiff(on) {
|
|
159
|
+
if (!this.partsReady) {
|
|
160
|
+
this._pendingDiff = on;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const { diffHost } = this.parts;
|
|
65
164
|
if (on) {
|
|
66
165
|
if (this.diffOverlay === undefined) {
|
|
67
|
-
this.diffOverlay = tosiDiff(
|
|
68
|
-
|
|
69
|
-
position: 'absolute',
|
|
70
|
-
inset: '0',
|
|
71
|
-
zIndex: '5',
|
|
72
|
-
overflow: 'auto',
|
|
73
|
-
background: 'var(--tosi-diff-bg, var(--background, #fff))',
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
this.append(this.diffOverlay);
|
|
166
|
+
this.diffOverlay = tosiDiff();
|
|
167
|
+
diffHost.append(this.diffOverlay);
|
|
77
168
|
}
|
|
78
169
|
this.diffOverlay.original = this.original;
|
|
79
170
|
this.diffOverlay.modified = this.value;
|
|
80
|
-
this.diffOverlay.hidden = false;
|
|
81
|
-
}
|
|
82
|
-
else if (this.diffOverlay !== undefined) {
|
|
83
|
-
this.diffOverlay.hidden = true;
|
|
84
171
|
}
|
|
172
|
+
diffHost.hidden = !on;
|
|
85
173
|
}
|
|
86
174
|
static initAttributes = {
|
|
87
175
|
mode: 'javascript',
|
|
@@ -92,6 +180,32 @@ export class CodeEditor extends WebComponent {
|
|
|
92
180
|
get editor() {
|
|
93
181
|
return this._handle?.view;
|
|
94
182
|
}
|
|
183
|
+
// ── Removed in 1.7 (ACE → CodeMirror 6) ────────────────────────────────────
|
|
184
|
+
// `^1.6.x` resolves 1.7.0, so existing consumers auto-upgrade into this. Left as
|
|
185
|
+
// warn-once no-ops rather than simply deleted, so `theme`/`options`/`ace` fail
|
|
186
|
+
// with an actionable message instead of silently doing nothing (or, for `ace`,
|
|
187
|
+
// a bare `TypeError` on undefined). See CHANGELOG 1.7.0 → Breaking.
|
|
188
|
+
/** @deprecated Removed in 1.7 — CodeMirror themes via `--code-bg`/`--text-color`. */
|
|
189
|
+
get theme() {
|
|
190
|
+
warnRemoved('theme', 'style the editor with --code-bg / --text-color instead');
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
set theme(_) {
|
|
194
|
+
warnRemoved('theme', 'style the editor with --code-bg / --text-color instead');
|
|
195
|
+
}
|
|
196
|
+
/** @deprecated Removed in 1.7 — ACE-shaped options have no CodeMirror equivalent. */
|
|
197
|
+
get options() {
|
|
198
|
+
warnRemoved('options', 'configure CodeMirror via the `editor` (EditorView) instead');
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
set options(_) {
|
|
202
|
+
warnRemoved('options', 'configure CodeMirror via the `editor` (EditorView) instead');
|
|
203
|
+
}
|
|
204
|
+
/** @deprecated Removed in 1.7 — there is no ACE global; use `editor` (an EditorView). */
|
|
205
|
+
get ace() {
|
|
206
|
+
warnRemoved('ace', 'use `editor`, which is now a CodeMirror EditorView');
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
95
209
|
// History control — so consumers use these instead of reaching into `editor`.
|
|
96
210
|
undo() {
|
|
97
211
|
this._handle?.undo();
|
|
@@ -105,7 +219,12 @@ export class CodeEditor extends WebComponent {
|
|
|
105
219
|
canRedo() {
|
|
106
220
|
return this._handle?.canRedo() ?? false;
|
|
107
221
|
}
|
|
108
|
-
|
|
222
|
+
// `diffHost` starts hidden — an always-present absolutely-positioned overlay would
|
|
223
|
+
// otherwise sit on top of the editor and swallow every click.
|
|
224
|
+
content = () => [
|
|
225
|
+
div({ part: 'host' }),
|
|
226
|
+
div({ part: 'diffHost', hidden: true }),
|
|
227
|
+
];
|
|
109
228
|
static shadowStyleSpec = {
|
|
110
229
|
':host': {
|
|
111
230
|
display: 'block',
|
|
@@ -114,6 +233,13 @@ export class CodeEditor extends WebComponent {
|
|
|
114
233
|
height: '100%',
|
|
115
234
|
},
|
|
116
235
|
'[part="host"]': { height: '100%' },
|
|
236
|
+
'[part="diffHost"]': {
|
|
237
|
+
position: 'absolute',
|
|
238
|
+
inset: '0',
|
|
239
|
+
zIndex: '5',
|
|
240
|
+
overflow: 'auto',
|
|
241
|
+
background: varDefault.tosiDiffBg(varDefault.background('#fff')),
|
|
242
|
+
},
|
|
117
243
|
'.cm-editor': { height: '100%' },
|
|
118
244
|
'.cm-scroller': {
|
|
119
245
|
outline: 'none',
|
|
@@ -125,12 +251,33 @@ export class CodeEditor extends WebComponent {
|
|
|
125
251
|
}
|
|
126
252
|
connectedCallback() {
|
|
127
253
|
super.connectedCallback();
|
|
254
|
+
// super.connectedCallback() hydrates, so `this.parts` is safe from here on.
|
|
255
|
+
this._partsHydrated = true;
|
|
128
256
|
if (this.source === '') {
|
|
129
257
|
this.value = this.textContent !== null ? this.textContent.trim() : '';
|
|
130
258
|
}
|
|
259
|
+
// Replay a showDiff() that arrived before we were hydrated.
|
|
260
|
+
if (this._pendingDiff !== undefined) {
|
|
261
|
+
const pending = this._pendingDiff;
|
|
262
|
+
this._pendingDiff = undefined;
|
|
263
|
+
this.showDiff(pending);
|
|
264
|
+
}
|
|
131
265
|
if (this._loadPromise === undefined) {
|
|
132
266
|
// Lazy chunk — CodeMirror only enters the bundle here, on first editor use.
|
|
267
|
+
const generation = ++this._loadGeneration;
|
|
133
268
|
this._loadPromise = import('./code-editor-cm').then(({ createCmEditor }) => {
|
|
269
|
+
// The chunk fetch is async, and the pre-load window is WIDE on a cold fetch —
|
|
270
|
+
// doc-browser navigation and closeEditor() both remove editors mid-flight. If we
|
|
271
|
+
// mounted regardless:
|
|
272
|
+
// append → remove → an EditorView built into a detached shadow root,
|
|
273
|
+
// with no disconnectedCallback left to destroy it.
|
|
274
|
+
// append → remove → append → TWO views in the host; `_handle` points only at
|
|
275
|
+
// the second, so the first (and its darkmode
|
|
276
|
+
// listener) is retained forever.
|
|
277
|
+
// That is precisely the leak disconnectedCallback exists to prevent, relocated
|
|
278
|
+
// into the load window. Bail if a newer connect superseded us, or we're detached.
|
|
279
|
+
if (generation !== this._loadGeneration || !this.isConnected)
|
|
280
|
+
return undefined;
|
|
134
281
|
const handle = createCmEditor(this.parts.host, {
|
|
135
282
|
value: this.source,
|
|
136
283
|
mode: this.mode,
|
|
@@ -146,6 +293,26 @@ export class CodeEditor extends WebComponent {
|
|
|
146
293
|
});
|
|
147
294
|
}
|
|
148
295
|
}
|
|
296
|
+
disconnectedCallback() {
|
|
297
|
+
super.disconnectedCallback();
|
|
298
|
+
// Without this, `CmHandle.destroy()` has no call sites: the EditorView (and its
|
|
299
|
+
// darkmode listener) outlive the element, so every doc-page navigation leaked all
|
|
300
|
+
// ~20 editors on the page.
|
|
301
|
+
if (this._handle) {
|
|
302
|
+
// Preserve the live text first — `value` falls back to `source` once the handle
|
|
303
|
+
// is gone, and `source` would otherwise hold a stale pre-edit value.
|
|
304
|
+
this.source = this._handle.getValue();
|
|
305
|
+
this._handle.destroy();
|
|
306
|
+
this._handle = undefined;
|
|
307
|
+
}
|
|
308
|
+
// Re-connecting rebuilds the editor (a destroyed view can't be reused).
|
|
309
|
+
this._loadPromise = undefined;
|
|
310
|
+
this._appliedMode = '';
|
|
311
|
+
this._appliedDisabled = undefined;
|
|
312
|
+
// NB: do NOT clear `diffOverlay` — hydrate() runs once, so the shadow DOM (and the
|
|
313
|
+
// overlay inside `diffHost`) survives disconnect. Clearing it would append a second
|
|
314
|
+
// overlay on reconnect.
|
|
315
|
+
}
|
|
149
316
|
isTjsMode() {
|
|
150
317
|
return this.mode === 'tjs' || this.mode === 'ajs';
|
|
151
318
|
}
|
|
@@ -159,7 +326,7 @@ export class CodeEditor extends WebComponent {
|
|
|
159
326
|
if (!handle || !this.isTjsMode())
|
|
160
327
|
return;
|
|
161
328
|
import('./code-editor-cm')
|
|
162
|
-
.then(({ loadTjsExtension }) => loadTjsExtension())
|
|
329
|
+
.then(({ loadTjsExtension }) => loadTjsExtension(this._tjsAutocomplete ?? {}))
|
|
163
330
|
.then((ext) => {
|
|
164
331
|
if (ext && this._handle === handle && this.isTjsMode()) {
|
|
165
332
|
handle.setLanguageExtension(ext);
|