@antglobal/copilot-cards-web 1.0.2 → 1.0.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/dist/index.d.ts +438 -12
- package/dist/index.js +3623 -291
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBuiltinIcon,
|
|
1
|
+
import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, resolveActionRef, createA2UIParameterResolver, createExpressionContext, resolveA2UIDeep, hasExpression, resolveExpression, resolveDeep, resolveExpressionValue, replaceRootContents, isBoundRenderTreeNode, runActionSteps, normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, StreamingParser, StreamingEngine, findAffectedRepeatOwners, bindingTopologyFingerprint, findTemplateRepeatOwners, extractPartialSchema, runActionStep, registry } from '@antglobal/copilot-cards-core';
|
|
2
2
|
export { ActionRegistry, a2uiComponentToElement, a2uiToCommand, convertLegacySchema, createLifecycleManager, hasExpression, isA2UIEnvelope, isLegacySchema, normalizeSchema, parseSchema, registerActionHandler, registry, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, validateSchema } from '@antglobal/copilot-cards-core';
|
|
3
3
|
import * as echarts from 'echarts/core';
|
|
4
4
|
import { LineChart, BarChart, PieChart, ScatterChart, FunnelChart, HeatmapChart } from 'echarts/charts';
|
|
@@ -117,6 +117,57 @@ echarts.use([
|
|
|
117
117
|
// ─── Entry ──────────────────────────────────────────────────────
|
|
118
118
|
/** Attribute marking a DOM node that hosts an ECharts instance. */
|
|
119
119
|
const CHART_ROOT_ATTR = 'data-echarts-root';
|
|
120
|
+
const chartResizeRegistrations = new WeakMap();
|
|
121
|
+
function toChartCSSSize(value, fallback) {
|
|
122
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
123
|
+
return `${value}px`;
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === 'string' && value.trim()) {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
return `${fallback}px`;
|
|
129
|
+
}
|
|
130
|
+
function disconnectChartResize(chartRoot) {
|
|
131
|
+
const registration = chartResizeRegistrations.get(chartRoot);
|
|
132
|
+
if (!registration)
|
|
133
|
+
return;
|
|
134
|
+
registration.observer.disconnect();
|
|
135
|
+
if (registration.frameID !== null) {
|
|
136
|
+
cancelAnimationFrame(registration.frameID);
|
|
137
|
+
}
|
|
138
|
+
chartResizeRegistrations.delete(chartRoot);
|
|
139
|
+
}
|
|
140
|
+
function observeChartResize(chartRoot, chart) {
|
|
141
|
+
disconnectChartResize(chartRoot);
|
|
142
|
+
if (typeof ResizeObserver === 'undefined')
|
|
143
|
+
return;
|
|
144
|
+
const observer = new ResizeObserver(() => {
|
|
145
|
+
const registration = chartResizeRegistrations.get(chartRoot);
|
|
146
|
+
if (!registration)
|
|
147
|
+
return;
|
|
148
|
+
if (!chartRoot.isConnected || chart.isDisposed?.()) {
|
|
149
|
+
disconnectChartResize(chartRoot);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (registration.frameID !== null) {
|
|
153
|
+
cancelAnimationFrame(registration.frameID);
|
|
154
|
+
}
|
|
155
|
+
registration.frameID = requestAnimationFrame(() => {
|
|
156
|
+
registration.frameID = null;
|
|
157
|
+
if (chartResizeRegistrations.get(chartRoot) === registration &&
|
|
158
|
+
chartRoot.isConnected &&
|
|
159
|
+
!chart.isDisposed?.()) {
|
|
160
|
+
chart.resize();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
const registration = {
|
|
165
|
+
observer,
|
|
166
|
+
frameID: null,
|
|
167
|
+
};
|
|
168
|
+
chartResizeRegistrations.set(chartRoot, registration);
|
|
169
|
+
observer.observe(chartRoot);
|
|
170
|
+
}
|
|
120
171
|
/**
|
|
121
172
|
* Dispose every ECharts instance inside `container` and release their global
|
|
122
173
|
* resize listeners. Call before clearing/detaching a card's DOM (re-render or
|
|
@@ -126,6 +177,7 @@ const CHART_ROOT_ATTR = 'data-echarts-root';
|
|
|
126
177
|
function disposeChartsIn(container) {
|
|
127
178
|
const roots = container.querySelectorAll(`[${CHART_ROOT_ATTR}]`);
|
|
128
179
|
roots.forEach((node) => {
|
|
180
|
+
disconnectChartResize(node);
|
|
129
181
|
const inst = echarts.getInstanceByDom(node);
|
|
130
182
|
if (inst)
|
|
131
183
|
inst.dispose();
|
|
@@ -135,10 +187,9 @@ function renderChartSlot(container, slotContent, actionContext) {
|
|
|
135
187
|
const config = slotContent?.config;
|
|
136
188
|
if (!config || !config.type)
|
|
137
189
|
return;
|
|
138
|
-
const width = config.width ?? 340;
|
|
139
|
-
const height = config.height ?? 240;
|
|
140
190
|
const chartDiv = document.createElement('div');
|
|
141
|
-
chartDiv.style.
|
|
191
|
+
chartDiv.style.width = toChartCSSSize(config.width, 340);
|
|
192
|
+
chartDiv.style.height = toChartCSSSize(config.height, 240);
|
|
142
193
|
// Marker so disposeChartsIn() can find and tear down the ECharts instance
|
|
143
194
|
// before its host DOM is detached (otherwise the instance and its global
|
|
144
195
|
// resize listener leak on every re-render).
|
|
@@ -146,6 +197,8 @@ function renderChartSlot(container, slotContent, actionContext) {
|
|
|
146
197
|
container.appendChild(chartDiv);
|
|
147
198
|
// Defer init to next frame to ensure DOM is mounted
|
|
148
199
|
requestAnimationFrame(() => {
|
|
200
|
+
if (!chartDiv.isConnected)
|
|
201
|
+
return;
|
|
149
202
|
const chart = echarts.init(chartDiv, undefined, { renderer: 'svg' });
|
|
150
203
|
let option;
|
|
151
204
|
switch (config.type) {
|
|
@@ -171,6 +224,7 @@ function renderChartSlot(container, slotContent, actionContext) {
|
|
|
171
224
|
return;
|
|
172
225
|
}
|
|
173
226
|
chart.setOption(option);
|
|
227
|
+
observeChartResize(chartDiv, chart);
|
|
174
228
|
// Click action
|
|
175
229
|
if (config.action && actionContext) {
|
|
176
230
|
chart.on('click', (params) => {
|
|
@@ -1447,12 +1501,24 @@ class BaseElement extends HTMLElement {
|
|
|
1447
1501
|
const processed = isExpressionResult
|
|
1448
1502
|
? style
|
|
1449
1503
|
: this.resolveSizeInStyle(style);
|
|
1450
|
-
return buildStyleString(processed);
|
|
1504
|
+
return this.escapeAttribute(buildStyleString(processed));
|
|
1451
1505
|
}
|
|
1452
1506
|
/** Resolve a single size value (number → px). */
|
|
1453
1507
|
toCSS(value) {
|
|
1454
1508
|
return resolveSize(value);
|
|
1455
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Escape a value before interpolating it into a double-quoted HTML
|
|
1512
|
+
* attribute. Browsers decode the entities before parsing inline CSS, so
|
|
1513
|
+
* valid CSS strings keep working while quotes cannot create attributes.
|
|
1514
|
+
*/
|
|
1515
|
+
escapeAttribute(value) {
|
|
1516
|
+
return value
|
|
1517
|
+
.replace(/&/g, '&')
|
|
1518
|
+
.replace(/"/g, '"')
|
|
1519
|
+
.replace(/</g, '<')
|
|
1520
|
+
.replace(/>/g, '>');
|
|
1521
|
+
}
|
|
1456
1522
|
resolveSizeInStyle(style) {
|
|
1457
1523
|
const resolved = {};
|
|
1458
1524
|
for (const [key, value] of Object.entries(style)) {
|
|
@@ -2229,6 +2295,82 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardButton.is))
|
|
|
2229
2295
|
customElements.define(CardButton.is, CardButton);
|
|
2230
2296
|
}
|
|
2231
2297
|
|
|
2298
|
+
function escapeAttr$1(value) {
|
|
2299
|
+
return value
|
|
2300
|
+
.replace(/&/g, '&')
|
|
2301
|
+
.replace(/"/g, '"')
|
|
2302
|
+
.replace(/</g, '<')
|
|
2303
|
+
.replace(/>/g, '>');
|
|
2304
|
+
}
|
|
2305
|
+
function escapeText$1(value) {
|
|
2306
|
+
return value
|
|
2307
|
+
.replace(/&/g, '&')
|
|
2308
|
+
.replace(/</g, '<')
|
|
2309
|
+
.replace(/>/g, '>');
|
|
2310
|
+
}
|
|
2311
|
+
function sanitizeImageSrc(src) {
|
|
2312
|
+
const trimmed = String(src ?? '').trim();
|
|
2313
|
+
const scheme = trimmed.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
|
2314
|
+
if (/^(javascript|vbscript|file):/.test(scheme))
|
|
2315
|
+
return '';
|
|
2316
|
+
if (/^data:/.test(scheme) && !/^data:image\//.test(scheme))
|
|
2317
|
+
return '';
|
|
2318
|
+
return trimmed;
|
|
2319
|
+
}
|
|
2320
|
+
function renderIconContent(input) {
|
|
2321
|
+
const size = Number(input.size) || 24;
|
|
2322
|
+
const name = input.name == null ? undefined : String(input.name);
|
|
2323
|
+
if (input.src) {
|
|
2324
|
+
return `<img class="icon-img" src="${escapeAttr$1(sanitizeImageSrc(input.src))}" ` +
|
|
2325
|
+
`width="${size}" height="${size}" alt="${escapeAttr$1(name ?? 'icon')}" />`;
|
|
2326
|
+
}
|
|
2327
|
+
const icon = getBuiltinIcon(name);
|
|
2328
|
+
if (icon) {
|
|
2329
|
+
return `<svg class="icon-svg" viewBox="${escapeAttr$1(icon.viewBox)}" width="${size}" ` +
|
|
2330
|
+
`height="${size}" color="${escapeAttr$1(String(input.color ?? 'currentColor'))}" ` +
|
|
2331
|
+
`aria-hidden="true">${icon.body}</svg>`;
|
|
2332
|
+
}
|
|
2333
|
+
return `<span class="icon-text" style="font-size:${size}px; line-height:1;">` +
|
|
2334
|
+
`${escapeText$1(name ?? '?')}</span>`;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function escapeText(value) {
|
|
2338
|
+
return String(value)
|
|
2339
|
+
.replace(/&/g, '&')
|
|
2340
|
+
.replace(/</g, '<')
|
|
2341
|
+
.replace(/>/g, '>');
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Normalizes the compact Input affix API into safe markup.
|
|
2345
|
+
*
|
|
2346
|
+
* A string is the common shorthand. Objects either render `text`, or reuse
|
|
2347
|
+
* the shared Icon renderer for built-in icons and external images.
|
|
2348
|
+
*/
|
|
2349
|
+
function renderInputAffix(affix) {
|
|
2350
|
+
if (affix == null || affix === '')
|
|
2351
|
+
return undefined;
|
|
2352
|
+
if (typeof affix === 'string') {
|
|
2353
|
+
return {
|
|
2354
|
+
content: escapeText(affix),
|
|
2355
|
+
ariaLabel: affix,
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
if (affix.text != null) {
|
|
2359
|
+
return {
|
|
2360
|
+
content: escapeText(affix.text),
|
|
2361
|
+
ariaLabel: affix.ariaLabel ?? String(affix.text),
|
|
2362
|
+
style: affix.style,
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
if (affix.name == null && affix.src == null)
|
|
2366
|
+
return undefined;
|
|
2367
|
+
return {
|
|
2368
|
+
content: renderIconContent(affix),
|
|
2369
|
+
ariaLabel: affix.ariaLabel,
|
|
2370
|
+
style: affix.style,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2232
2374
|
/**
|
|
2233
2375
|
* CardInput — Custom Element for rendering input fields in a card.
|
|
2234
2376
|
*
|
|
@@ -2246,12 +2388,11 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardButton.is))
|
|
|
2246
2388
|
* {
|
|
2247
2389
|
* "type": "Input",
|
|
2248
2390
|
* "props": {
|
|
2249
|
-
* "placeholder": "
|
|
2250
|
-
* "inputType": "
|
|
2251
|
-
* "variableKey": "
|
|
2252
|
-
* "
|
|
2253
|
-
* "
|
|
2254
|
-
* "disabled": false,
|
|
2391
|
+
* "placeholder": "Phone Number",
|
|
2392
|
+
* "inputType": "tel",
|
|
2393
|
+
* "variableKey": "phone",
|
|
2394
|
+
* "prefix": "+86",
|
|
2395
|
+
* "suffix": { "name": "info", "size": 18 },
|
|
2255
2396
|
* "style": { "width": 300 }
|
|
2256
2397
|
* },
|
|
2257
2398
|
* "events": {
|
|
@@ -2260,33 +2401,146 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardButton.is))
|
|
|
2260
2401
|
* }
|
|
2261
2402
|
* ```
|
|
2262
2403
|
*/
|
|
2404
|
+
function getNumberStepperDisabledState(input) {
|
|
2405
|
+
const blocked = input.disabled || input.readOnly;
|
|
2406
|
+
if (blocked) {
|
|
2407
|
+
return { increase: true, decrease: true };
|
|
2408
|
+
}
|
|
2409
|
+
const value = input.valueAsNumber;
|
|
2410
|
+
if (!Number.isFinite(value)) {
|
|
2411
|
+
return { increase: false, decrease: false };
|
|
2412
|
+
}
|
|
2413
|
+
const min = input.min === '' ? undefined : Number(input.min);
|
|
2414
|
+
const max = input.max === '' ? undefined : Number(input.max);
|
|
2415
|
+
return {
|
|
2416
|
+
increase: Number.isFinite(max) && value >= Number(max),
|
|
2417
|
+
decrease: Number.isFinite(min) && value <= Number(min),
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function stepNumberInput(input, direction) {
|
|
2421
|
+
if (input.disabled || input.readOnly)
|
|
2422
|
+
return false;
|
|
2423
|
+
const previousValue = input.value;
|
|
2424
|
+
try {
|
|
2425
|
+
if (direction === 'increase') {
|
|
2426
|
+
input.stepUp();
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
input.stepDown();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
catch {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
if (input.value === previousValue)
|
|
2436
|
+
return false;
|
|
2437
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2438
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2441
|
+
function normalizeNumberAttribute(value, options = {}) {
|
|
2442
|
+
if (value == null || value === '')
|
|
2443
|
+
return undefined;
|
|
2444
|
+
const numeric = Number(value);
|
|
2445
|
+
if (!Number.isFinite(numeric))
|
|
2446
|
+
return undefined;
|
|
2447
|
+
if (options.positive && numeric <= 0)
|
|
2448
|
+
return undefined;
|
|
2449
|
+
return numeric;
|
|
2450
|
+
}
|
|
2263
2451
|
class CardInput extends BaseElement {
|
|
2264
2452
|
render() {
|
|
2265
2453
|
if (!this.shadowRoot || !this._node)
|
|
2266
2454
|
return;
|
|
2267
|
-
const { placeholder = '', inputType = 'text', label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, style, isExpressionResultStyle, } = this._props;
|
|
2455
|
+
const { placeholder = '', inputType = 'text', label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, min, max, step = 1, controls = true, prefix, suffix, style, inputStyle, isExpressionResultStyle, } = this._props;
|
|
2268
2456
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
2457
|
+
const legacyInputStyle = style && typeof style === 'object' && style.resize != null
|
|
2458
|
+
? { resize: style.resize }
|
|
2459
|
+
: {};
|
|
2460
|
+
const nativeInlineStyle = this.buildInlineStyle({ ...legacyInputStyle, ...inputStyle }, isExpressionResultStyle);
|
|
2269
2461
|
const isMobile = this._isMobile;
|
|
2270
2462
|
const isTextarea = inputType === 'textarea';
|
|
2463
|
+
const isNumber = inputType === 'number';
|
|
2464
|
+
const showNumberStepper = isNumber && controls !== false;
|
|
2465
|
+
const normalizedMin = normalizeNumberAttribute(min);
|
|
2466
|
+
const normalizedMax = normalizeNumberAttribute(max);
|
|
2467
|
+
const normalizedStep = normalizeNumberAttribute(step, { positive: true }) ?? 1;
|
|
2468
|
+
const inputId = `${this._domId(this._node.id)}-control`;
|
|
2469
|
+
const describedByIds = [];
|
|
2470
|
+
const prefixAffix = renderInputAffix(prefix);
|
|
2471
|
+
const suffixAffix = renderInputAffix(suffix);
|
|
2472
|
+
const renderAffix = (position, affix) => {
|
|
2473
|
+
if (!affix)
|
|
2474
|
+
return '';
|
|
2475
|
+
const affixId = `${inputId}-${position}`;
|
|
2476
|
+
const affixStyle = this.buildInlineStyle(affix.style, isExpressionResultStyle);
|
|
2477
|
+
const accessibility = affix.ariaLabel
|
|
2478
|
+
? `id="${affixId}" aria-label="${this.escapeAttr(affix.ariaLabel)}"`
|
|
2479
|
+
: 'aria-hidden="true"';
|
|
2480
|
+
if (affix.ariaLabel)
|
|
2481
|
+
describedByIds.push(affixId);
|
|
2482
|
+
return `<span class="input-affix input-${position}" ${accessibility} style="${affixStyle}">${affix.content}</span>`;
|
|
2483
|
+
};
|
|
2484
|
+
const prefixHtml = renderAffix('prefix', prefixAffix);
|
|
2485
|
+
const suffixHtml = renderAffix('suffix', suffixAffix);
|
|
2486
|
+
const describedBy = describedByIds.length > 0
|
|
2487
|
+
? `aria-describedby="${describedByIds.join(' ')}"`
|
|
2488
|
+
: '';
|
|
2489
|
+
const numberAttributes = isNumber
|
|
2490
|
+
? [
|
|
2491
|
+
normalizedMin == null ? '' : `min="${normalizedMin}"`,
|
|
2492
|
+
normalizedMax == null ? '' : `max="${normalizedMax}"`,
|
|
2493
|
+
`step="${normalizedStep}"`,
|
|
2494
|
+
].filter(Boolean).join(' ')
|
|
2495
|
+
: '';
|
|
2496
|
+
const numberStepperHtml = showNumberStepper
|
|
2497
|
+
? `<span class="number-stepper">
|
|
2498
|
+
<button
|
|
2499
|
+
class="number-step-button increase"
|
|
2500
|
+
type="button"
|
|
2501
|
+
aria-label="Increase value"
|
|
2502
|
+
${disabled || readOnly ? 'disabled' : ''}
|
|
2503
|
+
>${renderIconContent({
|
|
2504
|
+
name: 'caret_up',
|
|
2505
|
+
size: 12,
|
|
2506
|
+
color: 'currentColor',
|
|
2507
|
+
})}</button>
|
|
2508
|
+
<button
|
|
2509
|
+
class="number-step-button decrease"
|
|
2510
|
+
type="button"
|
|
2511
|
+
aria-label="Decrease value"
|
|
2512
|
+
${disabled || readOnly ? 'disabled' : ''}
|
|
2513
|
+
>${renderIconContent({
|
|
2514
|
+
name: 'caret_down',
|
|
2515
|
+
size: 12,
|
|
2516
|
+
color: 'currentColor',
|
|
2517
|
+
})}</button>
|
|
2518
|
+
</span>`
|
|
2519
|
+
: '';
|
|
2271
2520
|
const inputTag = isTextarea
|
|
2272
2521
|
? `<textarea
|
|
2522
|
+
id="${inputId}"
|
|
2273
2523
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2274
|
-
placeholder="${this.escapeAttr(placeholder)}"
|
|
2524
|
+
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
2525
|
+
${describedBy}
|
|
2275
2526
|
${disabled ? 'disabled' : ''}
|
|
2276
2527
|
${readOnly ? 'readonly' : ''}
|
|
2277
|
-
${maxLength ? `maxlength="${maxLength}"` : ''}
|
|
2278
|
-
rows="${rows ?? 3}"
|
|
2279
|
-
style="${
|
|
2528
|
+
${maxLength != null ? `maxlength="${Number(maxLength)}"` : ''}
|
|
2529
|
+
rows="${Number(rows ?? 3)}"
|
|
2530
|
+
style="${nativeInlineStyle}"
|
|
2280
2531
|
>${this.escapeHtml(String(defaultValue))}</textarea>`
|
|
2281
2532
|
: `<input
|
|
2533
|
+
id="${inputId}"
|
|
2282
2534
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2283
|
-
type="${inputType}"
|
|
2284
|
-
placeholder="${this.escapeAttr(placeholder)}"
|
|
2535
|
+
type="${this.escapeAttr(String(inputType))}"
|
|
2536
|
+
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
2285
2537
|
value="${this.escapeAttr(String(defaultValue))}"
|
|
2538
|
+
${describedBy}
|
|
2286
2539
|
${disabled ? 'disabled' : ''}
|
|
2287
2540
|
${readOnly ? 'readonly' : ''}
|
|
2288
|
-
${maxLength ? `maxlength="${maxLength}"` : ''}
|
|
2289
|
-
|
|
2541
|
+
${maxLength != null ? `maxlength="${Number(maxLength)}"` : ''}
|
|
2542
|
+
${numberAttributes}
|
|
2543
|
+
style="${nativeInlineStyle}"
|
|
2290
2544
|
/>`;
|
|
2291
2545
|
this.shadowRoot.innerHTML = `
|
|
2292
2546
|
<style>
|
|
@@ -2307,36 +2561,132 @@ class CardInput extends BaseElement {
|
|
|
2307
2561
|
color: #666;
|
|
2308
2562
|
line-height: 1.4;
|
|
2309
2563
|
}
|
|
2310
|
-
.
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2564
|
+
.input-control {
|
|
2565
|
+
display: flex;
|
|
2566
|
+
align-items: center;
|
|
2567
|
+
gap: 8px;
|
|
2568
|
+
box-sizing: border-box;
|
|
2569
|
+
width: 100%;
|
|
2315
2570
|
padding: 8px 12px;
|
|
2316
2571
|
border: 1px solid #d9d9d9;
|
|
2317
2572
|
border-radius: 6px;
|
|
2318
2573
|
outline: none;
|
|
2319
2574
|
color: #333;
|
|
2320
2575
|
background-color: #fff;
|
|
2321
|
-
|
|
2322
|
-
|
|
2576
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
2577
|
+
'Helvetica Neue', Arial, sans-serif;
|
|
2578
|
+
font-size: 14px;
|
|
2579
|
+
line-height: 1.5;
|
|
2323
2580
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
2324
2581
|
}
|
|
2325
|
-
.
|
|
2326
|
-
color: #bfbfbf;
|
|
2327
|
-
}
|
|
2328
|
-
.card-input:focus {
|
|
2582
|
+
.input-control:focus-within {
|
|
2329
2583
|
border-color: #1677ff;
|
|
2330
2584
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.15);
|
|
2331
2585
|
}
|
|
2332
|
-
.
|
|
2586
|
+
.input-control.disabled {
|
|
2333
2587
|
background-color: #f5f5f5;
|
|
2334
2588
|
color: #bfbfbf;
|
|
2335
2589
|
cursor: not-allowed;
|
|
2336
2590
|
}
|
|
2337
|
-
.
|
|
2591
|
+
.input-control.readonly {
|
|
2338
2592
|
background-color: #fafafa;
|
|
2339
2593
|
}
|
|
2594
|
+
.input-control.textarea-control {
|
|
2595
|
+
align-items: flex-start;
|
|
2596
|
+
}
|
|
2597
|
+
.input-control.number-control {
|
|
2598
|
+
overflow: hidden;
|
|
2599
|
+
}
|
|
2600
|
+
.card-input {
|
|
2601
|
+
flex: 1;
|
|
2602
|
+
min-width: 0;
|
|
2603
|
+
box-sizing: border-box;
|
|
2604
|
+
width: 100%;
|
|
2605
|
+
padding: 0;
|
|
2606
|
+
border: 0;
|
|
2607
|
+
outline: none;
|
|
2608
|
+
color: inherit;
|
|
2609
|
+
background: transparent;
|
|
2610
|
+
font: inherit;
|
|
2611
|
+
line-height: inherit;
|
|
2612
|
+
}
|
|
2613
|
+
.card-input::placeholder {
|
|
2614
|
+
color: #bfbfbf;
|
|
2615
|
+
}
|
|
2616
|
+
.card-input:disabled {
|
|
2617
|
+
cursor: not-allowed;
|
|
2618
|
+
}
|
|
2619
|
+
input.card-input[type='number'] {
|
|
2620
|
+
-moz-appearance: textfield;
|
|
2621
|
+
appearance: textfield;
|
|
2622
|
+
}
|
|
2623
|
+
input.card-input[type='number']::-webkit-inner-spin-button,
|
|
2624
|
+
input.card-input[type='number']::-webkit-outer-spin-button {
|
|
2625
|
+
margin: 0;
|
|
2626
|
+
-webkit-appearance: none;
|
|
2627
|
+
appearance: none;
|
|
2628
|
+
}
|
|
2629
|
+
.input-affix {
|
|
2630
|
+
display: inline-flex;
|
|
2631
|
+
flex: none;
|
|
2632
|
+
align-items: center;
|
|
2633
|
+
min-width: 0;
|
|
2634
|
+
color: #666;
|
|
2635
|
+
white-space: nowrap;
|
|
2636
|
+
line-height: inherit;
|
|
2637
|
+
}
|
|
2638
|
+
.input-affix .icon-img,
|
|
2639
|
+
.input-affix .icon-svg,
|
|
2640
|
+
.input-affix .icon-text {
|
|
2641
|
+
display: block;
|
|
2642
|
+
}
|
|
2643
|
+
.number-stepper {
|
|
2644
|
+
display: flex;
|
|
2645
|
+
flex: 0 0 32px;
|
|
2646
|
+
align-self: stretch;
|
|
2647
|
+
flex-direction: column;
|
|
2648
|
+
margin: -8px -12px -8px 0;
|
|
2649
|
+
border-left: 1px solid #d9d9d9;
|
|
2650
|
+
}
|
|
2651
|
+
.number-step-button {
|
|
2652
|
+
display: inline-flex;
|
|
2653
|
+
flex: 1 1 50%;
|
|
2654
|
+
align-items: center;
|
|
2655
|
+
justify-content: center;
|
|
2656
|
+
min-width: 0;
|
|
2657
|
+
min-height: 0;
|
|
2658
|
+
margin: 0;
|
|
2659
|
+
padding: 0;
|
|
2660
|
+
border: 0;
|
|
2661
|
+
border-radius: 0;
|
|
2662
|
+
outline: none;
|
|
2663
|
+
color: #8c8c8c;
|
|
2664
|
+
background: transparent;
|
|
2665
|
+
font: inherit;
|
|
2666
|
+
cursor: pointer;
|
|
2667
|
+
transition: color 0.15s ease, background-color 0.15s ease;
|
|
2668
|
+
}
|
|
2669
|
+
.number-step-button + .number-step-button {
|
|
2670
|
+
border-top: 1px solid #d9d9d9;
|
|
2671
|
+
}
|
|
2672
|
+
.number-step-button:hover:not(:disabled) {
|
|
2673
|
+
color: #1677ff;
|
|
2674
|
+
background-color: #e6f4ff;
|
|
2675
|
+
}
|
|
2676
|
+
.number-step-button:active:not(:disabled) {
|
|
2677
|
+
background-color: #bae0ff;
|
|
2678
|
+
}
|
|
2679
|
+
.number-step-button:focus-visible {
|
|
2680
|
+
color: #1677ff;
|
|
2681
|
+
box-shadow: inset 0 0 0 2px rgba(22, 119, 255, 0.35);
|
|
2682
|
+
}
|
|
2683
|
+
.number-step-button:disabled {
|
|
2684
|
+
color: #d9d9d9;
|
|
2685
|
+
cursor: not-allowed;
|
|
2686
|
+
}
|
|
2687
|
+
.number-step-button .icon-svg {
|
|
2688
|
+
display: block;
|
|
2689
|
+
}
|
|
2340
2690
|
|
|
2341
2691
|
/* ─── Textarea ──────────────────────────── */
|
|
2342
2692
|
textarea.card-input {
|
|
@@ -2345,14 +2695,26 @@ class CardInput extends BaseElement {
|
|
|
2345
2695
|
}
|
|
2346
2696
|
|
|
2347
2697
|
/* ─── Mobile adjustments ───────────────── */
|
|
2348
|
-
.
|
|
2698
|
+
.input-control.card-mobile {
|
|
2349
2699
|
font-size: 16px;
|
|
2350
2700
|
padding: 10px 12px;
|
|
2351
2701
|
}
|
|
2702
|
+
.input-control.card-mobile .number-stepper {
|
|
2703
|
+
margin-top: -10px;
|
|
2704
|
+
margin-bottom: -10px;
|
|
2705
|
+
}
|
|
2352
2706
|
</style>
|
|
2353
2707
|
<div class="card-input-wrapper">
|
|
2354
|
-
${label ? `<label class="card-input-label">${this.escapeHtml(String(label))}</label>` : ''}
|
|
2355
|
-
|
|
2708
|
+
${label ? `<label class="card-input-label" for="${inputId}">${this.escapeHtml(String(label))}</label>` : ''}
|
|
2709
|
+
<div
|
|
2710
|
+
class="input-control ${isTextarea ? 'textarea-control' : ''} ${isNumber ? 'number-control' : ''} ${isMobile ? 'card-mobile' : 'card-desktop'} ${disabled ? 'disabled' : ''} ${readOnly ? 'readonly' : ''}"
|
|
2711
|
+
style="${inlineStyle}"
|
|
2712
|
+
>
|
|
2713
|
+
${prefixHtml}
|
|
2714
|
+
${inputTag}
|
|
2715
|
+
${suffixHtml}
|
|
2716
|
+
${numberStepperHtml}
|
|
2717
|
+
</div>
|
|
2356
2718
|
</div>
|
|
2357
2719
|
`;
|
|
2358
2720
|
// Wire up native input/change events that bubble out of Shadow DOM.
|
|
@@ -2376,6 +2738,31 @@ class CardInput extends BaseElement {
|
|
|
2376
2738
|
detail: { value: inputEl.value },
|
|
2377
2739
|
}));
|
|
2378
2740
|
});
|
|
2741
|
+
if (isNumber &&
|
|
2742
|
+
showNumberStepper &&
|
|
2743
|
+
inputEl instanceof HTMLInputElement) {
|
|
2744
|
+
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
2745
|
+
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
2746
|
+
const refreshStepperState = () => {
|
|
2747
|
+
const state = getNumberStepperDisabledState(inputEl);
|
|
2748
|
+
if (increaseButton)
|
|
2749
|
+
increaseButton.disabled = state.increase;
|
|
2750
|
+
if (decreaseButton)
|
|
2751
|
+
decreaseButton.disabled = state.decrease;
|
|
2752
|
+
};
|
|
2753
|
+
inputEl.addEventListener('input', refreshStepperState);
|
|
2754
|
+
increaseButton?.addEventListener('click', () => {
|
|
2755
|
+
stepNumberInput(inputEl, 'increase');
|
|
2756
|
+
inputEl.focus();
|
|
2757
|
+
refreshStepperState();
|
|
2758
|
+
});
|
|
2759
|
+
decreaseButton?.addEventListener('click', () => {
|
|
2760
|
+
stepNumberInput(inputEl, 'decrease');
|
|
2761
|
+
inputEl.focus();
|
|
2762
|
+
refreshStepperState();
|
|
2763
|
+
});
|
|
2764
|
+
refreshStepperState();
|
|
2765
|
+
}
|
|
2379
2766
|
}
|
|
2380
2767
|
}
|
|
2381
2768
|
// ─── Helpers ──────────────────────────────────────────────────
|
|
@@ -2394,6 +2781,10 @@ class CardInput extends BaseElement {
|
|
|
2394
2781
|
.replace(/</g, '<')
|
|
2395
2782
|
.replace(/>/g, '>');
|
|
2396
2783
|
}
|
|
2784
|
+
_domId(value) {
|
|
2785
|
+
const normalized = String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
2786
|
+
return normalized || 'input';
|
|
2787
|
+
}
|
|
2397
2788
|
}
|
|
2398
2789
|
CardInput.is = 'ai-card-input';
|
|
2399
2790
|
// Register the custom element (safe for SSR / non-browser envs)
|
|
@@ -2684,6 +3075,25 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardDivider.is)
|
|
|
2684
3075
|
customElements.define(CardDivider.is, CardDivider);
|
|
2685
3076
|
}
|
|
2686
3077
|
|
|
3078
|
+
function emitValueChange(host, value, previousValue, source, extra = {}) {
|
|
3079
|
+
const detail = {
|
|
3080
|
+
...extra,
|
|
3081
|
+
value,
|
|
3082
|
+
previousValue,
|
|
3083
|
+
source,
|
|
3084
|
+
};
|
|
3085
|
+
host.dispatchEvent(new CustomEvent('input', {
|
|
3086
|
+
bubbles: true,
|
|
3087
|
+
composed: true,
|
|
3088
|
+
detail,
|
|
3089
|
+
}));
|
|
3090
|
+
host.dispatchEvent(new CustomEvent('change', {
|
|
3091
|
+
bubbles: true,
|
|
3092
|
+
composed: true,
|
|
3093
|
+
detail,
|
|
3094
|
+
}));
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2687
3097
|
/**
|
|
2688
3098
|
* CardRate — Custom Element for star rating display/input.
|
|
2689
3099
|
*
|
|
@@ -2712,29 +3122,59 @@ class CardRate extends BaseElement {
|
|
|
2712
3122
|
render() {
|
|
2713
3123
|
if (!this.shadowRoot || !this._node)
|
|
2714
3124
|
return;
|
|
2715
|
-
const { value = 0, count = 5, readonly: readOnly = false, allowHalf = false, size = 24, gap = 4, color = '#fadb14', inactiveColor = '#e8e8e8', style, isExpressionResultStyle, } = this._props;
|
|
3125
|
+
const { value, defaultValue = 0, count: rawCount = 5, readonly: readOnly = false, disabled = false, allowHalf = false, allowClear = true, size = 24, gap = 4, color = '#fadb14', inactiveColor = '#e8e8e8', ariaLabel = 'Rating', style, starStyle, activeStarStyle, inactiveStarStyle, isExpressionResultStyle, } = this._props;
|
|
2716
3126
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
2717
|
-
const
|
|
3127
|
+
const count = Math.max(1, Math.min(100, Math.round(Number(rawCount) || 5)));
|
|
3128
|
+
const step = allowHalf ? 0.5 : 1;
|
|
3129
|
+
const externalValue = this.normalizeValue(value ?? defaultValue, count, step);
|
|
3130
|
+
const displayValue = this._currentValue ?? externalValue;
|
|
2718
3131
|
const starSize = Number(size) || 24;
|
|
2719
3132
|
const starGap = Number(gap) || 4;
|
|
3133
|
+
const interactive = !disabled && !readOnly;
|
|
2720
3134
|
const stars = Array.from({ length: count }, (_, i) => {
|
|
2721
3135
|
const starIndex = i + 1;
|
|
2722
3136
|
const isFull = displayValue >= starIndex;
|
|
2723
3137
|
const isHalf = !isFull && allowHalf && displayValue >= starIndex - 0.5;
|
|
3138
|
+
const stateStyle = isFull || isHalf
|
|
3139
|
+
? activeStarStyle || {}
|
|
3140
|
+
: inactiveStarStyle || {};
|
|
3141
|
+
const itemInline = this.buildInlineStyle({
|
|
3142
|
+
...(starStyle || {}),
|
|
3143
|
+
...stateStyle,
|
|
3144
|
+
}, isExpressionResultStyle);
|
|
2724
3145
|
if (allowHalf) {
|
|
3146
|
+
const wrapperInline = this.buildInlineStyle({
|
|
3147
|
+
fontSize: starSize,
|
|
3148
|
+
position: 'relative',
|
|
3149
|
+
display: 'inline-block',
|
|
3150
|
+
width: '1em',
|
|
3151
|
+
height: '1em',
|
|
3152
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
3153
|
+
});
|
|
3154
|
+
const backgroundInline = this.buildInlineStyle({
|
|
3155
|
+
color: inactiveColor,
|
|
3156
|
+
});
|
|
3157
|
+
const fillInline = this.buildInlineStyle({
|
|
3158
|
+
color,
|
|
3159
|
+
width: isFull ? '100%' : isHalf ? '50%' : '0%',
|
|
3160
|
+
});
|
|
2725
3161
|
// Two clickable halves per star
|
|
2726
3162
|
return `
|
|
2727
|
-
<span class="star-wrapper" style="
|
|
2728
|
-
<span class="star-bg" style="
|
|
2729
|
-
<span class="star-fill" style="
|
|
3163
|
+
<span class="star-wrapper" style="${wrapperInline};${itemInline}">
|
|
3164
|
+
<span class="star-bg" style="${backgroundInline}">★</span>
|
|
3165
|
+
<span class="star-fill" style="${fillInline}">★</span>
|
|
2730
3166
|
<span class="star-left" data-value="${starIndex - 0.5}"></span>
|
|
2731
3167
|
<span class="star-right" data-value="${starIndex}"></span>
|
|
2732
3168
|
</span>`;
|
|
2733
3169
|
}
|
|
3170
|
+
const starInline = this.buildInlineStyle({
|
|
3171
|
+
fontSize: starSize,
|
|
3172
|
+
color: isFull ? color : inactiveColor,
|
|
3173
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
3174
|
+
});
|
|
2734
3175
|
// Whole star mode
|
|
2735
3176
|
return `<span class="star ${isFull ? 'filled' : ''}" data-value="${starIndex}"
|
|
2736
|
-
style="
|
|
2737
|
-
cursor:${readOnly ? 'default' : 'pointer'};">★</span>`;
|
|
3177
|
+
style="${starInline};${itemInline}">★</span>`;
|
|
2738
3178
|
}).join('');
|
|
2739
3179
|
this.shadowRoot.innerHTML = `
|
|
2740
3180
|
<style>
|
|
@@ -2742,6 +3182,7 @@ class CardRate extends BaseElement {
|
|
|
2742
3182
|
display: inline-flex;
|
|
2743
3183
|
align-items: center;
|
|
2744
3184
|
box-sizing: border-box;
|
|
3185
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2745
3186
|
}
|
|
2746
3187
|
.rate-wrapper {
|
|
2747
3188
|
display: inline-flex;
|
|
@@ -2780,31 +3221,80 @@ class CardRate extends BaseElement {
|
|
|
2780
3221
|
.star-left { left: 0; cursor: pointer; }
|
|
2781
3222
|
.star-right { right: 0; cursor: pointer; }
|
|
2782
3223
|
</style>
|
|
2783
|
-
<div
|
|
3224
|
+
<div
|
|
3225
|
+
class="rate-wrapper"
|
|
3226
|
+
role="slider"
|
|
3227
|
+
tabindex="${disabled ? '-1' : '0'}"
|
|
3228
|
+
aria-label="${this.escapeAttr(String(ariaLabel))}"
|
|
3229
|
+
aria-valuemin="0"
|
|
3230
|
+
aria-valuemax="${count}"
|
|
3231
|
+
aria-valuenow="${displayValue}"
|
|
3232
|
+
aria-valuetext="${displayValue} of ${count}"
|
|
3233
|
+
aria-disabled="${Boolean(disabled)}"
|
|
3234
|
+
aria-readonly="${Boolean(readOnly)}"
|
|
3235
|
+
style="${inlineStyle}"
|
|
3236
|
+
>${stars}</div>
|
|
2784
3237
|
`;
|
|
2785
|
-
if (
|
|
3238
|
+
if (!interactive)
|
|
2786
3239
|
return;
|
|
2787
3240
|
// Bind click handlers
|
|
2788
3241
|
const clickTargets = this.shadowRoot.querySelectorAll('[data-value]');
|
|
2789
3242
|
clickTargets.forEach((el) => {
|
|
2790
3243
|
el.addEventListener('click', () => {
|
|
2791
3244
|
const val = Number(el.dataset.value);
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
this.
|
|
2795
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
2796
|
-
bubbles: true,
|
|
2797
|
-
composed: true,
|
|
2798
|
-
detail: { value: this._currentValue },
|
|
2799
|
-
}));
|
|
3245
|
+
const previousValue = displayValue;
|
|
3246
|
+
const nextValue = allowClear && previousValue === val ? 0 : val;
|
|
3247
|
+
this.commit(nextValue, previousValue, 'pointer', count, step);
|
|
2800
3248
|
});
|
|
2801
3249
|
});
|
|
3250
|
+
this.shadowRoot
|
|
3251
|
+
.querySelector('.rate-wrapper')
|
|
3252
|
+
?.addEventListener('keydown', (event) => {
|
|
3253
|
+
let nextValue = null;
|
|
3254
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
|
|
3255
|
+
nextValue = displayValue + step;
|
|
3256
|
+
}
|
|
3257
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
|
|
3258
|
+
nextValue = displayValue - step;
|
|
3259
|
+
}
|
|
3260
|
+
else if (event.key === 'Home') {
|
|
3261
|
+
nextValue = 0;
|
|
3262
|
+
}
|
|
3263
|
+
else if (event.key === 'End') {
|
|
3264
|
+
nextValue = count;
|
|
3265
|
+
}
|
|
3266
|
+
if (nextValue == null)
|
|
3267
|
+
return;
|
|
3268
|
+
event.preventDefault();
|
|
3269
|
+
this.commit(nextValue, displayValue, 'keyboard', count, step);
|
|
3270
|
+
});
|
|
2802
3271
|
}
|
|
2803
3272
|
/** Reset internal state when props update externally */
|
|
2804
3273
|
updateProps(props, isMobile) {
|
|
2805
3274
|
this._currentValue = null;
|
|
2806
3275
|
super.updateProps(props, isMobile);
|
|
2807
3276
|
}
|
|
3277
|
+
commit(next, previous, source, count, step) {
|
|
3278
|
+
const normalized = this.normalizeValue(next, count, step);
|
|
3279
|
+
if (normalized === previous)
|
|
3280
|
+
return;
|
|
3281
|
+
this._currentValue = normalized;
|
|
3282
|
+
this.render();
|
|
3283
|
+
emitValueChange(this, normalized, previous, source);
|
|
3284
|
+
}
|
|
3285
|
+
normalizeValue(value, count, step) {
|
|
3286
|
+
const numeric = Number(value);
|
|
3287
|
+
const safe = Number.isFinite(numeric) ? numeric : 0;
|
|
3288
|
+
const clamped = Math.min(count, Math.max(0, safe));
|
|
3289
|
+
return Math.round(clamped / step) * step;
|
|
3290
|
+
}
|
|
3291
|
+
escapeAttr(value) {
|
|
3292
|
+
return value
|
|
3293
|
+
.replace(/&/g, '&')
|
|
3294
|
+
.replace(/"/g, '"')
|
|
3295
|
+
.replace(/</g, '<')
|
|
3296
|
+
.replace(/>/g, '>');
|
|
3297
|
+
}
|
|
2808
3298
|
}
|
|
2809
3299
|
CardRate.is = 'ai-card-rate';
|
|
2810
3300
|
if (typeof customElements !== 'undefined' && !customElements.get(CardRate.is)) {
|
|
@@ -2862,13 +3352,26 @@ class CardCounter extends BaseElement {
|
|
|
2862
3352
|
render() {
|
|
2863
3353
|
if (!this.shadowRoot || !this._node)
|
|
2864
3354
|
return;
|
|
2865
|
-
const { defaultValue, value, min = 0, max = Number.POSITIVE_INFINITY, step = 1, disabled = false, readonly: readOnly = false, size = 28, valueWidth = 32, minusIcon, plusIcon, style, buttonStyle, minusStyle, plusStyle, valueStyle, isExpressionResultStyle, } = this._props;
|
|
2866
|
-
const
|
|
2867
|
-
const
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3355
|
+
const { defaultValue, value, min = 0, max = Number.POSITIVE_INFINITY, step = 1, disabled = false, readonly: readOnly = false, size = 28, valueWidth = 32, decreaseAriaLabel = 'decrease', increaseAriaLabel = 'increase', minusIcon, plusIcon, style, buttonStyle, minusStyle, plusStyle, valueStyle, isExpressionResultStyle, } = this._props;
|
|
3356
|
+
const rawMin = Number(min);
|
|
3357
|
+
const rawMax = Number(max);
|
|
3358
|
+
let minVal = Number.isFinite(rawMin) ? rawMin : 0;
|
|
3359
|
+
let maxVal = Number.isNaN(rawMax)
|
|
3360
|
+
? Number.POSITIVE_INFINITY
|
|
3361
|
+
: rawMax;
|
|
3362
|
+
if (minVal > maxVal) {
|
|
3363
|
+
[minVal, maxVal] = [maxVal, minVal];
|
|
3364
|
+
}
|
|
3365
|
+
const rawStep = Number(step);
|
|
3366
|
+
const stepVal = Number.isFinite(rawStep) && rawStep > 0 ? rawStep : 1;
|
|
3367
|
+
const rawSize = Number(size);
|
|
3368
|
+
const btnSize = Number.isFinite(rawSize) && rawSize > 0 ? rawSize : 28;
|
|
3369
|
+
const rawValueWidth = Number(valueWidth);
|
|
3370
|
+
const numWidth = Number.isFinite(rawValueWidth) && rawValueWidth > 0
|
|
3371
|
+
? rawValueWidth
|
|
3372
|
+
: 32;
|
|
3373
|
+
const rawInitial = Number(value ?? defaultValue ?? minVal);
|
|
3374
|
+
const initial = this.clamp(Number.isFinite(rawInitial) ? rawInitial : minVal, minVal, maxVal);
|
|
2872
3375
|
const current = this._currentValue ?? initial;
|
|
2873
3376
|
const atMin = current <= minVal;
|
|
2874
3377
|
const atMax = current >= maxVal;
|
|
@@ -2944,7 +3447,7 @@ class CardCounter extends BaseElement {
|
|
|
2944
3447
|
</style>
|
|
2945
3448
|
<div class="counter" style="${wrapperStyle}">
|
|
2946
3449
|
<button type="button" class="c-btn c-minus" part="minus"
|
|
2947
|
-
${minusDisabled ? 'disabled' : ''} aria-label="
|
|
3450
|
+
${minusDisabled ? 'disabled' : ''} aria-label="${this.escapeAttr(String(decreaseAriaLabel))}"
|
|
2948
3451
|
style="${minusInline}">${minusContent}</button>
|
|
2949
3452
|
<span class="c-value" part="value" role="spinbutton"
|
|
2950
3453
|
aria-valuenow="${current}"
|
|
@@ -2952,7 +3455,7 @@ class CardCounter extends BaseElement {
|
|
|
2952
3455
|
${Number.isFinite(maxVal) ? `aria-valuemax="${maxVal}"` : ''}
|
|
2953
3456
|
style="${numInline}">${current}</span>
|
|
2954
3457
|
<button type="button" class="c-btn c-plus" part="plus"
|
|
2955
|
-
${plusDisabled ? 'disabled' : ''} aria-label="
|
|
3458
|
+
${plusDisabled ? 'disabled' : ''} aria-label="${this.escapeAttr(String(increaseAriaLabel))}"
|
|
2956
3459
|
style="${plusInline}">${plusContent}</button>
|
|
2957
3460
|
</div>
|
|
2958
3461
|
`;
|
|
@@ -2960,27 +3463,17 @@ class CardCounter extends BaseElement {
|
|
|
2960
3463
|
return;
|
|
2961
3464
|
const minusBtn = this.shadowRoot.querySelector('.c-minus');
|
|
2962
3465
|
const plusBtn = this.shadowRoot.querySelector('.c-plus');
|
|
2963
|
-
minusBtn?.addEventListener('click', () => this.commit(current - stepVal, minVal, maxVal));
|
|
2964
|
-
plusBtn?.addEventListener('click', () => this.commit(current + stepVal, minVal, maxVal));
|
|
3466
|
+
minusBtn?.addEventListener('click', (event) => this.commit(current - stepVal, current, minVal, maxVal, event.detail === 0 ? 'keyboard' : 'pointer'));
|
|
3467
|
+
plusBtn?.addEventListener('click', (event) => this.commit(current + stepVal, current, minVal, maxVal, event.detail === 0 ? 'keyboard' : 'pointer'));
|
|
2965
3468
|
}
|
|
2966
3469
|
/** Clamp, de-dup, update internal value and emit input + change. */
|
|
2967
|
-
commit(next, min, max) {
|
|
3470
|
+
commit(next, previous, min, max, source) {
|
|
2968
3471
|
const clamped = this.round(this.clamp(next, min, max));
|
|
2969
|
-
if (clamped ===
|
|
3472
|
+
if (clamped === previous)
|
|
2970
3473
|
return; // no-op at a bound
|
|
2971
3474
|
this._currentValue = clamped;
|
|
2972
3475
|
this.render();
|
|
2973
|
-
|
|
2974
|
-
this.dispatchEvent(new CustomEvent('input', {
|
|
2975
|
-
bubbles: true,
|
|
2976
|
-
composed: true,
|
|
2977
|
-
detail: { value: clamped },
|
|
2978
|
-
}));
|
|
2979
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
2980
|
-
bubbles: true,
|
|
2981
|
-
composed: true,
|
|
2982
|
-
detail: { value: clamped },
|
|
2983
|
-
}));
|
|
3476
|
+
emitValueChange(this, clamped, previous, source);
|
|
2984
3477
|
}
|
|
2985
3478
|
clamp(v, min, max) {
|
|
2986
3479
|
return Math.min(max, Math.max(min, v));
|
|
@@ -3008,11 +3501,18 @@ class CardCounter extends BaseElement {
|
|
|
3008
3501
|
*/
|
|
3009
3502
|
renderIcon(icon, fallback, size) {
|
|
3010
3503
|
if (icon?.src) {
|
|
3011
|
-
|
|
3012
|
-
|
|
3504
|
+
return renderIconContent({
|
|
3505
|
+
src: icon.src,
|
|
3506
|
+
name: icon.name,
|
|
3507
|
+
size: Math.round(size * 0.6),
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
if (icon?.name) {
|
|
3511
|
+
return renderIconContent({
|
|
3512
|
+
name: icon.name,
|
|
3513
|
+
size: Math.round(size * 0.6),
|
|
3514
|
+
});
|
|
3013
3515
|
}
|
|
3014
|
-
if (icon?.name)
|
|
3015
|
-
return this.escapeText(String(icon.name));
|
|
3016
3516
|
return fallback;
|
|
3017
3517
|
}
|
|
3018
3518
|
/** Reset internal state when props update externally. */
|
|
@@ -3028,22 +3528,6 @@ class CardCounter extends BaseElement {
|
|
|
3028
3528
|
.replace(/</g, '<')
|
|
3029
3529
|
.replace(/>/g, '>');
|
|
3030
3530
|
}
|
|
3031
|
-
escapeText(str) {
|
|
3032
|
-
return str
|
|
3033
|
-
.replace(/&/g, '&')
|
|
3034
|
-
.replace(/</g, '<')
|
|
3035
|
-
.replace(/>/g, '>');
|
|
3036
|
-
}
|
|
3037
|
-
/** Reject script-bearing / non-image data URIs; pass legit image URLs through. */
|
|
3038
|
-
sanitizeImageSrc(src) {
|
|
3039
|
-
const trimmed = String(src).trim();
|
|
3040
|
-
const scheme = trimmed.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
|
3041
|
-
if (/^(javascript|vbscript|file):/.test(scheme))
|
|
3042
|
-
return '';
|
|
3043
|
-
if (/^data:/.test(scheme) && !/^data:image\//.test(scheme))
|
|
3044
|
-
return '';
|
|
3045
|
-
return trimmed;
|
|
3046
|
-
}
|
|
3047
3531
|
}
|
|
3048
3532
|
CardCounter.is = 'ai-card-counter';
|
|
3049
3533
|
if (typeof customElements !== 'undefined' && !customElements.get(CardCounter.is)) {
|
|
@@ -3168,28 +3652,94 @@ class CardSelect extends BaseElement {
|
|
|
3168
3652
|
constructor() {
|
|
3169
3653
|
super(...arguments);
|
|
3170
3654
|
this._open = false;
|
|
3655
|
+
this._activeIndex = -1;
|
|
3171
3656
|
/** Option picked locally; shown until props.value changes externally. */
|
|
3172
3657
|
this._localValue = null;
|
|
3173
3658
|
this._onDocClick = (e) => {
|
|
3174
|
-
|
|
3659
|
+
const includesHost = e.composedPath().includes(this);
|
|
3660
|
+
if (!includesHost)
|
|
3175
3661
|
this._setOpen(false);
|
|
3176
3662
|
};
|
|
3177
3663
|
}
|
|
3178
3664
|
render() {
|
|
3179
3665
|
if (!this.shadowRoot || !this._node)
|
|
3180
3666
|
return;
|
|
3181
|
-
const { options = [], placeholder = '请选择', value:
|
|
3667
|
+
const { options = [], placeholder = '请选择', value: externalValue, defaultValue = '', disabled = false, readonly: readOnly = false, ariaLabel = placeholder, size = 'medium', variant = 'outlined', accentColor = '#1677ff', style, styles = {}, isExpressionResultStyle, } = this._props;
|
|
3668
|
+
const propValue = String(externalValue ?? defaultValue ?? '');
|
|
3669
|
+
const resolvedSize = (['small', 'medium', 'large'].includes(size)
|
|
3670
|
+
? size
|
|
3671
|
+
: 'medium');
|
|
3672
|
+
const resolvedVariant = (['outlined', 'filled', 'borderless'].includes(variant)
|
|
3673
|
+
? variant
|
|
3674
|
+
: 'outlined');
|
|
3675
|
+
const semanticStyles = (styles && typeof styles === 'object' ? styles : {});
|
|
3676
|
+
const sizePreset = {
|
|
3677
|
+
small: {
|
|
3678
|
+
triggerHeight: 24,
|
|
3679
|
+
triggerPadding: '0 7px',
|
|
3680
|
+
fontSize: 12,
|
|
3681
|
+
arrowSize: 12,
|
|
3682
|
+
optionFontSize: 12,
|
|
3683
|
+
optionHeight: 24,
|
|
3684
|
+
optionPadding: '1px 8px',
|
|
3685
|
+
},
|
|
3686
|
+
medium: {
|
|
3687
|
+
triggerHeight: 32,
|
|
3688
|
+
triggerPadding: '3px 8px',
|
|
3689
|
+
fontSize: 14,
|
|
3690
|
+
arrowSize: 14,
|
|
3691
|
+
optionFontSize: 14,
|
|
3692
|
+
optionHeight: 32,
|
|
3693
|
+
optionPadding: '5px 12px',
|
|
3694
|
+
},
|
|
3695
|
+
large: {
|
|
3696
|
+
triggerHeight: 40,
|
|
3697
|
+
triggerPadding: '7px 11px',
|
|
3698
|
+
fontSize: 14,
|
|
3699
|
+
arrowSize: 14,
|
|
3700
|
+
optionFontSize: 14,
|
|
3701
|
+
optionHeight: 32,
|
|
3702
|
+
optionPadding: '5px 12px',
|
|
3703
|
+
},
|
|
3704
|
+
}[resolvedSize];
|
|
3705
|
+
const variantPreset = {
|
|
3706
|
+
outlined: {
|
|
3707
|
+
background: '#fff',
|
|
3708
|
+
borderColor: '#d9d9d9',
|
|
3709
|
+
},
|
|
3710
|
+
filled: {
|
|
3711
|
+
background: '#f5f5f5',
|
|
3712
|
+
borderColor: 'transparent',
|
|
3713
|
+
},
|
|
3714
|
+
borderless: {
|
|
3715
|
+
background: 'transparent',
|
|
3716
|
+
borderColor: 'transparent',
|
|
3717
|
+
},
|
|
3718
|
+
}[resolvedVariant];
|
|
3182
3719
|
// External value change (controlled usage) overrides the local pick
|
|
3183
3720
|
if (propValue !== this._propValue) {
|
|
3184
3721
|
this._propValue = propValue;
|
|
3185
3722
|
this._localValue = null;
|
|
3186
3723
|
}
|
|
3187
3724
|
const value = this._localValue ?? propValue;
|
|
3188
|
-
|
|
3725
|
+
this.applyHostStyles(style, isExpressionResultStyle, String(accentColor));
|
|
3726
|
+
const triggerInline = this.buildInlineStyle(semanticStyles.trigger, isExpressionResultStyle);
|
|
3727
|
+
const dropdownInline = this.buildInlineStyle(semanticStyles.dropdown, isExpressionResultStyle);
|
|
3189
3728
|
const selectedOption = options.find((o) => o.value === value);
|
|
3190
3729
|
const displayText = selectedOption ? selectedOption.label : placeholder;
|
|
3191
3730
|
const isPlaceholder = !selectedOption;
|
|
3192
3731
|
const openClass = this._open ? 'open' : '';
|
|
3732
|
+
const arrowIcon = renderIconContent({
|
|
3733
|
+
name: 'caret_down',
|
|
3734
|
+
size: sizePreset.arrowSize,
|
|
3735
|
+
color: 'currentColor',
|
|
3736
|
+
});
|
|
3737
|
+
const listboxId = `${this._node.id}-listbox`;
|
|
3738
|
+
const activeOptionId = (this._open
|
|
3739
|
+
&& this._activeIndex >= 0
|
|
3740
|
+
&& this._activeIndex < options.length)
|
|
3741
|
+
? `${this._node.id}-option-${this._activeIndex}`
|
|
3742
|
+
: '';
|
|
3193
3743
|
this.shadowRoot.innerHTML = `
|
|
3194
3744
|
<style>
|
|
3195
3745
|
:host {
|
|
@@ -3204,33 +3754,59 @@ class CardSelect extends BaseElement {
|
|
|
3204
3754
|
display: flex;
|
|
3205
3755
|
align-items: center;
|
|
3206
3756
|
justify-content: space-between;
|
|
3207
|
-
|
|
3208
|
-
|
|
3757
|
+
box-sizing: border-box;
|
|
3758
|
+
width: 100%;
|
|
3759
|
+
min-height: ${sizePreset.triggerHeight}px;
|
|
3760
|
+
padding: ${sizePreset.triggerPadding};
|
|
3761
|
+
font-size: ${sizePreset.fontSize}px;
|
|
3762
|
+
border: 1px solid ${disabled ? '#e5e7eb' : variantPreset.borderColor};
|
|
3209
3763
|
border-radius: 6px;
|
|
3210
|
-
|
|
3764
|
+
outline: none;
|
|
3765
|
+
background: ${disabled ? '#f5f5f5' : variantPreset.background};
|
|
3766
|
+
color: ${disabled ? '#bfbfbf' : isPlaceholder ? '#bfbfbf' : '#333'};
|
|
3211
3767
|
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
|
3212
|
-
transition:
|
|
3768
|
+
transition:
|
|
3769
|
+
border-color 0.2s,
|
|
3770
|
+
box-shadow 0.2s,
|
|
3771
|
+
background-color 0.2s;
|
|
3213
3772
|
user-select: none;
|
|
3214
3773
|
}
|
|
3215
3774
|
.select-trigger:hover {
|
|
3216
|
-
${!disabled ? 'border-color:
|
|
3775
|
+
${!disabled ? 'border-color: var(--select-accent);' : ''}
|
|
3217
3776
|
}
|
|
3777
|
+
.select-trigger:focus-visible,
|
|
3218
3778
|
.select-trigger.open {
|
|
3219
|
-
border-color:
|
|
3220
|
-
box-shadow: 0 0 0 2px rgba(
|
|
3779
|
+
border-color: var(--select-accent);
|
|
3780
|
+
box-shadow: 0 0 0 2px rgba(5,145,255,0.1);
|
|
3781
|
+
box-shadow: 0 0 0 2px color-mix(
|
|
3782
|
+
in srgb,
|
|
3783
|
+
var(--select-accent) 10%,
|
|
3784
|
+
transparent
|
|
3785
|
+
);
|
|
3221
3786
|
}
|
|
3222
3787
|
.select-text {
|
|
3223
3788
|
flex: 1;
|
|
3789
|
+
min-width: 0;
|
|
3224
3790
|
overflow: hidden;
|
|
3225
3791
|
text-overflow: ellipsis;
|
|
3226
3792
|
white-space: nowrap;
|
|
3227
|
-
color:
|
|
3793
|
+
color: inherit;
|
|
3228
3794
|
}
|
|
3229
3795
|
.select-arrow {
|
|
3796
|
+
display: inline-flex;
|
|
3797
|
+
flex: none;
|
|
3798
|
+
align-items: center;
|
|
3799
|
+
justify-content: center;
|
|
3800
|
+
width: ${sizePreset.arrowSize}px;
|
|
3801
|
+
height: ${sizePreset.arrowSize}px;
|
|
3230
3802
|
margin-left: 8px;
|
|
3231
|
-
|
|
3232
|
-
color: #999;
|
|
3803
|
+
color: ${disabled ? '#bfbfbf' : '#999'};
|
|
3233
3804
|
transition: transform 0.2s;
|
|
3805
|
+
transform-origin: center;
|
|
3806
|
+
pointer-events: none;
|
|
3807
|
+
}
|
|
3808
|
+
.select-arrow .icon-svg {
|
|
3809
|
+
display: block;
|
|
3234
3810
|
}
|
|
3235
3811
|
.select-arrow.open { transform: rotate(180deg); }
|
|
3236
3812
|
.dropdown {
|
|
@@ -3240,63 +3816,242 @@ class CardSelect extends BaseElement {
|
|
|
3240
3816
|
left: 0;
|
|
3241
3817
|
right: 0;
|
|
3242
3818
|
background: #fff;
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3819
|
+
box-sizing: border-box;
|
|
3820
|
+
padding: 4px;
|
|
3821
|
+
border: 0;
|
|
3822
|
+
border-radius: 8px;
|
|
3823
|
+
box-shadow:
|
|
3824
|
+
0 6px 16px 0 rgba(0,0,0,0.08),
|
|
3825
|
+
0 3px 6px -4px rgba(0,0,0,0.12),
|
|
3826
|
+
0 9px 28px 8px rgba(0,0,0,0.05);
|
|
3827
|
+
max-height: 256px;
|
|
3247
3828
|
overflow-y: auto;
|
|
3248
3829
|
z-index: 1000;
|
|
3249
3830
|
}
|
|
3250
3831
|
.dropdown.open { display: block; }
|
|
3251
3832
|
.option {
|
|
3252
|
-
|
|
3833
|
+
display: flex;
|
|
3834
|
+
align-items: center;
|
|
3835
|
+
box-sizing: border-box;
|
|
3836
|
+
min-width: 0;
|
|
3837
|
+
min-height: ${sizePreset.optionHeight}px;
|
|
3838
|
+
padding: ${sizePreset.optionPadding};
|
|
3839
|
+
font-size: ${sizePreset.optionFontSize}px;
|
|
3840
|
+
border-radius: 4px;
|
|
3841
|
+
color: rgba(0,0,0,0.88);
|
|
3253
3842
|
cursor: pointer;
|
|
3254
3843
|
transition: background 0.15s;
|
|
3844
|
+
}
|
|
3845
|
+
.option-label {
|
|
3846
|
+
flex: 1;
|
|
3847
|
+
min-width: 0;
|
|
3255
3848
|
overflow: hidden;
|
|
3256
3849
|
text-overflow: ellipsis;
|
|
3257
3850
|
white-space: nowrap;
|
|
3258
3851
|
}
|
|
3259
|
-
.option:hover
|
|
3260
|
-
.option.
|
|
3852
|
+
.option:hover,
|
|
3853
|
+
.option.active { background: #f5f5f5; }
|
|
3854
|
+
.option.selected {
|
|
3855
|
+
background: #e6f4ff;
|
|
3856
|
+
background: color-mix(
|
|
3857
|
+
in srgb,
|
|
3858
|
+
var(--select-accent) 10%,
|
|
3859
|
+
#fff
|
|
3860
|
+
);
|
|
3861
|
+
color: rgba(0,0,0,0.88);
|
|
3862
|
+
font-weight: 600;
|
|
3863
|
+
}
|
|
3864
|
+
.option.disabled {
|
|
3865
|
+
color: #bfbfbf;
|
|
3866
|
+
cursor: not-allowed;
|
|
3867
|
+
}
|
|
3868
|
+
.option.disabled:hover {
|
|
3869
|
+
background: transparent;
|
|
3870
|
+
}
|
|
3261
3871
|
</style>
|
|
3262
|
-
<div
|
|
3263
|
-
|
|
3264
|
-
|
|
3872
|
+
<div
|
|
3873
|
+
class="select-trigger ${openClass}"
|
|
3874
|
+
role="combobox"
|
|
3875
|
+
tabindex="${disabled ? '-1' : '0'}"
|
|
3876
|
+
aria-label="${this.escapeAttr(String(ariaLabel))}"
|
|
3877
|
+
aria-haspopup="listbox"
|
|
3878
|
+
aria-controls="${this.escapeAttr(listboxId)}"
|
|
3879
|
+
${activeOptionId
|
|
3880
|
+
? `aria-activedescendant="${this.escapeAttr(activeOptionId)}"`
|
|
3881
|
+
: ''}
|
|
3882
|
+
aria-expanded="${this._open}"
|
|
3883
|
+
aria-disabled="${Boolean(disabled)}"
|
|
3884
|
+
aria-readonly="${Boolean(readOnly)}"
|
|
3885
|
+
style="${triggerInline}"
|
|
3886
|
+
>
|
|
3887
|
+
<span class="select-text">${this.escapeText(String(displayText))}</span>
|
|
3888
|
+
<span class="select-arrow ${openClass}">${arrowIcon}</span>
|
|
3265
3889
|
</div>
|
|
3266
|
-
<div
|
|
3267
|
-
|
|
3890
|
+
<div
|
|
3891
|
+
class="dropdown ${openClass}"
|
|
3892
|
+
id="${this.escapeAttr(listboxId)}"
|
|
3893
|
+
role="listbox"
|
|
3894
|
+
style="${dropdownInline}"
|
|
3895
|
+
>
|
|
3896
|
+
${options.map((opt, index) => {
|
|
3897
|
+
const optionInline = this.buildInlineStyle({
|
|
3898
|
+
...(semanticStyles.option || {}),
|
|
3899
|
+
...(opt.value === value ? semanticStyles.selectedOption || {} : {}),
|
|
3900
|
+
}, isExpressionResultStyle);
|
|
3901
|
+
return (`<div
|
|
3902
|
+
id="${this.escapeAttr(`${this._node.id}-option-${index}`)}"
|
|
3903
|
+
class="option ${opt.value === value ? 'selected' : ''} ${opt.disabled ? 'disabled' : ''} ${index === this._activeIndex ? 'active' : ''}"
|
|
3904
|
+
role="option"
|
|
3905
|
+
aria-selected="${opt.value === value}"
|
|
3906
|
+
aria-disabled="${Boolean(opt.disabled)}"
|
|
3907
|
+
data-index="${index}"
|
|
3908
|
+
data-value="${this.escapeAttr(String(opt.value))}"
|
|
3909
|
+
data-disabled="${opt.disabled ? 'true' : 'false'}"
|
|
3910
|
+
style="${optionInline}"
|
|
3911
|
+
><span class="option-label">${this.escapeText(String(opt.label))}</span></div>`);
|
|
3912
|
+
}).join('')}
|
|
3268
3913
|
</div>
|
|
3269
3914
|
`;
|
|
3270
3915
|
if (disabled)
|
|
3271
3916
|
return;
|
|
3272
3917
|
const trigger = this.shadowRoot.querySelector('.select-trigger');
|
|
3273
|
-
trigger.addEventListener('click', () =>
|
|
3918
|
+
trigger.addEventListener('click', (event) => {
|
|
3919
|
+
if (readOnly)
|
|
3920
|
+
return;
|
|
3921
|
+
event.stopPropagation();
|
|
3922
|
+
this.openWithCurrentOption(options, value);
|
|
3923
|
+
});
|
|
3924
|
+
trigger.addEventListener('keydown', (event) => {
|
|
3925
|
+
if (readOnly)
|
|
3926
|
+
return;
|
|
3927
|
+
this.handleKeydown(event, options, value);
|
|
3928
|
+
});
|
|
3274
3929
|
this.shadowRoot.querySelectorAll('.option').forEach((opt) => {
|
|
3275
3930
|
opt.addEventListener('click', () => {
|
|
3931
|
+
if (opt.dataset.disabled === 'true')
|
|
3932
|
+
return;
|
|
3276
3933
|
const val = opt.dataset.value ?? '';
|
|
3277
|
-
this.
|
|
3278
|
-
this._setOpen(false);
|
|
3279
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
3280
|
-
bubbles: true,
|
|
3281
|
-
composed: true,
|
|
3282
|
-
detail: { value: val },
|
|
3283
|
-
}));
|
|
3934
|
+
this.commit(val, value, 'pointer');
|
|
3284
3935
|
});
|
|
3285
3936
|
});
|
|
3286
3937
|
}
|
|
3287
3938
|
disconnectedCallback() {
|
|
3288
3939
|
document.removeEventListener('click', this._onDocClick);
|
|
3289
3940
|
}
|
|
3941
|
+
updateProps(props, isMobile) {
|
|
3942
|
+
if (Object.prototype.hasOwnProperty.call(props, 'value')
|
|
3943
|
+
|| Object.prototype.hasOwnProperty.call(props, 'defaultValue')) {
|
|
3944
|
+
this._localValue = null;
|
|
3945
|
+
this._propValue = undefined;
|
|
3946
|
+
}
|
|
3947
|
+
super.updateProps(props, isMobile);
|
|
3948
|
+
}
|
|
3290
3949
|
_setOpen(open) {
|
|
3291
3950
|
if (this._open === open)
|
|
3292
3951
|
return;
|
|
3293
3952
|
this._open = open;
|
|
3294
|
-
if (open)
|
|
3295
|
-
|
|
3296
|
-
|
|
3953
|
+
if (open) {
|
|
3954
|
+
queueMicrotask(() => {
|
|
3955
|
+
if (this._open && this.isConnected) {
|
|
3956
|
+
document.addEventListener('click', this._onDocClick);
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
else {
|
|
3297
3961
|
document.removeEventListener('click', this._onDocClick);
|
|
3962
|
+
}
|
|
3298
3963
|
this.render();
|
|
3299
3964
|
}
|
|
3965
|
+
openWithCurrentOption(options, value) {
|
|
3966
|
+
if (this._open) {
|
|
3967
|
+
this._setOpen(false);
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
const selectedIndex = options.findIndex((option) => !option.disabled && String(option.value) === value);
|
|
3971
|
+
this._activeIndex = selectedIndex >= 0
|
|
3972
|
+
? selectedIndex
|
|
3973
|
+
: this.findNextEnabled(options, -1, 1);
|
|
3974
|
+
this._setOpen(true);
|
|
3975
|
+
}
|
|
3976
|
+
handleKeydown(event, options, value) {
|
|
3977
|
+
if (event.key === 'Escape') {
|
|
3978
|
+
event.preventDefault();
|
|
3979
|
+
this._setOpen(false);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
3983
|
+
event.preventDefault();
|
|
3984
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
|
3985
|
+
if (!this._open) {
|
|
3986
|
+
const selectedIndex = options.findIndex((option) => !option.disabled && String(option.value) === value);
|
|
3987
|
+
this._activeIndex = this.findNextEnabled(options, selectedIndex, direction);
|
|
3988
|
+
this._setOpen(true);
|
|
3989
|
+
}
|
|
3990
|
+
else {
|
|
3991
|
+
this._activeIndex = this.findNextEnabled(options, this._activeIndex, direction);
|
|
3992
|
+
this.render();
|
|
3993
|
+
}
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if ((event.key === 'Enter' || event.key === ' ') && this._open) {
|
|
3997
|
+
event.preventDefault();
|
|
3998
|
+
const option = options[this._activeIndex];
|
|
3999
|
+
if (option && !option.disabled) {
|
|
4000
|
+
this.commit(String(option.value), value, 'keyboard');
|
|
4001
|
+
}
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
4005
|
+
event.preventDefault();
|
|
4006
|
+
this.openWithCurrentOption(options, value);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
findNextEnabled(options, start, direction) {
|
|
4010
|
+
if (options.length === 0)
|
|
4011
|
+
return -1;
|
|
4012
|
+
for (let offset = 1; offset <= options.length; offset += 1) {
|
|
4013
|
+
const index = (start + direction * offset + options.length) % options.length;
|
|
4014
|
+
if (!options[index]?.disabled)
|
|
4015
|
+
return index;
|
|
4016
|
+
}
|
|
4017
|
+
return -1;
|
|
4018
|
+
}
|
|
4019
|
+
commit(value, previousValue, source) {
|
|
4020
|
+
if (value === previousValue) {
|
|
4021
|
+
this._setOpen(false);
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
this._localValue = value;
|
|
4025
|
+
this._activeIndex = -1;
|
|
4026
|
+
if (this._open)
|
|
4027
|
+
this._setOpen(false);
|
|
4028
|
+
else
|
|
4029
|
+
this.render();
|
|
4030
|
+
emitValueChange(this, value, previousValue, source);
|
|
4031
|
+
}
|
|
4032
|
+
escapeAttr(value) {
|
|
4033
|
+
return value
|
|
4034
|
+
.replace(/&/g, '&')
|
|
4035
|
+
.replace(/"/g, '"')
|
|
4036
|
+
.replace(/</g, '<')
|
|
4037
|
+
.replace(/>/g, '>');
|
|
4038
|
+
}
|
|
4039
|
+
escapeText(value) {
|
|
4040
|
+
return value
|
|
4041
|
+
.replace(/&/g, '&')
|
|
4042
|
+
.replace(/</g, '<')
|
|
4043
|
+
.replace(/>/g, '>');
|
|
4044
|
+
}
|
|
4045
|
+
applyHostStyles(style, isExpressionResultStyle, accentColor) {
|
|
4046
|
+
const serialized = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
4047
|
+
const parser = document.createElement('div');
|
|
4048
|
+
parser.innerHTML = `<span style="${serialized}"></span>`;
|
|
4049
|
+
const parsedStyle = parser.firstElementChild instanceof HTMLElement
|
|
4050
|
+
? parser.firstElementChild.style.cssText
|
|
4051
|
+
: '';
|
|
4052
|
+
this.style.cssText = parsedStyle;
|
|
4053
|
+
this.style.setProperty('--select-accent', accentColor || '#1677ff');
|
|
4054
|
+
}
|
|
3300
4055
|
}
|
|
3301
4056
|
CardSelect.is = 'ai-card-select';
|
|
3302
4057
|
if (typeof customElements !== 'undefined' && !customElements.get(CardSelect.is)) {
|
|
@@ -3428,45 +4183,6 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardPasscodeInp
|
|
|
3428
4183
|
customElements.define(CardPasscodeInput.is, CardPasscodeInput);
|
|
3429
4184
|
}
|
|
3430
4185
|
|
|
3431
|
-
function escapeAttr$1(value) {
|
|
3432
|
-
return value
|
|
3433
|
-
.replace(/&/g, '&')
|
|
3434
|
-
.replace(/"/g, '"')
|
|
3435
|
-
.replace(/</g, '<')
|
|
3436
|
-
.replace(/>/g, '>');
|
|
3437
|
-
}
|
|
3438
|
-
function escapeText(value) {
|
|
3439
|
-
return value
|
|
3440
|
-
.replace(/&/g, '&')
|
|
3441
|
-
.replace(/</g, '<')
|
|
3442
|
-
.replace(/>/g, '>');
|
|
3443
|
-
}
|
|
3444
|
-
function sanitizeImageSrc(src) {
|
|
3445
|
-
const trimmed = String(src ?? '').trim();
|
|
3446
|
-
const scheme = trimmed.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
|
3447
|
-
if (/^(javascript|vbscript|file):/.test(scheme))
|
|
3448
|
-
return '';
|
|
3449
|
-
if (/^data:/.test(scheme) && !/^data:image\//.test(scheme))
|
|
3450
|
-
return '';
|
|
3451
|
-
return trimmed;
|
|
3452
|
-
}
|
|
3453
|
-
function renderIconContent(input) {
|
|
3454
|
-
const size = Number(input.size) || 24;
|
|
3455
|
-
const name = input.name == null ? undefined : String(input.name);
|
|
3456
|
-
if (input.src) {
|
|
3457
|
-
return `<img class="icon-img" src="${escapeAttr$1(sanitizeImageSrc(input.src))}" ` +
|
|
3458
|
-
`width="${size}" height="${size}" alt="${escapeAttr$1(name ?? 'icon')}" />`;
|
|
3459
|
-
}
|
|
3460
|
-
const icon = getBuiltinIcon(name);
|
|
3461
|
-
if (icon) {
|
|
3462
|
-
return `<svg class="icon-svg" viewBox="${escapeAttr$1(icon.viewBox)}" width="${size}" ` +
|
|
3463
|
-
`height="${size}" color="${escapeAttr$1(String(input.color ?? 'currentColor'))}" ` +
|
|
3464
|
-
`aria-hidden="true">${icon.body}</svg>`;
|
|
3465
|
-
}
|
|
3466
|
-
return `<span class="icon-text" style="font-size:${size}px; line-height:1;">` +
|
|
3467
|
-
`${escapeText(name ?? '?')}</span>`;
|
|
3468
|
-
}
|
|
3469
|
-
|
|
3470
4186
|
/**
|
|
3471
4187
|
* CardIcon — Custom Element for rendering icons.
|
|
3472
4188
|
*
|
|
@@ -3554,6 +4270,31 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardIcon.is)) {
|
|
|
3554
4270
|
* }
|
|
3555
4271
|
* ```
|
|
3556
4272
|
*/
|
|
4273
|
+
const MAX_VALIDATION_PATTERN_LENGTH = 256;
|
|
4274
|
+
const MAX_PATTERN_INPUT_LENGTH = 4096;
|
|
4275
|
+
/**
|
|
4276
|
+
* Restricts schema-provided patterns to a predictable subset.
|
|
4277
|
+
*
|
|
4278
|
+
* Nested quantified groups, quantified alternations, backreferences, and
|
|
4279
|
+
* lookarounds are rejected because they can introduce catastrophic
|
|
4280
|
+
* backtracking in JavaScript's synchronous RegExp engine.
|
|
4281
|
+
*/
|
|
4282
|
+
function isSafeValidationPattern(pattern) {
|
|
4283
|
+
if (pattern.length === 0 || pattern.length > MAX_VALIDATION_PATTERN_LENGTH) {
|
|
4284
|
+
return false;
|
|
4285
|
+
}
|
|
4286
|
+
if (/\\[1-9]/.test(pattern))
|
|
4287
|
+
return false;
|
|
4288
|
+
if (/\(\?(?:[=!]|<[=!])/.test(pattern))
|
|
4289
|
+
return false;
|
|
4290
|
+
if (/\((?:\\.|[^)])*(?:[+*]|\{\d+(?:,\d*)?\})(?:\\.|[^)])*\)(?:[+*]|\{\d+(?:,\d*)?\})/.test(pattern)) {
|
|
4291
|
+
return false;
|
|
4292
|
+
}
|
|
4293
|
+
if (/\((?:\\.|[^)])*\|(?:\\.|[^)])*\)(?:[+*]|\{\d+(?:,\d*)?\})/.test(pattern)) {
|
|
4294
|
+
return false;
|
|
4295
|
+
}
|
|
4296
|
+
return true;
|
|
4297
|
+
}
|
|
3557
4298
|
class CardForm extends BaseElement {
|
|
3558
4299
|
constructor() {
|
|
3559
4300
|
super(...arguments);
|
|
@@ -3564,8 +4305,10 @@ class CardForm extends BaseElement {
|
|
|
3564
4305
|
render() {
|
|
3565
4306
|
if (!this.shadowRoot || !this._node)
|
|
3566
4307
|
return;
|
|
3567
|
-
const { fields = [], submitText = '提交', layout = 'vertical', disabled = false, style, isExpressionResultStyle, } = this._props;
|
|
4308
|
+
const { fields = [], submitText = '提交', layout = 'vertical', disabled = false, invalidStyle, errorStyle, style, isExpressionResultStyle, } = this._props;
|
|
3568
4309
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
4310
|
+
const invalidInlineStyle = this.buildInlineStyle(invalidStyle, isExpressionResultStyle);
|
|
4311
|
+
const errorInlineStyle = this.buildInlineStyle(errorStyle, isExpressionResultStyle);
|
|
3569
4312
|
const isHorizontal = layout === 'horizontal';
|
|
3570
4313
|
// Init default values on first render
|
|
3571
4314
|
if (!this._submitted && Object.keys(this._values).length === 0) {
|
|
@@ -3573,7 +4316,9 @@ class CardForm extends BaseElement {
|
|
|
3573
4316
|
this._values[f.name] = f.defaultValue ?? '';
|
|
3574
4317
|
});
|
|
3575
4318
|
}
|
|
3576
|
-
const fieldItems = fields
|
|
4319
|
+
const fieldItems = fields
|
|
4320
|
+
.map((field, index) => this._renderField(field, index))
|
|
4321
|
+
.join('');
|
|
3577
4322
|
this.shadowRoot.innerHTML = `
|
|
3578
4323
|
<style>
|
|
3579
4324
|
:host {
|
|
@@ -3604,7 +4349,12 @@ class CardForm extends BaseElement {
|
|
|
3604
4349
|
color: #ff4d4f;
|
|
3605
4350
|
margin-right: 2px;
|
|
3606
4351
|
}
|
|
3607
|
-
.field-
|
|
4352
|
+
.field-control {
|
|
4353
|
+
display: flex;
|
|
4354
|
+
align-items: center;
|
|
4355
|
+
gap: 8px;
|
|
4356
|
+
box-sizing: border-box;
|
|
4357
|
+
width: 100%;
|
|
3608
4358
|
padding: 6px 12px;
|
|
3609
4359
|
border: 1px solid #d9d9d9;
|
|
3610
4360
|
border-radius: 6px;
|
|
@@ -3614,35 +4364,73 @@ class CardForm extends BaseElement {
|
|
|
3614
4364
|
font-family: inherit;
|
|
3615
4365
|
background: #fff;
|
|
3616
4366
|
}
|
|
3617
|
-
.field-
|
|
4367
|
+
.field-control.textarea-control {
|
|
4368
|
+
align-items: flex-start;
|
|
4369
|
+
}
|
|
4370
|
+
.field-control:focus-within {
|
|
3618
4371
|
border-color: #1677ff;
|
|
3619
4372
|
box-shadow: 0 0 0 2px rgba(22,119,255,0.1);
|
|
3620
4373
|
}
|
|
3621
|
-
.field-
|
|
4374
|
+
.field-control.error {
|
|
3622
4375
|
border-color: #ff4d4f;
|
|
4376
|
+
${invalidInlineStyle}
|
|
4377
|
+
}
|
|
4378
|
+
.field-input {
|
|
4379
|
+
flex: 1;
|
|
4380
|
+
min-width: 0;
|
|
4381
|
+
box-sizing: border-box;
|
|
4382
|
+
width: 100%;
|
|
4383
|
+
padding: 0;
|
|
4384
|
+
border: 0;
|
|
4385
|
+
outline: none;
|
|
4386
|
+
color: inherit;
|
|
4387
|
+
background: transparent;
|
|
4388
|
+
font: inherit;
|
|
4389
|
+
line-height: 1.5;
|
|
3623
4390
|
}
|
|
3624
4391
|
.field-input:disabled {
|
|
3625
|
-
background: #f5f5f5;
|
|
3626
4392
|
cursor: not-allowed;
|
|
3627
4393
|
}
|
|
4394
|
+
.field-affix {
|
|
4395
|
+
display: inline-flex;
|
|
4396
|
+
flex: none;
|
|
4397
|
+
align-items: center;
|
|
4398
|
+
color: #666;
|
|
4399
|
+
white-space: nowrap;
|
|
4400
|
+
line-height: 1.5;
|
|
4401
|
+
}
|
|
4402
|
+
.field-affix .icon-img,
|
|
4403
|
+
.field-affix .icon-svg,
|
|
4404
|
+
.field-affix .icon-text {
|
|
4405
|
+
display: block;
|
|
4406
|
+
}
|
|
3628
4407
|
.field-input-wrapper {
|
|
3629
4408
|
flex: 1;
|
|
3630
4409
|
display: flex;
|
|
3631
4410
|
flex-direction: column;
|
|
3632
4411
|
min-width: 0;
|
|
3633
4412
|
}
|
|
4413
|
+
.form-select {
|
|
4414
|
+
display: block;
|
|
4415
|
+
width: 100%;
|
|
4416
|
+
min-width: 0;
|
|
4417
|
+
}
|
|
4418
|
+
.form-rate {
|
|
4419
|
+
display: inline-flex;
|
|
4420
|
+
width: fit-content;
|
|
4421
|
+
}
|
|
3634
4422
|
textarea.field-input {
|
|
3635
4423
|
min-height: 60px;
|
|
3636
4424
|
resize: vertical;
|
|
3637
4425
|
}
|
|
3638
|
-
select.field-input {
|
|
3639
|
-
cursor: pointer;
|
|
3640
|
-
appearance: auto;
|
|
3641
|
-
}
|
|
3642
4426
|
.field-error {
|
|
3643
4427
|
font-size: 12px;
|
|
3644
4428
|
color: #ff4d4f;
|
|
3645
|
-
|
|
4429
|
+
line-height: 18px;
|
|
4430
|
+
${errorInlineStyle}
|
|
4431
|
+
}
|
|
4432
|
+
.field-error:empty {
|
|
4433
|
+
display: none;
|
|
3646
4434
|
}
|
|
3647
4435
|
/* Passcode */
|
|
3648
4436
|
.passcode-row {
|
|
@@ -3664,20 +4452,10 @@ class CardForm extends BaseElement {
|
|
|
3664
4452
|
border-color: #1677ff;
|
|
3665
4453
|
box-shadow: 0 0 0 2px rgba(22,119,255,0.1);
|
|
3666
4454
|
}
|
|
3667
|
-
.passcode-cell.error {
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
display: flex;
|
|
3671
|
-
gap: 4px;
|
|
3672
|
-
}
|
|
3673
|
-
.rate-star {
|
|
3674
|
-
font-size: 24px;
|
|
3675
|
-
cursor: pointer;
|
|
3676
|
-
user-select: none;
|
|
3677
|
-
transition: transform 0.15s;
|
|
3678
|
-
line-height: 1;
|
|
4455
|
+
.passcode-cell.error {
|
|
4456
|
+
border-color: #ff4d4f;
|
|
4457
|
+
${invalidInlineStyle}
|
|
3679
4458
|
}
|
|
3680
|
-
.rate-star:hover { transform: scale(1.15); }
|
|
3681
4459
|
/* Submit */
|
|
3682
4460
|
.submit-btn {
|
|
3683
4461
|
padding: 8px 24px;
|
|
@@ -3702,71 +4480,268 @@ class CardForm extends BaseElement {
|
|
|
3702
4480
|
<button class="submit-btn" ${disabled ? 'disabled' : ''} style="${isHorizontal ? `margin-left: 84px;` : ''}">${submitText}</button>
|
|
3703
4481
|
</div>
|
|
3704
4482
|
`;
|
|
4483
|
+
this._initializeSelectFields(fields, disabled);
|
|
4484
|
+
this._initializeRateFields(fields, disabled);
|
|
3705
4485
|
this._bindEvents(fields, disabled);
|
|
3706
4486
|
}
|
|
3707
|
-
_renderField(field) {
|
|
3708
|
-
const { name, label, type = 'text', placeholder = '', required = false, options = [], length = 6 } = field;
|
|
4487
|
+
_renderField(field, index) {
|
|
4488
|
+
const { name, label, type = 'text', placeholder = '', required = false, rules = [], prefix, suffix, options = [], length = 6, } = field;
|
|
3709
4489
|
const val = this._values[name] ?? '';
|
|
3710
4490
|
const error = this._errors[name] || '';
|
|
3711
4491
|
const errorClass = error ? 'error' : '';
|
|
3712
|
-
const
|
|
4492
|
+
const escapedName = this._escapeAttr(name);
|
|
4493
|
+
const idBase = `${this._domId(this._node?.id || 'form')}-field-${index}`;
|
|
4494
|
+
const controlId = `${idBase}-control`;
|
|
4495
|
+
const labelId = `${idBase}-label`;
|
|
4496
|
+
const errorId = `${idBase}-error`;
|
|
4497
|
+
const supportsAffixes = type !== 'passcode'
|
|
4498
|
+
&& type !== 'rate'
|
|
4499
|
+
&& type !== 'select';
|
|
4500
|
+
const prefixAffix = supportsAffixes
|
|
4501
|
+
? this._renderAffix('prefix', prefix, `${idBase}-prefix`)
|
|
4502
|
+
: undefined;
|
|
4503
|
+
const suffixAffix = supportsAffixes
|
|
4504
|
+
? this._renderAffix('suffix', suffix, `${idBase}-suffix`)
|
|
4505
|
+
: undefined;
|
|
4506
|
+
const affixDescriptionIds = [
|
|
4507
|
+
prefixAffix?.descriptionId,
|
|
4508
|
+
suffixAffix?.descriptionId,
|
|
4509
|
+
].filter((id) => Boolean(id));
|
|
4510
|
+
const describedByIds = [
|
|
4511
|
+
...affixDescriptionIds,
|
|
4512
|
+
...(error ? [errorId] : []),
|
|
4513
|
+
];
|
|
4514
|
+
const ariaDescription = describedByIds.length > 0
|
|
4515
|
+
? `aria-describedby="${describedByIds.join(' ')}"`
|
|
4516
|
+
: '';
|
|
4517
|
+
const affixDescriptionData = affixDescriptionIds.length > 0
|
|
4518
|
+
? `data-affix-describedby="${affixDescriptionIds.join(' ')}"`
|
|
4519
|
+
: '';
|
|
4520
|
+
const ariaError = `aria-invalid="${Boolean(error)}" ${ariaDescription} ${affixDescriptionData}`;
|
|
4521
|
+
const ariaLabel = label ? `aria-labelledby="${labelId}"` : '';
|
|
4522
|
+
const isRequired = required || rules.some((rule) => rule.required);
|
|
4523
|
+
const labelHtml = label
|
|
4524
|
+
? `<label class="field-label" id="${labelId}" for="${type === 'passcode' ? `${controlId}-0` : controlId}">${isRequired ? '<span class="required">*</span>' : ''}${this._escapeHtml(label)}</label>`
|
|
4525
|
+
: '';
|
|
4526
|
+
const prefixHtml = prefixAffix?.html ?? '';
|
|
4527
|
+
const suffixHtml = suffixAffix?.html ?? '';
|
|
3713
4528
|
let inputHtml = '';
|
|
4529
|
+
let usesFieldControl = false;
|
|
3714
4530
|
switch (type) {
|
|
3715
4531
|
case 'textarea':
|
|
3716
|
-
|
|
4532
|
+
usesFieldControl = true;
|
|
4533
|
+
inputHtml = `<textarea id="${controlId}" class="field-input" data-name="${escapedName}" placeholder="${this._escapeAttr(placeholder)}" ${ariaLabel} ${ariaError}>${this._escapeHtml(String(val))}</textarea>`;
|
|
3717
4534
|
break;
|
|
3718
4535
|
case 'select':
|
|
3719
|
-
inputHtml =
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
4536
|
+
inputHtml = `<${CardSelect.is}
|
|
4537
|
+
id="${controlId}"
|
|
4538
|
+
class="form-select"
|
|
4539
|
+
data-name="${escapedName}"
|
|
4540
|
+
${ariaError}
|
|
4541
|
+
></${CardSelect.is}>`;
|
|
3723
4542
|
break;
|
|
3724
4543
|
case 'passcode': {
|
|
3725
4544
|
const cells = Array.from({ length }, (_, i) => {
|
|
3726
4545
|
const cellVal = typeof val === 'string' ? (val[i] || '') : '';
|
|
3727
|
-
return `<input class="passcode-cell ${errorClass}" type="text" inputmode="numeric" maxlength="1" data-name="${
|
|
4546
|
+
return `<input id="${controlId}-${i}" class="passcode-cell ${errorClass}" type="text" inputmode="numeric" maxlength="1" data-name="${escapedName}" data-index="${i}" value="${this._escapeAttr(cellVal)}" ${ariaLabel} ${ariaError} />`;
|
|
3728
4547
|
}).join('');
|
|
3729
|
-
inputHtml = `<div class="passcode-row" data-passcode-name="${
|
|
4548
|
+
inputHtml = `<div class="passcode-row" data-passcode-name="${escapedName}">${cells}</div>`;
|
|
3730
4549
|
break;
|
|
3731
4550
|
}
|
|
3732
4551
|
case 'rate': {
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4552
|
+
inputHtml = `<${CardRate.is}
|
|
4553
|
+
id="${controlId}"
|
|
4554
|
+
class="form-rate"
|
|
4555
|
+
data-name="${escapedName}"
|
|
4556
|
+
${ariaError}
|
|
4557
|
+
></${CardRate.is}>`;
|
|
3737
4558
|
break;
|
|
3738
4559
|
}
|
|
3739
|
-
default:
|
|
3740
|
-
|
|
4560
|
+
default: {
|
|
4561
|
+
usesFieldControl = true;
|
|
4562
|
+
const nativeType = type === 'password' || type === 'number'
|
|
4563
|
+
? type
|
|
4564
|
+
: 'text';
|
|
4565
|
+
inputHtml = `<input id="${controlId}" class="field-input" type="${nativeType}" data-name="${escapedName}" placeholder="${this._escapeAttr(placeholder)}" value="${this._escapeAttr(String(val))}" ${ariaLabel} ${ariaError} />`;
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
if (usesFieldControl) {
|
|
4569
|
+
inputHtml = `
|
|
4570
|
+
<div class="field-control ${type === 'textarea' ? 'textarea-control' : ''} ${errorClass}">
|
|
4571
|
+
${prefixHtml}
|
|
4572
|
+
${inputHtml}
|
|
4573
|
+
${suffixHtml}
|
|
4574
|
+
</div>`;
|
|
3741
4575
|
}
|
|
3742
4576
|
return `
|
|
3743
4577
|
<div class="form-field">
|
|
3744
4578
|
${labelHtml}
|
|
3745
4579
|
<div class="field-input-wrapper">
|
|
3746
4580
|
${inputHtml}
|
|
3747
|
-
<div class="field-error">${error}</div>
|
|
4581
|
+
<div class="field-error" id="${errorId}" role="alert">${this._escapeHtml(error)}</div>
|
|
3748
4582
|
</div>
|
|
3749
4583
|
</div>`;
|
|
3750
4584
|
}
|
|
4585
|
+
_initializeSelectFields(fields, disabled) {
|
|
4586
|
+
if (!this.shadowRoot)
|
|
4587
|
+
return;
|
|
4588
|
+
this.shadowRoot.querySelectorAll('.form-select').forEach((selectElement) => {
|
|
4589
|
+
const name = selectElement.dataset.name;
|
|
4590
|
+
const field = fields.find((item) => item.name === name);
|
|
4591
|
+
if (!field)
|
|
4592
|
+
return;
|
|
4593
|
+
const props = this._selectProps(field, this._errors[field.name] || '', disabled);
|
|
4594
|
+
const node = {
|
|
4595
|
+
id: selectElement.id,
|
|
4596
|
+
type: 'Select',
|
|
4597
|
+
props,
|
|
4598
|
+
children: [],
|
|
4599
|
+
lifecycle: undefined,
|
|
4600
|
+
events: undefined,
|
|
4601
|
+
directives: undefined,
|
|
4602
|
+
};
|
|
4603
|
+
selectElement.setData(node, props, this._isMobile);
|
|
4604
|
+
const label = selectElement
|
|
4605
|
+
.closest('.form-field')
|
|
4606
|
+
?.querySelector('.field-label');
|
|
4607
|
+
label?.addEventListener('click', () => {
|
|
4608
|
+
selectElement.shadowRoot
|
|
4609
|
+
?.querySelector('.select-trigger')
|
|
4610
|
+
?.focus();
|
|
4611
|
+
});
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
_selectProps(field, error, disabled) {
|
|
4615
|
+
return {
|
|
4616
|
+
options: field.options ?? [],
|
|
4617
|
+
placeholder: field.placeholder || '请选择',
|
|
4618
|
+
value: String(this._values[field.name] ?? ''),
|
|
4619
|
+
disabled,
|
|
4620
|
+
ariaLabel: field.label || field.placeholder || '请选择',
|
|
4621
|
+
size: field.size,
|
|
4622
|
+
variant: field.variant,
|
|
4623
|
+
accentColor: field.accentColor,
|
|
4624
|
+
style: {
|
|
4625
|
+
width: '100%',
|
|
4626
|
+
},
|
|
4627
|
+
styles: {
|
|
4628
|
+
...(field.styles ?? {}),
|
|
4629
|
+
trigger: {
|
|
4630
|
+
...(field.style ?? {}),
|
|
4631
|
+
...(field.styles?.trigger ?? {}),
|
|
4632
|
+
...(error ? this._props.invalidStyle ?? {} : {}),
|
|
4633
|
+
},
|
|
4634
|
+
},
|
|
4635
|
+
isExpressionResultStyle: this._props.isExpressionResultStyle,
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
_initializeRateFields(fields, disabled) {
|
|
4639
|
+
if (!this.shadowRoot)
|
|
4640
|
+
return;
|
|
4641
|
+
this.shadowRoot.querySelectorAll('.form-rate').forEach((rateElement) => {
|
|
4642
|
+
const name = rateElement.dataset.name;
|
|
4643
|
+
const field = fields.find((item) => item.name === name);
|
|
4644
|
+
if (!field)
|
|
4645
|
+
return;
|
|
4646
|
+
const props = this._rateProps(field, this._errors[field.name] || '', disabled);
|
|
4647
|
+
const node = {
|
|
4648
|
+
id: rateElement.id,
|
|
4649
|
+
type: 'Rate',
|
|
4650
|
+
props,
|
|
4651
|
+
children: [],
|
|
4652
|
+
lifecycle: undefined,
|
|
4653
|
+
events: undefined,
|
|
4654
|
+
directives: undefined,
|
|
4655
|
+
};
|
|
4656
|
+
rateElement.setData(node, props, this._isMobile);
|
|
4657
|
+
const label = rateElement
|
|
4658
|
+
.closest('.form-field')
|
|
4659
|
+
?.querySelector('.field-label');
|
|
4660
|
+
label?.addEventListener('click', () => {
|
|
4661
|
+
rateElement.shadowRoot
|
|
4662
|
+
?.querySelector('.rate-wrapper')
|
|
4663
|
+
?.focus();
|
|
4664
|
+
});
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
_rateProps(field, error, disabled) {
|
|
4668
|
+
return {
|
|
4669
|
+
value: Number(this._values[field.name]) || 0,
|
|
4670
|
+
disabled,
|
|
4671
|
+
ariaLabel: field.label || 'Rating',
|
|
4672
|
+
style: {
|
|
4673
|
+
padding: '3px',
|
|
4674
|
+
border: '1px solid transparent',
|
|
4675
|
+
borderRadius: '6px',
|
|
4676
|
+
...(field.style ?? {}),
|
|
4677
|
+
...(error ? this._props.invalidStyle ?? {} : {}),
|
|
4678
|
+
},
|
|
4679
|
+
isExpressionResultStyle: this._props.isExpressionResultStyle,
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
_renderAffix(position, affix, id) {
|
|
4683
|
+
const rendered = renderInputAffix(affix);
|
|
4684
|
+
if (!rendered)
|
|
4685
|
+
return undefined;
|
|
4686
|
+
const style = this.buildInlineStyle(rendered.style, this._props.isExpressionResultStyle);
|
|
4687
|
+
const accessibility = rendered.ariaLabel
|
|
4688
|
+
? `id="${id}" aria-label="${this._escapeAttr(rendered.ariaLabel)}"`
|
|
4689
|
+
: 'aria-hidden="true"';
|
|
4690
|
+
return {
|
|
4691
|
+
html: `<span class="field-affix field-${position}" ${accessibility} style="${style}">${rendered.content}</span>`,
|
|
4692
|
+
descriptionId: rendered.ariaLabel ? id : undefined,
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
3751
4695
|
_bindEvents(fields, disabled) {
|
|
3752
4696
|
if (!this.shadowRoot || disabled)
|
|
3753
4697
|
return;
|
|
3754
|
-
// Text / textarea
|
|
4698
|
+
// Text / textarea
|
|
3755
4699
|
this.shadowRoot.querySelectorAll('.field-input').forEach((el) => {
|
|
3756
4700
|
const handler = () => {
|
|
3757
4701
|
const name = el.dataset.name;
|
|
3758
4702
|
this._values[name] = el.value;
|
|
3759
|
-
if (this.
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
}
|
|
4703
|
+
if (!this._submitted)
|
|
4704
|
+
return;
|
|
4705
|
+
const field = fields.find((item) => item.name === name);
|
|
4706
|
+
if (!field)
|
|
4707
|
+
return;
|
|
4708
|
+
this._updateFieldValidation(field, this._validateField(field));
|
|
3766
4709
|
};
|
|
3767
4710
|
el.addEventListener('input', handler);
|
|
3768
4711
|
el.addEventListener('change', handler);
|
|
3769
4712
|
});
|
|
4713
|
+
// Select
|
|
4714
|
+
this.shadowRoot.querySelectorAll('.form-select').forEach((selectElement) => {
|
|
4715
|
+
selectElement.addEventListener('change', (event) => {
|
|
4716
|
+
const name = selectElement.dataset.name;
|
|
4717
|
+
const value = event instanceof CustomEvent
|
|
4718
|
+
? event.detail?.value
|
|
4719
|
+
: undefined;
|
|
4720
|
+
this._values[name] = value ?? '';
|
|
4721
|
+
if (!this._submitted)
|
|
4722
|
+
return;
|
|
4723
|
+
const field = fields.find((item) => item.name === name);
|
|
4724
|
+
if (!field)
|
|
4725
|
+
return;
|
|
4726
|
+
this._updateFieldValidation(field, this._validateField(field));
|
|
4727
|
+
});
|
|
4728
|
+
});
|
|
4729
|
+
// Rate
|
|
4730
|
+
this.shadowRoot.querySelectorAll('.form-rate').forEach((rateElement) => {
|
|
4731
|
+
rateElement.addEventListener('change', (event) => {
|
|
4732
|
+
const name = rateElement.dataset.name;
|
|
4733
|
+
const value = event instanceof CustomEvent
|
|
4734
|
+
? event.detail?.value
|
|
4735
|
+
: undefined;
|
|
4736
|
+
this._values[name] = Number(value) || 0;
|
|
4737
|
+
if (!this._submitted)
|
|
4738
|
+
return;
|
|
4739
|
+
const field = fields.find((item) => item.name === name);
|
|
4740
|
+
if (!field)
|
|
4741
|
+
return;
|
|
4742
|
+
this._updateFieldValidation(field, this._validateField(field));
|
|
4743
|
+
});
|
|
4744
|
+
});
|
|
3770
4745
|
// Passcode
|
|
3771
4746
|
this.shadowRoot.querySelectorAll('.passcode-cell').forEach((cell) => {
|
|
3772
4747
|
const name = cell.dataset.name;
|
|
@@ -3776,45 +4751,35 @@ class CardForm extends BaseElement {
|
|
|
3776
4751
|
cell.addEventListener('input', () => {
|
|
3777
4752
|
const v = cell.value.replace(/[^0-9]/g, '');
|
|
3778
4753
|
cell.value = v.slice(0, 1);
|
|
3779
|
-
|
|
3780
|
-
this._values[name] = full;
|
|
4754
|
+
this._syncPasscodeValue(name, cells, fields);
|
|
3781
4755
|
if (v && idx < cells.length - 1)
|
|
3782
4756
|
cells[idx + 1].focus();
|
|
3783
4757
|
});
|
|
3784
4758
|
cell.addEventListener('keydown', (e) => {
|
|
3785
4759
|
if (e.key === 'Backspace' && !cell.value && idx > 0) {
|
|
4760
|
+
e.preventDefault();
|
|
3786
4761
|
cells[idx - 1].focus();
|
|
3787
4762
|
cells[idx - 1].value = '';
|
|
4763
|
+
this._syncPasscodeValue(name, cells, fields);
|
|
3788
4764
|
}
|
|
3789
4765
|
});
|
|
3790
4766
|
cell.addEventListener('paste', (e) => {
|
|
3791
4767
|
e.preventDefault();
|
|
3792
4768
|
const paste = (e.clipboardData?.getData('text') || '').replace(/[^0-9]/g, '');
|
|
4769
|
+
cells.forEach((item) => {
|
|
4770
|
+
item.value = '';
|
|
4771
|
+
});
|
|
3793
4772
|
for (let j = 0; j < cells.length && j < paste.length; j++)
|
|
3794
4773
|
cells[j].value = paste[j];
|
|
3795
|
-
this.
|
|
4774
|
+
this._syncPasscodeValue(name, cells, fields);
|
|
3796
4775
|
cells[Math.min(paste.length, cells.length - 1)].focus();
|
|
3797
4776
|
});
|
|
3798
4777
|
cell.addEventListener('focus', () => cell.select());
|
|
3799
4778
|
});
|
|
3800
|
-
// Rate
|
|
3801
|
-
this.shadowRoot.querySelectorAll('.rate-star').forEach((star) => {
|
|
3802
|
-
star.addEventListener('click', () => {
|
|
3803
|
-
const name = star.dataset.name;
|
|
3804
|
-
const val = Number(star.dataset.value);
|
|
3805
|
-
this._values[name] = this._values[name] === val ? 0 : val;
|
|
3806
|
-
// Re-color siblings
|
|
3807
|
-
const row = star.parentElement;
|
|
3808
|
-
row.querySelectorAll('.rate-star').forEach((s) => {
|
|
3809
|
-
const sv = Number(s.dataset.value);
|
|
3810
|
-
s.style.color = sv <= this._values[name] ? '#fadb14' : '#e8e8e8';
|
|
3811
|
-
});
|
|
3812
|
-
});
|
|
3813
|
-
});
|
|
3814
4779
|
// Submit
|
|
3815
4780
|
this.shadowRoot.querySelector('.submit-btn')?.addEventListener('click', () => {
|
|
4781
|
+
this._submitted = true;
|
|
3816
4782
|
if (this._validate(fields)) {
|
|
3817
|
-
this._submitted = true;
|
|
3818
4783
|
this.dispatchEvent(new CustomEvent('submit', {
|
|
3819
4784
|
bubbles: true,
|
|
3820
4785
|
composed: true,
|
|
@@ -3823,29 +4788,147 @@ class CardForm extends BaseElement {
|
|
|
3823
4788
|
}
|
|
3824
4789
|
});
|
|
3825
4790
|
}
|
|
4791
|
+
_syncPasscodeValue(name, cells, fields) {
|
|
4792
|
+
this._values[name] = Array.from(cells).map((item) => item.value).join('');
|
|
4793
|
+
if (!this._submitted)
|
|
4794
|
+
return;
|
|
4795
|
+
const field = fields.find((item) => item.name === name);
|
|
4796
|
+
if (field) {
|
|
4797
|
+
this._updateFieldValidation(field, this._validateField(field));
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
3826
4800
|
_validate(fields) {
|
|
3827
4801
|
this._errors = {};
|
|
3828
4802
|
let valid = true;
|
|
3829
4803
|
for (const f of fields) {
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
this._errors[f.name] = `${f.label || f.name}不能为空`;
|
|
3835
|
-
valid = false;
|
|
3836
|
-
}
|
|
3837
|
-
if (f.type === 'passcode' && typeof val === 'string' && val.length < (f.length || 6)) {
|
|
3838
|
-
this._errors[f.name] = `请输入完整的${f.label || '验证码'}`;
|
|
3839
|
-
valid = false;
|
|
3840
|
-
}
|
|
4804
|
+
const error = this._validateField(f);
|
|
4805
|
+
if (error) {
|
|
4806
|
+
this._errors[f.name] = error;
|
|
4807
|
+
valid = false;
|
|
3841
4808
|
}
|
|
3842
4809
|
}
|
|
3843
4810
|
if (!valid)
|
|
3844
4811
|
this.render();
|
|
3845
4812
|
return valid;
|
|
3846
4813
|
}
|
|
4814
|
+
_validateField(field) {
|
|
4815
|
+
const value = this._values[field.name];
|
|
4816
|
+
const rules = [...(field.rules ?? [])];
|
|
4817
|
+
if (field.required && !rules.some((rule) => rule.required)) {
|
|
4818
|
+
rules.unshift({ required: true });
|
|
4819
|
+
}
|
|
4820
|
+
const empty = value === undefined
|
|
4821
|
+
|| value === null
|
|
4822
|
+
|| value === 0
|
|
4823
|
+
|| (typeof value === 'string' && value.trim() === '');
|
|
4824
|
+
for (const rule of rules) {
|
|
4825
|
+
const message = this._ruleMessage(field, rule);
|
|
4826
|
+
if (rule.required && empty)
|
|
4827
|
+
return message;
|
|
4828
|
+
if (empty)
|
|
4829
|
+
continue;
|
|
4830
|
+
const text = String(value);
|
|
4831
|
+
if (rule.pattern != null) {
|
|
4832
|
+
if (text.length > MAX_PATTERN_INPUT_LENGTH
|
|
4833
|
+
|| !isSafeValidationPattern(rule.pattern)) {
|
|
4834
|
+
return message;
|
|
4835
|
+
}
|
|
4836
|
+
try {
|
|
4837
|
+
if (!new RegExp(rule.pattern).test(text))
|
|
4838
|
+
return message;
|
|
4839
|
+
}
|
|
4840
|
+
catch {
|
|
4841
|
+
return message;
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
if (rule.minLength != null && text.length < rule.minLength) {
|
|
4845
|
+
return message;
|
|
4846
|
+
}
|
|
4847
|
+
if (rule.maxLength != null && text.length > rule.maxLength) {
|
|
4848
|
+
return message;
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
const requiresValue = field.required || rules.some((rule) => rule.required);
|
|
4852
|
+
if (requiresValue
|
|
4853
|
+
&& field.type === 'passcode'
|
|
4854
|
+
&& typeof value === 'string'
|
|
4855
|
+
&& value.length < (field.length || 6)) {
|
|
4856
|
+
return `请输入完整的${field.label || '验证码'}`;
|
|
4857
|
+
}
|
|
4858
|
+
return '';
|
|
4859
|
+
}
|
|
4860
|
+
_ruleMessage(field, rule) {
|
|
4861
|
+
if (rule.message)
|
|
4862
|
+
return rule.message;
|
|
4863
|
+
const label = field.label || field.name;
|
|
4864
|
+
if (rule.required)
|
|
4865
|
+
return `${label}不能为空`;
|
|
4866
|
+
if (rule.pattern != null)
|
|
4867
|
+
return `${label}格式不正确`;
|
|
4868
|
+
if (rule.minLength != null) {
|
|
4869
|
+
return `${label}至少输入${rule.minLength}个字符`;
|
|
4870
|
+
}
|
|
4871
|
+
if (rule.maxLength != null) {
|
|
4872
|
+
return `${label}最多输入${rule.maxLength}个字符`;
|
|
4873
|
+
}
|
|
4874
|
+
return `${label}输入不正确`;
|
|
4875
|
+
}
|
|
4876
|
+
_updateFieldValidation(field, error) {
|
|
4877
|
+
if (!this.shadowRoot)
|
|
4878
|
+
return;
|
|
4879
|
+
this._errors[field.name] = error;
|
|
4880
|
+
const fieldElement = Array.from(this.shadowRoot.querySelectorAll('.form-field')).find((element) => {
|
|
4881
|
+
const control = element.querySelector('[data-name]');
|
|
4882
|
+
return control?.dataset.name === field.name;
|
|
4883
|
+
});
|
|
4884
|
+
if (!fieldElement)
|
|
4885
|
+
return;
|
|
4886
|
+
const errorElement = fieldElement.querySelector('.field-error');
|
|
4887
|
+
const fieldControl = fieldElement.querySelector('.field-control');
|
|
4888
|
+
const formSelect = fieldElement.querySelector('.form-select');
|
|
4889
|
+
const formRate = fieldElement.querySelector('.form-rate');
|
|
4890
|
+
const controls = fieldElement.querySelectorAll('.field-input, .form-select, .form-rate, .passcode-cell');
|
|
4891
|
+
errorElement.textContent = error;
|
|
4892
|
+
fieldControl?.classList.toggle('error', Boolean(error));
|
|
4893
|
+
if (formSelect) {
|
|
4894
|
+
formSelect.updateProps(this._selectProps(field, error, Boolean(this._props.disabled)), this._isMobile);
|
|
4895
|
+
}
|
|
4896
|
+
if (formRate) {
|
|
4897
|
+
formRate.updateProps(this._rateProps(field, error, Boolean(this._props.disabled)), this._isMobile);
|
|
4898
|
+
}
|
|
4899
|
+
controls.forEach((control) => {
|
|
4900
|
+
if (control.classList.contains('passcode-cell')) {
|
|
4901
|
+
control.classList.toggle('error', Boolean(error));
|
|
4902
|
+
}
|
|
4903
|
+
control.setAttribute('aria-invalid', String(Boolean(error)));
|
|
4904
|
+
const descriptionIds = [
|
|
4905
|
+
control.dataset.affixDescribedby,
|
|
4906
|
+
error ? errorElement.id : '',
|
|
4907
|
+
].filter(Boolean).join(' ');
|
|
4908
|
+
if (descriptionIds) {
|
|
4909
|
+
control.setAttribute('aria-describedby', descriptionIds);
|
|
4910
|
+
}
|
|
4911
|
+
else {
|
|
4912
|
+
control.removeAttribute('aria-describedby');
|
|
4913
|
+
}
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
_escapeHtml(value) {
|
|
4917
|
+
return String(value)
|
|
4918
|
+
.replace(/&/g, '&')
|
|
4919
|
+
.replace(/</g, '<')
|
|
4920
|
+
.replace(/>/g, '>');
|
|
4921
|
+
}
|
|
4922
|
+
_escapeAttr(value) {
|
|
4923
|
+
return this._escapeHtml(value).replace(/"/g, '"');
|
|
4924
|
+
}
|
|
4925
|
+
_domId(value) {
|
|
4926
|
+
const normalized = String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
4927
|
+
return normalized || 'form';
|
|
4928
|
+
}
|
|
3847
4929
|
updateProps(props, isMobile) {
|
|
3848
4930
|
this._errors = {};
|
|
4931
|
+
this._submitted = false;
|
|
3849
4932
|
super.updateProps(props, isMobile);
|
|
3850
4933
|
}
|
|
3851
4934
|
}
|
|
@@ -4150,6 +5233,7 @@ if (typeof customElements !== 'undefined' && !customElements.get(CardProgress.is
|
|
|
4150
5233
|
* { "title": "Final review", "status": "pending" }
|
|
4151
5234
|
* ],
|
|
4152
5235
|
* "iconSize": 28,
|
|
5236
|
+
* "textLayout": "vertical",
|
|
4153
5237
|
* "titleProps": { "fontSize": "14px", "fontWeight": 500, "color": "#1a1a1a" },
|
|
4154
5238
|
* "descriptionProps": { "fontSize": "12px", "color": "#8c8c8c" },
|
|
4155
5239
|
* "connector": { "width": 2, "minHeight": 24, "style": "solid", "color": "#e8e8e8", "completedColor": "#52c41a" }
|
|
@@ -4217,12 +5301,13 @@ class CardSteps extends BaseElement {
|
|
|
4217
5301
|
render() {
|
|
4218
5302
|
if (!this.shadowRoot || !this._node)
|
|
4219
5303
|
return;
|
|
4220
|
-
const { items = [], iconSize: iconSizeProp = 28, titleProps = {}, descriptionProps = {}, connector = {}, style,
|
|
5304
|
+
const { items = [], iconSize: iconSizeProp = 28, textLayout: textLayoutProp = 'vertical', titleProps = {}, descriptionProps = {}, connector = {}, style,
|
|
4221
5305
|
// Keep backward compat: old "size" prop
|
|
4222
5306
|
size, isExpressionResultStyle, } = this._props;
|
|
4223
5307
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
4224
5308
|
// iconSize: prefer iconSize prop, fallback to legacy "size" prop
|
|
4225
5309
|
const iconSize = Number(iconSizeProp != null ? iconSizeProp : size) || 28;
|
|
5310
|
+
const textLayout = textLayoutProp === 'horizontal' ? 'horizontal' : 'vertical';
|
|
4226
5311
|
// Runtime guard: items must be an array
|
|
4227
5312
|
const stepItems = Array.isArray(items) ? items : [];
|
|
4228
5313
|
if (!Array.isArray(items)) {
|
|
@@ -4284,7 +5369,7 @@ class CardSteps extends BaseElement {
|
|
|
4284
5369
|
<div class="step-item" data-status="${status}">
|
|
4285
5370
|
<div class="step-head${item.clickable ? ' step-clickable' : ''}" data-index="${index}">
|
|
4286
5371
|
<div class="step-icon"${iconStyle}>${iconHTML}</div>
|
|
4287
|
-
<div class="step-text">
|
|
5372
|
+
<div class="step-text step-text-${textLayout}">
|
|
4288
5373
|
<div class="step-title">${title}</div>
|
|
4289
5374
|
${descHTML}
|
|
4290
5375
|
</div>
|
|
@@ -4344,11 +5429,28 @@ class CardSteps extends BaseElement {
|
|
|
4344
5429
|
line-height: 1;
|
|
4345
5430
|
user-select: none;
|
|
4346
5431
|
}
|
|
4347
|
-
.step-text {
|
|
4348
|
-
display: flex;
|
|
4349
|
-
flex-direction: column;
|
|
4350
|
-
justify-content: center;
|
|
4351
|
-
|
|
5432
|
+
.step-text {
|
|
5433
|
+
display: flex;
|
|
5434
|
+
flex-direction: column;
|
|
5435
|
+
justify-content: center;
|
|
5436
|
+
flex: 1;
|
|
5437
|
+
min-width: 0;
|
|
5438
|
+
min-height: ${iconSize}px;
|
|
5439
|
+
}
|
|
5440
|
+
.step-text-horizontal {
|
|
5441
|
+
flex-direction: row;
|
|
5442
|
+
align-items: center;
|
|
5443
|
+
justify-content: space-between;
|
|
5444
|
+
gap: 12px;
|
|
5445
|
+
}
|
|
5446
|
+
.step-text-horizontal .step-title {
|
|
5447
|
+
min-width: 0;
|
|
5448
|
+
}
|
|
5449
|
+
.step-text-horizontal .step-description {
|
|
5450
|
+
flex-shrink: 0;
|
|
5451
|
+
margin-top: 0;
|
|
5452
|
+
text-align: right;
|
|
5453
|
+
white-space: nowrap;
|
|
4352
5454
|
}
|
|
4353
5455
|
.step-title {
|
|
4354
5456
|
font-size: ${titleFontSize};
|
|
@@ -5437,9 +6539,680 @@ class CardVideo extends BaseElement {
|
|
|
5437
6539
|
}
|
|
5438
6540
|
}
|
|
5439
6541
|
}
|
|
5440
|
-
CardVideo.is = 'ai-card-video';
|
|
5441
|
-
if (typeof customElements !== 'undefined' && !customElements.get(CardVideo.is)) {
|
|
5442
|
-
customElements.define(CardVideo.is, CardVideo);
|
|
6542
|
+
CardVideo.is = 'ai-card-video';
|
|
6543
|
+
if (typeof customElements !== 'undefined' && !customElements.get(CardVideo.is)) {
|
|
6544
|
+
customElements.define(CardVideo.is, CardVideo);
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
class CardSwitch extends BaseElement {
|
|
6548
|
+
constructor() {
|
|
6549
|
+
super(...arguments);
|
|
6550
|
+
this._currentChecked = null;
|
|
6551
|
+
}
|
|
6552
|
+
render() {
|
|
6553
|
+
if (!this.shadowRoot || !this._node)
|
|
6554
|
+
return;
|
|
6555
|
+
const { checked, defaultChecked = false, disabled = false, readonly: readOnly = false, size = 22, activeColor = '#626a78', inactiveColor = '#a9afb9', thumbColor = '#ffffff', ariaLabel = 'Switch', style, trackStyle, thumbStyle, isExpressionResultStyle, } = this._props;
|
|
6556
|
+
const initial = Boolean(checked ?? defaultChecked);
|
|
6557
|
+
const current = this._currentChecked ?? initial;
|
|
6558
|
+
const numericSize = Number(size);
|
|
6559
|
+
const trackHeight = Number.isFinite(numericSize) && numericSize > 0
|
|
6560
|
+
? numericSize
|
|
6561
|
+
: 22;
|
|
6562
|
+
const trackWidth = Math.round(trackHeight * 2);
|
|
6563
|
+
const thumbSize = Math.max(8, trackHeight - 4);
|
|
6564
|
+
const rootInline = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
6565
|
+
const trackInline = this.buildInlineStyle({
|
|
6566
|
+
width: trackWidth,
|
|
6567
|
+
height: trackHeight,
|
|
6568
|
+
...(trackStyle || {}),
|
|
6569
|
+
backgroundColor: current ? activeColor : inactiveColor,
|
|
6570
|
+
}, isExpressionResultStyle);
|
|
6571
|
+
const thumbInline = this.buildInlineStyle({
|
|
6572
|
+
width: thumbSize,
|
|
6573
|
+
height: thumbSize,
|
|
6574
|
+
...(thumbStyle || {}),
|
|
6575
|
+
backgroundColor: thumbColor,
|
|
6576
|
+
}, isExpressionResultStyle);
|
|
6577
|
+
this.shadowRoot.innerHTML = `
|
|
6578
|
+
<style>
|
|
6579
|
+
:host {
|
|
6580
|
+
display: inline-flex;
|
|
6581
|
+
box-sizing: border-box;
|
|
6582
|
+
vertical-align: middle;
|
|
6583
|
+
-webkit-tap-highlight-color: transparent;
|
|
6584
|
+
}
|
|
6585
|
+
.switch-root {
|
|
6586
|
+
display: inline-flex;
|
|
6587
|
+
box-sizing: border-box;
|
|
6588
|
+
}
|
|
6589
|
+
.switch-track {
|
|
6590
|
+
position: relative;
|
|
6591
|
+
display: inline-block;
|
|
6592
|
+
box-sizing: border-box;
|
|
6593
|
+
min-width: 28px;
|
|
6594
|
+
min-height: 16px;
|
|
6595
|
+
padding: 0;
|
|
6596
|
+
margin: 0;
|
|
6597
|
+
border: none;
|
|
6598
|
+
border-radius: 999px;
|
|
6599
|
+
cursor: pointer;
|
|
6600
|
+
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
|
6601
|
+
}
|
|
6602
|
+
.switch-track:focus-visible {
|
|
6603
|
+
outline: 2px solid #1677ff;
|
|
6604
|
+
outline-offset: 2px;
|
|
6605
|
+
}
|
|
6606
|
+
.switch-track:disabled {
|
|
6607
|
+
opacity: 0.45;
|
|
6608
|
+
cursor: not-allowed;
|
|
6609
|
+
}
|
|
6610
|
+
.switch-track[aria-readonly="true"] {
|
|
6611
|
+
cursor: default;
|
|
6612
|
+
}
|
|
6613
|
+
.switch-thumb {
|
|
6614
|
+
position: absolute;
|
|
6615
|
+
top: 50%;
|
|
6616
|
+
left: 2px;
|
|
6617
|
+
display: block;
|
|
6618
|
+
box-sizing: border-box;
|
|
6619
|
+
border-radius: 50%;
|
|
6620
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
|
6621
|
+
transform: translateY(-50%);
|
|
6622
|
+
transition: left 0.2s ease, transform 0.2s ease;
|
|
6623
|
+
pointer-events: none;
|
|
6624
|
+
}
|
|
6625
|
+
.switch-track.checked .switch-thumb {
|
|
6626
|
+
left: calc(100% - 2px);
|
|
6627
|
+
transform: translate(-100%, -50%);
|
|
6628
|
+
}
|
|
6629
|
+
</style>
|
|
6630
|
+
<div class="switch-root" style="${rootInline}">
|
|
6631
|
+
<button
|
|
6632
|
+
type="button"
|
|
6633
|
+
class="switch-track ${current ? 'checked' : ''}"
|
|
6634
|
+
part="track"
|
|
6635
|
+
role="switch"
|
|
6636
|
+
aria-checked="${current}"
|
|
6637
|
+
aria-label="${this.escapeAttr(String(ariaLabel))}"
|
|
6638
|
+
aria-readonly="${Boolean(readOnly)}"
|
|
6639
|
+
${disabled ? 'disabled' : ''}
|
|
6640
|
+
style="${trackInline}"
|
|
6641
|
+
>
|
|
6642
|
+
<span class="switch-thumb" part="thumb" style="${thumbInline}"></span>
|
|
6643
|
+
</button>
|
|
6644
|
+
</div>
|
|
6645
|
+
`;
|
|
6646
|
+
const button = this.shadowRoot.querySelector('.switch-track');
|
|
6647
|
+
button?.addEventListener('click', () => {
|
|
6648
|
+
if (disabled || readOnly)
|
|
6649
|
+
return;
|
|
6650
|
+
this.commit(!current, current, 'pointer');
|
|
6651
|
+
});
|
|
6652
|
+
button?.addEventListener('keydown', (event) => {
|
|
6653
|
+
if (disabled
|
|
6654
|
+
|| readOnly
|
|
6655
|
+
|| (event.key !== ' ' && event.key !== 'Enter')) {
|
|
6656
|
+
return;
|
|
6657
|
+
}
|
|
6658
|
+
event.preventDefault();
|
|
6659
|
+
this.commit(!current, current, 'keyboard');
|
|
6660
|
+
});
|
|
6661
|
+
}
|
|
6662
|
+
updateProps(props, isMobile) {
|
|
6663
|
+
this._currentChecked = null;
|
|
6664
|
+
super.updateProps(props, isMobile);
|
|
6665
|
+
}
|
|
6666
|
+
commit(next, previous, source) {
|
|
6667
|
+
if (next === previous)
|
|
6668
|
+
return;
|
|
6669
|
+
this._currentChecked = next;
|
|
6670
|
+
this.render();
|
|
6671
|
+
emitValueChange(this, next, previous, source, { checked: next });
|
|
6672
|
+
}
|
|
6673
|
+
escapeAttr(value) {
|
|
6674
|
+
return value
|
|
6675
|
+
.replace(/&/g, '&')
|
|
6676
|
+
.replace(/"/g, '"')
|
|
6677
|
+
.replace(/</g, '<')
|
|
6678
|
+
.replace(/>/g, '>');
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
CardSwitch.is = 'ai-card-switch';
|
|
6682
|
+
if (typeof customElements !== 'undefined' && !customElements.get(CardSwitch.is)) {
|
|
6683
|
+
customElements.define(CardSwitch.is, CardSwitch);
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6686
|
+
const INTERACTIVE_TAGS = new Set([
|
|
6687
|
+
'A',
|
|
6688
|
+
'AUDIO',
|
|
6689
|
+
'BUTTON',
|
|
6690
|
+
'INPUT',
|
|
6691
|
+
'SELECT',
|
|
6692
|
+
'TEXTAREA',
|
|
6693
|
+
'VIDEO',
|
|
6694
|
+
]);
|
|
6695
|
+
const INTERACTIVE_CARD_TYPES = new Set([
|
|
6696
|
+
'Button',
|
|
6697
|
+
'Counter',
|
|
6698
|
+
'DatePicker',
|
|
6699
|
+
'Input',
|
|
6700
|
+
'PasscodeInput',
|
|
6701
|
+
'Rate',
|
|
6702
|
+
'Select',
|
|
6703
|
+
'Switch',
|
|
6704
|
+
]);
|
|
6705
|
+
class CardChoiceItem extends BaseElement {
|
|
6706
|
+
constructor() {
|
|
6707
|
+
super(...arguments);
|
|
6708
|
+
this._context = {
|
|
6709
|
+
selectionMode: 'single',
|
|
6710
|
+
variant: 'plain',
|
|
6711
|
+
selected: false,
|
|
6712
|
+
listDisabled: false,
|
|
6713
|
+
listReadonly: false,
|
|
6714
|
+
selectionBlocked: false,
|
|
6715
|
+
tabIndex: 0,
|
|
6716
|
+
indicatorPosition: 'start',
|
|
6717
|
+
};
|
|
6718
|
+
}
|
|
6719
|
+
get value() {
|
|
6720
|
+
return String(this._props.value ?? '');
|
|
6721
|
+
}
|
|
6722
|
+
get itemDisabled() {
|
|
6723
|
+
return Boolean(this._props.disabled);
|
|
6724
|
+
}
|
|
6725
|
+
setSelectionContext(context) {
|
|
6726
|
+
this._context = context;
|
|
6727
|
+
this.render();
|
|
6728
|
+
}
|
|
6729
|
+
focusIndicator() {
|
|
6730
|
+
this.shadowRoot
|
|
6731
|
+
?.querySelector('.choice-indicator')
|
|
6732
|
+
?.focus();
|
|
6733
|
+
}
|
|
6734
|
+
render() {
|
|
6735
|
+
if (!this.shadowRoot || !this._node)
|
|
6736
|
+
return;
|
|
6737
|
+
const { disabled = false, ariaLabel = this.value || 'Choice', style, selectedStyle, indicatorStyle, selectedIndicatorStyle, checkedIcon, isExpressionResultStyle, } = this._props;
|
|
6738
|
+
const { selectionMode, variant, selected, listDisabled, listReadonly, selectionBlocked, tabIndex, indicatorPosition, itemStyle, selectedItemStyle, indicatorStyle: listIndicatorStyle, selectedIndicatorStyle: listSelectedIndicatorStyle, checkedIcon: listCheckedIcon, } = this._context;
|
|
6739
|
+
const fullyDisabled = Boolean(disabled || listDisabled);
|
|
6740
|
+
const selectionUnavailable = fullyDisabled || listReadonly;
|
|
6741
|
+
const rootInline = this.buildInlineStyle({
|
|
6742
|
+
...(itemStyle || {}),
|
|
6743
|
+
...(style || {}),
|
|
6744
|
+
...(selected ? selectedItemStyle || {} : {}),
|
|
6745
|
+
...(selected ? selectedStyle || {} : {}),
|
|
6746
|
+
}, isExpressionResultStyle);
|
|
6747
|
+
const indicatorInline = this.buildInlineStyle({
|
|
6748
|
+
...(listIndicatorStyle || {}),
|
|
6749
|
+
...(indicatorStyle || {}),
|
|
6750
|
+
...(selected ? listSelectedIndicatorStyle || {} : {}),
|
|
6751
|
+
...(selected ? selectedIndicatorStyle || {} : {}),
|
|
6752
|
+
}, isExpressionResultStyle);
|
|
6753
|
+
const indicatorRole = selectionMode === 'single' ? 'radio' : 'checkbox';
|
|
6754
|
+
const resolvedCheckedIcon = checkedIcon ?? listCheckedIcon ?? {
|
|
6755
|
+
name: 'check_bold',
|
|
6756
|
+
size: 12,
|
|
6757
|
+
};
|
|
6758
|
+
const checkedMark = selectionMode === 'multiple'
|
|
6759
|
+
? `<span class="choice-icon" aria-hidden="true">${renderIconContent(resolvedCheckedIcon)}</span>`
|
|
6760
|
+
: '<span class="radio-dot" aria-hidden="true"></span>';
|
|
6761
|
+
this.setAttribute('data-selected', String(selected));
|
|
6762
|
+
this.toggleAttribute('data-disabled', fullyDisabled);
|
|
6763
|
+
this.toggleAttribute('data-selection-blocked', selectionBlocked);
|
|
6764
|
+
this.shadowRoot.innerHTML = `
|
|
6765
|
+
<style>
|
|
6766
|
+
:host {
|
|
6767
|
+
display: block;
|
|
6768
|
+
box-sizing: border-box;
|
|
6769
|
+
}
|
|
6770
|
+
.choice-item {
|
|
6771
|
+
display: flex;
|
|
6772
|
+
align-items: center;
|
|
6773
|
+
gap: 12px;
|
|
6774
|
+
box-sizing: border-box;
|
|
6775
|
+
min-width: 0;
|
|
6776
|
+
padding: 6px 0;
|
|
6777
|
+
border: 1px solid transparent;
|
|
6778
|
+
border-radius: 8px;
|
|
6779
|
+
background: transparent;
|
|
6780
|
+
color: #343b4a;
|
|
6781
|
+
cursor: pointer;
|
|
6782
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease,
|
|
6783
|
+
background-color 0.15s ease;
|
|
6784
|
+
}
|
|
6785
|
+
.choice-item.outlined {
|
|
6786
|
+
padding: 10px 12px;
|
|
6787
|
+
border-color: #d9dce2;
|
|
6788
|
+
background: #fff;
|
|
6789
|
+
}
|
|
6790
|
+
.choice-item.outlined.selected {
|
|
6791
|
+
border-color: #626a78;
|
|
6792
|
+
background: #f6f7f9;
|
|
6793
|
+
}
|
|
6794
|
+
.choice-item.disabled {
|
|
6795
|
+
opacity: 0.45;
|
|
6796
|
+
cursor: not-allowed;
|
|
6797
|
+
}
|
|
6798
|
+
.choice-item.readonly {
|
|
6799
|
+
cursor: default;
|
|
6800
|
+
}
|
|
6801
|
+
.choice-indicator {
|
|
6802
|
+
width: 20px;
|
|
6803
|
+
height: 20px;
|
|
6804
|
+
min-width: 20px;
|
|
6805
|
+
display: inline-flex;
|
|
6806
|
+
align-items: center;
|
|
6807
|
+
justify-content: center;
|
|
6808
|
+
box-sizing: border-box;
|
|
6809
|
+
padding: 0;
|
|
6810
|
+
border: 2px solid #c9cdd4;
|
|
6811
|
+
border-radius: ${selectionMode === 'single' ? '50%' : '5px'};
|
|
6812
|
+
background: #fff;
|
|
6813
|
+
color: #fff;
|
|
6814
|
+
cursor: pointer;
|
|
6815
|
+
}
|
|
6816
|
+
.choice-indicator.checked {
|
|
6817
|
+
border-color: #626a78;
|
|
6818
|
+
background: #626a78;
|
|
6819
|
+
}
|
|
6820
|
+
.choice-indicator[aria-disabled="true"] {
|
|
6821
|
+
cursor: not-allowed;
|
|
6822
|
+
}
|
|
6823
|
+
.choice-indicator:focus-visible {
|
|
6824
|
+
outline: 2px solid #1677ff;
|
|
6825
|
+
outline-offset: 2px;
|
|
6826
|
+
}
|
|
6827
|
+
.radio-dot {
|
|
6828
|
+
width: 8px;
|
|
6829
|
+
height: 8px;
|
|
6830
|
+
border-radius: 50%;
|
|
6831
|
+
background: currentColor;
|
|
6832
|
+
}
|
|
6833
|
+
.choice-icon,
|
|
6834
|
+
.choice-icon svg,
|
|
6835
|
+
.choice-icon img {
|
|
6836
|
+
display: block;
|
|
6837
|
+
flex: none;
|
|
6838
|
+
}
|
|
6839
|
+
.choice-content {
|
|
6840
|
+
flex: 1;
|
|
6841
|
+
min-width: 0;
|
|
6842
|
+
}
|
|
6843
|
+
</style>
|
|
6844
|
+
<div
|
|
6845
|
+
class="choice-item ${variant} ${selected ? 'selected' : ''} ${fullyDisabled ? 'disabled' : ''} ${listReadonly ? 'readonly' : ''}"
|
|
6846
|
+
part="item"
|
|
6847
|
+
style="${rootInline}"
|
|
6848
|
+
>
|
|
6849
|
+
${indicatorPosition === 'start' ? this.renderIndicator(indicatorRole, selected, selectionUnavailable, tabIndex, ariaLabel, indicatorInline, checkedMark) : ''}
|
|
6850
|
+
<div class="choice-content" part="content" ${fullyDisabled ? 'inert' : ''}>
|
|
6851
|
+
<slot></slot>
|
|
6852
|
+
</div>
|
|
6853
|
+
${indicatorPosition === 'end' ? this.renderIndicator(indicatorRole, selected, selectionUnavailable, tabIndex, ariaLabel, indicatorInline, checkedMark) : ''}
|
|
6854
|
+
</div>
|
|
6855
|
+
`;
|
|
6856
|
+
const root = this.shadowRoot.querySelector('.choice-item');
|
|
6857
|
+
const indicator = this.shadowRoot.querySelector('.choice-indicator');
|
|
6858
|
+
indicator?.addEventListener('click', (event) => {
|
|
6859
|
+
event.stopPropagation();
|
|
6860
|
+
this.requestChange('pointer');
|
|
6861
|
+
});
|
|
6862
|
+
indicator?.addEventListener('keydown', (event) => {
|
|
6863
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
6864
|
+
event.preventDefault();
|
|
6865
|
+
event.stopPropagation();
|
|
6866
|
+
this.requestChange('keyboard');
|
|
6867
|
+
return;
|
|
6868
|
+
}
|
|
6869
|
+
if (event.key === 'ArrowDown'
|
|
6870
|
+
|| event.key === 'ArrowRight'
|
|
6871
|
+
|| event.key === 'ArrowUp'
|
|
6872
|
+
|| event.key === 'ArrowLeft'
|
|
6873
|
+
|| event.key === 'Home'
|
|
6874
|
+
|| event.key === 'End') {
|
|
6875
|
+
event.preventDefault();
|
|
6876
|
+
event.stopPropagation();
|
|
6877
|
+
this.dispatchEvent(new CustomEvent('choice-navigate', {
|
|
6878
|
+
bubbles: true,
|
|
6879
|
+
composed: true,
|
|
6880
|
+
detail: {
|
|
6881
|
+
value: this.value,
|
|
6882
|
+
key: event.key,
|
|
6883
|
+
},
|
|
6884
|
+
}));
|
|
6885
|
+
}
|
|
6886
|
+
});
|
|
6887
|
+
root?.addEventListener('click', (event) => {
|
|
6888
|
+
if (this.hasInteractiveTarget(event))
|
|
6889
|
+
return;
|
|
6890
|
+
this.requestChange('pointer');
|
|
6891
|
+
});
|
|
6892
|
+
}
|
|
6893
|
+
renderIndicator(role, selected, unavailable, tabIndex, ariaLabel, inlineStyle, mark) {
|
|
6894
|
+
return `
|
|
6895
|
+
<button
|
|
6896
|
+
type="button"
|
|
6897
|
+
class="choice-indicator ${selected ? 'checked' : ''}"
|
|
6898
|
+
part="indicator"
|
|
6899
|
+
role="${role}"
|
|
6900
|
+
aria-checked="${selected}"
|
|
6901
|
+
aria-disabled="${unavailable}"
|
|
6902
|
+
tabindex="${unavailable ? -1 : tabIndex}"
|
|
6903
|
+
${unavailable ? 'disabled' : ''}
|
|
6904
|
+
aria-label="${this.escapeAttr(String(ariaLabel))}"
|
|
6905
|
+
style="${inlineStyle}"
|
|
6906
|
+
>${selected ? mark : ''}</button>
|
|
6907
|
+
`;
|
|
6908
|
+
}
|
|
6909
|
+
requestChange(source) {
|
|
6910
|
+
if (this.itemDisabled
|
|
6911
|
+
|| this._context.listDisabled
|
|
6912
|
+
|| this._context.listReadonly) {
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
this.dispatchEvent(new CustomEvent('choice-request-change', {
|
|
6916
|
+
bubbles: true,
|
|
6917
|
+
composed: true,
|
|
6918
|
+
detail: {
|
|
6919
|
+
value: this.value,
|
|
6920
|
+
source,
|
|
6921
|
+
},
|
|
6922
|
+
}));
|
|
6923
|
+
}
|
|
6924
|
+
hasInteractiveTarget(event) {
|
|
6925
|
+
for (const target of event.composedPath()) {
|
|
6926
|
+
if (target === this)
|
|
6927
|
+
break;
|
|
6928
|
+
if (!(target instanceof HTMLElement))
|
|
6929
|
+
continue;
|
|
6930
|
+
if (INTERACTIVE_TAGS.has(target.tagName))
|
|
6931
|
+
return true;
|
|
6932
|
+
if (target.isContentEditable)
|
|
6933
|
+
return true;
|
|
6934
|
+
const cardType = target.getAttribute('data-card-type');
|
|
6935
|
+
if (cardType && INTERACTIVE_CARD_TYPES.has(cardType))
|
|
6936
|
+
return true;
|
|
6937
|
+
}
|
|
6938
|
+
return false;
|
|
6939
|
+
}
|
|
6940
|
+
escapeAttr(value) {
|
|
6941
|
+
return value
|
|
6942
|
+
.replace(/&/g, '&')
|
|
6943
|
+
.replace(/"/g, '"')
|
|
6944
|
+
.replace(/</g, '<')
|
|
6945
|
+
.replace(/>/g, '>');
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
CardChoiceItem.is = 'ai-card-choice-item';
|
|
6949
|
+
if (typeof customElements !== 'undefined'
|
|
6950
|
+
&& !customElements.get(CardChoiceItem.is)) {
|
|
6951
|
+
customElements.define(CardChoiceItem.is, CardChoiceItem);
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
class CardChoiceList extends BaseElement {
|
|
6955
|
+
constructor() {
|
|
6956
|
+
super(...arguments);
|
|
6957
|
+
this._onChoiceRequest = (event) => {
|
|
6958
|
+
const sourceItem = event.target;
|
|
6959
|
+
if (!(sourceItem instanceof CardChoiceItem)
|
|
6960
|
+
|| sourceItem.parentElement !== this) {
|
|
6961
|
+
return;
|
|
6962
|
+
}
|
|
6963
|
+
const customEvent = event;
|
|
6964
|
+
event.stopPropagation();
|
|
6965
|
+
this.commitRequest(customEvent.detail.value, customEvent.detail.source);
|
|
6966
|
+
};
|
|
6967
|
+
this._onChoiceNavigate = (event) => {
|
|
6968
|
+
const sourceItem = event.target;
|
|
6969
|
+
if (!(sourceItem instanceof CardChoiceItem)
|
|
6970
|
+
|| sourceItem.parentElement !== this) {
|
|
6971
|
+
return;
|
|
6972
|
+
}
|
|
6973
|
+
const customEvent = event;
|
|
6974
|
+
event.stopPropagation();
|
|
6975
|
+
this.navigateFrom(customEvent.detail.value, customEvent.detail.key);
|
|
6976
|
+
};
|
|
6977
|
+
}
|
|
6978
|
+
render() {
|
|
6979
|
+
if (!this.shadowRoot || !this._node)
|
|
6980
|
+
return;
|
|
6981
|
+
const { selectionMode = 'single', value, defaultValue, disabled = false, readonly: readOnly = false, direction = 'vertical', gap = 8, ariaLabel = 'Choices', style, isExpressionResultStyle, } = this._props;
|
|
6982
|
+
const mode = selectionMode === 'multiple' ? 'multiple' : 'single';
|
|
6983
|
+
if (this._currentValue === undefined) {
|
|
6984
|
+
this._currentValue = this.normalizeValue(value !== undefined ? value : defaultValue, mode);
|
|
6985
|
+
}
|
|
6986
|
+
const inlineStyle = this.buildInlineStyle({
|
|
6987
|
+
display: 'flex',
|
|
6988
|
+
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
|
6989
|
+
gap,
|
|
6990
|
+
...(style || {}),
|
|
6991
|
+
}, isExpressionResultStyle);
|
|
6992
|
+
this.removeEventListener('choice-request-change', this._onChoiceRequest);
|
|
6993
|
+
this.addEventListener('choice-request-change', this._onChoiceRequest);
|
|
6994
|
+
this.removeEventListener('choice-navigate', this._onChoiceNavigate);
|
|
6995
|
+
this.addEventListener('choice-navigate', this._onChoiceNavigate);
|
|
6996
|
+
this.shadowRoot.innerHTML = `
|
|
6997
|
+
<style>
|
|
6998
|
+
:host {
|
|
6999
|
+
display: block;
|
|
7000
|
+
box-sizing: border-box;
|
|
7001
|
+
min-width: 0;
|
|
7002
|
+
}
|
|
7003
|
+
.choice-list {
|
|
7004
|
+
box-sizing: border-box;
|
|
7005
|
+
min-width: 0;
|
|
7006
|
+
}
|
|
7007
|
+
</style>
|
|
7008
|
+
<div
|
|
7009
|
+
class="choice-list"
|
|
7010
|
+
part="list"
|
|
7011
|
+
role="${mode === 'single' ? 'radiogroup' : 'group'}"
|
|
7012
|
+
aria-label="${this.escapeAttr(String(ariaLabel))}"
|
|
7013
|
+
aria-disabled="${Boolean(disabled)}"
|
|
7014
|
+
aria-readonly="${Boolean(readOnly)}"
|
|
7015
|
+
style="${inlineStyle}"
|
|
7016
|
+
>
|
|
7017
|
+
<slot></slot>
|
|
7018
|
+
</div>
|
|
7019
|
+
`;
|
|
7020
|
+
this.shadowRoot.querySelector('slot')?.addEventListener('slotchange', () => this.syncItems());
|
|
7021
|
+
queueMicrotask(() => this.syncItems());
|
|
7022
|
+
}
|
|
7023
|
+
updateProps(props, isMobile) {
|
|
7024
|
+
this._currentValue = undefined;
|
|
7025
|
+
super.updateProps(props, isMobile);
|
|
7026
|
+
}
|
|
7027
|
+
commitRequest(value, source) {
|
|
7028
|
+
if (this._props.disabled || this._props.readonly)
|
|
7029
|
+
return;
|
|
7030
|
+
const mode = this.mode;
|
|
7031
|
+
const previousValue = this.cloneValue(this.currentValue);
|
|
7032
|
+
let nextValue;
|
|
7033
|
+
let checked;
|
|
7034
|
+
if (mode === 'multiple') {
|
|
7035
|
+
const current = this.values;
|
|
7036
|
+
const index = current.indexOf(value);
|
|
7037
|
+
if (index >= 0) {
|
|
7038
|
+
nextValue = current.filter((entry) => entry !== value);
|
|
7039
|
+
checked = false;
|
|
7040
|
+
}
|
|
7041
|
+
else {
|
|
7042
|
+
const maxSelected = this.maxSelected;
|
|
7043
|
+
if (maxSelected != null && current.length >= maxSelected) {
|
|
7044
|
+
this.dispatchEvent(new CustomEvent('max-reached', {
|
|
7045
|
+
bubbles: true,
|
|
7046
|
+
composed: true,
|
|
7047
|
+
detail: {
|
|
7048
|
+
maxSelected,
|
|
7049
|
+
attemptedValue: value,
|
|
7050
|
+
values: current,
|
|
7051
|
+
},
|
|
7052
|
+
}));
|
|
7053
|
+
return;
|
|
7054
|
+
}
|
|
7055
|
+
nextValue = [...current, value];
|
|
7056
|
+
checked = true;
|
|
7057
|
+
}
|
|
7058
|
+
}
|
|
7059
|
+
else {
|
|
7060
|
+
const current = typeof this.currentValue === 'string'
|
|
7061
|
+
? this.currentValue
|
|
7062
|
+
: null;
|
|
7063
|
+
if (current === value) {
|
|
7064
|
+
if (!this._props.allowClear)
|
|
7065
|
+
return;
|
|
7066
|
+
nextValue = null;
|
|
7067
|
+
checked = false;
|
|
7068
|
+
}
|
|
7069
|
+
else {
|
|
7070
|
+
nextValue = value;
|
|
7071
|
+
checked = true;
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
this._currentValue = nextValue;
|
|
7075
|
+
this.syncItems();
|
|
7076
|
+
const values = this.values;
|
|
7077
|
+
emitValueChange(this, nextValue, previousValue, source, {
|
|
7078
|
+
values,
|
|
7079
|
+
changedValue: value,
|
|
7080
|
+
checked,
|
|
7081
|
+
selectionMode: mode,
|
|
7082
|
+
});
|
|
7083
|
+
}
|
|
7084
|
+
syncItems() {
|
|
7085
|
+
if (!this._node)
|
|
7086
|
+
return;
|
|
7087
|
+
const seen = new Set();
|
|
7088
|
+
const selected = new Set(this.values);
|
|
7089
|
+
const maxSelected = this.maxSelected;
|
|
7090
|
+
const maxReached = (this.mode === 'multiple'
|
|
7091
|
+
&& maxSelected != null
|
|
7092
|
+
&& selected.size >= maxSelected);
|
|
7093
|
+
const { disabled = false, readonly: readOnly = false, variant = 'plain', indicatorPosition = 'start', itemStyle, selectedItemStyle, indicatorStyle, selectedIndicatorStyle, checkedIcon, } = this._props;
|
|
7094
|
+
const items = Array.from(this.children).filter((child) => child instanceof CardChoiceItem);
|
|
7095
|
+
const selectedFocusable = items.find((item) => selected.has(item.value) && !item.itemDisabled);
|
|
7096
|
+
const firstFocusable = items.find((item) => !item.itemDisabled);
|
|
7097
|
+
const tabStop = selectedFocusable ?? firstFocusable;
|
|
7098
|
+
for (const child of items) {
|
|
7099
|
+
if (!(child instanceof CardChoiceItem))
|
|
7100
|
+
continue;
|
|
7101
|
+
const duplicate = seen.has(child.value);
|
|
7102
|
+
seen.add(child.value);
|
|
7103
|
+
const isSelected = selected.has(child.value);
|
|
7104
|
+
const context = {
|
|
7105
|
+
selectionMode: this.mode,
|
|
7106
|
+
variant: variant === 'outlined' ? 'outlined' : 'plain',
|
|
7107
|
+
selected: isSelected,
|
|
7108
|
+
listDisabled: Boolean(disabled || duplicate),
|
|
7109
|
+
listReadonly: Boolean(readOnly),
|
|
7110
|
+
selectionBlocked: Boolean(maxReached && !isSelected),
|
|
7111
|
+
tabIndex: (!disabled
|
|
7112
|
+
&& !readOnly
|
|
7113
|
+
&& !duplicate
|
|
7114
|
+
&& child === tabStop) ? 0 : -1,
|
|
7115
|
+
indicatorPosition: indicatorPosition === 'end' ? 'end' : 'start',
|
|
7116
|
+
itemStyle,
|
|
7117
|
+
selectedItemStyle,
|
|
7118
|
+
indicatorStyle,
|
|
7119
|
+
selectedIndicatorStyle,
|
|
7120
|
+
checkedIcon,
|
|
7121
|
+
};
|
|
7122
|
+
child.setSelectionContext(context);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
navigateFrom(value, key) {
|
|
7126
|
+
if (this._props.disabled || this._props.readonly)
|
|
7127
|
+
return;
|
|
7128
|
+
const seen = new Set();
|
|
7129
|
+
const items = Array.from(this.children).filter((child) => {
|
|
7130
|
+
if (!(child instanceof CardChoiceItem) || child.itemDisabled) {
|
|
7131
|
+
return false;
|
|
7132
|
+
}
|
|
7133
|
+
if (seen.has(child.value))
|
|
7134
|
+
return false;
|
|
7135
|
+
seen.add(child.value);
|
|
7136
|
+
return true;
|
|
7137
|
+
});
|
|
7138
|
+
if (items.length === 0)
|
|
7139
|
+
return;
|
|
7140
|
+
const currentIndex = Math.max(0, items.findIndex((item) => item.value === value));
|
|
7141
|
+
let nextIndex = currentIndex;
|
|
7142
|
+
if (key === 'Home') {
|
|
7143
|
+
nextIndex = 0;
|
|
7144
|
+
}
|
|
7145
|
+
else if (key === 'End') {
|
|
7146
|
+
nextIndex = items.length - 1;
|
|
7147
|
+
}
|
|
7148
|
+
else if (key === 'ArrowDown' || key === 'ArrowRight') {
|
|
7149
|
+
nextIndex = (currentIndex + 1) % items.length;
|
|
7150
|
+
}
|
|
7151
|
+
else if (key === 'ArrowUp' || key === 'ArrowLeft') {
|
|
7152
|
+
nextIndex = (currentIndex - 1 + items.length) % items.length;
|
|
7153
|
+
}
|
|
7154
|
+
const target = items[nextIndex];
|
|
7155
|
+
if (!target || target.value === value) {
|
|
7156
|
+
target?.focusIndicator();
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
if (this.mode === 'single') {
|
|
7160
|
+
this.commitRequest(target.value, 'keyboard');
|
|
7161
|
+
}
|
|
7162
|
+
queueMicrotask(() => target.focusIndicator());
|
|
7163
|
+
}
|
|
7164
|
+
get mode() {
|
|
7165
|
+
return this._props.selectionMode === 'multiple' ? 'multiple' : 'single';
|
|
7166
|
+
}
|
|
7167
|
+
get currentValue() {
|
|
7168
|
+
if (this._currentValue === undefined) {
|
|
7169
|
+
this._currentValue = this.normalizeValue(this._props.value !== undefined
|
|
7170
|
+
? this._props.value
|
|
7171
|
+
: this._props.defaultValue, this.mode);
|
|
7172
|
+
}
|
|
7173
|
+
return this._currentValue;
|
|
7174
|
+
}
|
|
7175
|
+
get values() {
|
|
7176
|
+
const value = this.currentValue;
|
|
7177
|
+
if (Array.isArray(value))
|
|
7178
|
+
return [...value];
|
|
7179
|
+
return typeof value === 'string' && value ? [value] : [];
|
|
7180
|
+
}
|
|
7181
|
+
get maxSelected() {
|
|
7182
|
+
if (this.mode !== 'multiple')
|
|
7183
|
+
return null;
|
|
7184
|
+
const value = Number(this._props.maxSelected);
|
|
7185
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
7186
|
+
}
|
|
7187
|
+
normalizeValue(input, mode) {
|
|
7188
|
+
if (mode === 'multiple') {
|
|
7189
|
+
const source = Array.isArray(input)
|
|
7190
|
+
? input
|
|
7191
|
+
: input == null || input === ''
|
|
7192
|
+
? []
|
|
7193
|
+
: [input];
|
|
7194
|
+
return Array.from(new Set(source.map((entry) => String(entry))));
|
|
7195
|
+
}
|
|
7196
|
+
if (Array.isArray(input)) {
|
|
7197
|
+
return input.length > 0 ? String(input[0]) : null;
|
|
7198
|
+
}
|
|
7199
|
+
return input == null || input === '' ? null : String(input);
|
|
7200
|
+
}
|
|
7201
|
+
cloneValue(value) {
|
|
7202
|
+
return Array.isArray(value) ? [...value] : value;
|
|
7203
|
+
}
|
|
7204
|
+
escapeAttr(value) {
|
|
7205
|
+
return value
|
|
7206
|
+
.replace(/&/g, '&')
|
|
7207
|
+
.replace(/"/g, '"')
|
|
7208
|
+
.replace(/</g, '<')
|
|
7209
|
+
.replace(/>/g, '>');
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
CardChoiceList.is = 'ai-card-choice-list';
|
|
7213
|
+
if (typeof customElements !== 'undefined'
|
|
7214
|
+
&& !customElements.get(CardChoiceList.is)) {
|
|
7215
|
+
customElements.define(CardChoiceList.is, CardChoiceList);
|
|
5443
7216
|
}
|
|
5444
7217
|
|
|
5445
7218
|
/**
|
|
@@ -5591,6 +7364,24 @@ function renderVideo(node, props, isMobile) {
|
|
|
5591
7364
|
el.setData(node, props, isMobile);
|
|
5592
7365
|
return el;
|
|
5593
7366
|
}
|
|
7367
|
+
/** Render a Switch node. */
|
|
7368
|
+
function renderSwitch(node, props, isMobile) {
|
|
7369
|
+
const el = document.createElement(CardSwitch.is);
|
|
7370
|
+
el.setData(node, props, isMobile);
|
|
7371
|
+
return el;
|
|
7372
|
+
}
|
|
7373
|
+
/** Render a ChoiceList node. */
|
|
7374
|
+
function renderChoiceList(node, props, isMobile) {
|
|
7375
|
+
const el = document.createElement(CardChoiceList.is);
|
|
7376
|
+
el.setData(node, props, isMobile);
|
|
7377
|
+
return el;
|
|
7378
|
+
}
|
|
7379
|
+
/** Render a ChoiceItem node. */
|
|
7380
|
+
function renderChoiceItem(node, props, isMobile) {
|
|
7381
|
+
const el = document.createElement(CardChoiceItem.is);
|
|
7382
|
+
el.setData(node, props, isMobile);
|
|
7383
|
+
return el;
|
|
7384
|
+
}
|
|
5594
7385
|
/**
|
|
5595
7386
|
* Fallback renderer for unknown / container component types.
|
|
5596
7387
|
* Uses a plain `<div>` (no Shadow DOM) so that child elements
|
|
@@ -5634,6 +7425,9 @@ const componentRenderers = {
|
|
|
5634
7425
|
DatePicker: renderDatePicker,
|
|
5635
7426
|
Audio: renderAudio,
|
|
5636
7427
|
Video: renderVideo,
|
|
7428
|
+
Switch: renderSwitch,
|
|
7429
|
+
ChoiceList: renderChoiceList,
|
|
7430
|
+
ChoiceItem: renderChoiceItem,
|
|
5637
7431
|
_default: renderDefault,
|
|
5638
7432
|
};
|
|
5639
7433
|
/**
|
|
@@ -5661,6 +7455,438 @@ function resolveSizeInStyle(style) {
|
|
|
5661
7455
|
return resolved;
|
|
5662
7456
|
}
|
|
5663
7457
|
|
|
7458
|
+
/**
|
|
7459
|
+
* Render the scoped/materialized branch of a card.
|
|
7460
|
+
*
|
|
7461
|
+
* This implementation deliberately owns its state independently from the
|
|
7462
|
+
* legacy static renderer. Bound writes happen in isolated drafts and publish
|
|
7463
|
+
* only after materialization and detached DOM construction both succeed.
|
|
7464
|
+
*/
|
|
7465
|
+
function renderBoundCard(container, schema, options) {
|
|
7466
|
+
const variables = cloneJsonData({
|
|
7467
|
+
...schema.variables,
|
|
7468
|
+
...options.variables,
|
|
7469
|
+
});
|
|
7470
|
+
const schemaActions = schema.actions ?? {};
|
|
7471
|
+
const lifecycleManager = createLifecycleManager();
|
|
7472
|
+
const abortController = new AbortController();
|
|
7473
|
+
const inflightRequests = new Map();
|
|
7474
|
+
const activeLifecycleNodes = new Map();
|
|
7475
|
+
let currentMaterialized;
|
|
7476
|
+
let revision = 0;
|
|
7477
|
+
let disposed = false;
|
|
7478
|
+
let isMobile = options.isMobile ?? isMobileViewport();
|
|
7479
|
+
let actionQueue = Promise.resolve();
|
|
7480
|
+
let lifecycleQueue = Promise.resolve();
|
|
7481
|
+
const removeViewportListener = options.isMobile == null
|
|
7482
|
+
? onViewportChange((mobile) => {
|
|
7483
|
+
if (disposed)
|
|
7484
|
+
return;
|
|
7485
|
+
isMobile = mobile;
|
|
7486
|
+
const candidate = prepareCandidate(variables);
|
|
7487
|
+
publishDOM(candidate, false);
|
|
7488
|
+
})
|
|
7489
|
+
: () => { };
|
|
7490
|
+
function expressionContextFor(node) {
|
|
7491
|
+
return isBoundRenderTreeNode(node)
|
|
7492
|
+
? createExpressionContext(node.scope)
|
|
7493
|
+
: variables;
|
|
7494
|
+
}
|
|
7495
|
+
function resolveNodeValue(value, node, renderVariables) {
|
|
7496
|
+
if (node.bindingDialect === 'a2ui') {
|
|
7497
|
+
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
7498
|
+
}
|
|
7499
|
+
if (typeof value === 'string' && hasExpression(value)) {
|
|
7500
|
+
return resolveExpression(value, expressionContextFor(node));
|
|
7501
|
+
}
|
|
7502
|
+
return value;
|
|
7503
|
+
}
|
|
7504
|
+
function resolveNodeProps(node, renderVariables) {
|
|
7505
|
+
const resolvedProps = (node.bindingDialect === 'a2ui'
|
|
7506
|
+
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
7507
|
+
: resolveDeep(node.props, expressionContextFor(node)));
|
|
7508
|
+
if (resolvedProps.content
|
|
7509
|
+
&& typeof resolvedProps.content === 'object'
|
|
7510
|
+
&& 'type' in resolvedProps.content) {
|
|
7511
|
+
resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContextFor(node));
|
|
7512
|
+
}
|
|
7513
|
+
return resolvedProps;
|
|
7514
|
+
}
|
|
7515
|
+
function createPassiveActionContext(renderVariables) {
|
|
7516
|
+
return {
|
|
7517
|
+
...createWebActionContext({
|
|
7518
|
+
...options,
|
|
7519
|
+
abortSignal: abortController.signal,
|
|
7520
|
+
}),
|
|
7521
|
+
variables: renderVariables,
|
|
7522
|
+
botId: options.botId,
|
|
7523
|
+
inflightRequests,
|
|
7524
|
+
};
|
|
7525
|
+
}
|
|
7526
|
+
function renderNode(node, renderVariables, lifecycleIds) {
|
|
7527
|
+
if (node.directives?.visible) {
|
|
7528
|
+
const resolved = resolveNodeValue(node.directives.visible, node, renderVariables);
|
|
7529
|
+
if (resolved === false
|
|
7530
|
+
|| resolved === 'false'
|
|
7531
|
+
|| resolved === ''
|
|
7532
|
+
|| resolved === 0) {
|
|
7533
|
+
const placeholder = document.createElement('div');
|
|
7534
|
+
placeholder.style.display = 'none';
|
|
7535
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
7536
|
+
return placeholder;
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
const resolvedProps = resolveNodeProps(node, renderVariables);
|
|
7540
|
+
let isDisabled = false;
|
|
7541
|
+
if (node.directives?.disabled) {
|
|
7542
|
+
const resolved = resolveNodeValue(node.directives.disabled, node, renderVariables);
|
|
7543
|
+
isDisabled = (resolved === true
|
|
7544
|
+
|| resolved === 'true'
|
|
7545
|
+
|| resolved === 1);
|
|
7546
|
+
}
|
|
7547
|
+
const renderer = (componentRenderers[node.type] ?? componentRenderers._default);
|
|
7548
|
+
const el = renderer(node, resolvedProps, isMobile);
|
|
7549
|
+
if (isDisabled) {
|
|
7550
|
+
el.setAttribute('data-disabled', 'true');
|
|
7551
|
+
el.style.background = '#F5F5F5';
|
|
7552
|
+
el.style.color = '#C0C0C0';
|
|
7553
|
+
el.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
7554
|
+
el.style.pointerEvents = 'none';
|
|
7555
|
+
el.style.cursor = 'default';
|
|
7556
|
+
}
|
|
7557
|
+
const variableKey = resolvedProps.variableKey;
|
|
7558
|
+
if (variableKey) {
|
|
7559
|
+
el.addEventListener('input', ((event) => {
|
|
7560
|
+
if (event.target !== el || disposed)
|
|
7561
|
+
return;
|
|
7562
|
+
const value = event.detail?.value
|
|
7563
|
+
?? event.target?.value;
|
|
7564
|
+
if (value !== undefined) {
|
|
7565
|
+
// Keep the existing focus-preserving, no-rerender behavior. Bumping
|
|
7566
|
+
// the revision prevents an older async action from overwriting it.
|
|
7567
|
+
variables[variableKey] = value;
|
|
7568
|
+
revision += 1;
|
|
7569
|
+
}
|
|
7570
|
+
}));
|
|
7571
|
+
}
|
|
7572
|
+
if (node.events && !isDisabled) {
|
|
7573
|
+
for (const [eventName, eventValue] of Object.entries(node.events)) {
|
|
7574
|
+
if (!eventValue || !resolveActionRef(eventValue, schemaActions)) {
|
|
7575
|
+
continue;
|
|
7576
|
+
}
|
|
7577
|
+
const domEvent = EVENT_MAP[eventName] ?? eventName;
|
|
7578
|
+
el.addEventListener(domEvent, ((event) => {
|
|
7579
|
+
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
7580
|
+
&& VALUE_CONTROL_TYPES$1.has(node.type));
|
|
7581
|
+
if (ownsValueEvent && event.target !== el)
|
|
7582
|
+
return;
|
|
7583
|
+
const eventDetail = (event instanceof CustomEvent && event.detail != null)
|
|
7584
|
+
? event.detail
|
|
7585
|
+
: undefined;
|
|
7586
|
+
enqueueBoundEvent(node.id, eventName, eventDetail);
|
|
7587
|
+
}));
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
if (node.lifecycle) {
|
|
7591
|
+
lifecycleIds.add(node.id);
|
|
7592
|
+
}
|
|
7593
|
+
const renderChild = (child) => renderNode(child, renderVariables, lifecycleIds);
|
|
7594
|
+
const childrenMap = {};
|
|
7595
|
+
for (const child of node.children) {
|
|
7596
|
+
childrenMap[child.id] = child;
|
|
7597
|
+
}
|
|
7598
|
+
const layoutApplied = renderSlotLayout(el, node.children, resolvedProps, child => renderChild(child), childrenMap, createPassiveActionContext(renderVariables));
|
|
7599
|
+
if (!layoutApplied) {
|
|
7600
|
+
for (const child of node.children) {
|
|
7601
|
+
el.appendChild(renderChild(child));
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
if (isDisabled) {
|
|
7605
|
+
el.querySelectorAll('*').forEach((child) => {
|
|
7606
|
+
const htmlChild = child;
|
|
7607
|
+
htmlChild.setAttribute('data-disabled', 'true');
|
|
7608
|
+
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
7609
|
+
});
|
|
7610
|
+
}
|
|
7611
|
+
return el;
|
|
7612
|
+
}
|
|
7613
|
+
function prepareCandidate(draft) {
|
|
7614
|
+
const materialized = materializeCard(schema, draft);
|
|
7615
|
+
const lifecycleIds = new Set();
|
|
7616
|
+
const dom = renderNode(materialized.root, draft, lifecycleIds);
|
|
7617
|
+
return { materialized, dom, lifecycleIds };
|
|
7618
|
+
}
|
|
7619
|
+
function indexNodes(root) {
|
|
7620
|
+
const nodes = new Map();
|
|
7621
|
+
const visit = (node) => {
|
|
7622
|
+
nodes.set(node.id, node);
|
|
7623
|
+
node.children.forEach(visit);
|
|
7624
|
+
};
|
|
7625
|
+
visit(root);
|
|
7626
|
+
return nodes;
|
|
7627
|
+
}
|
|
7628
|
+
function selectLifecycleNodes(materialized, ids) {
|
|
7629
|
+
const indexed = indexNodes(materialized.root);
|
|
7630
|
+
const selected = new Map();
|
|
7631
|
+
for (const id of ids) {
|
|
7632
|
+
const node = indexed.get(id);
|
|
7633
|
+
if (node)
|
|
7634
|
+
selected.set(id, node);
|
|
7635
|
+
}
|
|
7636
|
+
return selected;
|
|
7637
|
+
}
|
|
7638
|
+
function createLifecycleActionContext(node) {
|
|
7639
|
+
const writeLiveVariable = (key, value) => {
|
|
7640
|
+
if (disposed)
|
|
7641
|
+
return;
|
|
7642
|
+
updateVariables({ [key]: value });
|
|
7643
|
+
};
|
|
7644
|
+
return {
|
|
7645
|
+
...createWebActionContext({
|
|
7646
|
+
...options,
|
|
7647
|
+
setVariable: writeLiveVariable,
|
|
7648
|
+
abortSignal: abortController.signal,
|
|
7649
|
+
}),
|
|
7650
|
+
variables,
|
|
7651
|
+
expressionContext: createExpressionContext(node.scope),
|
|
7652
|
+
parameterResolver: node.bindingDialect === 'a2ui'
|
|
7653
|
+
? createA2UIParameterResolver(variables, node.dataPath)
|
|
7654
|
+
: undefined,
|
|
7655
|
+
variableWriter: (key, value) => writeLiveVariable(key, value),
|
|
7656
|
+
botId: options.botId,
|
|
7657
|
+
inflightRequests,
|
|
7658
|
+
};
|
|
7659
|
+
}
|
|
7660
|
+
function reconcileLifecycles(nextNodes) {
|
|
7661
|
+
const removed = [...activeLifecycleNodes.entries()]
|
|
7662
|
+
.filter(([id]) => !nextNodes.has(id));
|
|
7663
|
+
const added = [...nextNodes.entries()]
|
|
7664
|
+
.filter(([id]) => !activeLifecycleNodes.has(id));
|
|
7665
|
+
activeLifecycleNodes.clear();
|
|
7666
|
+
for (const [id, node] of nextNodes) {
|
|
7667
|
+
activeLifecycleNodes.set(id, node);
|
|
7668
|
+
}
|
|
7669
|
+
lifecycleQueue = lifecycleQueue.then(async () => {
|
|
7670
|
+
for (const [id, node] of removed) {
|
|
7671
|
+
await lifecycleManager.destroy(id, createLifecycleActionContext(node));
|
|
7672
|
+
lifecycleManager.unregister(id);
|
|
7673
|
+
}
|
|
7674
|
+
if (disposed)
|
|
7675
|
+
return;
|
|
7676
|
+
for (const [id, node] of added) {
|
|
7677
|
+
lifecycleManager.register(id, node.lifecycle);
|
|
7678
|
+
await lifecycleManager.mount(id, createLifecycleActionContext(node));
|
|
7679
|
+
}
|
|
7680
|
+
}).catch((error) => {
|
|
7681
|
+
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
7682
|
+
});
|
|
7683
|
+
}
|
|
7684
|
+
function publishDOM(candidate, incrementRevision) {
|
|
7685
|
+
const scrollPositions = captureScrollPositions$1(container);
|
|
7686
|
+
const mediaStates = captureMediaStates$1(container);
|
|
7687
|
+
disposeChartsIn(container);
|
|
7688
|
+
container.replaceChildren(candidate.dom);
|
|
7689
|
+
currentMaterialized = candidate.materialized;
|
|
7690
|
+
if (incrementRevision)
|
|
7691
|
+
revision += 1;
|
|
7692
|
+
reconcileLifecycles(selectLifecycleNodes(currentMaterialized, candidate.lifecycleIds));
|
|
7693
|
+
restoreScrollPositions$1(container, scrollPositions);
|
|
7694
|
+
restoreMediaStates$1(container, mediaStates);
|
|
7695
|
+
}
|
|
7696
|
+
function assertCurrentRevision(baseRevision) {
|
|
7697
|
+
if (disposed || revision !== baseRevision) {
|
|
7698
|
+
const error = new Error('[renderCard] BOUND_TRANSACTION_CONFLICT');
|
|
7699
|
+
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
7700
|
+
throw error;
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
function commitDraft(draft, candidate, baseRevision) {
|
|
7704
|
+
assertCurrentRevision(baseRevision);
|
|
7705
|
+
replaceRootContents(variables, draft);
|
|
7706
|
+
// Re-materialize against the stable public variables root. The detached
|
|
7707
|
+
// DOM already represents the exact same validated JSON data.
|
|
7708
|
+
candidate.materialized = materializeCard(schema, variables);
|
|
7709
|
+
publishDOM(candidate, true);
|
|
7710
|
+
}
|
|
7711
|
+
function writeDraftVariable(draft, key, value) {
|
|
7712
|
+
Object.defineProperty(draft, String(key), {
|
|
7713
|
+
value,
|
|
7714
|
+
enumerable: true,
|
|
7715
|
+
configurable: true,
|
|
7716
|
+
writable: true,
|
|
7717
|
+
});
|
|
7718
|
+
}
|
|
7719
|
+
function createDraftActionContext(node, draft) {
|
|
7720
|
+
const write = (key, value) => {
|
|
7721
|
+
writeDraftVariable(draft, key, value);
|
|
7722
|
+
};
|
|
7723
|
+
return {
|
|
7724
|
+
...createWebActionContext({
|
|
7725
|
+
...options,
|
|
7726
|
+
setVariable: write,
|
|
7727
|
+
abortSignal: abortController.signal,
|
|
7728
|
+
}),
|
|
7729
|
+
variables: draft,
|
|
7730
|
+
expressionContext: createExpressionContext(node.scope),
|
|
7731
|
+
parameterResolver: node.bindingDialect === 'a2ui'
|
|
7732
|
+
? createA2UIParameterResolver(draft, node.dataPath)
|
|
7733
|
+
: undefined,
|
|
7734
|
+
variableWriter: (key, value) => write(key, value),
|
|
7735
|
+
botId: options.botId,
|
|
7736
|
+
inflightRequests,
|
|
7737
|
+
};
|
|
7738
|
+
}
|
|
7739
|
+
async function runBoundEvent(runtimeId, eventName, eventDetail) {
|
|
7740
|
+
if (disposed)
|
|
7741
|
+
return;
|
|
7742
|
+
const baseRevision = revision;
|
|
7743
|
+
const draft = cloneJsonData(variables);
|
|
7744
|
+
if (eventDetail !== undefined) {
|
|
7745
|
+
writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
|
|
7746
|
+
}
|
|
7747
|
+
const freshMaterialized = materializeCard(schema, draft);
|
|
7748
|
+
const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
|
|
7749
|
+
if (!freshNode) {
|
|
7750
|
+
throw new Error(`[renderCard] Bound runtime node "${runtimeId}" no longer exists`);
|
|
7751
|
+
}
|
|
7752
|
+
const eventValue = freshNode.events?.[eventName];
|
|
7753
|
+
const steps = eventValue
|
|
7754
|
+
? resolveActionRef(eventValue, schemaActions)
|
|
7755
|
+
: undefined;
|
|
7756
|
+
if (!steps)
|
|
7757
|
+
return;
|
|
7758
|
+
await runActionSteps(steps, createDraftActionContext(freshNode, draft));
|
|
7759
|
+
assertCurrentRevision(baseRevision);
|
|
7760
|
+
const candidate = prepareCandidate(draft);
|
|
7761
|
+
commitDraft(draft, candidate, baseRevision);
|
|
7762
|
+
}
|
|
7763
|
+
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
7764
|
+
if (disposed)
|
|
7765
|
+
return;
|
|
7766
|
+
actionQueue = actionQueue
|
|
7767
|
+
.then(() => runBoundEvent(runtimeId, eventName, eventDetail))
|
|
7768
|
+
.catch((error) => {
|
|
7769
|
+
if (error
|
|
7770
|
+
&& typeof error === 'object'
|
|
7771
|
+
&& error.code
|
|
7772
|
+
=== 'BOUND_TRANSACTION_CONFLICT') {
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
console.error('[renderCard] Bound action failed', error);
|
|
7776
|
+
});
|
|
7777
|
+
}
|
|
7778
|
+
function updateVariables(newVariables) {
|
|
7779
|
+
if (disposed)
|
|
7780
|
+
return;
|
|
7781
|
+
const baseRevision = revision;
|
|
7782
|
+
const draft = cloneJsonData(variables);
|
|
7783
|
+
const patch = cloneJsonData(newVariables);
|
|
7784
|
+
for (const key of Object.keys(patch)) {
|
|
7785
|
+
writeDraftVariable(draft, key, patch[key]);
|
|
7786
|
+
}
|
|
7787
|
+
const candidate = prepareCandidate(draft);
|
|
7788
|
+
commitDraft(draft, candidate, baseRevision);
|
|
7789
|
+
}
|
|
7790
|
+
const initialCandidate = prepareCandidate(variables);
|
|
7791
|
+
currentMaterialized = initialCandidate.materialized;
|
|
7792
|
+
publishDOM(initialCandidate, false);
|
|
7793
|
+
return {
|
|
7794
|
+
dispose() {
|
|
7795
|
+
if (disposed)
|
|
7796
|
+
return;
|
|
7797
|
+
disposed = true;
|
|
7798
|
+
abortController.abort();
|
|
7799
|
+
removeViewportListener();
|
|
7800
|
+
disposeChartsIn(container);
|
|
7801
|
+
container.replaceChildren();
|
|
7802
|
+
const lifecycleNodes = [...activeLifecycleNodes.entries()];
|
|
7803
|
+
activeLifecycleNodes.clear();
|
|
7804
|
+
lifecycleQueue = lifecycleQueue
|
|
7805
|
+
.then(async () => {
|
|
7806
|
+
for (const [id, node] of lifecycleNodes) {
|
|
7807
|
+
await lifecycleManager.destroy(id, createLifecycleActionContext(node));
|
|
7808
|
+
lifecycleManager.unregister(id);
|
|
7809
|
+
}
|
|
7810
|
+
await lifecycleManager.dispose(createPassiveActionContext(variables));
|
|
7811
|
+
})
|
|
7812
|
+
.catch((error) => {
|
|
7813
|
+
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
7814
|
+
});
|
|
7815
|
+
},
|
|
7816
|
+
updateVariables,
|
|
7817
|
+
};
|
|
7818
|
+
}
|
|
7819
|
+
function captureScrollPositions$1(root) {
|
|
7820
|
+
const positions = new Map();
|
|
7821
|
+
root.querySelectorAll('[data-scroll-id]').forEach((element) => {
|
|
7822
|
+
const id = element.getAttribute('data-scroll-id');
|
|
7823
|
+
if (id && element.scrollLeft > 0) {
|
|
7824
|
+
positions.set(id, element.scrollLeft);
|
|
7825
|
+
}
|
|
7826
|
+
});
|
|
7827
|
+
return positions;
|
|
7828
|
+
}
|
|
7829
|
+
function restoreScrollPositions$1(root, positions) {
|
|
7830
|
+
if (positions.size === 0)
|
|
7831
|
+
return;
|
|
7832
|
+
root.querySelectorAll('[data-scroll-id]').forEach((element) => {
|
|
7833
|
+
const id = element.getAttribute('data-scroll-id');
|
|
7834
|
+
const saved = id ? positions.get(id) : undefined;
|
|
7835
|
+
if (saved == null)
|
|
7836
|
+
return;
|
|
7837
|
+
const scroller = element;
|
|
7838
|
+
scroller.dataset.restoreScrollLeft = String(saved);
|
|
7839
|
+
const previousSnap = scroller.style.scrollSnapType;
|
|
7840
|
+
scroller.style.scrollSnapType = 'none';
|
|
7841
|
+
scroller.scrollLeft = saved;
|
|
7842
|
+
requestAnimationFrame(() => {
|
|
7843
|
+
scroller.style.scrollSnapType = previousSnap;
|
|
7844
|
+
});
|
|
7845
|
+
});
|
|
7846
|
+
}
|
|
7847
|
+
function captureMediaStates$1(root) {
|
|
7848
|
+
const states = new Map();
|
|
7849
|
+
root.querySelectorAll('ai-card-audio, ai-card-video').forEach((element) => {
|
|
7850
|
+
const id = element.getAttribute('data-card-id');
|
|
7851
|
+
const media = element.shadowRoot?.querySelector('audio, video');
|
|
7852
|
+
const snapshot = snapshotMedia(media);
|
|
7853
|
+
if (id && snapshot)
|
|
7854
|
+
states.set(id, snapshot);
|
|
7855
|
+
});
|
|
7856
|
+
return states;
|
|
7857
|
+
}
|
|
7858
|
+
function restoreMediaStates$1(root, states) {
|
|
7859
|
+
if (states.size === 0)
|
|
7860
|
+
return;
|
|
7861
|
+
root.querySelectorAll('ai-card-audio, ai-card-video').forEach((element) => {
|
|
7862
|
+
const id = element.getAttribute('data-card-id');
|
|
7863
|
+
const snapshot = id ? states.get(id) : undefined;
|
|
7864
|
+
if (!snapshot)
|
|
7865
|
+
return;
|
|
7866
|
+
const media = element.shadowRoot?.querySelector('audio, video');
|
|
7867
|
+
restoreMedia(media, snapshot);
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
const EVENT_MAP = {
|
|
7871
|
+
onClick: 'click',
|
|
7872
|
+
onChange: 'change',
|
|
7873
|
+
onInput: 'input',
|
|
7874
|
+
onSubmit: 'submit',
|
|
7875
|
+
onStepClick: 'step-click',
|
|
7876
|
+
onPlay: 'play',
|
|
7877
|
+
onPause: 'pause',
|
|
7878
|
+
onEnded: 'ended',
|
|
7879
|
+
onMaxReached: 'max-reached',
|
|
7880
|
+
};
|
|
7881
|
+
const VALUE_CONTROL_TYPES$1 = new Set([
|
|
7882
|
+
'ChoiceList',
|
|
7883
|
+
'Counter',
|
|
7884
|
+
'Input',
|
|
7885
|
+
'Rate',
|
|
7886
|
+
'Select',
|
|
7887
|
+
'Switch',
|
|
7888
|
+
]);
|
|
7889
|
+
|
|
5664
7890
|
/**
|
|
5665
7891
|
* renderCard — the primary public API for @antglobal/copilot-cards-web.
|
|
5666
7892
|
*
|
|
@@ -5688,6 +7914,12 @@ function renderCard(container, schemaInput, options = {}) {
|
|
|
5688
7914
|
if (errors.length > 0) {
|
|
5689
7915
|
throw new Error(`[renderCard] Invalid schema:\n${errors.join('\n')}`);
|
|
5690
7916
|
}
|
|
7917
|
+
if (!requiresBindingMaterialization(schema)) {
|
|
7918
|
+
return renderStaticCard(container, schema, options);
|
|
7919
|
+
}
|
|
7920
|
+
return renderBoundCard(container, schema, options);
|
|
7921
|
+
}
|
|
7922
|
+
function renderStaticCard(container, schema, options) {
|
|
5691
7923
|
// 2. Parse into render tree
|
|
5692
7924
|
const tree = parseSchema(schema);
|
|
5693
7925
|
// 3. Reactive variables store (mutable copy, merged with external variables)
|
|
@@ -5878,6 +8110,10 @@ function renderNode(node, variables, actionContext, isMobile, lifecycleManager,
|
|
|
5878
8110
|
const variableKey = resolvedProps.variableKey;
|
|
5879
8111
|
if (variableKey) {
|
|
5880
8112
|
el.addEventListener('input', ((e) => {
|
|
8113
|
+
// Nested value controls own their variableKey. Only a value event
|
|
8114
|
+
// dispatched by this component may update this component's variable.
|
|
8115
|
+
if (e.target !== el)
|
|
8116
|
+
return;
|
|
5881
8117
|
const value = e.detail?.value
|
|
5882
8118
|
?? e.target?.value;
|
|
5883
8119
|
if (value !== undefined) {
|
|
@@ -5896,6 +8132,14 @@ function renderNode(node, variables, actionContext, isMobile, lifecycleManager,
|
|
|
5896
8132
|
continue;
|
|
5897
8133
|
const domEvent = eventMap[event] ?? event;
|
|
5898
8134
|
el.addEventListener(domEvent, ((e) => {
|
|
8135
|
+
// Value controls own their input/change handlers; a nested control's
|
|
8136
|
+
// same-named event must not trigger the parent's schema action.
|
|
8137
|
+
// Other component/event pairs keep the renderer's existing bubbling
|
|
8138
|
+
// behavior (for example a Container-level click handler).
|
|
8139
|
+
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
8140
|
+
&& VALUE_CONTROL_TYPES.has(node.type));
|
|
8141
|
+
if (ownsValueEvent && e.target !== el)
|
|
8142
|
+
return;
|
|
5899
8143
|
// Write event.detail to _event variable so action params can use ${_event.xxx}
|
|
5900
8144
|
if (e instanceof CustomEvent && e.detail != null) {
|
|
5901
8145
|
variables._event = e.detail;
|
|
@@ -5943,7 +8187,16 @@ const eventMap = {
|
|
|
5943
8187
|
onPlay: 'play',
|
|
5944
8188
|
onPause: 'pause',
|
|
5945
8189
|
onEnded: 'ended',
|
|
8190
|
+
onMaxReached: 'max-reached',
|
|
5946
8191
|
};
|
|
8192
|
+
const VALUE_CONTROL_TYPES = new Set([
|
|
8193
|
+
'ChoiceList',
|
|
8194
|
+
'Counter',
|
|
8195
|
+
'Input',
|
|
8196
|
+
'Rate',
|
|
8197
|
+
'Select',
|
|
8198
|
+
'Switch',
|
|
8199
|
+
]);
|
|
5947
8200
|
|
|
5948
8201
|
/**
|
|
5949
8202
|
* Streaming Card Renderer — enables progressive card rendering via streaming commands.
|
|
@@ -5999,6 +8252,15 @@ function renderStreamingCard(container, options = {}) {
|
|
|
5999
8252
|
let isMobile = options.isMobile ?? isMobileViewport();
|
|
6000
8253
|
let currentSchema = null;
|
|
6001
8254
|
let currentSurfaceId = null;
|
|
8255
|
+
let currentMaterialized = null;
|
|
8256
|
+
let boundRevision = 0;
|
|
8257
|
+
let boundActionQueue = Promise.resolve();
|
|
8258
|
+
let boundLifecycleEpoch = 0;
|
|
8259
|
+
let disposed = false;
|
|
8260
|
+
const sourceOccurrences = new Map();
|
|
8261
|
+
const activeBoundLifecycles = new Map();
|
|
8262
|
+
const mountedBoundLifecycles = new Map();
|
|
8263
|
+
const boundLifecycleGenerations = new Map();
|
|
6002
8264
|
// Responsive viewport detection
|
|
6003
8265
|
const removeViewportListener = options.isMobile == null
|
|
6004
8266
|
? onViewportChange((mobile) => {
|
|
@@ -6007,35 +8269,200 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6007
8269
|
if (currentSchema) {
|
|
6008
8270
|
rerenderAll();
|
|
6009
8271
|
}
|
|
6010
|
-
})
|
|
6011
|
-
: () => { };
|
|
6012
|
-
// ─── Action Context ─────────────────────────────────────────────
|
|
6013
|
-
function buildActionContext() {
|
|
6014
|
-
return {
|
|
6015
|
-
...createWebActionContext({
|
|
6016
|
-
...options,
|
|
6017
|
-
setVariable: (key, value) => {
|
|
6018
|
-
variables[key] = value;
|
|
6019
|
-
// On variable change, patch only affected elements (keyed diff)
|
|
6020
|
-
diffAllElements();
|
|
6021
|
-
},
|
|
6022
|
-
abortSignal: abortController.signal,
|
|
6023
|
-
}),
|
|
6024
|
-
variables,
|
|
6025
|
-
botId: options.botId,
|
|
6026
|
-
inflightRequests,
|
|
6027
|
-
};
|
|
8272
|
+
})
|
|
8273
|
+
: () => { };
|
|
8274
|
+
// ─── Action Context ─────────────────────────────────────────────
|
|
8275
|
+
function buildActionContext() {
|
|
8276
|
+
return {
|
|
8277
|
+
...createWebActionContext({
|
|
8278
|
+
...options,
|
|
8279
|
+
setVariable: (key, value) => {
|
|
8280
|
+
variables[key] = value;
|
|
8281
|
+
// On variable change, patch only affected elements (keyed diff)
|
|
8282
|
+
diffAllElements();
|
|
8283
|
+
},
|
|
8284
|
+
abortSignal: abortController.signal,
|
|
8285
|
+
}),
|
|
8286
|
+
variables,
|
|
8287
|
+
botId: options.botId,
|
|
8288
|
+
inflightRequests,
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
8291
|
+
let actionContext = buildActionContext();
|
|
8292
|
+
// ─── Rendering Helpers ──────────────────────────────────────────
|
|
8293
|
+
/** Event name map: schema events → DOM events */
|
|
8294
|
+
const eventMap = {
|
|
8295
|
+
onClick: 'click',
|
|
8296
|
+
onChange: 'change',
|
|
8297
|
+
onInput: 'input',
|
|
8298
|
+
onSubmit: 'submit',
|
|
8299
|
+
onStepClick: 'step-click',
|
|
8300
|
+
onPlay: 'play',
|
|
8301
|
+
onPause: 'pause',
|
|
8302
|
+
onEnded: 'ended',
|
|
8303
|
+
onMaxReached: 'max-reached',
|
|
8304
|
+
};
|
|
8305
|
+
const valueControlTypes = new Set([
|
|
8306
|
+
'ChoiceList',
|
|
8307
|
+
'Counter',
|
|
8308
|
+
'Input',
|
|
8309
|
+
'Rate',
|
|
8310
|
+
'Select',
|
|
8311
|
+
'Switch',
|
|
8312
|
+
]);
|
|
8313
|
+
function createBoundIndexes() {
|
|
8314
|
+
return {
|
|
8315
|
+
elements: new Map(),
|
|
8316
|
+
props: new Map(),
|
|
8317
|
+
hidden: new Set(),
|
|
8318
|
+
sources: new Map(),
|
|
8319
|
+
};
|
|
8320
|
+
}
|
|
8321
|
+
function addSourceOccurrence(indexes, node) {
|
|
8322
|
+
const occurrences = indexes.sources.get(node.sourceId);
|
|
8323
|
+
if (occurrences) {
|
|
8324
|
+
occurrences.add(node.id);
|
|
8325
|
+
}
|
|
8326
|
+
else {
|
|
8327
|
+
indexes.sources.set(node.sourceId, new Set([node.id]));
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
function boundExpressionContext(node) {
|
|
8331
|
+
return isBoundRenderTreeNode(node)
|
|
8332
|
+
? createExpressionContext(node.scope)
|
|
8333
|
+
: variables;
|
|
8334
|
+
}
|
|
8335
|
+
function resolveBoundValue(value, node, renderVariables) {
|
|
8336
|
+
if (node.bindingDialect === 'a2ui') {
|
|
8337
|
+
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
8338
|
+
}
|
|
8339
|
+
if (typeof value === 'string' && hasExpression(value)) {
|
|
8340
|
+
return resolveExpression(value, boundExpressionContext(node));
|
|
8341
|
+
}
|
|
8342
|
+
return value;
|
|
8343
|
+
}
|
|
8344
|
+
function resolveBoundNodeProps(node, renderVariables) {
|
|
8345
|
+
const resolved = (node.bindingDialect === 'a2ui'
|
|
8346
|
+
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
8347
|
+
: resolveDeep(node.props, boundExpressionContext(node)));
|
|
8348
|
+
if (resolved.content
|
|
8349
|
+
&& typeof resolved.content === 'object'
|
|
8350
|
+
&& 'type' in resolved.content) {
|
|
8351
|
+
resolved.content = resolveExpressionValue(resolved.content, boundExpressionContext(node));
|
|
8352
|
+
}
|
|
8353
|
+
return resolved;
|
|
8354
|
+
}
|
|
8355
|
+
function computeBoundVisible(node, renderVariables) {
|
|
8356
|
+
const visible = node.directives?.visible;
|
|
8357
|
+
if (!visible)
|
|
8358
|
+
return true;
|
|
8359
|
+
const resolved = resolveBoundValue(visible, node, renderVariables);
|
|
8360
|
+
return !(resolved === false
|
|
8361
|
+
|| resolved === 'false'
|
|
8362
|
+
|| resolved === ''
|
|
8363
|
+
|| resolved === 0);
|
|
8364
|
+
}
|
|
8365
|
+
function computeBoundDisabled(node, renderVariables) {
|
|
8366
|
+
const disabled = node.directives?.disabled;
|
|
8367
|
+
if (!disabled)
|
|
8368
|
+
return false;
|
|
8369
|
+
const resolved = resolveBoundValue(disabled, node, renderVariables);
|
|
8370
|
+
return resolved === true || resolved === 'true' || resolved === 1;
|
|
8371
|
+
}
|
|
8372
|
+
function boundNodeFingerprint(node, renderVariables, resolvedProps = resolveBoundNodeProps(node, renderVariables)) {
|
|
8373
|
+
return fingerprint(resolvedProps, {
|
|
8374
|
+
visible: computeBoundVisible(node, renderVariables),
|
|
8375
|
+
disabled: computeBoundDisabled(node, renderVariables),
|
|
8376
|
+
});
|
|
8377
|
+
}
|
|
8378
|
+
function writeLiveBoundVariable(key, value) {
|
|
8379
|
+
Object.defineProperty(variables, String(key), {
|
|
8380
|
+
value,
|
|
8381
|
+
enumerable: true,
|
|
8382
|
+
configurable: true,
|
|
8383
|
+
writable: true,
|
|
8384
|
+
});
|
|
8385
|
+
const schema = currentSurfaceId
|
|
8386
|
+
? engine.getSchema(currentSurfaceId)
|
|
8387
|
+
: currentSchema;
|
|
8388
|
+
if (schema) {
|
|
8389
|
+
Object.defineProperty(schema.variables, String(key), {
|
|
8390
|
+
value: cloneJsonData(value),
|
|
8391
|
+
enumerable: true,
|
|
8392
|
+
configurable: true,
|
|
8393
|
+
writable: true,
|
|
8394
|
+
});
|
|
8395
|
+
}
|
|
8396
|
+
boundRevision += 1;
|
|
8397
|
+
}
|
|
8398
|
+
function renderBoundNode(node, schema, renderVariables, indexes, lifecycleCollector) {
|
|
8399
|
+
addSourceOccurrence(indexes, node);
|
|
8400
|
+
if (!computeBoundVisible(node, renderVariables)) {
|
|
8401
|
+
const placeholder = document.createElement('div');
|
|
8402
|
+
placeholder.style.display = 'none';
|
|
8403
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
8404
|
+
indexes.elements.set(node.id, placeholder);
|
|
8405
|
+
indexes.hidden.add(node.id);
|
|
8406
|
+
return placeholder;
|
|
8407
|
+
}
|
|
8408
|
+
const resolvedProps = resolveBoundNodeProps(node, renderVariables);
|
|
8409
|
+
indexes.hidden.delete(node.id);
|
|
8410
|
+
indexes.props.set(node.id, boundNodeFingerprint(node, renderVariables, resolvedProps));
|
|
8411
|
+
const isDisabled = computeBoundDisabled(node, renderVariables);
|
|
8412
|
+
const renderer = (componentRenderers[node.type] ?? componentRenderers._default);
|
|
8413
|
+
const element = renderer(node, resolvedProps, isMobile);
|
|
8414
|
+
if (isDisabled) {
|
|
8415
|
+
element.setAttribute('data-disabled', 'true');
|
|
8416
|
+
element.style.background = '#F5F5F5';
|
|
8417
|
+
element.style.color = '#C0C0C0';
|
|
8418
|
+
element.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
8419
|
+
element.style.pointerEvents = 'none';
|
|
8420
|
+
}
|
|
8421
|
+
const variableKey = resolvedProps.variableKey;
|
|
8422
|
+
if (variableKey) {
|
|
8423
|
+
element.addEventListener('input', ((event) => {
|
|
8424
|
+
if (event.target !== element || !currentMaterialized)
|
|
8425
|
+
return;
|
|
8426
|
+
const value = event.detail?.value
|
|
8427
|
+
?? event.target?.value;
|
|
8428
|
+
if (value !== undefined)
|
|
8429
|
+
writeLiveBoundVariable(variableKey, value);
|
|
8430
|
+
}));
|
|
8431
|
+
}
|
|
8432
|
+
if (node.events && !isDisabled) {
|
|
8433
|
+
for (const [eventName, eventValue] of Object.entries(node.events)) {
|
|
8434
|
+
if (!eventValue || !resolveActionRef(eventValue, schema.actions ?? {})) {
|
|
8435
|
+
continue;
|
|
8436
|
+
}
|
|
8437
|
+
const domEvent = eventMap[eventName] ?? eventName;
|
|
8438
|
+
element.addEventListener(domEvent, ((event) => {
|
|
8439
|
+
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
8440
|
+
&& valueControlTypes.has(node.type));
|
|
8441
|
+
if (ownsValueEvent && event.target !== element)
|
|
8442
|
+
return;
|
|
8443
|
+
const detail = (event instanceof CustomEvent && event.detail != null)
|
|
8444
|
+
? event.detail
|
|
8445
|
+
: undefined;
|
|
8446
|
+
enqueueBoundEvent(node.id, eventName, detail);
|
|
8447
|
+
}));
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
if (node.lifecycle) {
|
|
8451
|
+
lifecycleCollector.set(node.id, node);
|
|
8452
|
+
}
|
|
8453
|
+
indexes.elements.set(node.id, element);
|
|
8454
|
+
const renderChild = (child) => renderBoundNode(child, schema, renderVariables, indexes, lifecycleCollector);
|
|
8455
|
+
const childrenMap = {};
|
|
8456
|
+
for (const child of node.children)
|
|
8457
|
+
childrenMap[child.id] = child;
|
|
8458
|
+
const layoutApplied = renderSlotLayout(element, node.children, resolvedProps, child => renderChild(child), childrenMap, actionContext);
|
|
8459
|
+
if (!layoutApplied) {
|
|
8460
|
+
for (const child of node.children) {
|
|
8461
|
+
element.appendChild(renderChild(child));
|
|
8462
|
+
}
|
|
8463
|
+
}
|
|
8464
|
+
return element;
|
|
6028
8465
|
}
|
|
6029
|
-
let actionContext = buildActionContext();
|
|
6030
|
-
// ─── Rendering Helpers ──────────────────────────────────────────
|
|
6031
|
-
/** Event name map: schema events → DOM events */
|
|
6032
|
-
const eventMap = {
|
|
6033
|
-
onClick: 'click',
|
|
6034
|
-
onChange: 'change',
|
|
6035
|
-
onInput: 'input',
|
|
6036
|
-
onSubmit: 'submit',
|
|
6037
|
-
onStepClick: 'step-click',
|
|
6038
|
-
};
|
|
6039
8466
|
/**
|
|
6040
8467
|
* Render a single node and register it in elementMap.
|
|
6041
8468
|
*/
|
|
@@ -6271,6 +8698,540 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6271
8698
|
}
|
|
6272
8699
|
}
|
|
6273
8700
|
}
|
|
8701
|
+
function captureBoundScrollPositions(root) {
|
|
8702
|
+
const positions = new Map();
|
|
8703
|
+
root.querySelectorAll('[data-scroll-id]').forEach((element) => {
|
|
8704
|
+
const id = element.getAttribute('data-scroll-id');
|
|
8705
|
+
if (id && element.scrollLeft > 0) {
|
|
8706
|
+
positions.set(id, element.scrollLeft);
|
|
8707
|
+
}
|
|
8708
|
+
});
|
|
8709
|
+
return positions;
|
|
8710
|
+
}
|
|
8711
|
+
function restoreBoundScrollPositions(root, positions) {
|
|
8712
|
+
if (positions.size === 0)
|
|
8713
|
+
return;
|
|
8714
|
+
root.querySelectorAll('[data-scroll-id]').forEach((element) => {
|
|
8715
|
+
const id = element.getAttribute('data-scroll-id');
|
|
8716
|
+
const saved = id ? positions.get(id) : undefined;
|
|
8717
|
+
if (saved == null)
|
|
8718
|
+
return;
|
|
8719
|
+
const scroller = element;
|
|
8720
|
+
scroller.dataset.restoreScrollLeft = String(saved);
|
|
8721
|
+
const previousSnap = scroller.style.scrollSnapType;
|
|
8722
|
+
scroller.style.scrollSnapType = 'none';
|
|
8723
|
+
scroller.scrollLeft = saved;
|
|
8724
|
+
requestAnimationFrame(() => {
|
|
8725
|
+
if (scroller.isConnected) {
|
|
8726
|
+
scroller.style.scrollSnapType = previousSnap;
|
|
8727
|
+
}
|
|
8728
|
+
});
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
function captureBoundMediaStates(root) {
|
|
8732
|
+
const states = new Map();
|
|
8733
|
+
root.querySelectorAll('ai-card-audio, ai-card-video')
|
|
8734
|
+
.forEach((element) => {
|
|
8735
|
+
const id = element.getAttribute('data-card-id');
|
|
8736
|
+
const media = element.shadowRoot?.querySelector('audio, video');
|
|
8737
|
+
const snapshot = snapshotMedia(media);
|
|
8738
|
+
if (id && snapshot)
|
|
8739
|
+
states.set(id, snapshot);
|
|
8740
|
+
});
|
|
8741
|
+
return states;
|
|
8742
|
+
}
|
|
8743
|
+
function restoreBoundMediaStates(root, states) {
|
|
8744
|
+
if (states.size === 0)
|
|
8745
|
+
return;
|
|
8746
|
+
root.querySelectorAll('ai-card-audio, ai-card-video')
|
|
8747
|
+
.forEach((element) => {
|
|
8748
|
+
const id = element.getAttribute('data-card-id');
|
|
8749
|
+
const snapshot = id ? states.get(id) : undefined;
|
|
8750
|
+
if (!snapshot)
|
|
8751
|
+
return;
|
|
8752
|
+
const media = element.shadowRoot?.querySelector('audio, video');
|
|
8753
|
+
restoreMedia(media, snapshot);
|
|
8754
|
+
});
|
|
8755
|
+
}
|
|
8756
|
+
class IncompleteBoundSchemaError extends Error {
|
|
8757
|
+
constructor() {
|
|
8758
|
+
super(...arguments);
|
|
8759
|
+
this.code = 'INCOMPLETE_BOUND_SCHEMA';
|
|
8760
|
+
}
|
|
8761
|
+
}
|
|
8762
|
+
class InvalidBoundSchemaError extends Error {
|
|
8763
|
+
constructor() {
|
|
8764
|
+
super(...arguments);
|
|
8765
|
+
this.code = 'INVALID_BOUND_SCHEMA';
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
class BoundPatchPreconditionError extends Error {
|
|
8769
|
+
constructor() {
|
|
8770
|
+
super(...arguments);
|
|
8771
|
+
this.code = 'BOUND_PATCH_PRECONDITION';
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
function indexBoundNodes(root) {
|
|
8775
|
+
const nodes = new Map();
|
|
8776
|
+
const visit = (node) => {
|
|
8777
|
+
nodes.set(node.id, node);
|
|
8778
|
+
node.children.forEach(visit);
|
|
8779
|
+
};
|
|
8780
|
+
visit(root);
|
|
8781
|
+
return nodes;
|
|
8782
|
+
}
|
|
8783
|
+
function collectBoundNodeIds(root, output = new Set()) {
|
|
8784
|
+
output.add(root.id);
|
|
8785
|
+
for (const child of root.children)
|
|
8786
|
+
collectBoundNodeIds(child, output);
|
|
8787
|
+
return output;
|
|
8788
|
+
}
|
|
8789
|
+
function assertCompleteBoundDefinitions(schema, materialized) {
|
|
8790
|
+
for (const owner of materialized.repeatOwners.values()) {
|
|
8791
|
+
if (!schema.elements[owner.templateId]) {
|
|
8792
|
+
throw new IncompleteBoundSchemaError(`[renderStreamingCard] Missing repeat template "${owner.templateId}"`);
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
function materializeStreamingCard(schema, renderVariables) {
|
|
8797
|
+
try {
|
|
8798
|
+
const materialized = materializeCard(schema, renderVariables);
|
|
8799
|
+
assertCompleteBoundDefinitions(schema, materialized);
|
|
8800
|
+
const validationErrors = validateSchema(schema);
|
|
8801
|
+
if (validationErrors.length > 0) {
|
|
8802
|
+
throw new InvalidBoundSchemaError(`[renderStreamingCard] Invalid bound schema:\n${validationErrors.join('\n')}`);
|
|
8803
|
+
}
|
|
8804
|
+
return materialized;
|
|
8805
|
+
}
|
|
8806
|
+
catch (error) {
|
|
8807
|
+
if (error instanceof Error
|
|
8808
|
+
&& /Missing element for id/.test(error.message)) {
|
|
8809
|
+
throw new IncompleteBoundSchemaError(error.message);
|
|
8810
|
+
}
|
|
8811
|
+
throw error;
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
function selectLifecycleNodes(materialized, ids) {
|
|
8815
|
+
const indexed = indexBoundNodes(materialized.root);
|
|
8816
|
+
const selected = new Map();
|
|
8817
|
+
for (const id of ids) {
|
|
8818
|
+
const node = indexed.get(id);
|
|
8819
|
+
if (node)
|
|
8820
|
+
selected.set(id, node);
|
|
8821
|
+
}
|
|
8822
|
+
return selected;
|
|
8823
|
+
}
|
|
8824
|
+
async function runBoundLifecycleStepsAtomically(node, steps) {
|
|
8825
|
+
if (!steps?.length)
|
|
8826
|
+
return;
|
|
8827
|
+
const baseRevision = boundRevision;
|
|
8828
|
+
const before = cloneJsonData(variables);
|
|
8829
|
+
const draft = cloneJsonData(variables);
|
|
8830
|
+
await runBoundSteps(steps, node, createBoundActionContext(node, draft));
|
|
8831
|
+
if (topLevelChangedPaths(before, draft).length === 0)
|
|
8832
|
+
return;
|
|
8833
|
+
try {
|
|
8834
|
+
commitBoundDraftTransaction(before, draft, baseRevision);
|
|
8835
|
+
}
|
|
8836
|
+
catch (error) {
|
|
8837
|
+
if (error
|
|
8838
|
+
&& typeof error === 'object'
|
|
8839
|
+
&& error.code
|
|
8840
|
+
=== 'BOUND_TRANSACTION_CONFLICT') {
|
|
8841
|
+
return;
|
|
8842
|
+
}
|
|
8843
|
+
throw error;
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
function boundLifecycleFingerprint(node) {
|
|
8847
|
+
try {
|
|
8848
|
+
return JSON.stringify({
|
|
8849
|
+
sourceId: node.sourceId,
|
|
8850
|
+
lifecycle: node.lifecycle ?? null,
|
|
8851
|
+
dataPath: node.dataPath ?? null,
|
|
8852
|
+
bindingDialect: node.bindingDialect ?? null,
|
|
8853
|
+
});
|
|
8854
|
+
}
|
|
8855
|
+
catch {
|
|
8856
|
+
return `${node.id}:${node.sourceId}`;
|
|
8857
|
+
}
|
|
8858
|
+
}
|
|
8859
|
+
function sameBoundLifecycle(left, right) {
|
|
8860
|
+
return !!left
|
|
8861
|
+
&& !!right
|
|
8862
|
+
&& boundLifecycleFingerprint(left) === boundLifecycleFingerprint(right);
|
|
8863
|
+
}
|
|
8864
|
+
function enqueueBoundOperation(operation, failureLabel) {
|
|
8865
|
+
boundActionQueue = boundActionQueue
|
|
8866
|
+
.then(operation)
|
|
8867
|
+
.catch((error) => {
|
|
8868
|
+
if (error
|
|
8869
|
+
&& typeof error === 'object'
|
|
8870
|
+
&& error.code
|
|
8871
|
+
=== 'BOUND_TRANSACTION_CONFLICT') {
|
|
8872
|
+
return;
|
|
8873
|
+
}
|
|
8874
|
+
console.error(failureLabel, error);
|
|
8875
|
+
});
|
|
8876
|
+
}
|
|
8877
|
+
async function destroyMountedBoundLifecycle(id, fallbackNode) {
|
|
8878
|
+
const mountedNode = mountedBoundLifecycles.get(id);
|
|
8879
|
+
if (!mountedNode)
|
|
8880
|
+
return;
|
|
8881
|
+
mountedBoundLifecycles.delete(id);
|
|
8882
|
+
const node = mountedNode ?? fallbackNode;
|
|
8883
|
+
await runBoundLifecycleStepsAtomically(node, node.lifecycle?.onDestroy);
|
|
8884
|
+
}
|
|
8885
|
+
async function mountDesiredBoundLifecycle(id, node, epoch, generation) {
|
|
8886
|
+
const desired = activeBoundLifecycles.get(id);
|
|
8887
|
+
if (disposed
|
|
8888
|
+
|| epoch !== boundLifecycleEpoch
|
|
8889
|
+
|| boundLifecycleGenerations.get(id) !== generation
|
|
8890
|
+
|| !sameBoundLifecycle(desired, node)) {
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
await runBoundLifecycleStepsAtomically(node, node.lifecycle?.onMount);
|
|
8894
|
+
const stillDesired = activeBoundLifecycles.get(id);
|
|
8895
|
+
if (disposed
|
|
8896
|
+
|| epoch !== boundLifecycleEpoch
|
|
8897
|
+
|| boundLifecycleGenerations.get(id) !== generation
|
|
8898
|
+
|| !sameBoundLifecycle(stillDesired, node)) {
|
|
8899
|
+
await runBoundLifecycleStepsAtomically(node, node.lifecycle?.onDestroy);
|
|
8900
|
+
return;
|
|
8901
|
+
}
|
|
8902
|
+
mountedBoundLifecycles.set(id, node);
|
|
8903
|
+
}
|
|
8904
|
+
function reconcileBoundLifecycles(next, forceReplaceIds = new Set()) {
|
|
8905
|
+
const previous = new Map(activeBoundLifecycles);
|
|
8906
|
+
const replacedIds = new Set(forceReplaceIds);
|
|
8907
|
+
for (const [id, nextNode] of next) {
|
|
8908
|
+
const previousNode = previous.get(id);
|
|
8909
|
+
if (previousNode
|
|
8910
|
+
&& !sameBoundLifecycle(previousNode, nextNode)) {
|
|
8911
|
+
replacedIds.add(id);
|
|
8912
|
+
}
|
|
8913
|
+
}
|
|
8914
|
+
const removed = [...previous.entries()]
|
|
8915
|
+
.filter(([id]) => !next.has(id) || replacedIds.has(id));
|
|
8916
|
+
const added = [...next.entries()]
|
|
8917
|
+
.filter(([id]) => !previous.has(id) || replacedIds.has(id));
|
|
8918
|
+
const touchedGenerations = new Map();
|
|
8919
|
+
for (const id of new Set([
|
|
8920
|
+
...removed.map(([id]) => id),
|
|
8921
|
+
...added.map(([id]) => id),
|
|
8922
|
+
])) {
|
|
8923
|
+
const generation = (boundLifecycleGenerations.get(id) ?? 0) + 1;
|
|
8924
|
+
boundLifecycleGenerations.set(id, generation);
|
|
8925
|
+
touchedGenerations.set(id, generation);
|
|
8926
|
+
}
|
|
8927
|
+
activeBoundLifecycles.clear();
|
|
8928
|
+
for (const [id, node] of next)
|
|
8929
|
+
activeBoundLifecycles.set(id, node);
|
|
8930
|
+
const epoch = boundLifecycleEpoch;
|
|
8931
|
+
enqueueBoundOperation(async () => {
|
|
8932
|
+
for (const [id, node] of removed) {
|
|
8933
|
+
await destroyMountedBoundLifecycle(id, node);
|
|
8934
|
+
}
|
|
8935
|
+
for (const [id, node] of added) {
|
|
8936
|
+
const generation = touchedGenerations.get(id);
|
|
8937
|
+
if (generation != null) {
|
|
8938
|
+
await mountDesiredBoundLifecycle(id, node, epoch, generation);
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8941
|
+
for (const [id] of removed) {
|
|
8942
|
+
const generation = touchedGenerations.get(id);
|
|
8943
|
+
if (generation != null
|
|
8944
|
+
&& boundLifecycleGenerations.get(id) === generation
|
|
8945
|
+
&& !activeBoundLifecycles.has(id)
|
|
8946
|
+
&& !mountedBoundLifecycles.has(id)) {
|
|
8947
|
+
boundLifecycleGenerations.delete(id);
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
}, '[renderStreamingCard] Bound lifecycle action failed');
|
|
8951
|
+
}
|
|
8952
|
+
function teardownBoundLifecycles() {
|
|
8953
|
+
if (activeBoundLifecycles.size === 0
|
|
8954
|
+
&& mountedBoundLifecycles.size === 0) {
|
|
8955
|
+
boundLifecycleEpoch += 1;
|
|
8956
|
+
return;
|
|
8957
|
+
}
|
|
8958
|
+
const nodes = new Map(activeBoundLifecycles);
|
|
8959
|
+
const invalidatedGenerations = new Map();
|
|
8960
|
+
for (const id of new Set([
|
|
8961
|
+
...activeBoundLifecycles.keys(),
|
|
8962
|
+
...mountedBoundLifecycles.keys(),
|
|
8963
|
+
])) {
|
|
8964
|
+
const generation = (boundLifecycleGenerations.get(id) ?? 0) + 1;
|
|
8965
|
+
boundLifecycleGenerations.set(id, generation);
|
|
8966
|
+
invalidatedGenerations.set(id, generation);
|
|
8967
|
+
}
|
|
8968
|
+
activeBoundLifecycles.clear();
|
|
8969
|
+
boundLifecycleEpoch += 1;
|
|
8970
|
+
enqueueBoundOperation(async () => {
|
|
8971
|
+
const ids = new Set([
|
|
8972
|
+
...nodes.keys(),
|
|
8973
|
+
...mountedBoundLifecycles.keys(),
|
|
8974
|
+
]);
|
|
8975
|
+
for (const id of ids) {
|
|
8976
|
+
await destroyMountedBoundLifecycle(id, nodes.get(id));
|
|
8977
|
+
if (boundLifecycleGenerations.get(id)
|
|
8978
|
+
=== invalidatedGenerations.get(id)
|
|
8979
|
+
&& !activeBoundLifecycles.has(id)
|
|
8980
|
+
&& !mountedBoundLifecycles.has(id)) {
|
|
8981
|
+
boundLifecycleGenerations.delete(id);
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
8984
|
+
}, '[renderStreamingCard] Bound lifecycle dispose failed');
|
|
8985
|
+
}
|
|
8986
|
+
function replaceProductionIndexes(indexes) {
|
|
8987
|
+
elementMap.clear();
|
|
8988
|
+
propsCache.clear();
|
|
8989
|
+
hiddenSet.clear();
|
|
8990
|
+
sourceOccurrences.clear();
|
|
8991
|
+
for (const [id, element] of indexes.elements) {
|
|
8992
|
+
elementMap.set(id, element);
|
|
8993
|
+
}
|
|
8994
|
+
for (const [id, value] of indexes.props)
|
|
8995
|
+
propsCache.set(id, value);
|
|
8996
|
+
for (const id of indexes.hidden)
|
|
8997
|
+
hiddenSet.add(id);
|
|
8998
|
+
for (const [sourceId, ids] of indexes.sources) {
|
|
8999
|
+
sourceOccurrences.set(sourceId, new Set(ids));
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
function mergeProductionIndexes(indexes) {
|
|
9003
|
+
for (const [id, element] of indexes.elements) {
|
|
9004
|
+
elementMap.set(id, element);
|
|
9005
|
+
}
|
|
9006
|
+
for (const [id, value] of indexes.props)
|
|
9007
|
+
propsCache.set(id, value);
|
|
9008
|
+
for (const id of indexes.hidden)
|
|
9009
|
+
hiddenSet.add(id);
|
|
9010
|
+
for (const [sourceId, ids] of indexes.sources) {
|
|
9011
|
+
const occurrences = sourceOccurrences.get(sourceId)
|
|
9012
|
+
?? new Set();
|
|
9013
|
+
for (const id of ids)
|
|
9014
|
+
occurrences.add(id);
|
|
9015
|
+
sourceOccurrences.set(sourceId, occurrences);
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
function unindexBoundSubtree(node) {
|
|
9019
|
+
const visit = (current) => {
|
|
9020
|
+
elementMap.delete(current.id);
|
|
9021
|
+
propsCache.delete(current.id);
|
|
9022
|
+
hiddenSet.delete(current.id);
|
|
9023
|
+
const occurrences = sourceOccurrences.get(current.sourceId);
|
|
9024
|
+
occurrences?.delete(current.id);
|
|
9025
|
+
if (occurrences?.size === 0) {
|
|
9026
|
+
sourceOccurrences.delete(current.sourceId);
|
|
9027
|
+
}
|
|
9028
|
+
current.children.forEach(visit);
|
|
9029
|
+
};
|
|
9030
|
+
visit(node);
|
|
9031
|
+
}
|
|
9032
|
+
function prepareBoundFull(schema, draft) {
|
|
9033
|
+
const materialized = materializeStreamingCard(schema, draft);
|
|
9034
|
+
const indexes = createBoundIndexes();
|
|
9035
|
+
const lifecycles = new Map();
|
|
9036
|
+
const dom = renderBoundNode(materialized.root, schema, draft, indexes, lifecycles);
|
|
9037
|
+
return {
|
|
9038
|
+
materialized,
|
|
9039
|
+
dom,
|
|
9040
|
+
indexes,
|
|
9041
|
+
lifecycleIds: new Set(lifecycles.keys()),
|
|
9042
|
+
};
|
|
9043
|
+
}
|
|
9044
|
+
function assertBoundRevision(baseRevision) {
|
|
9045
|
+
if (boundRevision !== baseRevision) {
|
|
9046
|
+
const error = new Error('[renderStreamingCard] BOUND_TRANSACTION_CONFLICT');
|
|
9047
|
+
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
9048
|
+
throw error;
|
|
9049
|
+
}
|
|
9050
|
+
}
|
|
9051
|
+
function commitBoundFull(prepared, schema, draft, baseRevision, commitAuthoritativeVariables) {
|
|
9052
|
+
assertBoundRevision(baseRevision);
|
|
9053
|
+
commitAuthoritativeVariables?.();
|
|
9054
|
+
replaceRootContents(variables, draft);
|
|
9055
|
+
const stableMaterialized = materializeStreamingCard(schema, variables);
|
|
9056
|
+
const scrollPositions = captureBoundScrollPositions(container);
|
|
9057
|
+
const mediaStates = captureBoundMediaStates(container);
|
|
9058
|
+
disposeChartsIn(container);
|
|
9059
|
+
container.replaceChildren(prepared.dom);
|
|
9060
|
+
replaceProductionIndexes(prepared.indexes);
|
|
9061
|
+
currentSchema = schema;
|
|
9062
|
+
currentMaterialized = stableMaterialized;
|
|
9063
|
+
boundRevision += 1;
|
|
9064
|
+
actionContext = buildActionContext();
|
|
9065
|
+
const nextLifecycles = selectLifecycleNodes(stableMaterialized, prepared.lifecycleIds);
|
|
9066
|
+
reconcileBoundLifecycles(nextLifecycles, new Set([
|
|
9067
|
+
...activeBoundLifecycles.keys(),
|
|
9068
|
+
...nextLifecycles.keys(),
|
|
9069
|
+
]));
|
|
9070
|
+
restoreBoundScrollPositions(container, scrollPositions);
|
|
9071
|
+
restoreBoundMediaStates(container, mediaStates);
|
|
9072
|
+
}
|
|
9073
|
+
function ownerKeysWithoutNestedDuplicates(materialized, owners) {
|
|
9074
|
+
const selected = new Set(owners.map(owner => owner.key));
|
|
9075
|
+
return owners.filter((owner) => {
|
|
9076
|
+
let parentKey = owner.parentKey;
|
|
9077
|
+
while (parentKey) {
|
|
9078
|
+
if (selected.has(parentKey))
|
|
9079
|
+
return false;
|
|
9080
|
+
parentKey = materialized.repeatOwners.get(parentKey)?.parentKey;
|
|
9081
|
+
}
|
|
9082
|
+
return true;
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
function prepareBoundIncremental(schema, draft, requestedOwners, changedSourceIds = new Set()) {
|
|
9086
|
+
if (!currentMaterialized) {
|
|
9087
|
+
throw new BoundPatchPreconditionError('[renderStreamingCard] Bound metadata is unavailable');
|
|
9088
|
+
}
|
|
9089
|
+
const materialized = materializeStreamingCard(schema, draft);
|
|
9090
|
+
const oldNodes = indexBoundNodes(currentMaterialized.root);
|
|
9091
|
+
const nextNodes = indexBoundNodes(materialized.root);
|
|
9092
|
+
const owners = ownerKeysWithoutNestedDuplicates(currentMaterialized, requestedOwners);
|
|
9093
|
+
const ownerPatches = [];
|
|
9094
|
+
const coveredIds = new Set();
|
|
9095
|
+
const lifecycleIds = new Set(activeBoundLifecycles.keys());
|
|
9096
|
+
const replacedLifecycleIds = new Set();
|
|
9097
|
+
for (const owner of owners) {
|
|
9098
|
+
const oldNode = oldNodes.get(owner.runtimeOwnerId);
|
|
9099
|
+
const nextNode = nextNodes.get(owner.runtimeOwnerId);
|
|
9100
|
+
const oldElement = elementMap.get(owner.runtimeOwnerId);
|
|
9101
|
+
if (!oldNode
|
|
9102
|
+
|| !nextNode
|
|
9103
|
+
|| !oldElement
|
|
9104
|
+
|| !oldElement.isConnected
|
|
9105
|
+
|| !oldElement.parentNode) {
|
|
9106
|
+
throw new BoundPatchPreconditionError(`[renderStreamingCard] Missing repeat owner "${owner.runtimeOwnerId}"`);
|
|
9107
|
+
}
|
|
9108
|
+
const indexes = createBoundIndexes();
|
|
9109
|
+
const lifecycles = new Map();
|
|
9110
|
+
const nextElement = renderBoundNode(nextNode, schema, draft, indexes, lifecycles);
|
|
9111
|
+
const oldIds = collectBoundNodeIds(oldNode);
|
|
9112
|
+
const nextIds = collectBoundNodeIds(nextNode);
|
|
9113
|
+
for (const id of oldIds) {
|
|
9114
|
+
lifecycleIds.delete(id);
|
|
9115
|
+
if (activeBoundLifecycles.has(id)) {
|
|
9116
|
+
replacedLifecycleIds.add(id);
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
for (const id of lifecycles.keys()) {
|
|
9120
|
+
lifecycleIds.add(id);
|
|
9121
|
+
replacedLifecycleIds.add(id);
|
|
9122
|
+
}
|
|
9123
|
+
for (const id of nextIds)
|
|
9124
|
+
coveredIds.add(id);
|
|
9125
|
+
ownerPatches.push({
|
|
9126
|
+
oldNode,
|
|
9127
|
+
oldElement,
|
|
9128
|
+
nextNode,
|
|
9129
|
+
nextElement,
|
|
9130
|
+
indexes,
|
|
9131
|
+
lifecycleIds: new Set(lifecycles.keys()),
|
|
9132
|
+
});
|
|
9133
|
+
}
|
|
9134
|
+
const staticPatches = [];
|
|
9135
|
+
for (const nextNode of nextNodes.values()) {
|
|
9136
|
+
if (nextNode.id !== nextNode.sourceId || coveredIds.has(nextNode.id)) {
|
|
9137
|
+
continue;
|
|
9138
|
+
}
|
|
9139
|
+
const oldNode = oldNodes.get(nextNode.id);
|
|
9140
|
+
const oldElement = elementMap.get(nextNode.id);
|
|
9141
|
+
if (!oldNode || !oldElement || !oldElement.isConnected)
|
|
9142
|
+
continue;
|
|
9143
|
+
const nextVisible = computeBoundVisible(nextNode, draft);
|
|
9144
|
+
const nextProps = nextVisible
|
|
9145
|
+
? resolveBoundNodeProps(nextNode, draft)
|
|
9146
|
+
: undefined;
|
|
9147
|
+
const nextFingerprint = nextVisible
|
|
9148
|
+
? boundNodeFingerprint(nextNode, draft, nextProps)
|
|
9149
|
+
: undefined;
|
|
9150
|
+
const wasHidden = hiddenSet.has(nextNode.id);
|
|
9151
|
+
const definitionChanged = changedSourceIds.has(nextNode.sourceId);
|
|
9152
|
+
if (!definitionChanged
|
|
9153
|
+
&& ((!nextVisible && wasHidden)
|
|
9154
|
+
|| (nextVisible && !wasHidden
|
|
9155
|
+
&& propsCache.get(nextNode.id) === nextFingerprint))) {
|
|
9156
|
+
continue;
|
|
9157
|
+
}
|
|
9158
|
+
const indexes = createBoundIndexes();
|
|
9159
|
+
const lifecycles = new Map();
|
|
9160
|
+
const nextElement = renderBoundNode(nextNode, schema, draft, indexes, lifecycles);
|
|
9161
|
+
const oldIds = collectBoundNodeIds(oldNode);
|
|
9162
|
+
const nextIds = collectBoundNodeIds(nextNode);
|
|
9163
|
+
for (const id of oldIds) {
|
|
9164
|
+
lifecycleIds.delete(id);
|
|
9165
|
+
if (activeBoundLifecycles.has(id)) {
|
|
9166
|
+
replacedLifecycleIds.add(id);
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
9169
|
+
for (const id of lifecycles.keys()) {
|
|
9170
|
+
lifecycleIds.add(id);
|
|
9171
|
+
replacedLifecycleIds.add(id);
|
|
9172
|
+
}
|
|
9173
|
+
for (const id of nextIds)
|
|
9174
|
+
coveredIds.add(id);
|
|
9175
|
+
staticPatches.push({
|
|
9176
|
+
oldNode,
|
|
9177
|
+
oldElement,
|
|
9178
|
+
nextNode,
|
|
9179
|
+
nextElement,
|
|
9180
|
+
indexes,
|
|
9181
|
+
lifecycleIds: new Set(lifecycles.keys()),
|
|
9182
|
+
});
|
|
9183
|
+
}
|
|
9184
|
+
return {
|
|
9185
|
+
materialized,
|
|
9186
|
+
owners: ownerPatches,
|
|
9187
|
+
statics: staticPatches,
|
|
9188
|
+
lifecycleIds,
|
|
9189
|
+
replacedLifecycleIds,
|
|
9190
|
+
};
|
|
9191
|
+
}
|
|
9192
|
+
function commitBoundIncremental(prepared, schema, draft, baseRevision, commitAuthoritativeVariables) {
|
|
9193
|
+
assertBoundRevision(baseRevision);
|
|
9194
|
+
commitAuthoritativeVariables?.();
|
|
9195
|
+
replaceRootContents(variables, draft);
|
|
9196
|
+
const stableMaterialized = materializeStreamingCard(schema, variables);
|
|
9197
|
+
const scrollPositions = captureBoundScrollPositions(container);
|
|
9198
|
+
const mediaStates = captureBoundMediaStates(container);
|
|
9199
|
+
for (const patch of prepared.owners) {
|
|
9200
|
+
disposeChartsIn(patch.oldElement);
|
|
9201
|
+
unindexBoundSubtree(patch.oldNode);
|
|
9202
|
+
}
|
|
9203
|
+
for (const patch of prepared.statics) {
|
|
9204
|
+
disposeChartsIn(patch.oldElement);
|
|
9205
|
+
unindexBoundSubtree(patch.oldNode);
|
|
9206
|
+
}
|
|
9207
|
+
for (const patch of prepared.owners) {
|
|
9208
|
+
patch.oldElement.replaceWith(patch.nextElement);
|
|
9209
|
+
mergeProductionIndexes(patch.indexes);
|
|
9210
|
+
}
|
|
9211
|
+
for (const patch of prepared.statics) {
|
|
9212
|
+
patch.oldElement.replaceWith(patch.nextElement);
|
|
9213
|
+
mergeProductionIndexes(patch.indexes);
|
|
9214
|
+
}
|
|
9215
|
+
currentSchema = schema;
|
|
9216
|
+
currentMaterialized = stableMaterialized;
|
|
9217
|
+
boundRevision += 1;
|
|
9218
|
+
actionContext = buildActionContext();
|
|
9219
|
+
reconcileBoundLifecycles(selectLifecycleNodes(stableMaterialized, prepared.lifecycleIds), prepared.replacedLifecycleIds);
|
|
9220
|
+
restoreBoundScrollPositions(container, scrollPositions);
|
|
9221
|
+
restoreBoundMediaStates(container, mediaStates);
|
|
9222
|
+
}
|
|
9223
|
+
function prepareAndCommitBoundUpdate(schema, draft, owners, baseRevision, commitAuthoritativeVariables, changedSourceIds = new Set()) {
|
|
9224
|
+
try {
|
|
9225
|
+
const prepared = prepareBoundIncremental(schema, draft, owners, changedSourceIds);
|
|
9226
|
+
commitBoundIncremental(prepared, schema, draft, baseRevision, commitAuthoritativeVariables);
|
|
9227
|
+
}
|
|
9228
|
+
catch (error) {
|
|
9229
|
+
if (!(error instanceof BoundPatchPreconditionError))
|
|
9230
|
+
throw error;
|
|
9231
|
+
const full = prepareBoundFull(schema, draft);
|
|
9232
|
+
commitBoundFull(full, schema, draft, baseRevision, commitAuthoritativeVariables);
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
6274
9235
|
// ─── Action Steps (A2UI `action` support) ───────────────────────
|
|
6275
9236
|
/**
|
|
6276
9237
|
* Run an action-step chain. Steps of type `action` are emitted through
|
|
@@ -6305,6 +9266,174 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6305
9266
|
}
|
|
6306
9267
|
}
|
|
6307
9268
|
}
|
|
9269
|
+
function writeDraftVariable(draft, key, value) {
|
|
9270
|
+
Object.defineProperty(draft, String(key), {
|
|
9271
|
+
value,
|
|
9272
|
+
enumerable: true,
|
|
9273
|
+
configurable: true,
|
|
9274
|
+
writable: true,
|
|
9275
|
+
});
|
|
9276
|
+
}
|
|
9277
|
+
function createBoundActionContext(node, draft) {
|
|
9278
|
+
const write = (key, value) => {
|
|
9279
|
+
writeDraftVariable(draft, key, value);
|
|
9280
|
+
};
|
|
9281
|
+
return {
|
|
9282
|
+
...createWebActionContext({
|
|
9283
|
+
...options,
|
|
9284
|
+
setVariable: write,
|
|
9285
|
+
abortSignal: abortController.signal,
|
|
9286
|
+
}),
|
|
9287
|
+
variables: draft,
|
|
9288
|
+
expressionContext: createExpressionContext(node.scope),
|
|
9289
|
+
parameterResolver: node.bindingDialect === 'a2ui'
|
|
9290
|
+
? createA2UIParameterResolver(draft, node.dataPath)
|
|
9291
|
+
: undefined,
|
|
9292
|
+
variableWriter: (key, value) => write(key, value),
|
|
9293
|
+
botId: options.botId,
|
|
9294
|
+
inflightRequests,
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9297
|
+
function resolveActionParams(params, context) {
|
|
9298
|
+
if (context.parameterResolver) {
|
|
9299
|
+
return context.parameterResolver(params);
|
|
9300
|
+
}
|
|
9301
|
+
const resolutionContext = (context.expressionContext ?? context.variables);
|
|
9302
|
+
return resolutionContext
|
|
9303
|
+
? resolveDeep(params, resolutionContext)
|
|
9304
|
+
: params;
|
|
9305
|
+
}
|
|
9306
|
+
async function runBoundSteps(steps, sourceNode, context) {
|
|
9307
|
+
for (const step of steps) {
|
|
9308
|
+
if (step.type !== 'action') {
|
|
9309
|
+
await runActionStep(step, context);
|
|
9310
|
+
continue;
|
|
9311
|
+
}
|
|
9312
|
+
const params = resolveActionParams(step.params ?? {}, context);
|
|
9313
|
+
const payload = {
|
|
9314
|
+
name: typeof params.name === 'string' ? params.name : 'action',
|
|
9315
|
+
surfaceId: currentSurfaceId,
|
|
9316
|
+
sourceComponentId: sourceNode.sourceId,
|
|
9317
|
+
timestamp: new Date().toISOString(),
|
|
9318
|
+
context: (params.context && typeof params.context === 'object')
|
|
9319
|
+
? params.context
|
|
9320
|
+
: {},
|
|
9321
|
+
};
|
|
9322
|
+
try {
|
|
9323
|
+
options.onAction?.(payload);
|
|
9324
|
+
if (step.onSuccess) {
|
|
9325
|
+
await runBoundSteps(step.onSuccess, sourceNode, context);
|
|
9326
|
+
}
|
|
9327
|
+
}
|
|
9328
|
+
catch (error) {
|
|
9329
|
+
if (step.onFail) {
|
|
9330
|
+
await runBoundSteps(step.onFail, sourceNode, context);
|
|
9331
|
+
}
|
|
9332
|
+
else {
|
|
9333
|
+
throw error;
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
function topLevelChangedPaths(before, after) {
|
|
9339
|
+
const keys = new Set([
|
|
9340
|
+
...Object.keys(before),
|
|
9341
|
+
...Object.keys(after),
|
|
9342
|
+
]);
|
|
9343
|
+
const paths = [];
|
|
9344
|
+
for (const key of keys) {
|
|
9345
|
+
let equal = false;
|
|
9346
|
+
try {
|
|
9347
|
+
equal = JSON.stringify(before[key]) === JSON.stringify(after[key]);
|
|
9348
|
+
}
|
|
9349
|
+
catch {
|
|
9350
|
+
equal = false;
|
|
9351
|
+
}
|
|
9352
|
+
if (!equal) {
|
|
9353
|
+
paths.push(`/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
return paths;
|
|
9357
|
+
}
|
|
9358
|
+
function affectedOwnersForPaths(materialized, paths) {
|
|
9359
|
+
const owners = new Map();
|
|
9360
|
+
for (const path of paths) {
|
|
9361
|
+
for (const owner of findAffectedRepeatOwners(materialized, path)) {
|
|
9362
|
+
owners.set(owner.key, owner);
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
return ownerKeysWithoutNestedDuplicates(materialized, [...owners.values()]);
|
|
9366
|
+
}
|
|
9367
|
+
function commitBoundDraftTransaction(before, draft, baseRevision) {
|
|
9368
|
+
assertBoundRevision(baseRevision);
|
|
9369
|
+
if (disposed
|
|
9370
|
+
|| !currentMaterialized
|
|
9371
|
+
|| !currentSchema
|
|
9372
|
+
|| !currentSurfaceId) {
|
|
9373
|
+
const error = new Error('[renderStreamingCard] BOUND_TRANSACTION_CONFLICT');
|
|
9374
|
+
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
9375
|
+
throw error;
|
|
9376
|
+
}
|
|
9377
|
+
const changedPaths = topLevelChangedPaths(before, draft);
|
|
9378
|
+
if (changedPaths.length === 0)
|
|
9379
|
+
return;
|
|
9380
|
+
const owners = currentMaterialized.unresolvedRepeatOwners.size > 0
|
|
9381
|
+
? []
|
|
9382
|
+
: affectedOwnersForPaths(currentMaterialized, changedPaths);
|
|
9383
|
+
const schema = currentSchema;
|
|
9384
|
+
const surfaceId = currentSurfaceId;
|
|
9385
|
+
const commitAuthoritative = () => {
|
|
9386
|
+
const authoritative = engine.getSchema(surfaceId);
|
|
9387
|
+
if (!authoritative || authoritative !== schema) {
|
|
9388
|
+
const error = new Error('[renderStreamingCard] BOUND_TRANSACTION_CONFLICT');
|
|
9389
|
+
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
9390
|
+
throw error;
|
|
9391
|
+
}
|
|
9392
|
+
replaceRootContents(authoritative.variables, draft);
|
|
9393
|
+
};
|
|
9394
|
+
if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
|
|
9395
|
+
const full = prepareBoundFull(schema, draft);
|
|
9396
|
+
commitBoundFull(full, schema, draft, baseRevision, commitAuthoritative);
|
|
9397
|
+
return;
|
|
9398
|
+
}
|
|
9399
|
+
prepareAndCommitBoundUpdate(schema, draft, owners, baseRevision, commitAuthoritative);
|
|
9400
|
+
}
|
|
9401
|
+
async function runBoundEvent(runtimeId, eventName, eventDetail) {
|
|
9402
|
+
if (!currentMaterialized || !currentSchema || !currentSurfaceId)
|
|
9403
|
+
return;
|
|
9404
|
+
const baseRevision = boundRevision;
|
|
9405
|
+
const before = cloneJsonData(variables);
|
|
9406
|
+
const draft = cloneJsonData(variables);
|
|
9407
|
+
if (eventDetail !== undefined) {
|
|
9408
|
+
writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
|
|
9409
|
+
}
|
|
9410
|
+
const fresh = materializeStreamingCard(currentSchema, draft);
|
|
9411
|
+
const sourceNode = indexBoundNodes(fresh.root).get(runtimeId);
|
|
9412
|
+
if (!sourceNode) {
|
|
9413
|
+
throw new Error(`[renderStreamingCard] Bound runtime node "${runtimeId}" no longer exists`);
|
|
9414
|
+
}
|
|
9415
|
+
const eventValue = sourceNode.events?.[eventName];
|
|
9416
|
+
const steps = eventValue
|
|
9417
|
+
? resolveActionRef(eventValue, currentSchema.actions ?? {})
|
|
9418
|
+
: undefined;
|
|
9419
|
+
if (!steps)
|
|
9420
|
+
return;
|
|
9421
|
+
await runBoundSteps(steps, sourceNode, createBoundActionContext(sourceNode, draft));
|
|
9422
|
+
commitBoundDraftTransaction(before, draft, baseRevision);
|
|
9423
|
+
}
|
|
9424
|
+
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
9425
|
+
boundActionQueue = boundActionQueue
|
|
9426
|
+
.then(() => runBoundEvent(runtimeId, eventName, eventDetail))
|
|
9427
|
+
.catch((error) => {
|
|
9428
|
+
if (error
|
|
9429
|
+
&& typeof error === 'object'
|
|
9430
|
+
&& error.code
|
|
9431
|
+
=== 'BOUND_TRANSACTION_CONFLICT') {
|
|
9432
|
+
return;
|
|
9433
|
+
}
|
|
9434
|
+
console.error('[renderStreamingCard] Bound action failed', error);
|
|
9435
|
+
});
|
|
9436
|
+
}
|
|
6308
9437
|
// ─── Partial-Schema Progressive Rendering ───────────────────────
|
|
6309
9438
|
/** Lock the instance to one feeding style; mixing the two corrupts state. */
|
|
6310
9439
|
function lockMode(mode) {
|
|
@@ -6327,6 +9456,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6327
9456
|
if (slot.groups)
|
|
6328
9457
|
for (const g of slot.groups)
|
|
6329
9458
|
out.push(...g);
|
|
9459
|
+
if (slot.repeat?.template)
|
|
9460
|
+
out.push(slot.repeat.template);
|
|
6330
9461
|
const overlays = slot.config?.overlays;
|
|
6331
9462
|
if (Array.isArray(overlays)) {
|
|
6332
9463
|
for (const o of overlays)
|
|
@@ -6372,6 +9503,9 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6372
9503
|
return root;
|
|
6373
9504
|
const prunedSlots = {};
|
|
6374
9505
|
for (const [k, slot] of Object.entries(slots)) {
|
|
9506
|
+
if (slot.repeat?.template && !mountable.has(slot.repeat.template)) {
|
|
9507
|
+
continue;
|
|
9508
|
+
}
|
|
6375
9509
|
const s = { ...slot };
|
|
6376
9510
|
if (s.children)
|
|
6377
9511
|
s.children = s.children.filter((id) => mountable.has(id));
|
|
@@ -6395,7 +9529,16 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6395
9529
|
if (!root)
|
|
6396
9530
|
return; // root 未到齐 —— 首块出现前的占位由宿主负责
|
|
6397
9531
|
if (!partialSurfaceCreated) {
|
|
6398
|
-
engine.apply({
|
|
9532
|
+
engine.apply({
|
|
9533
|
+
type: 'createSurface',
|
|
9534
|
+
surfaceId: PARTIAL_SURFACE_ID,
|
|
9535
|
+
schema: {
|
|
9536
|
+
version: result.version ?? '1.0',
|
|
9537
|
+
rootID,
|
|
9538
|
+
variables: {},
|
|
9539
|
+
elements: {},
|
|
9540
|
+
},
|
|
9541
|
+
});
|
|
6399
9542
|
partialSurfaceCreated = true;
|
|
6400
9543
|
}
|
|
6401
9544
|
if (!partialVariablesSent && result.variables && Object.keys(result.variables).length > 0) {
|
|
@@ -6461,6 +9604,16 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6461
9604
|
if (!schema)
|
|
6462
9605
|
return;
|
|
6463
9606
|
currentSchema = schema; // keep local ref pointing at the live engine schema
|
|
9607
|
+
if (requiresBindingMaterialization(schema)) {
|
|
9608
|
+
const baseRevision = boundRevision;
|
|
9609
|
+
const draft = cloneJsonData(variables);
|
|
9610
|
+
const prepared = prepareBoundFull(schema, draft);
|
|
9611
|
+
commitBoundFull(prepared, schema, draft, baseRevision, () => replaceRootContents(schema.variables, draft));
|
|
9612
|
+
return;
|
|
9613
|
+
}
|
|
9614
|
+
currentMaterialized = null;
|
|
9615
|
+
sourceOccurrences.clear();
|
|
9616
|
+
teardownBoundLifecycles();
|
|
6464
9617
|
disposeChartsIn(container); // release old ECharts instances before clearing
|
|
6465
9618
|
container.innerHTML = '';
|
|
6466
9619
|
elementMap.clear();
|
|
@@ -6482,6 +9635,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6482
9635
|
renderFull();
|
|
6483
9636
|
}
|
|
6484
9637
|
catch (error) {
|
|
9638
|
+
if (error instanceof InvalidBoundSchemaError)
|
|
9639
|
+
throw error;
|
|
6485
9640
|
console.warn('[renderStreamingCard] Deferred render (incomplete schema):', error);
|
|
6486
9641
|
}
|
|
6487
9642
|
}
|
|
@@ -6496,6 +9651,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6496
9651
|
// ─── Engine Event Handlers ──────────────────────────────────────
|
|
6497
9652
|
const engine = new StreamingEngine({
|
|
6498
9653
|
onSurfaceCreated(surfaceId, schemaInput) {
|
|
9654
|
+
teardownBoundLifecycles();
|
|
9655
|
+
boundRevision += 1;
|
|
6499
9656
|
currentSurfaceId = surfaceId;
|
|
6500
9657
|
if (schemaInput) {
|
|
6501
9658
|
currentSchema = normalizeSchema(schemaInput);
|
|
@@ -6505,6 +9662,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6505
9662
|
else {
|
|
6506
9663
|
// Empty surface (A2UI-style): structure arrives via updateComponents
|
|
6507
9664
|
currentSchema = null;
|
|
9665
|
+
currentMaterialized = null;
|
|
9666
|
+
sourceOccurrences.clear();
|
|
6508
9667
|
variables = { ...options.variables };
|
|
6509
9668
|
disposeChartsIn(container); // release old ECharts instances before clearing
|
|
6510
9669
|
container.innerHTML = '';
|
|
@@ -6517,6 +9676,71 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6517
9676
|
const schema = engine.getSchema(surfaceId);
|
|
6518
9677
|
if (!schema)
|
|
6519
9678
|
return;
|
|
9679
|
+
const previousSchema = currentSchema;
|
|
9680
|
+
const previousBound = currentMaterialized !== null
|
|
9681
|
+
|| (previousSchema !== null
|
|
9682
|
+
&& requiresBindingMaterialization(previousSchema));
|
|
9683
|
+
const nextBound = requiresBindingMaterialization(schema);
|
|
9684
|
+
if (previousBound || nextBound) {
|
|
9685
|
+
const baseRevision = boundRevision;
|
|
9686
|
+
if (!nextBound) {
|
|
9687
|
+
teardownBoundLifecycles();
|
|
9688
|
+
variables = {
|
|
9689
|
+
...schema.variables,
|
|
9690
|
+
...variables,
|
|
9691
|
+
};
|
|
9692
|
+
currentSchema = schema;
|
|
9693
|
+
currentMaterialized = null;
|
|
9694
|
+
boundRevision += 1;
|
|
9695
|
+
renderFull();
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
const draft = cloneJsonData(currentMaterialized
|
|
9699
|
+
? schema.variables
|
|
9700
|
+
: {
|
|
9701
|
+
...schema.variables,
|
|
9702
|
+
...variables,
|
|
9703
|
+
});
|
|
9704
|
+
const previousFingerprint = previousSchema
|
|
9705
|
+
? bindingTopologyFingerprint(previousSchema)
|
|
9706
|
+
: '';
|
|
9707
|
+
const nextFingerprint = bindingTopologyFingerprint(schema);
|
|
9708
|
+
const topologyChanged = (!previousBound
|
|
9709
|
+
|| previousFingerprint !== nextFingerprint);
|
|
9710
|
+
const rootMissing = (!elementMap.get(schema.rootID)?.isConnected);
|
|
9711
|
+
const commitAuthoritative = () => {
|
|
9712
|
+
replaceRootContents(schema.variables, draft);
|
|
9713
|
+
};
|
|
9714
|
+
try {
|
|
9715
|
+
if (!currentMaterialized
|
|
9716
|
+
|| topologyChanged
|
|
9717
|
+
|| rootMissing) {
|
|
9718
|
+
const full = prepareBoundFull(schema, draft);
|
|
9719
|
+
commitBoundFull(full, schema, draft, baseRevision, commitAuthoritative);
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
9722
|
+
const changedSourceIds = changes.map(change => change.elementId);
|
|
9723
|
+
const hasTemplateDependency = changedSourceIds.some(sourceId => currentMaterialized.dependencies.has(`component:${sourceId}`));
|
|
9724
|
+
const owners = findTemplateRepeatOwners(currentMaterialized, changedSourceIds);
|
|
9725
|
+
if (hasTemplateDependency && owners.length === 0) {
|
|
9726
|
+
const full = prepareBoundFull(schema, draft);
|
|
9727
|
+
commitBoundFull(full, schema, draft, baseRevision, commitAuthoritative);
|
|
9728
|
+
return;
|
|
9729
|
+
}
|
|
9730
|
+
prepareAndCommitBoundUpdate(schema, draft, owners, baseRevision, commitAuthoritative, new Set(changedSourceIds));
|
|
9731
|
+
}
|
|
9732
|
+
catch (error) {
|
|
9733
|
+
if (error instanceof IncompleteBoundSchemaError) {
|
|
9734
|
+
// Progressive component streams keep their authoritative candidate
|
|
9735
|
+
// until the missing template/descendant arrives.
|
|
9736
|
+
currentSchema = schema;
|
|
9737
|
+
boundRevision += 1;
|
|
9738
|
+
return;
|
|
9739
|
+
}
|
|
9740
|
+
throw error;
|
|
9741
|
+
}
|
|
9742
|
+
return;
|
|
9743
|
+
}
|
|
6520
9744
|
currentSchema = schema;
|
|
6521
9745
|
schema.actions ?? {};
|
|
6522
9746
|
let needFull = false;
|
|
@@ -6631,9 +9855,64 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6631
9855
|
if (needFull)
|
|
6632
9856
|
safeRenderFull();
|
|
6633
9857
|
},
|
|
6634
|
-
onDataModelUpdated(surfaceId, path, value) {
|
|
9858
|
+
onDataModelUpdated(surfaceId, path, value, pathDialect) {
|
|
6635
9859
|
if (!currentSchema && !engine.getSchema(surfaceId))
|
|
6636
9860
|
return;
|
|
9861
|
+
const candidateSchema = engine.getSchema(surfaceId);
|
|
9862
|
+
if (candidateSchema
|
|
9863
|
+
&& (currentMaterialized !== null
|
|
9864
|
+
|| requiresBindingMaterialization(candidateSchema))) {
|
|
9865
|
+
const baseRevision = boundRevision;
|
|
9866
|
+
const draft = cloneJsonData(currentMaterialized
|
|
9867
|
+
? candidateSchema.variables
|
|
9868
|
+
: {
|
|
9869
|
+
...candidateSchema.variables,
|
|
9870
|
+
...variables,
|
|
9871
|
+
});
|
|
9872
|
+
const commitAuthoritative = () => {
|
|
9873
|
+
replaceRootContents(candidateSchema.variables, draft);
|
|
9874
|
+
};
|
|
9875
|
+
try {
|
|
9876
|
+
if (!currentMaterialized) {
|
|
9877
|
+
const full = prepareBoundFull(candidateSchema, draft);
|
|
9878
|
+
commitBoundFull(full, candidateSchema, draft, baseRevision, commitAuthoritative);
|
|
9879
|
+
return;
|
|
9880
|
+
}
|
|
9881
|
+
if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
|
|
9882
|
+
const full = prepareBoundFull(candidateSchema, draft);
|
|
9883
|
+
commitBoundFull(full, candidateSchema, draft, baseRevision, commitAuthoritative);
|
|
9884
|
+
return;
|
|
9885
|
+
}
|
|
9886
|
+
const normalizedPath = pathDialect === 'a2ui'
|
|
9887
|
+
? path
|
|
9888
|
+
: path === '/' || path === ''
|
|
9889
|
+
? ''
|
|
9890
|
+
: `/${path.replace(/^\//, '')
|
|
9891
|
+
.split('/')
|
|
9892
|
+
.filter(Boolean)
|
|
9893
|
+
.join('/')}`;
|
|
9894
|
+
const owners = findAffectedRepeatOwners(currentMaterialized, normalizedPath);
|
|
9895
|
+
prepareAndCommitBoundUpdate(candidateSchema, draft, owners, baseRevision, commitAuthoritative);
|
|
9896
|
+
}
|
|
9897
|
+
catch (error) {
|
|
9898
|
+
if (error instanceof IncompleteBoundSchemaError) {
|
|
9899
|
+
currentSchema = candidateSchema;
|
|
9900
|
+
boundRevision += 1;
|
|
9901
|
+
return;
|
|
9902
|
+
}
|
|
9903
|
+
throw error;
|
|
9904
|
+
}
|
|
9905
|
+
return;
|
|
9906
|
+
}
|
|
9907
|
+
if (pathDialect === 'a2ui') {
|
|
9908
|
+
const authoritative = engine.getSchema(surfaceId);
|
|
9909
|
+
if (authoritative) {
|
|
9910
|
+
variables = cloneJsonData(authoritative.variables);
|
|
9911
|
+
actionContext = buildActionContext();
|
|
9912
|
+
}
|
|
9913
|
+
diffAllElements();
|
|
9914
|
+
return;
|
|
9915
|
+
}
|
|
6637
9916
|
// Update the local variables copy (JSON Pointer-style path).
|
|
6638
9917
|
// Mirror the engine's setByPath prototype-pollution guard: reject any
|
|
6639
9918
|
// __proto__ / constructor / prototype segment (stream is untrusted).
|
|
@@ -6666,6 +9945,18 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6666
9945
|
},
|
|
6667
9946
|
onContentAppended(surfaceId, elementId, content) {
|
|
6668
9947
|
const el = elementMap.get(elementId);
|
|
9948
|
+
if (currentMaterialized && currentSchema && el) {
|
|
9949
|
+
const nextMaterialized = materializeStreamingCard(currentSchema, variables);
|
|
9950
|
+
const node = indexBoundNodes(nextMaterialized.root).get(elementId);
|
|
9951
|
+
if (node && node.id === node.sourceId && 'updateProps' in el) {
|
|
9952
|
+
const resolved = resolveBoundNodeProps(node, variables);
|
|
9953
|
+
el.updateProps(resolved, isMobile);
|
|
9954
|
+
propsCache.set(elementId, boundNodeFingerprint(node, variables, resolved));
|
|
9955
|
+
currentMaterialized = nextMaterialized;
|
|
9956
|
+
boundRevision += 1;
|
|
9957
|
+
return;
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
6669
9960
|
if (el && 'updateProps' in el) {
|
|
6670
9961
|
// Append content to Text component via updateProps
|
|
6671
9962
|
const currentProps = el._props ?? {};
|
|
@@ -6694,6 +9985,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6694
9985
|
hiddenSet.clear();
|
|
6695
9986
|
currentSchema = null;
|
|
6696
9987
|
currentSurfaceId = null;
|
|
9988
|
+
currentMaterialized = null;
|
|
9989
|
+
sourceOccurrences.clear();
|
|
9990
|
+
teardownBoundLifecycles();
|
|
9991
|
+
boundRevision += 1;
|
|
6697
9992
|
lifecycleManager.dispose(actionContext);
|
|
6698
9993
|
},
|
|
6699
9994
|
});
|
|
@@ -6735,18 +10030,55 @@ function renderStreamingCard(container, options = {}) {
|
|
|
6735
10030
|
return surfaceId ? engine.getSchema(surfaceId) : currentSchema ?? undefined;
|
|
6736
10031
|
},
|
|
6737
10032
|
updateVariables(newVars) {
|
|
10033
|
+
if (currentMaterialized && currentSchema && currentSurfaceId) {
|
|
10034
|
+
const baseRevision = boundRevision;
|
|
10035
|
+
const before = cloneJsonData(variables);
|
|
10036
|
+
const draft = cloneJsonData(variables);
|
|
10037
|
+
const patch = cloneJsonData(newVars);
|
|
10038
|
+
for (const key of Object.keys(patch)) {
|
|
10039
|
+
writeDraftVariable(draft, key, patch[key]);
|
|
10040
|
+
}
|
|
10041
|
+
const changedPaths = topLevelChangedPaths(before, draft);
|
|
10042
|
+
const owners = currentMaterialized.unresolvedRepeatOwners.size > 0
|
|
10043
|
+
? []
|
|
10044
|
+
: affectedOwnersForPaths(currentMaterialized, changedPaths);
|
|
10045
|
+
const schema = currentSchema;
|
|
10046
|
+
const commitAuthoritative = () => {
|
|
10047
|
+
const authoritative = engine.getSchema(currentSurfaceId);
|
|
10048
|
+
if (!authoritative || authoritative !== schema) {
|
|
10049
|
+
const error = new Error('[renderStreamingCard] BOUND_TRANSACTION_CONFLICT');
|
|
10050
|
+
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
10051
|
+
throw error;
|
|
10052
|
+
}
|
|
10053
|
+
replaceRootContents(authoritative.variables, draft);
|
|
10054
|
+
};
|
|
10055
|
+
if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
|
|
10056
|
+
const full = prepareBoundFull(schema, draft);
|
|
10057
|
+
commitBoundFull(full, schema, draft, baseRevision, commitAuthoritative);
|
|
10058
|
+
}
|
|
10059
|
+
else {
|
|
10060
|
+
prepareAndCommitBoundUpdate(schema, draft, owners, baseRevision, commitAuthoritative);
|
|
10061
|
+
}
|
|
10062
|
+
return;
|
|
10063
|
+
}
|
|
6738
10064
|
variables = { ...variables, ...newVars };
|
|
6739
10065
|
if (currentSchema || (currentSurfaceId && engine.getSchema(currentSurfaceId))) {
|
|
6740
10066
|
diffAllElements();
|
|
6741
10067
|
}
|
|
6742
10068
|
},
|
|
6743
10069
|
dispose() {
|
|
10070
|
+
if (disposed)
|
|
10071
|
+
return;
|
|
10072
|
+
disposed = true;
|
|
10073
|
+
boundRevision += 1;
|
|
6744
10074
|
abortController.abort();
|
|
6745
10075
|
removeViewportListener();
|
|
10076
|
+
teardownBoundLifecycles();
|
|
6746
10077
|
lifecycleManager.dispose(actionContext);
|
|
6747
10078
|
disposeChartsIn(container); // release ECharts instances before clearing
|
|
6748
10079
|
container.innerHTML = '';
|
|
6749
10080
|
elementMap.clear();
|
|
10081
|
+
sourceOccurrences.clear();
|
|
6750
10082
|
engine.dispose();
|
|
6751
10083
|
parser.reset();
|
|
6752
10084
|
},
|
|
@@ -7090,4 +10422,4 @@ class RemoteActionConfigProvider {
|
|
|
7090
10422
|
}
|
|
7091
10423
|
}
|
|
7092
10424
|
|
|
7093
|
-
export { BaseElement, BotSDK, CardButton, CardCollapse, CardDivider, CardForm, CardHtml, CardIcon, CardImage, CardInput, CardLoading, CardPasscodeInput, CardProgress, CardRate, CardSelect, CardSteps, CardTag, CardText, LocalActionConfigProvider, RemoteActionConfigProvider, buildStyleString, componentRenderers, connectSSE, connectStreaming, createWebActionContext, isMobileViewport, onViewportChange, pxToRem, pxToVw, registerComponent, renderCard, renderStreamingCard, resolveSize, sanitizeHtml, trimIncompleteTag };
|
|
10425
|
+
export { BaseElement, BotSDK, CardButton, CardChoiceItem, CardChoiceList, CardCollapse, CardCounter, CardDivider, CardForm, CardHtml, CardIcon, CardImage, CardInput, CardLoading, CardPasscodeInput, CardProgress, CardRate, CardSelect, CardSteps, CardSwitch, CardTag, CardText, LocalActionConfigProvider, RemoteActionConfigProvider, buildStyleString, componentRenderers, connectSSE, connectStreaming, createWebActionContext, isMobileViewport, onViewportChange, pxToRem, pxToVw, registerComponent, renderCard, renderStreamingCard, resolveSize, sanitizeHtml, trimIncompleteTag };
|