tosijs-ui 1.7.2 → 1.7.3
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 +35 -0
- package/dist/color-input.d.ts +0 -1
- package/dist/color-input.js +24 -10
- package/dist/doc-browser.js +4 -2
- package/dist/doc-system/doc-system-styles.d.ts +2 -0
- package/dist/doc-system/doc-system-styles.js +15 -1
- package/dist/doc-system/site/check-examples-cli.js +5 -3
- package/dist/doc-system/site/check-examples.d.ts +13 -0
- package/dist/doc-system/site/check-examples.js +26 -10
- package/dist/doc-system/site/orchestrator.js +15 -2
- package/dist/form.d.ts +0 -1
- package/dist/form.js +35 -21
- package/dist/hydrate/hydrate.js +82 -82
- package/dist/hydrate/hydrate.js.map +15 -14
- package/dist/iife.js +114 -114
- package/dist/iife.js.map +15 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-example/code-transform.d.ts +14 -0
- package/dist/live-example/code-transform.js +20 -2
- package/dist/live-example/component.d.ts +24 -51
- package/dist/live-example/component.js +44 -51
- package/dist/live-example/styles.d.ts +22 -50
- package/dist/live-example/styles.js +48 -49
- package/dist/live-example/types.d.ts +2 -2
- package/dist/pocket-bar.d.ts +99 -0
- package/dist/pocket-bar.js +298 -0
- package/dist/pop-float.d.ts +1 -1
- package/dist/pop-float.js +13 -7
- package/dist/segmented.d.ts +0 -1
- package/dist/segmented.js +38 -20
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.3
|
|
4
|
+
|
|
5
|
+
- **New `<tosi-pocket-bar>` component.** A "pocket buttonbar": a single translucent icon at
|
|
6
|
+
rest that expands into a bar of slotted icon-button controls on hover/tap, then collapses
|
|
7
|
+
again. `direction` (the full `FloatPosition` vocabulary, `auto` by default) determines both
|
|
8
|
+
layout and growth direction; the handle stays put and toggles the bar; touch counts as hover.
|
|
9
|
+
Built as a thin wrapper around `positionFloat`.
|
|
10
|
+
- **The live-example toolbar is now a pocket bar**, pinned to the top-right of the preview (so
|
|
11
|
+
it never sits over the code editors in side-by-side mode). The collapsed handle carries the
|
|
12
|
+
example's pass/fail color, "run tests" is a checkbox that greys when off, and the bar stays
|
|
13
|
+
visible when the example is maximized.
|
|
14
|
+
- **Build system is fragility-tolerant.** An *unsupported import* in a live example (a bare
|
|
15
|
+
specifier the runtime can't resolve) now downgrades that block to display-only with a warning
|
|
16
|
+
instead of failing the whole build; genuine syntax errors still fail the build.
|
|
17
|
+
- **Removed a render-skip anti-pattern** from `<tosi-segmented>`, `<tosi-color>`, and
|
|
18
|
+
`<tosi-form>`. They previously depended on `render()` being *skipped* after interaction via a
|
|
19
|
+
`valueChanged` flag — fragile timing that broke under tosijs's parts changes. Renders are now
|
|
20
|
+
idempotent: they skip only *provably-redundant* writes by comparing values (colors compared
|
|
21
|
+
**parsed**, so a `#rrggbb ↔ rgba()` round-trip no longer clobbers the caret/selection in the
|
|
22
|
+
field being edited).
|
|
23
|
+
- **Code editor background is now a neutral off-white** (off-black in dark mode) instead of a
|
|
24
|
+
brand-tinted color — both the default `<tosi-code>` and live examples.
|
|
25
|
+
- **Fix: nav flickered on refresh / reflowed on hydration.** The static no-JS nav relied on the
|
|
26
|
+
browser's native `<details>` collapse (`::details-content { content-visibility: hidden }`),
|
|
27
|
+
which Chromium applies lazily and non-deterministically on first paint — a closed section
|
|
28
|
+
could render at full height for a frame before snapping shut. Closed sections now collapse
|
|
29
|
+
synchronously via `display: none` (still opening on a no-JS `<summary>` toggle). Enforced by
|
|
30
|
+
`tests/hydration.pw.ts`.
|
|
31
|
+
- **Moved the doc-browser test-counter widget to the bottom-left** so it no longer sits under
|
|
32
|
+
the haltija dev overlay.
|
|
33
|
+
- **tosijs bumped to `^1.7.8`**, which fixes the `this.parts` proxy crossing into nested
|
|
34
|
+
component instances (tosijs#20 — surfaced by the self-hosting doc demo, where clicking "edit"
|
|
35
|
+
on one example opened the editor in the wrong one) and change-handler value staleness
|
|
36
|
+
(tosijs#21).
|
|
37
|
+
|
|
3
38
|
## 1.7.2
|
|
4
39
|
|
|
5
40
|
- **Fix: doc sites are now mount-point-agnostic** (issue #25). The build baked `basePath` into
|
package/dist/color-input.d.ts
CHANGED
package/dist/color-input.js
CHANGED
|
@@ -71,7 +71,6 @@ class ColorInput extends Component {
|
|
|
71
71
|
}),
|
|
72
72
|
input({ title: 'css color spec', part: 'css' }),
|
|
73
73
|
];
|
|
74
|
-
valueChanged = false;
|
|
75
74
|
update = (event) => {
|
|
76
75
|
const { rgb, alpha, css } = this.parts;
|
|
77
76
|
if (event.type === 'input') {
|
|
@@ -86,7 +85,6 @@ class ColorInput extends Component {
|
|
|
86
85
|
}
|
|
87
86
|
rgb.style.opacity = String(this.color.a);
|
|
88
87
|
this.value = this.color.rgba;
|
|
89
|
-
this.valueChanged = true;
|
|
90
88
|
};
|
|
91
89
|
connectedCallback() {
|
|
92
90
|
super.connectedCallback();
|
|
@@ -96,16 +94,32 @@ class ColorInput extends Component {
|
|
|
96
94
|
css.addEventListener('change', this.update);
|
|
97
95
|
}
|
|
98
96
|
render() {
|
|
99
|
-
if (this.valueChanged) {
|
|
100
|
-
this.valueChanged = false;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
97
|
const { rgb, alpha, css } = this.parts;
|
|
104
98
|
this.color = Color.fromCss(this.value);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
const rgba = this.color.rgba;
|
|
100
|
+
const alphaStr = String(this.color.a);
|
|
101
|
+
// Idempotent: only WRITE an input whose value doesn't already represent this
|
|
102
|
+
// colour. Compare the PARSED colour, not the string — `#rrggbb` and `rgba(...)`
|
|
103
|
+
// are the same colour, so a string compare (or a blind write) would clobber the
|
|
104
|
+
// caret / selection and re-round-trip the user's format on the field they're
|
|
105
|
+
// editing, every render. Skipping provably-redundant work is right; a `valueChanged`
|
|
106
|
+
// timing flag to decide it was the fragile part (broke under tosijs 1.7.7).
|
|
107
|
+
const sameColor = (v) => {
|
|
108
|
+
try {
|
|
109
|
+
return Color.fromCss(v).rgba === rgba;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
if (!sameColor(rgb.value))
|
|
116
|
+
rgb.value = this.color.html.substring(0, 7);
|
|
117
|
+
if (rgb.style.opacity !== alphaStr)
|
|
118
|
+
rgb.style.opacity = alphaStr;
|
|
119
|
+
if (alpha.value !== alphaStr)
|
|
120
|
+
alpha.value = alphaStr;
|
|
121
|
+
if (!sameColor(css.value))
|
|
122
|
+
css.value = this.color.html;
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
export const colorInput = ColorInput.elementCreator();
|
package/dist/doc-browser.js
CHANGED
|
@@ -206,12 +206,14 @@ const testIndicatorStyleSpec = {
|
|
|
206
206
|
background: testColor.fail,
|
|
207
207
|
animation: 'test-pulse 2s ease-in-out infinite',
|
|
208
208
|
},
|
|
209
|
-
// Floating widget - position and colors only, inherits button structure
|
|
209
|
+
// Floating widget - position and colors only, inherits button structure.
|
|
210
|
+
// Bottom-LEFT: bottom-right is where the injected haltija dev widget sits, and it
|
|
211
|
+
// was overlaying this.
|
|
210
212
|
'.test-widget': {
|
|
211
213
|
_testBg: testColor.running,
|
|
212
214
|
position: 'fixed',
|
|
213
215
|
bottom: vars.spacing,
|
|
214
|
-
|
|
216
|
+
left: vars.spacing,
|
|
215
217
|
zIndex: '1000',
|
|
216
218
|
background: vars.testBg,
|
|
217
219
|
color: 'white',
|
|
@@ -8,6 +8,8 @@ export interface DocSystemTheme {
|
|
|
8
8
|
text?: string;
|
|
9
9
|
buttonBg?: string;
|
|
10
10
|
inputBg?: string;
|
|
11
|
+
/** code-editor surface (`--code-bg`); neutral off-white by default, not brand-tinted. */
|
|
12
|
+
codeBg?: string;
|
|
11
13
|
}
|
|
12
14
|
/** Compute the full set of `:root` color variables from a few base colors. */
|
|
13
15
|
export declare function docSystemColors(theme?: DocSystemTheme): XinStyleRule;
|
|
@@ -34,7 +34,9 @@ export function docSystemColors(theme = {}) {
|
|
|
34
34
|
_placeholderColor: brandColor.opacity(0.4),
|
|
35
35
|
_brandTextColor: brandColor.rotate(30).brighten(0.9),
|
|
36
36
|
_insetBg: brandColor.rotate(45).brighten(0.8),
|
|
37
|
-
|
|
37
|
+
// Neutral off-white (not brand-tinted); dark mode inverts its luminance to
|
|
38
|
+
// off-black, so the editor surface stays a clean neutral in both themes.
|
|
39
|
+
_codeBg: theme.codeBg ?? '#fdfdfd',
|
|
38
40
|
_linkColor: brandColor.rotate(-30).darken(0.5),
|
|
39
41
|
_shadowColor: '#0004',
|
|
40
42
|
_menuBg: '#fafafa',
|
|
@@ -476,6 +478,18 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
476
478
|
'.doc-nav details > ul': {
|
|
477
479
|
paddingLeft: vars.spacing,
|
|
478
480
|
},
|
|
481
|
+
// Collapse a closed section's list SYNCHRONOUSLY. The UA collapses <details>
|
|
482
|
+
// via `::details-content { content-visibility: hidden }`, which is applied
|
|
483
|
+
// lazily and — for this static tree — non-deterministically on first paint:
|
|
484
|
+
// sometimes the closed section is still laid out at full height for a frame
|
|
485
|
+
// before it snaps shut. That frame is a visible flicker on refresh, and it made
|
|
486
|
+
// the "nav does not move on hydration" test measure a 36px row that hydration
|
|
487
|
+
// then collapses to 0. An explicit display:none collapses it in the first layout
|
|
488
|
+
// pass with no content-visibility race, and still opens on a no-JS <summary>
|
|
489
|
+
// toggle (the UA adds [open], so this rule stops matching).
|
|
490
|
+
'.doc-nav details:not([open]) > ul': {
|
|
491
|
+
display: 'none',
|
|
492
|
+
},
|
|
479
493
|
// <summary> is the section header (its triangle toggles; the link navigates).
|
|
480
494
|
'.doc-nav summary': {
|
|
481
495
|
cursor: 'pointer',
|
|
@@ -20,8 +20,9 @@ the bundle step, the ePub step, and generate-css; see the Bun.build note in
|
|
|
20
20
|
orchestrator.ts.
|
|
21
21
|
|
|
22
22
|
bun check-examples-cli.ts <docs.json> [contextKey ...]
|
|
23
|
-
→ writes
|
|
24
|
-
found (a problem is data, not a crash — only a real failure exits
|
|
23
|
+
→ writes { problems, warnings, bakes } as JSON to stdout; exit 0 whether or not
|
|
24
|
+
problems were found (a problem is data, not a crash — only a real failure exits
|
|
25
|
+
non-zero). The parent fails the build on `problems`, warns on `warnings`.
|
|
25
26
|
|
|
26
27
|
Build-time only. Never import this from browser code.
|
|
27
28
|
*/
|
|
@@ -36,7 +37,7 @@ const contextKeys = process.argv.slice(3);
|
|
|
36
37
|
// non-context imports validate as dynamic `<prefix><spec>` imports rather than failing.
|
|
37
38
|
const importPrefix = process.env.TOSI_IMPORT_PREFIX || undefined;
|
|
38
39
|
const corpus = JSON.parse(await Bun.file(docsJson).text());
|
|
39
|
-
const { problems, bakes } = await checkExamples(corpus, {
|
|
40
|
+
const { problems, warnings, bakes } = await checkExamples(corpus, {
|
|
40
41
|
...(contextKeys.length ? { contextKeys } : {}),
|
|
41
42
|
...(importPrefix ? { importPrefix } : {}),
|
|
42
43
|
});
|
|
@@ -45,6 +46,7 @@ const { problems, bakes } = await checkExamples(corpus, {
|
|
|
45
46
|
// Maps can't JSON-roundtrip, so bakes go over as [filename, [source, {dialect,js}][]][].
|
|
46
47
|
process.stdout.write(JSON.stringify({
|
|
47
48
|
problems,
|
|
49
|
+
warnings,
|
|
48
50
|
bakes: Array.from(bakes.entries()).map(([filename, docBakes]) => [
|
|
49
51
|
filename,
|
|
50
52
|
Array.from(docBakes.entries()),
|
|
@@ -8,7 +8,20 @@ export interface ExampleProblem {
|
|
|
8
8
|
snippet: string;
|
|
9
9
|
}
|
|
10
10
|
export interface ExampleCheck {
|
|
11
|
+
/**
|
|
12
|
+
* Blocks that failed to build for a real reason (a tjs/TS syntax error, a
|
|
13
|
+
* transpile failure). These FAIL the build — broken code shouldn't ship.
|
|
14
|
+
*/
|
|
11
15
|
problems: ExampleProblem[];
|
|
16
|
+
/**
|
|
17
|
+
* Blocks that reference a package the environment can't provide (a non-context
|
|
18
|
+
* import, no import-resolver). The code isn't broken, it just can't run here —
|
|
19
|
+
* almost always illustrative code that should be tagged `typescript` (display-
|
|
20
|
+
* only) rather than `ts`. These WARN and are treated as display-only; they do
|
|
21
|
+
* NOT fail the build. (Enable `importResolver`, or tag the block display-only,
|
|
22
|
+
* to silence the warning.)
|
|
23
|
+
*/
|
|
24
|
+
warnings: ExampleProblem[];
|
|
12
25
|
/**
|
|
13
26
|
* Build-time transpiled JS for `tjs` blocks, grouped by doc filename, each keyed
|
|
14
27
|
* by exact source text. The renderer embeds a doc's bakes as hidden scripts (so
|
|
@@ -4,17 +4,25 @@ Build-time transpile check for live examples.
|
|
|
4
4
|
Every executable code block in the corpus (`js` / `tjs` / `ts` / `test`) is put
|
|
5
5
|
through the SAME front half of the runtime pipeline the live-example component
|
|
6
6
|
uses — `rewriteImports` → the tjs-lang transform → `new AsyncFunction(...)` — so a
|
|
7
|
-
block that can't build
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
block that can't build is caught *at authoring time*, on every page, instead of
|
|
8
|
+
silently rendering an error only when someone opens that page. The two outcomes
|
|
9
|
+
are graded differently (this is the point):
|
|
10
|
+
|
|
11
|
+
- a real **syntax / transpile error** is broken code → a `problem` that FAILS
|
|
12
|
+
the build;
|
|
13
|
+
- an **unsupported import** (a non-context package, no import-resolver) is fine
|
|
14
|
+
code the doc environment just can't run → a `warning`: the block is treated as
|
|
15
|
+
display-only and the build survives. It's almost always illustrative code that
|
|
16
|
+
should have been tagged `typescript` instead of `ts` — a mistagging shouldn't
|
|
17
|
+
take the whole build down with it.
|
|
18
|
+
|
|
19
|
+
`html` / `css` and display-only languages (`typescript`, `json`, …) are not
|
|
20
|
+
executed, so they're skipped.
|
|
13
21
|
|
|
14
22
|
Build-time only (bun). Never import from browser code.
|
|
15
23
|
*/
|
|
16
24
|
import { marked } from 'marked';
|
|
17
|
-
import { rewriteImports, AsyncFunction, loadTransform, } from '../../live-example/code-transform';
|
|
25
|
+
import { rewriteImports, AsyncFunction, loadTransform, UnsupportedImportError, } from '../../live-example/code-transform';
|
|
18
26
|
// The default live-example context (matches the IIFE globals the pages provide).
|
|
19
27
|
// A project that sets a custom `context` on its <tosi-doc-system> can pass its
|
|
20
28
|
// own keys; these are the tosijs-ui defaults.
|
|
@@ -58,6 +66,7 @@ function collectCodeTokens(text) {
|
|
|
58
66
|
export async function checkExamples(docs, opts = {}) {
|
|
59
67
|
const contextKeys = opts.contextKeys ?? DEFAULT_CONTEXT_KEYS;
|
|
60
68
|
const problems = [];
|
|
69
|
+
const warnings = [];
|
|
61
70
|
const bakes = new Map();
|
|
62
71
|
for (const doc of docs) {
|
|
63
72
|
for (const block of collectCodeTokens(doc.text)) {
|
|
@@ -97,17 +106,24 @@ export async function checkExamples(docs, opts = {}) {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
catch (err) {
|
|
100
|
-
|
|
109
|
+
const entry = {
|
|
101
110
|
filename: doc.filename,
|
|
102
111
|
title: doc.title,
|
|
103
112
|
lang: block.lang,
|
|
104
113
|
error: err.message || String(err),
|
|
105
114
|
snippet: block.text.split('\n').slice(0, 3).join('\n'),
|
|
106
|
-
}
|
|
115
|
+
};
|
|
116
|
+
// An unsupported import means the block references deps the environment
|
|
117
|
+
// can't provide — not broken code. Warn (display-only), don't fail the
|
|
118
|
+
// build. A real syntax/transpile error is a genuine problem.
|
|
119
|
+
if (err instanceof UnsupportedImportError)
|
|
120
|
+
warnings.push(entry);
|
|
121
|
+
else
|
|
122
|
+
problems.push(entry);
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
|
-
return { problems, bakes };
|
|
126
|
+
return { problems, warnings, bakes };
|
|
111
127
|
}
|
|
112
128
|
/** Format problems for a build log. */
|
|
113
129
|
export function formatExampleProblems(problems) {
|
|
@@ -132,6 +132,7 @@ async function checkExamplesInChild(docsJson, importPrefix) {
|
|
|
132
132
|
const payload = JSON.parse(out);
|
|
133
133
|
return {
|
|
134
134
|
problems: payload.problems,
|
|
135
|
+
warnings: payload.warnings ?? [],
|
|
135
136
|
bakes: new Map(payload.bakes.map(([file, e]) => [file, new Map(e)])),
|
|
136
137
|
};
|
|
137
138
|
}
|
|
@@ -239,14 +240,26 @@ export async function buildSite(config) {
|
|
|
239
240
|
: undefined;
|
|
240
241
|
let exampleBakes;
|
|
241
242
|
if (config.checkExamples !== false) {
|
|
242
|
-
const { problems, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
|
|
243
|
+
const { problems, warnings, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
|
|
243
244
|
exampleBakes = bakes;
|
|
245
|
+
// Unsupported imports don't fail the build — the code isn't broken, it just
|
|
246
|
+
// can't run in the doc environment (almost always illustrative code that
|
|
247
|
+
// should be tagged `typescript`). Warn loudly, treat the block as display-only,
|
|
248
|
+
// and keep going. Only real syntax/transpile errors below are fatal.
|
|
249
|
+
if (warnings.length) {
|
|
250
|
+
console.warn(`⚠️ doc-site build: ${warnings.length} live example block(s) can't run ` +
|
|
251
|
+
`here and were left as display-only:\n\n` +
|
|
252
|
+
formatExampleProblems(warnings) +
|
|
253
|
+
`\n\nTag each with a display-only language like \`typescript\` (instead of` +
|
|
254
|
+
` \`js\`/\`ts\`) to silence this, or enable \`importResolver\` to import` +
|
|
255
|
+
` other packages.\n`);
|
|
256
|
+
}
|
|
244
257
|
if (problems.length) {
|
|
245
258
|
throw new Error(`doc-site build: ${problems.length} live example(s) failed to build:\n\n` +
|
|
246
259
|
formatExampleProblems(problems) +
|
|
247
260
|
`\n\nFix the code, or — if a block is illustrative and not meant to run —` +
|
|
248
261
|
` tag it with a display-only language like \`typescript\` instead of` +
|
|
249
|
-
` \`js\`/\`ts\`. (Disable with checkExamples: false.)`);
|
|
262
|
+
` \`js\`/\`ts\`. (Disable all checking with checkExamples: false.)`);
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
// Copy static-asset dirs into the web root.
|
package/dist/form.d.ts
CHANGED
package/dist/form.js
CHANGED
|
@@ -430,6 +430,10 @@ function getInputValue(input) {
|
|
|
430
430
|
: input.value;
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
|
+
// Idempotent: write only when the value actually differs. Blindly re-assigning
|
|
434
|
+
// `input.value` (even the same string) moves the caret / drops the selection of a
|
|
435
|
+
// field being edited, so render must not do it as redundant work — and it lets
|
|
436
|
+
// render run any number of times without side-effects.
|
|
433
437
|
function setElementValue(input, value) {
|
|
434
438
|
if (!(input instanceof HTMLElement)) {
|
|
435
439
|
// do nothing
|
|
@@ -437,20 +441,26 @@ function setElementValue(input, value) {
|
|
|
437
441
|
else if (input instanceof HTMLInputElement) {
|
|
438
442
|
switch (input.type) {
|
|
439
443
|
case 'checkbox':
|
|
440
|
-
input.checked
|
|
444
|
+
if (input.checked !== Boolean(value))
|
|
445
|
+
input.checked = value;
|
|
441
446
|
break;
|
|
442
|
-
case 'radio':
|
|
443
|
-
|
|
447
|
+
case 'radio': {
|
|
448
|
+
const checked = value === input.value;
|
|
449
|
+
if (input.checked !== checked)
|
|
450
|
+
input.checked = checked;
|
|
444
451
|
break;
|
|
445
|
-
|
|
446
|
-
|
|
452
|
+
}
|
|
453
|
+
default: {
|
|
454
|
+
const v = String(value || '');
|
|
455
|
+
if (input.value !== v)
|
|
456
|
+
input.value = v;
|
|
457
|
+
}
|
|
447
458
|
}
|
|
448
459
|
}
|
|
449
|
-
else {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
input.value =
|
|
453
|
-
}
|
|
460
|
+
else if (value != null || input.value != null) {
|
|
461
|
+
const v = String(value || '');
|
|
462
|
+
if (input.value !== v)
|
|
463
|
+
input.value = v;
|
|
454
464
|
}
|
|
455
465
|
}
|
|
456
466
|
export class TosiField extends XinComponent {
|
|
@@ -499,7 +509,6 @@ export class TosiField extends XinComponent {
|
|
|
499
509
|
};
|
|
500
510
|
value = null;
|
|
501
511
|
content = label(tosiSlot({ part: 'caption' }), span({ part: 'field' }, tosiSlot({ part: 'input', name: 'input' }), input({ part: 'valueHolder' })));
|
|
502
|
-
valueChanged = false;
|
|
503
512
|
handleChange = () => {
|
|
504
513
|
const { input, valueHolder } = this.parts;
|
|
505
514
|
const inputElement = (input.children[0] || valueHolder);
|
|
@@ -507,7 +516,6 @@ export class TosiField extends XinComponent {
|
|
|
507
516
|
valueHolder.value = inputElement.value;
|
|
508
517
|
}
|
|
509
518
|
this.value = getInputValue(inputElement);
|
|
510
|
-
this.valueChanged = true;
|
|
511
519
|
const form = this.closest('tosi-form');
|
|
512
520
|
if (form && this.key !== '') {
|
|
513
521
|
switch (this.type) {
|
|
@@ -537,25 +545,31 @@ export class TosiField extends XinComponent {
|
|
|
537
545
|
input.addEventListener('change', this.handleChange, true);
|
|
538
546
|
}
|
|
539
547
|
render() {
|
|
540
|
-
if (this.valueChanged) {
|
|
541
|
-
this.valueChanged = false;
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
548
|
const { input, caption, valueHolder, field } = this.parts;
|
|
545
549
|
if (caption.textContent?.trim() === '') {
|
|
546
550
|
caption.append(this.caption !== '' ? this.caption : this.key);
|
|
547
551
|
}
|
|
552
|
+
// Build the type-specific child ONCE and reuse it. Re-creating the <textarea> /
|
|
553
|
+
// <tosi-color> every render (the old behavior, only avoided by a `valueChanged`
|
|
554
|
+
// timing flag) destroyed focus, caret and selection mid-edit — provably redundant
|
|
555
|
+
// when the child of the right kind already exists. The value is reconciled
|
|
556
|
+
// idempotently by setElementValue below.
|
|
548
557
|
if (this.type === 'text') {
|
|
549
|
-
|
|
550
|
-
|
|
558
|
+
let textarea = input.children[0];
|
|
559
|
+
if (!(textarea instanceof HTMLTextAreaElement)) {
|
|
560
|
+
input.textContent = '';
|
|
561
|
+
textarea = elements.textarea({ value: this.value });
|
|
562
|
+
input.append(textarea);
|
|
563
|
+
}
|
|
551
564
|
if (this.placeholder) {
|
|
552
565
|
textarea.setAttribute('placeholder', this.placeholder);
|
|
553
566
|
}
|
|
554
|
-
input.append(textarea);
|
|
555
567
|
}
|
|
556
568
|
else if (this.type === 'color') {
|
|
557
|
-
input.
|
|
558
|
-
|
|
569
|
+
if (!input.children[0]?.matches('tosi-color')) {
|
|
570
|
+
input.textContent = '';
|
|
571
|
+
input.append(colorInput({ value: this.value }));
|
|
572
|
+
}
|
|
559
573
|
}
|
|
560
574
|
else if (input.children.length === 0) {
|
|
561
575
|
attr(valueHolder, 'placeholder', this.placeholder);
|