@antglobal/copilot-cards-web 1.0.5 → 1.0.7
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 +60 -8
- package/dist/index.js +3358 -579
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, createA2UIParameterResolver, createExpressionContext,
|
|
1
|
+
import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, findAffectedRepeatOwners, createA2UIParameterResolver, createExpressionContext, replaceRootContents, resolveA2UIDeep, resolveDeep, resolveExpressionValue, resolveActionRef, isBoundRenderTreeNode, hasExpression, resolveExpression, runActionSteps, normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, StreamingParser, StreamingEngine, 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';
|
|
@@ -332,11 +332,21 @@ function disconnectChartResize(chartRoot) {
|
|
|
332
332
|
const registration = chartResizeRegistrations.get(chartRoot);
|
|
333
333
|
if (!registration)
|
|
334
334
|
return;
|
|
335
|
-
|
|
335
|
+
chartResizeRegistrations.delete(chartRoot);
|
|
336
|
+
try {
|
|
337
|
+
registration.observer.disconnect();
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
console.error('[renderCard] Chart observer cleanup failed', error);
|
|
341
|
+
}
|
|
336
342
|
if (registration.frameID !== null) {
|
|
337
|
-
|
|
343
|
+
try {
|
|
344
|
+
cancelAnimationFrame(registration.frameID);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error('[renderCard] Chart frame cleanup failed', error);
|
|
348
|
+
}
|
|
338
349
|
}
|
|
339
|
-
chartResizeRegistrations.delete(chartRoot);
|
|
340
350
|
}
|
|
341
351
|
function observeChartResize(chartRoot, chart) {
|
|
342
352
|
disconnectChartResize(chartRoot);
|
|
@@ -376,12 +386,20 @@ function observeChartResize(chartRoot, chart) {
|
|
|
376
386
|
* host node, so skipping this leaks one instance per chart per re-render.
|
|
377
387
|
*/
|
|
378
388
|
function disposeChartsIn(container) {
|
|
379
|
-
const roots =
|
|
389
|
+
const roots = [];
|
|
390
|
+
if (container.hasAttribute?.(CHART_ROOT_ATTR))
|
|
391
|
+
roots.push(container);
|
|
392
|
+
roots.push(...container.querySelectorAll(`[${CHART_ROOT_ATTR}]`));
|
|
380
393
|
roots.forEach((node) => {
|
|
381
394
|
disconnectChartResize(node);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
inst
|
|
395
|
+
try {
|
|
396
|
+
const inst = echarts.getInstanceByDom(node);
|
|
397
|
+
if (inst)
|
|
398
|
+
inst.dispose();
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.error('[renderCard] Chart instance cleanup failed', error);
|
|
402
|
+
}
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
function renderChartSlot(container, slotContent, actionContext) {
|
|
@@ -706,6 +724,8 @@ function buildHeatmapOption(config) {
|
|
|
706
724
|
*/
|
|
707
725
|
// ─── Slot Layout Constants ──────────────────────────────────────
|
|
708
726
|
const SLOT_LAYOUT = {
|
|
727
|
+
DEFAULT: 'default',
|
|
728
|
+
FLEX: 'flex',
|
|
709
729
|
COLUMNS: 'columns',
|
|
710
730
|
GRID: 'grid',
|
|
711
731
|
HORIZONTAL_SCROLL: 'horizontalScroll',
|
|
@@ -716,16 +736,411 @@ const SLOT_LAYOUT = {
|
|
|
716
736
|
TABLE: 'table',
|
|
717
737
|
CHART: 'chart',
|
|
718
738
|
};
|
|
739
|
+
const EMPTY_HOST_PRESENTATION_OWNERSHIP = {
|
|
740
|
+
attributes: [],
|
|
741
|
+
styleProperties: [],
|
|
742
|
+
};
|
|
743
|
+
const CAROUSEL_SCROLL_ID_ATTRIBUTE = 'data-scroll-id';
|
|
744
|
+
function carouselHostStyles(gap) {
|
|
745
|
+
return {
|
|
746
|
+
display: 'flex',
|
|
747
|
+
flexDirection: 'row',
|
|
748
|
+
overflowX: 'auto',
|
|
749
|
+
scrollSnapType: 'x mandatory',
|
|
750
|
+
WebkitOverflowScrolling: 'touch',
|
|
751
|
+
gap: `${gap}px`,
|
|
752
|
+
paddingTop: '16px',
|
|
753
|
+
paddingBottom: '16px',
|
|
754
|
+
scrollbarWidth: 'none',
|
|
755
|
+
cursor: 'grab',
|
|
756
|
+
position: 'relative',
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function toCSSPropertyName(property) {
|
|
760
|
+
return property.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
|
761
|
+
}
|
|
762
|
+
const CAROUSEL_HOST_PRESENTATION_OWNERSHIP = {
|
|
763
|
+
attributes: [CAROUSEL_SCROLL_ID_ATTRIBUTE],
|
|
764
|
+
styleProperties: Object.keys(carouselHostStyles(0)).map(toCSSPropertyName),
|
|
765
|
+
};
|
|
766
|
+
function findSpecialSlotKey(props) {
|
|
767
|
+
const slots = props.slots;
|
|
768
|
+
return slots && Object.keys(slots).find(key => key !== SLOT_LAYOUT.DEFAULT);
|
|
769
|
+
}
|
|
770
|
+
/** Host attributes/styles owned by the active slot renderer after shell render. */
|
|
771
|
+
function getSlotHostPresentationOwnership(props) {
|
|
772
|
+
return findSpecialSlotKey(props) === SLOT_LAYOUT.CAROUSEL
|
|
773
|
+
? CAROUSEL_HOST_PRESENTATION_OWNERSHIP
|
|
774
|
+
: EMPTY_HOST_PRESENTATION_OWNERSHIP;
|
|
775
|
+
}
|
|
776
|
+
/** Resolve the exact carousel values consumed by the renderer. */
|
|
777
|
+
function resolveCarouselConfig(props, childCount) {
|
|
778
|
+
const slotContent = props.slots?.[SLOT_LAYOUT.CAROUSEL];
|
|
779
|
+
const config = slotContent?.config ?? {};
|
|
780
|
+
const itemWidthPx = config.itemWidthPx;
|
|
781
|
+
return {
|
|
782
|
+
scale: config.scale ?? props.carouselScale ?? 0.85,
|
|
783
|
+
opacity: config.opacity ?? props.carouselOpacity ?? 0.4,
|
|
784
|
+
overlayColor: config.overlayColor
|
|
785
|
+
?? props.carouselOverlayColor
|
|
786
|
+
?? 'rgba(255,255,255,0.5)',
|
|
787
|
+
gap: config.gap ?? props.carouselGap ?? 16,
|
|
788
|
+
itemWidthPercent: config.itemWidthPercent
|
|
789
|
+
?? props.carouselItemWidth
|
|
790
|
+
?? 70,
|
|
791
|
+
itemWidthPx: itemWidthPx ? itemWidthPx : null,
|
|
792
|
+
initialIndex: Math.max(0, Math.min(childCount - 1, config.initialIndex ?? props.carouselInitialIndex ?? 0)),
|
|
793
|
+
autoplay: config.autoplay ?? props.carouselAutoplay ?? false,
|
|
794
|
+
autoplayInterval: config.autoplayInterval
|
|
795
|
+
?? props.carouselAutoplayInterval
|
|
796
|
+
?? 3000,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function carouselNumericFingerprint(value) {
|
|
800
|
+
const numeric = Number(value);
|
|
801
|
+
return Number.isFinite(numeric) ? numeric : String(numeric);
|
|
802
|
+
}
|
|
803
|
+
function carouselLayoutFingerprint(config) {
|
|
804
|
+
const autoplay = Boolean(config.autoplay);
|
|
805
|
+
return {
|
|
806
|
+
scale: carouselNumericFingerprint(config.scale),
|
|
807
|
+
opacity: carouselNumericFingerprint(config.opacity),
|
|
808
|
+
overlayColor: String(config.overlayColor),
|
|
809
|
+
gap: `${config.gap}px`,
|
|
810
|
+
itemBasis: config.itemWidthPx
|
|
811
|
+
? `${config.itemWidthPx}px`
|
|
812
|
+
: `${config.itemWidthPercent}%`,
|
|
813
|
+
initialIndex: carouselNumericFingerprint(config.initialIndex),
|
|
814
|
+
autoplay,
|
|
815
|
+
autoplayInterval: autoplay
|
|
816
|
+
? carouselNumericFingerprint(config.autoplayInterval)
|
|
817
|
+
: null,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/** JSON-like layout dependencies used by the static incremental boundary. */
|
|
821
|
+
function getSlotLayoutFingerprintInput(props, children, resolveChildProps) {
|
|
822
|
+
const slotKey = findSpecialSlotKey(props);
|
|
823
|
+
if (slotKey === SLOT_LAYOUT.CAROUSEL) {
|
|
824
|
+
return {
|
|
825
|
+
slotKey,
|
|
826
|
+
childOccurrences: children.map((child, index) => ({
|
|
827
|
+
index,
|
|
828
|
+
id: child.id,
|
|
829
|
+
})),
|
|
830
|
+
carousel: carouselLayoutFingerprint(resolveCarouselConfig(props, children.length)),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const input = {
|
|
834
|
+
childIds: children.map(child => child.id),
|
|
835
|
+
slots: props.slots ?? null,
|
|
836
|
+
};
|
|
837
|
+
if (slotKey === SLOT_LAYOUT.GRID) {
|
|
838
|
+
input.gridPlacements = children.map((child, index) => {
|
|
839
|
+
const placement = gridPlacementStyles(resolveChildProps?.(child) ?? child.props);
|
|
840
|
+
return {
|
|
841
|
+
index,
|
|
842
|
+
id: child.id,
|
|
843
|
+
gridColumn: placement['grid-column'] ?? null,
|
|
844
|
+
gridRow: placement['grid-row'] ?? null,
|
|
845
|
+
gridArea: placement['grid-area'] ?? null,
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return input;
|
|
850
|
+
}
|
|
851
|
+
const slotItemBindings = new WeakMap();
|
|
852
|
+
const slotOwnerBindings = new WeakMap();
|
|
853
|
+
function clearSlotOwnerBehavior(element) {
|
|
854
|
+
slotOwnerBindings.get(element)?.cleanup();
|
|
855
|
+
}
|
|
856
|
+
function createSlotOwnerCleanup(element) {
|
|
857
|
+
clearSlotOwnerBehavior(element);
|
|
858
|
+
const cleanups = [];
|
|
859
|
+
let disposed = false;
|
|
860
|
+
const binding = {
|
|
861
|
+
cleanup() {
|
|
862
|
+
if (disposed)
|
|
863
|
+
return;
|
|
864
|
+
disposed = true;
|
|
865
|
+
if (slotOwnerBindings.get(element) === binding) {
|
|
866
|
+
slotOwnerBindings.delete(element);
|
|
867
|
+
}
|
|
868
|
+
for (const cleanup of cleanups.splice(0).reverse()) {
|
|
869
|
+
try {
|
|
870
|
+
cleanup();
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
// Teardown is best-effort; one browser resource cannot block others.
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
slotOwnerBindings.set(element, binding);
|
|
879
|
+
return {
|
|
880
|
+
add(cleanup) {
|
|
881
|
+
if (disposed)
|
|
882
|
+
cleanup();
|
|
883
|
+
else
|
|
884
|
+
cleanups.push(cleanup);
|
|
885
|
+
},
|
|
886
|
+
dispose: binding.cleanup,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function addSlotOwnerListener(controller, target, type, listener, options) {
|
|
890
|
+
target.addEventListener(type, listener, options);
|
|
891
|
+
controller.add(() => target.removeEventListener(type, listener, options));
|
|
892
|
+
}
|
|
893
|
+
function clearSlotItemPresentation(element) {
|
|
894
|
+
const binding = slotItemBindings.get(element);
|
|
895
|
+
if (!binding)
|
|
896
|
+
return;
|
|
897
|
+
try {
|
|
898
|
+
binding.cleanup?.();
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
slotItemBindings.delete(element);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function bindSlotItemPresentation(element, descriptor) {
|
|
905
|
+
clearSlotItemPresentation(element);
|
|
906
|
+
const behavior = descriptor.apply(element) ?? {};
|
|
907
|
+
slotItemBindings.set(element, { descriptor, ...behavior });
|
|
908
|
+
}
|
|
909
|
+
function stylePropertyNames(style) {
|
|
910
|
+
const names = new Set();
|
|
911
|
+
for (let index = 0; index < style.length; index += 1) {
|
|
912
|
+
const name = style.item(index);
|
|
913
|
+
if (name)
|
|
914
|
+
names.add(name);
|
|
915
|
+
}
|
|
916
|
+
return names;
|
|
917
|
+
}
|
|
918
|
+
function captureSlotItemPresentation(element, ownership) {
|
|
919
|
+
const styleNames = stylePropertyNames(element.style);
|
|
920
|
+
return {
|
|
921
|
+
attributes: ownership.attributes.map(name => ({
|
|
922
|
+
name,
|
|
923
|
+
present: element.hasAttribute(name),
|
|
924
|
+
value: element.getAttribute(name) ?? '',
|
|
925
|
+
})),
|
|
926
|
+
styles: ownership.styleProperties.map(name => ({
|
|
927
|
+
name,
|
|
928
|
+
present: styleNames.has(name),
|
|
929
|
+
value: element.style.getPropertyValue(name),
|
|
930
|
+
priority: element.style.getPropertyPriority(name),
|
|
931
|
+
})),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function restoreSlotItemPresentation(element, snapshot) {
|
|
935
|
+
for (const attribute of snapshot.attributes) {
|
|
936
|
+
if (attribute.present)
|
|
937
|
+
element.setAttribute(attribute.name, attribute.value);
|
|
938
|
+
else
|
|
939
|
+
element.removeAttribute(attribute.name);
|
|
940
|
+
}
|
|
941
|
+
for (const style of snapshot.styles) {
|
|
942
|
+
if (style.present) {
|
|
943
|
+
element.style.setProperty(style.name, style.value, style.priority);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
element.style.removeProperty(style.name);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function slotItemOwnership(styles) {
|
|
951
|
+
return {
|
|
952
|
+
attributes: [],
|
|
953
|
+
styleProperties: Object.keys(styles),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function applySlotItemStyles(element, styles) {
|
|
957
|
+
for (const [name, value] of Object.entries(styles)) {
|
|
958
|
+
element.style.setProperty(name, value);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/** Copy an immediate parent's slot-item decoration onto a detached replacement. */
|
|
962
|
+
function transferSlotItemPresentation(current, replacement) {
|
|
963
|
+
const binding = slotItemBindings.get(current);
|
|
964
|
+
if (!binding)
|
|
965
|
+
return;
|
|
966
|
+
const resume = binding.suspend?.();
|
|
967
|
+
try {
|
|
968
|
+
const snapshot = captureSlotItemPresentation(current, binding.descriptor.ownership);
|
|
969
|
+
bindSlotItemPresentation(replacement, binding.descriptor);
|
|
970
|
+
restoreSlotItemPresentation(replacement, snapshot);
|
|
971
|
+
binding.descriptor.transfer?.(current, replacement);
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
resume?.();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/** Item styles owned by the live parent slot, used by shallow shell updates. */
|
|
978
|
+
function getSlotItemPresentationOwnership(element) {
|
|
979
|
+
return slotItemBindings.get(element)?.descriptor.ownership
|
|
980
|
+
?? EMPTY_HOST_PRESENTATION_OWNERSHIP;
|
|
981
|
+
}
|
|
982
|
+
/** Preserve parent-owned presentation and transient behavior around an item update. */
|
|
983
|
+
function updateSlotItemShellPresentation(element, update, additionalOwnership = EMPTY_HOST_PRESENTATION_OWNERSHIP) {
|
|
984
|
+
const binding = slotItemBindings.get(element);
|
|
985
|
+
const ownership = mergePresentationOwnership(binding?.descriptor.ownership ?? EMPTY_HOST_PRESENTATION_OWNERSHIP, additionalOwnership);
|
|
986
|
+
if (!binding
|
|
987
|
+
&& ownership.attributes.length === 0
|
|
988
|
+
&& ownership.styleProperties.length === 0) {
|
|
989
|
+
update();
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const resume = binding?.suspend?.();
|
|
993
|
+
try {
|
|
994
|
+
const snapshot = captureSlotItemPresentation(element, ownership);
|
|
995
|
+
try {
|
|
996
|
+
update();
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
restoreSlotItemPresentation(element, snapshot);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
finally {
|
|
1003
|
+
resume?.();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function mergePresentationOwnership(...ownership) {
|
|
1007
|
+
return {
|
|
1008
|
+
attributes: Array.from(new Set(ownership.flatMap(value => value.attributes))),
|
|
1009
|
+
styleProperties: Array.from(new Set(ownership.flatMap(value => value.styleProperties))),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
/** Patch renderer-owned shell presentation without overwriting layout state. */
|
|
1013
|
+
function patchElementShellPresentation(current, previous, desired, ...preservedOwnership) {
|
|
1014
|
+
const ownership = mergePresentationOwnership(...preservedOwnership);
|
|
1015
|
+
const layoutAttributes = new Set(ownership.attributes);
|
|
1016
|
+
const layoutStyleProperties = new Set(ownership.styleProperties);
|
|
1017
|
+
const attributeNames = new Set([
|
|
1018
|
+
...Array.from(previous.attributes, attribute => attribute.name),
|
|
1019
|
+
...Array.from(desired.attributes, attribute => attribute.name),
|
|
1020
|
+
]);
|
|
1021
|
+
attributeNames.delete('style');
|
|
1022
|
+
for (const name of attributeNames) {
|
|
1023
|
+
if (layoutAttributes.has(name))
|
|
1024
|
+
continue;
|
|
1025
|
+
const previousHas = previous.hasAttribute(name);
|
|
1026
|
+
const desiredHas = desired.hasAttribute(name);
|
|
1027
|
+
const previousValue = previous.getAttribute(name);
|
|
1028
|
+
const desiredValue = desired.getAttribute(name);
|
|
1029
|
+
if (previousHas === desiredHas
|
|
1030
|
+
&& previousValue === desiredValue) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (previousHas) {
|
|
1034
|
+
if (!current.hasAttribute(name)
|
|
1035
|
+
|| current.getAttribute(name) !== previousValue) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
else if (current.hasAttribute(name)) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (desiredHas)
|
|
1043
|
+
current.setAttribute(name, desiredValue);
|
|
1044
|
+
else
|
|
1045
|
+
current.removeAttribute(name);
|
|
1046
|
+
}
|
|
1047
|
+
const previousPropertyNames = stylePropertyNames(previous.style);
|
|
1048
|
+
const desiredPropertyNames = stylePropertyNames(desired.style);
|
|
1049
|
+
const propertyNames = new Set([
|
|
1050
|
+
...previousPropertyNames,
|
|
1051
|
+
...desiredPropertyNames,
|
|
1052
|
+
]);
|
|
1053
|
+
const currentPropertyNames = stylePropertyNames(current.style);
|
|
1054
|
+
for (const name of propertyNames) {
|
|
1055
|
+
if (layoutStyleProperties.has(name))
|
|
1056
|
+
continue;
|
|
1057
|
+
const previousHas = previousPropertyNames.has(name);
|
|
1058
|
+
const desiredHas = desiredPropertyNames.has(name);
|
|
1059
|
+
const previousValue = previous.style.getPropertyValue(name);
|
|
1060
|
+
const desiredValue = desired.style.getPropertyValue(name);
|
|
1061
|
+
const previousPriority = previous.style.getPropertyPriority(name);
|
|
1062
|
+
const desiredPriority = desired.style.getPropertyPriority(name);
|
|
1063
|
+
if (previousHas === desiredHas
|
|
1064
|
+
&& previousValue === desiredValue
|
|
1065
|
+
&& previousPriority === desiredPriority) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (previousHas) {
|
|
1069
|
+
if (!currentPropertyNames.has(name)
|
|
1070
|
+
|| current.style.getPropertyValue(name) !== previousValue
|
|
1071
|
+
|| current.style.getPropertyPriority(name) !== previousPriority) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else if (currentPropertyNames.has(name)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (desiredHas) {
|
|
1079
|
+
current.style.setProperty(name, desiredValue, desiredPriority);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
current.style.removeProperty(name);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/** Release slot-owned item listeners within a disposed subtree. */
|
|
1087
|
+
function clearSlotItemPresentationsIn(root) {
|
|
1088
|
+
let hasError = false;
|
|
1089
|
+
let firstError;
|
|
1090
|
+
const clear = (element) => {
|
|
1091
|
+
try {
|
|
1092
|
+
clearSlotItemPresentation(element);
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
if (!hasError)
|
|
1096
|
+
firstError = error;
|
|
1097
|
+
hasError = true;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
clear(root);
|
|
1101
|
+
root.querySelectorAll('*').forEach(clear);
|
|
1102
|
+
if (hasError)
|
|
1103
|
+
throw firstError;
|
|
1104
|
+
}
|
|
1105
|
+
/** Release slot-owner listeners, timers, frames, and observers in a subtree. */
|
|
1106
|
+
function clearSlotOwnerBehaviorsIn(root) {
|
|
1107
|
+
clearSlotOwnerBehavior(root);
|
|
1108
|
+
root.querySelectorAll('*').forEach(clearSlotOwnerBehavior);
|
|
1109
|
+
}
|
|
1110
|
+
// ─── Dispatcher ─────────────────────────────────────────────────
|
|
1111
|
+
/**
|
|
1112
|
+
* Apply host-level layout styles based on the slot key.
|
|
1113
|
+
* Called from renderDefault BEFORE props.style so explicit styles can override.
|
|
1114
|
+
* Layouts that require wrapper DOM continue to render in renderSlotLayout.
|
|
1115
|
+
*/
|
|
1116
|
+
function applySlotBaseStyle(container, props) {
|
|
1117
|
+
const flex = props.slots?.[SLOT_LAYOUT.FLEX];
|
|
1118
|
+
if (!flex || typeof flex !== 'object')
|
|
1119
|
+
return;
|
|
1120
|
+
const config = flex.config && typeof flex.config === 'object' ? flex.config : {};
|
|
1121
|
+
container.style.display = 'flex';
|
|
1122
|
+
container.style.flexDirection = config.direction ?? 'column';
|
|
1123
|
+
container.style.alignItems = config.align ?? 'flex-start';
|
|
1124
|
+
container.style.justifyContent = config.justify ?? 'flex-start';
|
|
1125
|
+
container.style.flexWrap = config.wrap ?? 'nowrap';
|
|
1126
|
+
container.style.gap = resolveFlexGap(config.gap);
|
|
1127
|
+
}
|
|
1128
|
+
function resolveFlexGap(value) {
|
|
1129
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1130
|
+
return `${value}px`;
|
|
1131
|
+
}
|
|
1132
|
+
return typeof value === 'string' && value.length > 0 ? value : '0px';
|
|
1133
|
+
}
|
|
719
1134
|
/**
|
|
720
1135
|
* Render children into a container using the slot layout strategy.
|
|
721
1136
|
* Returns true if a special layout was applied (children already appended).
|
|
722
1137
|
* Returns false for `default` — caller uses original append loop.
|
|
723
1138
|
*/
|
|
724
|
-
function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext) {
|
|
1139
|
+
function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext, resolveChildProps) {
|
|
725
1140
|
const slots = props.slots;
|
|
726
1141
|
if (!slots)
|
|
727
1142
|
return false;
|
|
728
|
-
const slotKey =
|
|
1143
|
+
const slotKey = findSpecialSlotKey(props);
|
|
729
1144
|
if (!slotKey)
|
|
730
1145
|
return false;
|
|
731
1146
|
const slotContent = slots[slotKey];
|
|
@@ -734,7 +1149,7 @@ function renderSlotLayout(container, children, props, renderChild, childrenMap,
|
|
|
734
1149
|
renderColumnsSlot(container, slotContent, childrenMap, renderChild);
|
|
735
1150
|
return true;
|
|
736
1151
|
case SLOT_LAYOUT.GRID:
|
|
737
|
-
renderGridSlot(container, children, slotContent, renderChild);
|
|
1152
|
+
renderGridSlot(container, children, slotContent, renderChild, resolveChildProps);
|
|
738
1153
|
return true;
|
|
739
1154
|
case SLOT_LAYOUT.HORIZONTAL_SCROLL:
|
|
740
1155
|
renderHorizontalScrollSlot(container, children, slotContent, renderChild);
|
|
@@ -808,7 +1223,7 @@ function renderColumnsSlot(container, slotContent, childrenMap, renderChild) {
|
|
|
808
1223
|
*
|
|
809
1224
|
* Schema: `slots: { grid: { children: [...], config: { columns: 2, gap: '8px', rows?: 3, rowHeight?: '48px' } } }`
|
|
810
1225
|
*/
|
|
811
|
-
function renderGridSlot(container, children, slotContent, renderChild) {
|
|
1226
|
+
function renderGridSlot(container, children, slotContent, renderChild, resolveChildProps) {
|
|
812
1227
|
const columns = slotContent?.config?.columns ?? 2;
|
|
813
1228
|
const gap = slotContent?.config?.gap ?? '8px';
|
|
814
1229
|
const rows = slotContent?.config?.rows;
|
|
@@ -826,7 +1241,7 @@ function renderGridSlot(container, children, slotContent, renderChild) {
|
|
|
826
1241
|
// regardless of whether the child component applies props.style to its host
|
|
827
1242
|
// or to an inner shadow-DOM node. No-op for children without placement, so
|
|
828
1243
|
// auto-flow cards are unaffected.
|
|
829
|
-
|
|
1244
|
+
bindSlotItemPresentation(el, gridItemDescriptor(resolveChildProps?.(child) ?? child.props));
|
|
830
1245
|
grid.appendChild(el);
|
|
831
1246
|
}
|
|
832
1247
|
container.appendChild(grid);
|
|
@@ -839,54 +1254,41 @@ function renderGridSlot(container, children, slotContent, renderChild) {
|
|
|
839
1254
|
* unresolved expression object) is ignored. Children that declare no placement
|
|
840
1255
|
* are left untouched — preserving auto-flow behaviour for legacy cards.
|
|
841
1256
|
*/
|
|
842
|
-
function
|
|
843
|
-
const style =
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1257
|
+
function gridPlacementStyles(props) {
|
|
1258
|
+
const style = props?.style;
|
|
1259
|
+
const placement = {};
|
|
1260
|
+
if (style && typeof style === 'object') {
|
|
1261
|
+
if (isCssPlacement(style.gridColumn)) {
|
|
1262
|
+
placement['grid-column'] = String(style.gridColumn);
|
|
1263
|
+
}
|
|
1264
|
+
if (isCssPlacement(style.gridRow)) {
|
|
1265
|
+
placement['grid-row'] = String(style.gridRow);
|
|
1266
|
+
}
|
|
1267
|
+
if (isCssPlacement(style.gridArea)) {
|
|
1268
|
+
placement['grid-area'] = String(style.gridArea);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return placement;
|
|
1272
|
+
}
|
|
1273
|
+
function gridItemDescriptor(props) {
|
|
1274
|
+
const placement = gridPlacementStyles(props);
|
|
1275
|
+
return {
|
|
1276
|
+
kind: 'grid',
|
|
1277
|
+
ownership: slotItemOwnership(placement),
|
|
1278
|
+
apply(element) {
|
|
1279
|
+
applySlotItemStyles(element, placement);
|
|
1280
|
+
},
|
|
1281
|
+
};
|
|
852
1282
|
}
|
|
853
1283
|
function isCssPlacement(value) {
|
|
854
1284
|
return typeof value === 'string' || typeof value === 'number';
|
|
855
1285
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
* Optional config:
|
|
863
|
-
* - `arrows` — show prev/next arrow buttons; each is only visible while
|
|
864
|
-
* the track can actually scroll in that direction (default false)
|
|
865
|
-
* - `arrowStyle` — free-form CSS object merged onto both arrow buttons; every
|
|
866
|
-
* default (circle, background, shadow…) can be overridden
|
|
867
|
-
* - `arrowIcon` — replaces the built-in chevron on both buttons: an image URL
|
|
868
|
-
* (http/https/data:image) or a text glyph/emoji. The left
|
|
869
|
-
* button gets a mirrored copy.
|
|
870
|
-
* - `arrowIconLeft` / `arrowIconRight` — per-side icon, same formats, never
|
|
871
|
-
* mirrored; takes precedence over `arrowIcon`
|
|
872
|
-
* - `arrowOffset` — distance of each arrow from its own edge (default '8px');
|
|
873
|
-
* number = px, negative values push the arrows outside /
|
|
874
|
-
* straddling the container edge
|
|
875
|
-
* - `scrollStep` — px scrolled per arrow click (default 80% of the visible width)
|
|
876
|
-
* - `mask` — show translucent edge-fade masks hinting at more content;
|
|
877
|
-
* follows the same can-scroll state as the arrows (default false)
|
|
878
|
-
* - `maskWidth` — width of each edge mask (default '48px')
|
|
879
|
-
* - `maskColor` — base color the mask fades out from; any CSS color incl.
|
|
880
|
-
* rgba for translucency (default '#fff')
|
|
881
|
-
* - `maskOpacity` — opacity of a mask while visible (default 1)
|
|
882
|
-
* - `itemHoverStyle` — CSS object applied to an item on hover and reverted on
|
|
883
|
-
* leave; pure inline-style swap, never triggers a re-render
|
|
884
|
-
* - `autoScroll` — while the pointer hovers the track, auto-advance one item
|
|
885
|
-
* every `autoScrollInterval` ms (default false)
|
|
886
|
-
* - `autoScrollInterval` — ms each item stays before advancing (default 2000)
|
|
887
|
-
* - `autoScrollLoop` — loop back to the first item after the last (default true);
|
|
888
|
-
* when false it stops on the last item until re-hovered
|
|
889
|
-
*/
|
|
1286
|
+
function resolveScrollbarMode(value) {
|
|
1287
|
+
if (value === 'auto' || value === 'hidden' || value === 'visible') {
|
|
1288
|
+
return value;
|
|
1289
|
+
}
|
|
1290
|
+
return undefined;
|
|
1291
|
+
}
|
|
890
1292
|
function renderHorizontalScrollSlot(container, children, slotContent, renderChild) {
|
|
891
1293
|
const config = slotContent?.config ?? {};
|
|
892
1294
|
const gap = config.gap ?? '8px';
|
|
@@ -898,13 +1300,23 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
898
1300
|
const autoScroll = config.autoScroll === true;
|
|
899
1301
|
const autoScrollInterval = typeof config.autoScrollInterval === 'number' ? config.autoScrollInterval : 2000;
|
|
900
1302
|
const autoScrollLoop = config.autoScrollLoop !== false;
|
|
1303
|
+
const scrollbar = resolveScrollbarMode(config.scrollbar);
|
|
1304
|
+
const hidesScrollbar = scrollbar == null || scrollbar === 'hidden';
|
|
1305
|
+
const itemDescriptor = horizontalScrollItemDescriptor(itemWidth, snap, itemHoverStyle);
|
|
901
1306
|
const track = document.createElement('div');
|
|
1307
|
+
const ownerCleanup = createSlotOwnerCleanup(track);
|
|
1308
|
+
let ownerDisposed = false;
|
|
1309
|
+
let wheelTimer = 0;
|
|
1310
|
+
let autoTimer = 0;
|
|
1311
|
+
let overlayFrame = 0;
|
|
1312
|
+
let resizeObserver = null;
|
|
902
1313
|
track.style.cssText = [
|
|
903
1314
|
'display:flex',
|
|
904
1315
|
'overflow-x:auto',
|
|
905
1316
|
'overflow-y:hidden',
|
|
906
1317
|
'-webkit-overflow-scrolling:touch',
|
|
907
|
-
'scrollbar-width:none',
|
|
1318
|
+
hidesScrollbar ? 'scrollbar-width:none' : '',
|
|
1319
|
+
scrollbar === 'visible' ? 'scrollbar-width:auto' : '',
|
|
908
1320
|
`gap:${gap}`,
|
|
909
1321
|
'cursor:grab',
|
|
910
1322
|
snap ? 'scroll-snap-type:x mandatory' : '',
|
|
@@ -913,27 +1325,21 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
913
1325
|
// across variable-driven re-renders (see captureScrollPositions/restoreScrollPositions).
|
|
914
1326
|
const cardId = container.getAttribute('data-card-id') ?? 'x';
|
|
915
1327
|
track.setAttribute('data-scroll-id', cardId);
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const trackClass = `card-scroll-${cardId}`;
|
|
919
|
-
styleEl.textContent = `.${trackClass}::-webkit-scrollbar{display:none}`;
|
|
920
|
-
track.classList.add(trackClass);
|
|
1328
|
+
if (scrollbar)
|
|
1329
|
+
track.dataset.scrollbar = scrollbar;
|
|
921
1330
|
for (const child of children) {
|
|
922
1331
|
const item = renderChild(child);
|
|
923
|
-
item
|
|
924
|
-
if (itemWidth)
|
|
925
|
-
item.style.width = itemWidth;
|
|
926
|
-
if (snap)
|
|
927
|
-
item.style.scrollSnapAlign = 'start';
|
|
928
|
-
if (itemHoverStyle)
|
|
929
|
-
applyHoverStyle(item, itemHoverStyle);
|
|
1332
|
+
bindSlotItemPresentation(item, itemDescriptor);
|
|
930
1333
|
track.appendChild(item);
|
|
931
1334
|
}
|
|
932
1335
|
// Mouse drag (desktop)
|
|
933
1336
|
let isDragging = false;
|
|
934
1337
|
let startX = 0;
|
|
935
1338
|
let scrollLeft = 0;
|
|
936
|
-
|
|
1339
|
+
const handleMouseDown = (event) => {
|
|
1340
|
+
if (ownerDisposed)
|
|
1341
|
+
return;
|
|
1342
|
+
const e = event;
|
|
937
1343
|
if (e.button !== 0)
|
|
938
1344
|
return; // left button only — keep middle/right clicks out of drag state
|
|
939
1345
|
// Nested scrollers: consume the gesture so an enclosing horizontalScroll doesn't
|
|
@@ -947,8 +1353,11 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
947
1353
|
track.style.userSelect = 'none';
|
|
948
1354
|
if (snap)
|
|
949
1355
|
track.style.scrollSnapType = 'none';
|
|
950
|
-
}
|
|
951
|
-
|
|
1356
|
+
};
|
|
1357
|
+
const handleMouseMove = (event) => {
|
|
1358
|
+
if (ownerDisposed)
|
|
1359
|
+
return;
|
|
1360
|
+
const e = event;
|
|
952
1361
|
if (!isDragging)
|
|
953
1362
|
return;
|
|
954
1363
|
// Only swallow the move while THIS track owns the drag — otherwise an in-progress
|
|
@@ -957,7 +1366,7 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
957
1366
|
e.stopPropagation();
|
|
958
1367
|
e.preventDefault();
|
|
959
1368
|
track.scrollLeft = scrollLeft - (e.pageX - startX);
|
|
960
|
-
}
|
|
1369
|
+
};
|
|
961
1370
|
const stopDrag = () => {
|
|
962
1371
|
if (!isDragging)
|
|
963
1372
|
return;
|
|
@@ -967,15 +1376,19 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
967
1376
|
if (snap)
|
|
968
1377
|
track.style.scrollSnapType = 'x mandatory';
|
|
969
1378
|
};
|
|
970
|
-
track
|
|
971
|
-
track
|
|
1379
|
+
addSlotOwnerListener(ownerCleanup, track, 'mousedown', handleMouseDown);
|
|
1380
|
+
addSlotOwnerListener(ownerCleanup, track, 'mousemove', handleMouseMove);
|
|
1381
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseup', stopDrag);
|
|
1382
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseleave', stopDrag);
|
|
972
1383
|
// Wheel / trackpad → horizontal scroll.
|
|
973
1384
|
// Mandatory snap would swallow small wheel deltas (each increment snaps right
|
|
974
1385
|
// back to the same item, so the wheel appears dead) — suspend snap while
|
|
975
1386
|
// wheeling and restore it shortly after the last tick, which also re-aligns
|
|
976
1387
|
// the track to the nearest snap point.
|
|
977
|
-
|
|
978
|
-
|
|
1388
|
+
const handleWheel = (event) => {
|
|
1389
|
+
if (ownerDisposed)
|
|
1390
|
+
return;
|
|
1391
|
+
const e = event;
|
|
979
1392
|
e.preventDefault();
|
|
980
1393
|
// Consume the wheel so a nested horizontalScroll scrolls only itself instead of
|
|
981
1394
|
// also driving an enclosing scroller (simplest nested model — no edge hand-off
|
|
@@ -988,12 +1401,16 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
988
1401
|
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
989
1402
|
track.scrollLeft += delta;
|
|
990
1403
|
if (snap) {
|
|
991
|
-
clearTimeout(wheelTimer);
|
|
1404
|
+
window.clearTimeout(wheelTimer);
|
|
992
1405
|
wheelTimer = window.setTimeout(() => {
|
|
1406
|
+
wheelTimer = 0;
|
|
1407
|
+
if (ownerDisposed)
|
|
1408
|
+
return;
|
|
993
1409
|
track.style.scrollSnapType = 'x mandatory';
|
|
994
1410
|
}, 150);
|
|
995
1411
|
}
|
|
996
|
-
}
|
|
1412
|
+
};
|
|
1413
|
+
addSlotOwnerListener(ownerCleanup, track, 'wheel', handleWheel, { passive: false });
|
|
997
1414
|
// Hover autoplay (desktop): while the pointer is over the track, glide to the next
|
|
998
1415
|
// item every `autoScrollInterval` ms, looping back to the first at the end when
|
|
999
1416
|
// `autoScrollLoop`. Opt-in via `autoScroll`. Manual drag pauses it (resumes on the
|
|
@@ -1002,9 +1419,10 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1002
1419
|
// chrome. Targets are item starts (= snap points), so mandatory snap won't fight the
|
|
1003
1420
|
// glide — same reason the arrow buttons can scrollBy smoothly without suspending snap.
|
|
1004
1421
|
if (autoScroll) {
|
|
1005
|
-
let autoTimer = 0;
|
|
1006
1422
|
const glideTo = (left) => track.scrollTo({ left, behavior: 'smooth' });
|
|
1007
1423
|
const advance = () => {
|
|
1424
|
+
if (ownerDisposed)
|
|
1425
|
+
return stopAuto();
|
|
1008
1426
|
// A variable-driven re-render rebuilds the track; bail and clear so the interval
|
|
1009
1427
|
// doesn't keep driving a detached node (mirrors the ResizeObserver self-teardown).
|
|
1010
1428
|
if (!track.isConnected)
|
|
@@ -1025,6 +1443,8 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1025
1443
|
glideTo(track.scrollLeft + (next.getBoundingClientRect().left - trackLeft));
|
|
1026
1444
|
};
|
|
1027
1445
|
const startAuto = () => {
|
|
1446
|
+
if (ownerDisposed)
|
|
1447
|
+
return;
|
|
1028
1448
|
if (autoTimer)
|
|
1029
1449
|
return;
|
|
1030
1450
|
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
|
|
@@ -1034,14 +1454,45 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1034
1454
|
function stopAuto() {
|
|
1035
1455
|
if (!autoTimer)
|
|
1036
1456
|
return;
|
|
1037
|
-
clearInterval(autoTimer);
|
|
1457
|
+
window.clearInterval(autoTimer);
|
|
1038
1458
|
autoTimer = 0;
|
|
1039
1459
|
}
|
|
1040
|
-
track
|
|
1041
|
-
track
|
|
1042
|
-
track
|
|
1460
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseenter', startAuto);
|
|
1461
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseleave', stopAuto);
|
|
1462
|
+
addSlotOwnerListener(ownerCleanup, track, 'mousedown', stopAuto);
|
|
1463
|
+
}
|
|
1464
|
+
ownerCleanup.add(() => {
|
|
1465
|
+
ownerDisposed = true;
|
|
1466
|
+
stopDrag();
|
|
1467
|
+
if (wheelTimer) {
|
|
1468
|
+
window.clearTimeout(wheelTimer);
|
|
1469
|
+
wheelTimer = 0;
|
|
1470
|
+
}
|
|
1471
|
+
if (autoTimer) {
|
|
1472
|
+
window.clearInterval(autoTimer);
|
|
1473
|
+
autoTimer = 0;
|
|
1474
|
+
}
|
|
1475
|
+
if (resizeObserver) {
|
|
1476
|
+
resizeObserver.disconnect();
|
|
1477
|
+
resizeObserver = null;
|
|
1478
|
+
}
|
|
1479
|
+
if (overlayFrame) {
|
|
1480
|
+
cancelAnimationFrame(overlayFrame);
|
|
1481
|
+
overlayFrame = 0;
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
if (hidesScrollbar) {
|
|
1485
|
+
const trackClass = `card-scroll-${cardId}`;
|
|
1486
|
+
track.classList.add(trackClass);
|
|
1487
|
+
const styleEl = document.createElement('style');
|
|
1488
|
+
styleEl.textContent = `.${trackClass}::-webkit-scrollbar{display:none}`;
|
|
1489
|
+
container.appendChild(styleEl);
|
|
1490
|
+
}
|
|
1491
|
+
else if (scrollbar === 'visible') {
|
|
1492
|
+
const styleEl = document.createElement('style');
|
|
1493
|
+
styleEl.textContent = '[data-scroll-id][data-scrollbar="visible"]::-webkit-scrollbar{display:block}';
|
|
1494
|
+
container.appendChild(styleEl);
|
|
1043
1495
|
}
|
|
1044
|
-
container.appendChild(styleEl);
|
|
1045
1496
|
// Plain track — no overlay chrome requested
|
|
1046
1497
|
if (!arrows && !mask) {
|
|
1047
1498
|
container.appendChild(track);
|
|
@@ -1074,13 +1525,25 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1074
1525
|
: typeof config.arrowOffset === 'number' ? `${config.arrowOffset}px` : String(config.arrowOffset);
|
|
1075
1526
|
leftArrow = createArrowButton('left', arrowStyle, arrowOffset, config.arrowIconLeft ?? config.arrowIcon, config.arrowIconLeft == null);
|
|
1076
1527
|
rightArrow = createArrowButton('right', arrowStyle, arrowOffset, config.arrowIconRight ?? config.arrowIcon, false);
|
|
1077
|
-
|
|
1078
|
-
|
|
1528
|
+
const scrollLeft = () => {
|
|
1529
|
+
if (!ownerDisposed) {
|
|
1530
|
+
track.scrollBy({ left: -step(), behavior: 'smooth' });
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
const scrollRight = () => {
|
|
1534
|
+
if (!ownerDisposed) {
|
|
1535
|
+
track.scrollBy({ left: step(), behavior: 'smooth' });
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
addSlotOwnerListener(ownerCleanup, leftArrow, 'click', scrollLeft);
|
|
1539
|
+
addSlotOwnerListener(ownerCleanup, rightArrow, 'click', scrollRight);
|
|
1079
1540
|
wrapper.appendChild(leftArrow);
|
|
1080
1541
|
wrapper.appendChild(rightArrow);
|
|
1081
1542
|
}
|
|
1082
1543
|
// Show each arrow/mask only while the track can scroll in that direction.
|
|
1083
1544
|
const updateOverlays = () => {
|
|
1545
|
+
if (ownerDisposed)
|
|
1546
|
+
return;
|
|
1084
1547
|
const canLeft = track.scrollLeft > 1;
|
|
1085
1548
|
const canRight = track.scrollLeft + track.clientWidth < track.scrollWidth - 1;
|
|
1086
1549
|
if (leftArrow)
|
|
@@ -1092,37 +1555,125 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1092
1555
|
if (rightMask)
|
|
1093
1556
|
rightMask.style.opacity = canRight ? maskOpacity : '0';
|
|
1094
1557
|
};
|
|
1095
|
-
track
|
|
1558
|
+
addSlotOwnerListener(ownerCleanup, track, 'scroll', updateOverlays, { passive: true });
|
|
1096
1559
|
// ResizeObserver fires once on observe (covers initial layout) and again on
|
|
1097
1560
|
// any size change; it self-disconnects once the track leaves the DOM.
|
|
1098
1561
|
if (typeof ResizeObserver !== 'undefined') {
|
|
1099
|
-
|
|
1562
|
+
resizeObserver = new ResizeObserver(() => {
|
|
1563
|
+
if (ownerDisposed)
|
|
1564
|
+
return;
|
|
1100
1565
|
if (!track.isConnected) {
|
|
1101
|
-
|
|
1566
|
+
resizeObserver?.disconnect();
|
|
1567
|
+
resizeObserver = null;
|
|
1102
1568
|
return;
|
|
1103
1569
|
}
|
|
1104
1570
|
updateOverlays();
|
|
1105
1571
|
});
|
|
1106
|
-
|
|
1572
|
+
resizeObserver.observe(track);
|
|
1107
1573
|
}
|
|
1108
1574
|
else {
|
|
1109
|
-
requestAnimationFrame(
|
|
1575
|
+
overlayFrame = requestAnimationFrame(() => {
|
|
1576
|
+
overlayFrame = 0;
|
|
1577
|
+
updateOverlays();
|
|
1578
|
+
});
|
|
1110
1579
|
}
|
|
1111
1580
|
}
|
|
1581
|
+
function horizontalScrollItemDescriptor(itemWidth, snap, hoverStyle) {
|
|
1582
|
+
const styles = {
|
|
1583
|
+
'flex-shrink': '0',
|
|
1584
|
+
};
|
|
1585
|
+
if (itemWidth)
|
|
1586
|
+
styles.width = itemWidth;
|
|
1587
|
+
if (snap)
|
|
1588
|
+
styles['scroll-snap-align'] = 'start';
|
|
1589
|
+
return {
|
|
1590
|
+
kind: 'horizontalScroll',
|
|
1591
|
+
ownership: slotItemOwnership(styles),
|
|
1592
|
+
apply(element) {
|
|
1593
|
+
applySlotItemStyles(element, styles);
|
|
1594
|
+
return hoverStyle ? applyHoverStyle(element, hoverStyle) : undefined;
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1112
1598
|
/** Apply `hoverStyle` on mouseenter and restore the previous inline values on leave. */
|
|
1113
1599
|
function applyHoverStyle(item, hoverStyle) {
|
|
1114
1600
|
const previous = {};
|
|
1115
|
-
|
|
1601
|
+
let hovered = false;
|
|
1602
|
+
const enter = () => {
|
|
1603
|
+
if (hovered)
|
|
1604
|
+
return;
|
|
1605
|
+
hovered = true;
|
|
1116
1606
|
for (const [key, value] of Object.entries(hoverStyle)) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1607
|
+
if (key.startsWith('--')) {
|
|
1608
|
+
const previousValue = item.style.getPropertyValue(key);
|
|
1609
|
+
const previousPriority = item.style.getPropertyPriority(key);
|
|
1610
|
+
previous[key] = {
|
|
1611
|
+
value: previousValue,
|
|
1612
|
+
priority: previousPriority,
|
|
1613
|
+
wasPresent: previousValue !== '' || previousPriority !== '',
|
|
1614
|
+
isCustomProperty: true,
|
|
1615
|
+
};
|
|
1616
|
+
item.style.setProperty(key, String(value));
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
previous[key] = {
|
|
1620
|
+
value: item.style[key],
|
|
1621
|
+
priority: '',
|
|
1622
|
+
wasPresent: true,
|
|
1623
|
+
isCustomProperty: false,
|
|
1624
|
+
};
|
|
1625
|
+
item.style[key] = value;
|
|
1626
|
+
}
|
|
1119
1627
|
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1628
|
+
};
|
|
1629
|
+
const leave = () => {
|
|
1630
|
+
if (!hovered)
|
|
1631
|
+
return;
|
|
1632
|
+
hovered = false;
|
|
1633
|
+
for (const [key, state] of Object.entries(previous)) {
|
|
1634
|
+
if (state.isCustomProperty) {
|
|
1635
|
+
if (state.wasPresent) {
|
|
1636
|
+
item.style.setProperty(key, state.value, state.priority);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
item.style.removeProperty(key);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
item.style[key] = state.value;
|
|
1644
|
+
}
|
|
1645
|
+
delete previous[key];
|
|
1124
1646
|
}
|
|
1125
|
-
}
|
|
1647
|
+
};
|
|
1648
|
+
item.addEventListener('mouseenter', enter);
|
|
1649
|
+
item.addEventListener('mouseleave', leave);
|
|
1650
|
+
return {
|
|
1651
|
+
cleanup() {
|
|
1652
|
+
let hasError = false;
|
|
1653
|
+
let firstError;
|
|
1654
|
+
const cleanup = (operation) => {
|
|
1655
|
+
try {
|
|
1656
|
+
operation();
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
if (!hasError)
|
|
1660
|
+
firstError = error;
|
|
1661
|
+
hasError = true;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
cleanup(leave);
|
|
1665
|
+
cleanup(() => item.removeEventListener('mouseenter', enter));
|
|
1666
|
+
cleanup(() => item.removeEventListener('mouseleave', leave));
|
|
1667
|
+
if (hasError)
|
|
1668
|
+
throw firstError;
|
|
1669
|
+
},
|
|
1670
|
+
suspend() {
|
|
1671
|
+
if (!hovered)
|
|
1672
|
+
return;
|
|
1673
|
+
leave();
|
|
1674
|
+
return enter;
|
|
1675
|
+
},
|
|
1676
|
+
};
|
|
1126
1677
|
}
|
|
1127
1678
|
/** Translucent edge-fade mask hinting that more content is available in that direction. */
|
|
1128
1679
|
function createEdgeMask(side, width, color) {
|
|
@@ -1202,6 +1753,66 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
|
|
|
1202
1753
|
return btn;
|
|
1203
1754
|
}
|
|
1204
1755
|
// ─── Carousel ───────────────────────────────────────────────────
|
|
1756
|
+
function carouselOverlay(item) {
|
|
1757
|
+
for (let index = item.children.length - 1; index >= 0; index -= 1) {
|
|
1758
|
+
const child = item.children.item(index);
|
|
1759
|
+
if (child instanceof HTMLElement
|
|
1760
|
+
&& child.classList.contains('carousel-overlay')) {
|
|
1761
|
+
return child;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return undefined;
|
|
1765
|
+
}
|
|
1766
|
+
function carouselItems(container) {
|
|
1767
|
+
return Array.from(container.children).filter((child) => (child instanceof HTMLElement
|
|
1768
|
+
&& slotItemBindings.get(child)?.descriptor.kind === 'carousel'));
|
|
1769
|
+
}
|
|
1770
|
+
function carouselItemDescriptor(index, itemCount, itemBasis, overlayColor) {
|
|
1771
|
+
const baseStyles = {
|
|
1772
|
+
'flex-grow': '0',
|
|
1773
|
+
'flex-shrink': '0',
|
|
1774
|
+
'flex-basis': itemBasis,
|
|
1775
|
+
'scroll-snap-align': 'center',
|
|
1776
|
+
'transform-origin': 'center center',
|
|
1777
|
+
position: 'relative',
|
|
1778
|
+
overflow: 'hidden',
|
|
1779
|
+
};
|
|
1780
|
+
const ownedStyles = {
|
|
1781
|
+
...baseStyles,
|
|
1782
|
+
transform: '',
|
|
1783
|
+
opacity: '',
|
|
1784
|
+
};
|
|
1785
|
+
if (index === 0)
|
|
1786
|
+
ownedStyles['margin-left'] = '';
|
|
1787
|
+
if (index === itemCount - 1)
|
|
1788
|
+
ownedStyles['margin-right'] = '';
|
|
1789
|
+
return {
|
|
1790
|
+
kind: 'carousel',
|
|
1791
|
+
ownership: slotItemOwnership(ownedStyles),
|
|
1792
|
+
apply(element) {
|
|
1793
|
+
applySlotItemStyles(element, baseStyles);
|
|
1794
|
+
const overlay = document.createElement('div');
|
|
1795
|
+
overlay.className = 'carousel-overlay';
|
|
1796
|
+
Object.assign(overlay.style, {
|
|
1797
|
+
position: 'absolute',
|
|
1798
|
+
inset: '0',
|
|
1799
|
+
background: overlayColor,
|
|
1800
|
+
opacity: '0',
|
|
1801
|
+
pointerEvents: 'none',
|
|
1802
|
+
borderRadius: 'inherit',
|
|
1803
|
+
zIndex: '1',
|
|
1804
|
+
});
|
|
1805
|
+
element.appendChild(overlay);
|
|
1806
|
+
},
|
|
1807
|
+
transfer(current, replacement) {
|
|
1808
|
+
const currentOverlay = carouselOverlay(current);
|
|
1809
|
+
const replacementOverlay = carouselOverlay(replacement);
|
|
1810
|
+
if (currentOverlay && replacementOverlay) {
|
|
1811
|
+
replacementOverlay.style.opacity = currentOverlay.style.opacity;
|
|
1812
|
+
}
|
|
1813
|
+
},
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1205
1816
|
/**
|
|
1206
1817
|
* Center-focused carousel with scale/opacity/overlay effects and snap.
|
|
1207
1818
|
*
|
|
@@ -1212,65 +1823,57 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
|
|
|
1212
1823
|
* to the first after the last (default false)
|
|
1213
1824
|
* - `autoplayInterval` — ms between auto-advances (default 3000)
|
|
1214
1825
|
*/
|
|
1215
|
-
function renderCarouselSlot(container, children,
|
|
1216
|
-
const
|
|
1217
|
-
const inactiveScale =
|
|
1218
|
-
const inactiveOpacity =
|
|
1219
|
-
const overlayColor =
|
|
1220
|
-
const gap =
|
|
1221
|
-
const itemWidth =
|
|
1222
|
-
const itemWidthPx =
|
|
1223
|
-
const initialIndex =
|
|
1224
|
-
const autoplay =
|
|
1225
|
-
const autoplayInterval =
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1826
|
+
function renderCarouselSlot(container, children, _slotContent, props, renderChild) {
|
|
1827
|
+
const effectiveConfig = resolveCarouselConfig(props, children.length);
|
|
1828
|
+
const inactiveScale = effectiveConfig.scale;
|
|
1829
|
+
const inactiveOpacity = effectiveConfig.opacity;
|
|
1830
|
+
const overlayColor = effectiveConfig.overlayColor;
|
|
1831
|
+
const gap = effectiveConfig.gap;
|
|
1832
|
+
const itemWidth = effectiveConfig.itemWidthPercent;
|
|
1833
|
+
const itemWidthPx = effectiveConfig.itemWidthPx;
|
|
1834
|
+
const initialIndex = effectiveConfig.initialIndex;
|
|
1835
|
+
const autoplay = effectiveConfig.autoplay;
|
|
1836
|
+
const autoplayInterval = effectiveConfig.autoplayInterval;
|
|
1837
|
+
const ownerCleanup = createSlotOwnerCleanup(container);
|
|
1838
|
+
let ownerDisposed = false;
|
|
1839
|
+
const pendingFrames = new Set();
|
|
1840
|
+
const requestOwnerFrame = (callback) => {
|
|
1841
|
+
let frameId = 0;
|
|
1842
|
+
let completedSynchronously = false;
|
|
1843
|
+
frameId = requestAnimationFrame((timestamp) => {
|
|
1844
|
+
completedSynchronously = true;
|
|
1845
|
+
pendingFrames.delete(frameId);
|
|
1846
|
+
if (!ownerDisposed)
|
|
1847
|
+
callback(timestamp);
|
|
1848
|
+
});
|
|
1849
|
+
if (!completedSynchronously)
|
|
1850
|
+
pendingFrames.add(frameId);
|
|
1851
|
+
return completedSynchronously ? 0 : frameId;
|
|
1852
|
+
};
|
|
1853
|
+
const cancelOwnerFrame = (frameId) => {
|
|
1854
|
+
if (!frameId)
|
|
1855
|
+
return;
|
|
1856
|
+
cancelAnimationFrame(frameId);
|
|
1857
|
+
pendingFrames.delete(frameId);
|
|
1858
|
+
};
|
|
1859
|
+
Object.assign(container.style, carouselHostStyles(gap));
|
|
1860
|
+
// Hide scrollbar
|
|
1861
|
+
const styleEl = document.createElement('style');
|
|
1241
1862
|
const id = container.getAttribute('data-card-id') ?? '';
|
|
1242
1863
|
styleEl.textContent = `[data-card-id="${id}"]{scrollbar-width:none}[data-card-id="${id}"]::-webkit-scrollbar{display:none}`;
|
|
1243
1864
|
container.appendChild(styleEl);
|
|
1244
1865
|
// The carousel element scrolls itself — tag it so renderCard can snapshot/restore
|
|
1245
1866
|
// its scrollLeft across variable-driven re-renders (see captureScrollPositions/restoreScrollPositions).
|
|
1246
|
-
container.setAttribute(
|
|
1867
|
+
container.setAttribute(CAROUSEL_SCROLL_ID_ATTRIBUTE, id || 'x');
|
|
1247
1868
|
// Render children
|
|
1248
|
-
const items = [];
|
|
1249
1869
|
for (let i = 0; i < children.length; i++) {
|
|
1250
1870
|
const child = children[i];
|
|
1251
1871
|
const item = renderChild(child);
|
|
1252
|
-
item.
|
|
1253
|
-
item.style.scrollSnapAlign = 'center';
|
|
1254
|
-
item.style.transformOrigin = 'center center';
|
|
1255
|
-
item.style.position = 'relative';
|
|
1256
|
-
item.style.overflow = 'hidden';
|
|
1257
|
-
const overlay = document.createElement('div');
|
|
1258
|
-
overlay.className = 'carousel-overlay';
|
|
1259
|
-
Object.assign(overlay.style, {
|
|
1260
|
-
position: 'absolute',
|
|
1261
|
-
inset: '0',
|
|
1262
|
-
background: overlayColor,
|
|
1263
|
-
opacity: '0',
|
|
1264
|
-
pointerEvents: 'none',
|
|
1265
|
-
borderRadius: 'inherit',
|
|
1266
|
-
zIndex: '1',
|
|
1267
|
-
});
|
|
1268
|
-
item.appendChild(overlay);
|
|
1872
|
+
bindSlotItemPresentation(item, carouselItemDescriptor(i, children.length, itemWidthPx ? `${itemWidthPx}px` : `${itemWidth}%`, overlayColor));
|
|
1269
1873
|
container.appendChild(item);
|
|
1270
|
-
items.push(item);
|
|
1271
1874
|
}
|
|
1272
1875
|
// Index of the item whose center is closest to the viewport center
|
|
1273
|
-
const nearestIndex = () => {
|
|
1876
|
+
const nearestIndex = (items) => {
|
|
1274
1877
|
const centerX = container.scrollLeft + container.offsetWidth / 2;
|
|
1275
1878
|
let idx = 0;
|
|
1276
1879
|
let minDist = Infinity;
|
|
@@ -1284,9 +1887,29 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1284
1887
|
});
|
|
1285
1888
|
return idx;
|
|
1286
1889
|
};
|
|
1890
|
+
let pendingSnapScrollEnd = null;
|
|
1891
|
+
let pendingSnapTimer = 0;
|
|
1892
|
+
const clearPendingSnap = () => {
|
|
1893
|
+
if (pendingSnapScrollEnd) {
|
|
1894
|
+
container.removeEventListener('scrollend', pendingSnapScrollEnd);
|
|
1895
|
+
pendingSnapScrollEnd = null;
|
|
1896
|
+
}
|
|
1897
|
+
if (pendingSnapTimer) {
|
|
1898
|
+
window.clearTimeout(pendingSnapTimer);
|
|
1899
|
+
pendingSnapTimer = 0;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
const restoreSnap = () => {
|
|
1903
|
+
clearPendingSnap();
|
|
1904
|
+
if (!ownerDisposed)
|
|
1905
|
+
container.style.scrollSnapType = 'x mandatory';
|
|
1906
|
+
};
|
|
1287
1907
|
// Center a given item. 'smooth' animates and restores CSS snap afterwards;
|
|
1288
1908
|
// 'auto' jumps instantly (used for initial positioning).
|
|
1289
1909
|
const centerItem = (item, behavior = 'smooth') => {
|
|
1910
|
+
if (ownerDisposed)
|
|
1911
|
+
return;
|
|
1912
|
+
clearPendingSnap();
|
|
1290
1913
|
const target = item.offsetLeft - (container.offsetWidth - item.offsetWidth) / 2;
|
|
1291
1914
|
if (behavior === 'auto') {
|
|
1292
1915
|
// Disable mandatory snap for the instant jump — a mandatory-snap container
|
|
@@ -1294,7 +1917,7 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1294
1917
|
// Restore snap next frame once the scroll offset is committed.
|
|
1295
1918
|
container.style.scrollSnapType = 'none';
|
|
1296
1919
|
container.scrollLeft = target;
|
|
1297
|
-
|
|
1920
|
+
requestOwnerFrame(() => {
|
|
1298
1921
|
container.style.scrollSnapType = 'x mandatory';
|
|
1299
1922
|
});
|
|
1300
1923
|
return;
|
|
@@ -1302,12 +1925,12 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1302
1925
|
// Disable CSS snap during the smooth animation, restore after for pixel-perfect alignment
|
|
1303
1926
|
container.style.scrollSnapType = 'none';
|
|
1304
1927
|
container.scrollTo({ left: target, behavior: 'smooth' });
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}, { once: true });
|
|
1928
|
+
pendingSnapScrollEnd = restoreSnap;
|
|
1929
|
+
container.addEventListener('scrollend', pendingSnapScrollEnd, { once: true });
|
|
1308
1930
|
// Fallback for browsers without scrollend event
|
|
1309
|
-
setTimeout(() => {
|
|
1310
|
-
|
|
1931
|
+
pendingSnapTimer = window.setTimeout(() => {
|
|
1932
|
+
pendingSnapTimer = 0;
|
|
1933
|
+
restoreSnap();
|
|
1311
1934
|
}, 400);
|
|
1312
1935
|
};
|
|
1313
1936
|
// Snap helper
|
|
@@ -1316,7 +1939,10 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1316
1939
|
let isDragging = false;
|
|
1317
1940
|
let startX = 0;
|
|
1318
1941
|
let scrollStart = 0;
|
|
1319
|
-
|
|
1942
|
+
const handleMouseDown = (event) => {
|
|
1943
|
+
if (ownerDisposed)
|
|
1944
|
+
return;
|
|
1945
|
+
const e = event;
|
|
1320
1946
|
if (e.button !== 0)
|
|
1321
1947
|
return; // left button only — keep middle/right clicks out of drag state
|
|
1322
1948
|
isDragging = true;
|
|
@@ -1325,35 +1951,52 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1325
1951
|
container.style.cursor = 'grabbing';
|
|
1326
1952
|
container.style.scrollSnapType = 'none';
|
|
1327
1953
|
e.preventDefault();
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (!isDragging)
|
|
1954
|
+
};
|
|
1955
|
+
const handleMouseMove = (event) => {
|
|
1956
|
+
if (ownerDisposed || !isDragging)
|
|
1331
1957
|
return;
|
|
1958
|
+
const e = event;
|
|
1332
1959
|
container.scrollLeft = scrollStart - (e.pageX - startX);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
if (!isDragging)
|
|
1960
|
+
};
|
|
1961
|
+
const handleMouseUp = () => {
|
|
1962
|
+
if (ownerDisposed || !isDragging)
|
|
1336
1963
|
return;
|
|
1337
1964
|
isDragging = false;
|
|
1338
1965
|
container.style.cursor = 'grab';
|
|
1339
1966
|
snapToNearest();
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1967
|
+
};
|
|
1968
|
+
const handleTouchEnd = () => {
|
|
1969
|
+
if (!ownerDisposed)
|
|
1970
|
+
snapToNearest();
|
|
1971
|
+
};
|
|
1972
|
+
addSlotOwnerListener(ownerCleanup, container, 'mousedown', handleMouseDown);
|
|
1973
|
+
addSlotOwnerListener(ownerCleanup, document, 'mousemove', handleMouseMove);
|
|
1974
|
+
addSlotOwnerListener(ownerCleanup, document, 'mouseup', handleMouseUp);
|
|
1975
|
+
addSlotOwnerListener(ownerCleanup, container, 'touchend', handleTouchEnd, { passive: true });
|
|
1344
1976
|
// Wheel support
|
|
1345
1977
|
let wheelTimer = 0;
|
|
1346
|
-
|
|
1978
|
+
const handleWheel = (event) => {
|
|
1979
|
+
if (ownerDisposed)
|
|
1980
|
+
return;
|
|
1981
|
+
const e = event;
|
|
1347
1982
|
if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) {
|
|
1348
1983
|
e.preventDefault();
|
|
1349
1984
|
container.style.scrollSnapType = 'none';
|
|
1350
1985
|
container.scrollLeft += e.deltaY;
|
|
1351
|
-
clearTimeout(wheelTimer);
|
|
1352
|
-
wheelTimer = window.setTimeout(() =>
|
|
1986
|
+
window.clearTimeout(wheelTimer);
|
|
1987
|
+
wheelTimer = window.setTimeout(() => {
|
|
1988
|
+
wheelTimer = 0;
|
|
1989
|
+
if (!ownerDisposed)
|
|
1990
|
+
snapToNearest();
|
|
1991
|
+
}, 150);
|
|
1353
1992
|
}
|
|
1354
|
-
}
|
|
1993
|
+
};
|
|
1994
|
+
addSlotOwnerListener(ownerCleanup, container, 'wheel', handleWheel, { passive: false });
|
|
1355
1995
|
// Scale/opacity effects
|
|
1356
1996
|
const updateEffects = () => {
|
|
1997
|
+
if (ownerDisposed)
|
|
1998
|
+
return;
|
|
1999
|
+
const items = carouselItems(container);
|
|
1357
2000
|
const containerWidth = container.offsetWidth;
|
|
1358
2001
|
const scrollCenter = container.scrollLeft + containerWidth / 2;
|
|
1359
2002
|
items.forEach((item) => {
|
|
@@ -1366,20 +2009,27 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1366
2009
|
const opacity = 1 - ratio * (1 - inactiveOpacity);
|
|
1367
2010
|
item.style.transform = `scale3d(${scale},${scale},1)`;
|
|
1368
2011
|
item.style.opacity = `${opacity}`;
|
|
1369
|
-
const overlayEl = item
|
|
2012
|
+
const overlayEl = carouselOverlay(item);
|
|
1370
2013
|
if (overlayEl)
|
|
1371
2014
|
overlayEl.style.opacity = `${ratio}`;
|
|
1372
2015
|
});
|
|
1373
2016
|
};
|
|
1374
2017
|
let rafId = 0;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
2018
|
+
const handleScroll = () => {
|
|
2019
|
+
if (ownerDisposed)
|
|
2020
|
+
return;
|
|
2021
|
+
cancelOwnerFrame(rafId);
|
|
2022
|
+
rafId = requestOwnerFrame(() => {
|
|
2023
|
+
rafId = 0;
|
|
2024
|
+
updateEffects();
|
|
2025
|
+
});
|
|
2026
|
+
};
|
|
2027
|
+
addSlotOwnerListener(ownerCleanup, container, 'scroll', handleScroll, { passive: true });
|
|
1379
2028
|
// Smooth snap to nearest item center + restore scrollSnapType for pixel-perfect final position
|
|
1380
2029
|
snapToNearest = () => {
|
|
2030
|
+
const items = carouselItems(container);
|
|
1381
2031
|
if (items.length > 0)
|
|
1382
|
-
centerItem(items[nearestIndex()]);
|
|
2032
|
+
centerItem(items[nearestIndex(items)]);
|
|
1383
2033
|
};
|
|
1384
2034
|
// Autoplay — advance to the next item on an interval, looping back to the
|
|
1385
2035
|
// first after the last. Pauses while the user hovers or interacts.
|
|
@@ -1387,33 +2037,64 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1387
2037
|
let autoplayTimer = 0;
|
|
1388
2038
|
const stopAutoplay = () => {
|
|
1389
2039
|
if (autoplayTimer) {
|
|
1390
|
-
clearInterval(autoplayTimer);
|
|
2040
|
+
window.clearInterval(autoplayTimer);
|
|
1391
2041
|
autoplayTimer = 0;
|
|
1392
2042
|
}
|
|
1393
2043
|
};
|
|
1394
2044
|
const startAutoplay = () => {
|
|
1395
|
-
|
|
2045
|
+
const items = carouselItems(container);
|
|
2046
|
+
if (ownerDisposed
|
|
2047
|
+
|| !container.isConnected
|
|
2048
|
+
|| !autoplay
|
|
2049
|
+
|| items.length < 2)
|
|
1396
2050
|
return;
|
|
1397
2051
|
stopAutoplay();
|
|
1398
2052
|
autoplayTimer = window.setInterval(() => {
|
|
1399
|
-
|
|
1400
|
-
if (!container.isConnected) {
|
|
2053
|
+
if (ownerDisposed || !container.isConnected) {
|
|
1401
2054
|
stopAutoplay();
|
|
1402
2055
|
return;
|
|
1403
2056
|
}
|
|
1404
2057
|
if (paused || isDragging)
|
|
1405
2058
|
return;
|
|
1406
|
-
|
|
2059
|
+
const items = carouselItems(container);
|
|
2060
|
+
if (items.length < 2)
|
|
2061
|
+
return;
|
|
2062
|
+
centerItem(items[(nearestIndex(items) + 1) % items.length]);
|
|
1407
2063
|
}, autoplayInterval);
|
|
1408
2064
|
};
|
|
1409
2065
|
if (autoplay) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
2066
|
+
const pauseAutoplay = () => {
|
|
2067
|
+
if (!ownerDisposed)
|
|
2068
|
+
paused = true;
|
|
2069
|
+
};
|
|
2070
|
+
const resumeAutoplay = () => {
|
|
2071
|
+
if (!ownerDisposed)
|
|
2072
|
+
paused = false;
|
|
2073
|
+
};
|
|
2074
|
+
addSlotOwnerListener(ownerCleanup, container, 'mouseenter', pauseAutoplay);
|
|
2075
|
+
addSlotOwnerListener(ownerCleanup, container, 'mouseleave', resumeAutoplay);
|
|
2076
|
+
addSlotOwnerListener(ownerCleanup, container, 'touchstart', pauseAutoplay, { passive: true });
|
|
2077
|
+
addSlotOwnerListener(ownerCleanup, container, 'touchend', resumeAutoplay, { passive: true });
|
|
2078
|
+
}
|
|
2079
|
+
ownerCleanup.add(() => {
|
|
2080
|
+
ownerDisposed = true;
|
|
2081
|
+
paused = true;
|
|
2082
|
+
isDragging = false;
|
|
2083
|
+
container.style.cursor = 'grab';
|
|
2084
|
+
if (wheelTimer) {
|
|
2085
|
+
window.clearTimeout(wheelTimer);
|
|
2086
|
+
wheelTimer = 0;
|
|
2087
|
+
}
|
|
2088
|
+
clearPendingSnap();
|
|
2089
|
+
stopAutoplay();
|
|
2090
|
+
for (const frameId of pendingFrames)
|
|
2091
|
+
cancelAnimationFrame(frameId);
|
|
2092
|
+
pendingFrames.clear();
|
|
2093
|
+
rafId = 0;
|
|
2094
|
+
});
|
|
1415
2095
|
// Initial position & effects — deferred until element is in DOM and laid out
|
|
1416
|
-
|
|
2096
|
+
requestOwnerFrame(() => {
|
|
2097
|
+
const items = carouselItems(container);
|
|
1417
2098
|
if (items.length > 0) {
|
|
1418
2099
|
// Use ACTUAL rendered width (includes padding in content-box mode) for perfect centering
|
|
1419
2100
|
const containerWidth = container.offsetWidth;
|
|
@@ -1428,13 +2109,13 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1428
2109
|
delete container.dataset.restoreScrollLeft;
|
|
1429
2110
|
container.style.scrollSnapType = 'none';
|
|
1430
2111
|
container.scrollLeft = parseFloat(saved);
|
|
1431
|
-
|
|
2112
|
+
requestOwnerFrame(() => {
|
|
1432
2113
|
container.style.scrollSnapType = 'x mandatory';
|
|
1433
2114
|
});
|
|
1434
2115
|
}
|
|
1435
2116
|
else {
|
|
1436
2117
|
// Jump to the configured initial item (defaults to the first) — CSS snap maintains alignment
|
|
1437
|
-
centerItem(items[initialIndex], 'auto');
|
|
2118
|
+
centerItem(items[Math.min(initialIndex, items.length - 1)], 'auto');
|
|
1438
2119
|
}
|
|
1439
2120
|
}
|
|
1440
2121
|
updateEffects();
|
|
@@ -1721,6 +2402,97 @@ class BaseElement extends HTMLElement {
|
|
|
1721
2402
|
this.shadowRoot.innerHTML = html;
|
|
1722
2403
|
applyResponsiveStyles(this.shadowRoot, this._responsive);
|
|
1723
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Patch freshly rendered markup around an existing interactive element.
|
|
2407
|
+
*
|
|
2408
|
+
* Replacing a focused native control through `shadowRoot.innerHTML` resets
|
|
2409
|
+
* its selection and, on mobile WebViews, can also recreate the soft
|
|
2410
|
+
* keyboard with a different layout. This helper keeps the selected element
|
|
2411
|
+
* connected while reconciling its surrounding markup.
|
|
2412
|
+
*/
|
|
2413
|
+
patchShadowHTMLPreservingElement(html, preserved, selector) {
|
|
2414
|
+
if (!this.shadowRoot || !this.shadowRoot.contains(preserved))
|
|
2415
|
+
return false;
|
|
2416
|
+
const template = document.createElement('template');
|
|
2417
|
+
template.innerHTML = html;
|
|
2418
|
+
const desiredPreserved = template.content.querySelector(selector);
|
|
2419
|
+
if (!desiredPreserved
|
|
2420
|
+
|| desiredPreserved.tagName !== preserved.tagName
|
|
2421
|
+
|| (preserved instanceof HTMLInputElement
|
|
2422
|
+
&& desiredPreserved instanceof HTMLInputElement
|
|
2423
|
+
&& desiredPreserved.type !== preserved.type)) {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
this.reconcilePreservedTree(this.shadowRoot, template.content, preserved);
|
|
2427
|
+
applyResponsiveStyles(this.shadowRoot, this._responsive);
|
|
2428
|
+
return this.shadowRoot.contains(preserved);
|
|
2429
|
+
}
|
|
2430
|
+
reconcilePreservedTree(currentParent, desiredParent, preserved) {
|
|
2431
|
+
const desiredChildren = Array.from(desiredParent.children);
|
|
2432
|
+
const currentChildren = Array.from(currentParent.children);
|
|
2433
|
+
const matches = new Map();
|
|
2434
|
+
const claimed = new Set();
|
|
2435
|
+
for (const desired of desiredChildren) {
|
|
2436
|
+
const desiredKey = this.reconcileKey(desired);
|
|
2437
|
+
const current = currentChildren.find(candidate => (!claimed.has(candidate)
|
|
2438
|
+
&& this.reconcileKey(candidate) === desiredKey));
|
|
2439
|
+
if (current) {
|
|
2440
|
+
claimed.add(current);
|
|
2441
|
+
matches.set(desired, current);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const current of currentChildren) {
|
|
2445
|
+
if (!claimed.has(current))
|
|
2446
|
+
current.remove();
|
|
2447
|
+
}
|
|
2448
|
+
desiredChildren.forEach((desired, index) => {
|
|
2449
|
+
let current = matches.get(desired);
|
|
2450
|
+
if (!current) {
|
|
2451
|
+
current = desired.cloneNode(true);
|
|
2452
|
+
}
|
|
2453
|
+
const childAtIndex = currentParent.children.item(index);
|
|
2454
|
+
if (childAtIndex !== current) {
|
|
2455
|
+
currentParent.insertBefore(current, childAtIndex);
|
|
2456
|
+
}
|
|
2457
|
+
this.patchPreservedElement(current, desired, preserved);
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
patchPreservedElement(current, desired, preserved) {
|
|
2461
|
+
for (const attribute of Array.from(current.attributes)) {
|
|
2462
|
+
if (!desired.hasAttribute(attribute.name)) {
|
|
2463
|
+
current.removeAttribute(attribute.name);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
for (const attribute of Array.from(desired.attributes)) {
|
|
2467
|
+
current.setAttribute(attribute.name, attribute.value);
|
|
2468
|
+
}
|
|
2469
|
+
if (current === preserved)
|
|
2470
|
+
return;
|
|
2471
|
+
if (current.contains(preserved)) {
|
|
2472
|
+
this.reconcilePreservedTree(current, desired, preserved);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
current.innerHTML = desired.innerHTML;
|
|
2476
|
+
}
|
|
2477
|
+
reconcileKey(element) {
|
|
2478
|
+
if (element.classList.contains('card-input'))
|
|
2479
|
+
return 'card-input';
|
|
2480
|
+
if (element.classList.contains('card-input-wrapper')) {
|
|
2481
|
+
return 'card-input-wrapper';
|
|
2482
|
+
}
|
|
2483
|
+
if (element.classList.contains('card-input-label')) {
|
|
2484
|
+
return 'card-input-label';
|
|
2485
|
+
}
|
|
2486
|
+
if (element.classList.contains('input-control'))
|
|
2487
|
+
return 'input-control';
|
|
2488
|
+
if (element.classList.contains('input-prefix'))
|
|
2489
|
+
return 'input-prefix';
|
|
2490
|
+
if (element.classList.contains('input-suffix'))
|
|
2491
|
+
return 'input-suffix';
|
|
2492
|
+
if (element.classList.contains('number-stepper'))
|
|
2493
|
+
return 'number-stepper';
|
|
2494
|
+
return element.tagName;
|
|
2495
|
+
}
|
|
1724
2496
|
/**
|
|
1725
2497
|
* Escape a value before interpolating it into a double-quoted HTML
|
|
1726
2498
|
* attribute. Browsers decode the entities before parsing inline CSS, so
|
|
@@ -2336,6 +3108,30 @@ function sanitizeIconHtml(icon) {
|
|
|
2336
3108
|
walk(tpl.content);
|
|
2337
3109
|
return tpl.innerHTML;
|
|
2338
3110
|
}
|
|
3111
|
+
function escapeIconAttribute(value) {
|
|
3112
|
+
return value
|
|
3113
|
+
.replace(/&/g, '&')
|
|
3114
|
+
.replace(/"/g, '"')
|
|
3115
|
+
.replace(/</g, '<')
|
|
3116
|
+
.replace(/>/g, '>');
|
|
3117
|
+
}
|
|
3118
|
+
function isDirectImageUrl(icon) {
|
|
3119
|
+
const normalized = icon.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
|
3120
|
+
return /^(?:https?:\/\/|\/\/|data:image\/)/.test(normalized)
|
|
3121
|
+
|| /^(?:\.{0,2}\/).+\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#].*)?$/i.test(icon);
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Button.icon historically accepts sanitized HTML or text. A direct image URL
|
|
3125
|
+
* is a common shorthand, so convert only recognizable image URLs to markup and
|
|
3126
|
+
* leave inline SVG, <img> HTML, emoji, and other text on the existing path.
|
|
3127
|
+
*/
|
|
3128
|
+
function renderButtonIcon(icon) {
|
|
3129
|
+
const value = String(icon ?? '').trim();
|
|
3130
|
+
if (isDirectImageUrl(value)) {
|
|
3131
|
+
return `<img class="card-btn-url-icon" src="${escapeIconAttribute(value)}" alt="" aria-hidden="true" />`;
|
|
3132
|
+
}
|
|
3133
|
+
return sanitizeIconHtml(value);
|
|
3134
|
+
}
|
|
2339
3135
|
class CardButton extends BaseElement {
|
|
2340
3136
|
render() {
|
|
2341
3137
|
if (!this.shadowRoot || !this._node)
|
|
@@ -2490,12 +3286,18 @@ class CardButton extends BaseElement {
|
|
|
2490
3286
|
width: 1em;
|
|
2491
3287
|
height: 1em;
|
|
2492
3288
|
}
|
|
3289
|
+
.card-btn-url-icon {
|
|
3290
|
+
display: block;
|
|
3291
|
+
width: 100%;
|
|
3292
|
+
height: 100%;
|
|
3293
|
+
object-fit: contain;
|
|
3294
|
+
}
|
|
2493
3295
|
</style>
|
|
2494
3296
|
<button
|
|
2495
3297
|
class="card-btn ${variant} ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2496
3298
|
${disabled ? 'disabled' : ''}
|
|
2497
3299
|
style="${inlineStyle}"
|
|
2498
|
-
><span class="card-btn-content">${icon ? `<span class="card-btn-icon">${
|
|
3300
|
+
><span class="card-btn-content">${icon ? `<span class="card-btn-icon">${renderButtonIcon(icon)}</span>` : ''}${displayText}</span></button>
|
|
2499
3301
|
`);
|
|
2500
3302
|
}
|
|
2501
3303
|
}
|
|
@@ -2679,10 +3481,18 @@ function normalizeNumberAttribute(value, options = {}) {
|
|
|
2679
3481
|
return numeric;
|
|
2680
3482
|
}
|
|
2681
3483
|
class CardInput extends BaseElement {
|
|
3484
|
+
constructor() {
|
|
3485
|
+
super(...arguments);
|
|
3486
|
+
this.wiredInputs = new WeakSet();
|
|
3487
|
+
this.wiredNumberInputs = new WeakSet();
|
|
3488
|
+
}
|
|
2682
3489
|
render() {
|
|
2683
3490
|
if (!this.shadowRoot || !this._node)
|
|
2684
3491
|
return;
|
|
2685
|
-
const
|
|
3492
|
+
const activeControl = this.shadowRoot.activeElement?.matches('.card-input')
|
|
3493
|
+
? this.shadowRoot.activeElement
|
|
3494
|
+
: null;
|
|
3495
|
+
const { placeholder = '', inputType = 'text', inputMode, label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, min, max, step = 1, controls = true, prefix, suffix, style, inputStyle, isExpressionResultStyle, } = this._props;
|
|
2686
3496
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
2687
3497
|
const legacyInputStyle = style && typeof style === 'object' && style.resize != null
|
|
2688
3498
|
? { resize: style.resize }
|
|
@@ -2752,6 +3562,7 @@ class CardInput extends BaseElement {
|
|
|
2752
3562
|
id="${inputId}"
|
|
2753
3563
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2754
3564
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
3565
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2755
3566
|
${describedBy}
|
|
2756
3567
|
${disabled ? 'disabled' : ''}
|
|
2757
3568
|
${readOnly ? 'readonly' : ''}
|
|
@@ -2763,6 +3574,7 @@ class CardInput extends BaseElement {
|
|
|
2763
3574
|
id="${inputId}"
|
|
2764
3575
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2765
3576
|
type="${this.escapeAttr(String(inputType))}"
|
|
3577
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2766
3578
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
2767
3579
|
value="${this.escapeAttr(String(defaultValue))}"
|
|
2768
3580
|
${describedBy}
|
|
@@ -2772,7 +3584,7 @@ class CardInput extends BaseElement {
|
|
|
2772
3584
|
${numberAttributes}
|
|
2773
3585
|
style="${nativeInlineStyle}"
|
|
2774
3586
|
/>`;
|
|
2775
|
-
|
|
3587
|
+
const markup = `
|
|
2776
3588
|
<style>
|
|
2777
3589
|
:host {
|
|
2778
3590
|
display: block;
|
|
@@ -2946,7 +3758,11 @@ class CardInput extends BaseElement {
|
|
|
2946
3758
|
${numberStepperHtml}
|
|
2947
3759
|
</div>
|
|
2948
3760
|
</div>
|
|
2949
|
-
|
|
3761
|
+
`;
|
|
3762
|
+
if (!activeControl
|
|
3763
|
+
|| !this.patchShadowHTMLPreservingElement(markup, activeControl, '.card-input')) {
|
|
3764
|
+
this.setShadowHTML(markup);
|
|
3765
|
+
}
|
|
2950
3766
|
// Wire up native input/change events that bubble out of Shadow DOM.
|
|
2951
3767
|
// renderCard listens for these standard event names (mapped from onInput / onChange).
|
|
2952
3768
|
// The `detail.value` carries the current input value so that:
|
|
@@ -2954,41 +3770,41 @@ class CardInput extends BaseElement {
|
|
|
2954
3770
|
// 2. action handlers can access it if needed
|
|
2955
3771
|
const inputEl = this.shadowRoot.querySelector('.card-input');
|
|
2956
3772
|
if (inputEl) {
|
|
2957
|
-
|
|
2958
|
-
this.
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
queueMicrotask(() => {
|
|
2973
|
-
const control = this.getAutoFocusControl();
|
|
2974
|
-
if (control && this.isFirstAvailableAutoFocusInput()) {
|
|
2975
|
-
control.focus();
|
|
2976
|
-
}
|
|
3773
|
+
if (!this.wiredInputs.has(inputEl)) {
|
|
3774
|
+
this.wiredInputs.add(inputEl);
|
|
3775
|
+
inputEl.addEventListener('input', () => {
|
|
3776
|
+
this.dispatchEvent(new CustomEvent('input', {
|
|
3777
|
+
bubbles: true,
|
|
3778
|
+
composed: true,
|
|
3779
|
+
detail: { value: inputEl.value },
|
|
3780
|
+
}));
|
|
3781
|
+
});
|
|
3782
|
+
inputEl.addEventListener('change', () => {
|
|
3783
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
3784
|
+
bubbles: true,
|
|
3785
|
+
composed: true,
|
|
3786
|
+
detail: { value: inputEl.value },
|
|
3787
|
+
}));
|
|
2977
3788
|
});
|
|
2978
3789
|
}
|
|
2979
3790
|
if (isNumber &&
|
|
2980
3791
|
showNumberStepper &&
|
|
2981
3792
|
inputEl instanceof HTMLInputElement) {
|
|
2982
|
-
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
2983
|
-
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
2984
3793
|
const refreshStepperState = () => {
|
|
2985
3794
|
const state = getNumberStepperDisabledState(inputEl);
|
|
3795
|
+
const increaseButton = this.shadowRoot?.querySelector('.number-step-button.increase');
|
|
3796
|
+
const decreaseButton = this.shadowRoot?.querySelector('.number-step-button.decrease');
|
|
2986
3797
|
if (increaseButton)
|
|
2987
3798
|
increaseButton.disabled = state.increase;
|
|
2988
3799
|
if (decreaseButton)
|
|
2989
3800
|
decreaseButton.disabled = state.decrease;
|
|
2990
3801
|
};
|
|
2991
|
-
|
|
3802
|
+
if (!this.wiredNumberInputs.has(inputEl)) {
|
|
3803
|
+
this.wiredNumberInputs.add(inputEl);
|
|
3804
|
+
inputEl.addEventListener('input', refreshStepperState);
|
|
3805
|
+
}
|
|
3806
|
+
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
3807
|
+
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
2992
3808
|
increaseButton?.addEventListener('click', () => {
|
|
2993
3809
|
stepNumberInput(inputEl, 'increase');
|
|
2994
3810
|
inputEl.focus();
|
|
@@ -3004,21 +3820,25 @@ class CardInput extends BaseElement {
|
|
|
3004
3820
|
}
|
|
3005
3821
|
}
|
|
3006
3822
|
// ─── Helpers ──────────────────────────────────────────────────
|
|
3007
|
-
|
|
3823
|
+
/**
|
|
3824
|
+
* Consume an interaction-driven focus request from the card renderer.
|
|
3825
|
+
* Mounts and ordinary updates never call this method.
|
|
3826
|
+
*/
|
|
3827
|
+
requestAutoFocus() {
|
|
3008
3828
|
if (!this.isConnected
|
|
3009
3829
|
|| !this._props.autoFocus
|
|
3010
3830
|
|| this.hasAttribute('data-disabled')) {
|
|
3011
|
-
return
|
|
3831
|
+
return false;
|
|
3012
3832
|
}
|
|
3013
3833
|
const control = this.shadowRoot?.querySelector('.card-input');
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
if (!('querySelectorAll' in root))
|
|
3834
|
+
if (!control
|
|
3835
|
+
|| control.disabled
|
|
3836
|
+
|| control.readOnly
|
|
3837
|
+
|| control.value !== '') {
|
|
3019
3838
|
return false;
|
|
3020
|
-
|
|
3021
|
-
|
|
3839
|
+
}
|
|
3840
|
+
control.focus({ preventScroll: true });
|
|
3841
|
+
return this.shadowRoot?.activeElement === control;
|
|
3022
3842
|
}
|
|
3023
3843
|
/** Escape HTML entities for safe insertion. */
|
|
3024
3844
|
escapeHtml(str) {
|
|
@@ -3080,15 +3900,37 @@ class CardImage extends BaseElement {
|
|
|
3080
3900
|
if (!this.shadowRoot || !this._node)
|
|
3081
3901
|
return;
|
|
3082
3902
|
const { src, alt = '', width, height, objectFit = 'cover', preview = true, style, isExpressionResultStyle, } = this._props;
|
|
3083
|
-
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
3084
3903
|
const imgSrc = this.resolveContent(src);
|
|
3085
3904
|
const imgAlt = this.resolveContent(alt);
|
|
3905
|
+
// Keep the custom-element box independent from the image's intrinsic
|
|
3906
|
+
// dimensions. A failed image has different intrinsic sizing rules, so
|
|
3907
|
+
// sizing only the inner <img> makes flex/grid layouts jump on error.
|
|
3908
|
+
const hostWidth = this.applyHostDimension('width', style?.width, width);
|
|
3909
|
+
const hostHeight = this.applyHostDimension('height', style?.height, height);
|
|
3910
|
+
const ownsWidth = hostWidth != null;
|
|
3911
|
+
const ownsHeight = hostHeight != null;
|
|
3912
|
+
const reservesWidth = ownsWidth && hostWidth !== 'auto';
|
|
3913
|
+
const reservesHeight = ownsHeight && hostHeight !== 'auto';
|
|
3914
|
+
// Width and height belong to the host layout box. Remove them from the
|
|
3915
|
+
// inner style so relative lengths (for example 50%) are not applied twice.
|
|
3916
|
+
const imageStyle = style && typeof style === 'object' ? { ...style } : style;
|
|
3917
|
+
if (imageStyle && typeof imageStyle === 'object') {
|
|
3918
|
+
if (ownsWidth)
|
|
3919
|
+
delete imageStyle.width;
|
|
3920
|
+
if (ownsHeight)
|
|
3921
|
+
delete imageStyle.height;
|
|
3922
|
+
}
|
|
3923
|
+
const inlineStyle = this.buildInlineStyle(imageStyle, isExpressionResultStyle);
|
|
3086
3924
|
// Build image inline styles
|
|
3087
3925
|
const imgStyles = [];
|
|
3088
|
-
if (
|
|
3089
|
-
imgStyles.push(`width:${
|
|
3090
|
-
if (
|
|
3091
|
-
imgStyles.push(`height:${
|
|
3926
|
+
if (ownsWidth)
|
|
3927
|
+
imgStyles.push(`width:${reservesWidth ? '100%' : 'auto'}`);
|
|
3928
|
+
if (ownsHeight)
|
|
3929
|
+
imgStyles.push(`height:${reservesHeight ? '100%' : 'auto'}`);
|
|
3930
|
+
if (reservesWidth)
|
|
3931
|
+
imgStyles.push('min-width:0');
|
|
3932
|
+
if (reservesHeight)
|
|
3933
|
+
imgStyles.push('min-height:0');
|
|
3092
3934
|
if (objectFit)
|
|
3093
3935
|
imgStyles.push(`object-fit:${objectFit}`);
|
|
3094
3936
|
if (inlineStyle)
|
|
@@ -3104,9 +3946,12 @@ class CardImage extends BaseElement {
|
|
|
3104
3946
|
position: relative;
|
|
3105
3947
|
display: inline-block;
|
|
3106
3948
|
max-width: 100%;
|
|
3949
|
+
${reservesWidth ? 'width: 100%;' : ''}
|
|
3950
|
+
${reservesHeight ? 'height: 100%;' : ''}
|
|
3107
3951
|
}
|
|
3108
3952
|
.card-image {
|
|
3109
3953
|
display: block;
|
|
3954
|
+
box-sizing: border-box;
|
|
3110
3955
|
max-width: 100%;
|
|
3111
3956
|
height: auto;
|
|
3112
3957
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
@@ -3127,8 +3972,8 @@ class CardImage extends BaseElement {
|
|
|
3127
3972
|
100% { background-position: -200% 0; }
|
|
3128
3973
|
}
|
|
3129
3974
|
.card-image.error {
|
|
3130
|
-
min-width: 100px;
|
|
3131
|
-
min-height: 60px;
|
|
3975
|
+
min-width: ${reservesWidth ? '0' : '100px'};
|
|
3976
|
+
min-height: ${reservesHeight ? '0' : '60px'};
|
|
3132
3977
|
background: #f5f5f5;
|
|
3133
3978
|
display: flex;
|
|
3134
3979
|
align-items: center;
|
|
@@ -3205,6 +4050,26 @@ class CardImage extends BaseElement {
|
|
|
3205
4050
|
`);
|
|
3206
4051
|
this.bindEvents(preview);
|
|
3207
4052
|
}
|
|
4053
|
+
applyHostDimension(property, styleValue, propValue) {
|
|
4054
|
+
this.style.removeProperty(property);
|
|
4055
|
+
for (const value of [styleValue, propValue]) {
|
|
4056
|
+
const resolved = this.resolveDeclaredDimension(value);
|
|
4057
|
+
if (resolved == null)
|
|
4058
|
+
continue;
|
|
4059
|
+
this.style.setProperty(property, resolved);
|
|
4060
|
+
const applied = this.style.getPropertyValue(property);
|
|
4061
|
+
if (applied)
|
|
4062
|
+
return applied;
|
|
4063
|
+
}
|
|
4064
|
+
return undefined;
|
|
4065
|
+
}
|
|
4066
|
+
resolveDeclaredDimension(value) {
|
|
4067
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
4068
|
+
return this.toCSS(value);
|
|
4069
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
4070
|
+
return this.toCSS(value.trim());
|
|
4071
|
+
return undefined;
|
|
4072
|
+
}
|
|
3208
4073
|
bindEvents(preview) {
|
|
3209
4074
|
if (!this.shadowRoot)
|
|
3210
4075
|
return;
|
|
@@ -3319,9 +4184,7 @@ class CardDivider extends BaseElement {
|
|
|
3319
4184
|
<div class="divider-line"></div>
|
|
3320
4185
|
${displayText ? `<span class="divider-text">${displayText}</span><div class="divider-line"></div>` : ''}
|
|
3321
4186
|
`);
|
|
3322
|
-
|
|
3323
|
-
this.style.cssText += ';' + inlineStyle;
|
|
3324
|
-
}
|
|
4187
|
+
this.style.cssText = inlineStyle;
|
|
3325
4188
|
}
|
|
3326
4189
|
}
|
|
3327
4190
|
CardDivider.is = 'ai-card-divider';
|
|
@@ -5250,9 +6113,7 @@ class CardLoading extends BaseElement {
|
|
|
5250
6113
|
<div class="spinner"></div>
|
|
5251
6114
|
${displayText ? `<span class="loading-text">${displayText}</span>` : ''}
|
|
5252
6115
|
`);
|
|
5253
|
-
|
|
5254
|
-
this.style.cssText += ';' + inlineStyle;
|
|
5255
|
-
}
|
|
6116
|
+
this.style.cssText = inlineStyle;
|
|
5256
6117
|
}
|
|
5257
6118
|
}
|
|
5258
6119
|
CardLoading.is = 'ai-card-loading';
|
|
@@ -7013,11 +7874,12 @@ class CardChoiceItem extends BaseElement {
|
|
|
7013
7874
|
...(selected ? selectedIndicatorStyle || {} : {}),
|
|
7014
7875
|
}, isExpressionResultStyle);
|
|
7015
7876
|
const indicatorRole = selectionMode === 'single' ? 'radio' : 'checkbox';
|
|
7016
|
-
const
|
|
7877
|
+
const configuredCheckedIcon = checkedIcon ?? listCheckedIcon;
|
|
7878
|
+
const resolvedCheckedIcon = configuredCheckedIcon ?? {
|
|
7017
7879
|
name: 'check_bold',
|
|
7018
7880
|
size: 12,
|
|
7019
7881
|
};
|
|
7020
|
-
const checkedMark = selectionMode === 'multiple'
|
|
7882
|
+
const checkedMark = selectionMode === 'multiple' || configuredCheckedIcon
|
|
7021
7883
|
? `<span class="choice-icon" aria-hidden="true">${renderIconContent(resolvedCheckedIcon)}</span>`
|
|
7022
7884
|
: '<span class="radio-dot" aria-hidden="true"></span>';
|
|
7023
7885
|
this.setAttribute('data-selected', String(selected));
|
|
@@ -7656,6 +8518,8 @@ function renderDefault(node, props, isMobile, responsive) {
|
|
|
7656
8518
|
div.className = `card-element card-${node.type.toLowerCase()} ${isMobile ? 'card-mobile' : 'card-desktop'}`;
|
|
7657
8519
|
div.setAttribute('data-card-id', node.id);
|
|
7658
8520
|
div.setAttribute('data-card-type', node.type);
|
|
8521
|
+
// Apply slot base styles FIRST (so props.style can override)
|
|
8522
|
+
applySlotBaseStyle(div, props);
|
|
7659
8523
|
if (props.style && typeof props.style === 'object') {
|
|
7660
8524
|
const resolvedStyle = buildStyleString(props.isExpressionResultStyle
|
|
7661
8525
|
? props.style
|
|
@@ -7718,158 +8582,936 @@ function resolveSizeInStyle(style) {
|
|
|
7718
8582
|
return resolved;
|
|
7719
8583
|
}
|
|
7720
8584
|
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
function expressionContextFor(node) {
|
|
7745
|
-
return isBoundRenderTreeNode(node)
|
|
7746
|
-
? createExpressionContext(node.scope)
|
|
7747
|
-
: variables;
|
|
7748
|
-
}
|
|
7749
|
-
function resolveNodeValue(value, node, renderVariables) {
|
|
7750
|
-
if (node.bindingDialect === 'a2ui') {
|
|
7751
|
-
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
7752
|
-
}
|
|
7753
|
-
if (typeof value === 'string' && hasExpression(value)) {
|
|
7754
|
-
return resolveExpression(value, expressionContextFor(node));
|
|
7755
|
-
}
|
|
7756
|
-
return value;
|
|
7757
|
-
}
|
|
7758
|
-
function resolveNodeProps(node, renderVariables) {
|
|
7759
|
-
const resolvedProps = (node.bindingDialect === 'a2ui'
|
|
7760
|
-
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
7761
|
-
: resolveDeep(node.props, expressionContextFor(node)));
|
|
7762
|
-
if (resolvedProps.content
|
|
7763
|
-
&& typeof resolvedProps.content === 'object'
|
|
7764
|
-
&& 'type' in resolvedProps.content) {
|
|
7765
|
-
resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContextFor(node));
|
|
7766
|
-
}
|
|
7767
|
-
return resolvedProps;
|
|
8585
|
+
const states = new WeakMap();
|
|
8586
|
+
function dispatchCurrent(element, state, domEvent, event) {
|
|
8587
|
+
const config = state.current;
|
|
8588
|
+
if (!config.isActive())
|
|
8589
|
+
return;
|
|
8590
|
+
if (domEvent === 'input'
|
|
8591
|
+
&& config.variableKey
|
|
8592
|
+
&& config.writeVariable
|
|
8593
|
+
&& event.target === element) {
|
|
8594
|
+
const value = event instanceof CustomEvent
|
|
8595
|
+
&& event.detail?.value !== undefined
|
|
8596
|
+
? event.detail.value
|
|
8597
|
+
: event.target?.value;
|
|
8598
|
+
if (value !== undefined) {
|
|
8599
|
+
config.writeVariable(config.variableKey, value);
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
for (const binding of config.events) {
|
|
8603
|
+
if (binding.domEvent !== domEvent)
|
|
8604
|
+
continue;
|
|
8605
|
+
if (binding.ownsValueEvent && event.target !== element)
|
|
8606
|
+
continue;
|
|
8607
|
+
binding.dispatch(event);
|
|
7768
8608
|
}
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
botId: options.botId,
|
|
7777
|
-
inflightRequests,
|
|
8609
|
+
}
|
|
8610
|
+
function syncElementBindings(element, config) {
|
|
8611
|
+
let state = states.get(element);
|
|
8612
|
+
if (!state) {
|
|
8613
|
+
state = {
|
|
8614
|
+
current: config,
|
|
8615
|
+
listeners: new Map(),
|
|
7778
8616
|
};
|
|
8617
|
+
states.set(element, state);
|
|
8618
|
+
}
|
|
8619
|
+
state.current = config;
|
|
8620
|
+
const desiredEvents = new Set(config.events.map(event => event.domEvent));
|
|
8621
|
+
if (config.variableKey)
|
|
8622
|
+
desiredEvents.add('input');
|
|
8623
|
+
for (const [name, listener] of state.listeners) {
|
|
8624
|
+
if (desiredEvents.has(name))
|
|
8625
|
+
continue;
|
|
8626
|
+
element.removeEventListener(name, listener);
|
|
8627
|
+
state.listeners.delete(name);
|
|
7779
8628
|
}
|
|
7780
|
-
|
|
7781
|
-
if (
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
const placeholder = document.createElement('div');
|
|
7788
|
-
placeholder.style.display = 'none';
|
|
7789
|
-
placeholder.setAttribute('data-card-id', node.id);
|
|
7790
|
-
return placeholder;
|
|
8629
|
+
for (const name of desiredEvents) {
|
|
8630
|
+
if (state.listeners.has(name))
|
|
8631
|
+
continue;
|
|
8632
|
+
const listener = event => {
|
|
8633
|
+
const latest = states.get(element);
|
|
8634
|
+
if (latest) {
|
|
8635
|
+
dispatchCurrent(element, latest, name, event);
|
|
7791
8636
|
}
|
|
8637
|
+
};
|
|
8638
|
+
element.addEventListener(name, listener);
|
|
8639
|
+
state.listeners.set(name, listener);
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
function clearElementBindings(element) {
|
|
8643
|
+
const state = states.get(element);
|
|
8644
|
+
if (!state)
|
|
8645
|
+
return;
|
|
8646
|
+
let hasError = false;
|
|
8647
|
+
let firstError;
|
|
8648
|
+
for (const [name, listener] of state.listeners) {
|
|
8649
|
+
try {
|
|
8650
|
+
element.removeEventListener(name, listener);
|
|
7792
8651
|
}
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
isDisabled = (resolved === true
|
|
7798
|
-
|| resolved === 'true'
|
|
7799
|
-
|| resolved === 1);
|
|
8652
|
+
catch (error) {
|
|
8653
|
+
if (!hasError)
|
|
8654
|
+
firstError = error;
|
|
8655
|
+
hasError = true;
|
|
7800
8656
|
}
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
8657
|
+
}
|
|
8658
|
+
state.listeners.clear();
|
|
8659
|
+
states.delete(element);
|
|
8660
|
+
if (hasError)
|
|
8661
|
+
throw firstError;
|
|
8662
|
+
}
|
|
8663
|
+
function clearElementBindingsIn(root) {
|
|
8664
|
+
let hasError = false;
|
|
8665
|
+
let firstError;
|
|
8666
|
+
const clear = (element) => {
|
|
8667
|
+
try {
|
|
8668
|
+
clearElementBindings(element);
|
|
7810
8669
|
}
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
return;
|
|
7816
|
-
const value = event.detail?.value
|
|
7817
|
-
?? event.target?.value;
|
|
7818
|
-
if (value !== undefined) {
|
|
7819
|
-
// Keep the existing focus-preserving, no-rerender behavior. Bumping
|
|
7820
|
-
// the revision prevents an older async action from overwriting it.
|
|
7821
|
-
variables[variableKey] = value;
|
|
7822
|
-
revision += 1;
|
|
7823
|
-
}
|
|
7824
|
-
}));
|
|
8670
|
+
catch (error) {
|
|
8671
|
+
if (!hasError)
|
|
8672
|
+
firstError = error;
|
|
8673
|
+
hasError = true;
|
|
7825
8674
|
}
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
8675
|
+
};
|
|
8676
|
+
clear(root);
|
|
8677
|
+
root.querySelectorAll('*').forEach(clear);
|
|
8678
|
+
if (hasError)
|
|
8679
|
+
throw firstError;
|
|
8680
|
+
}
|
|
8681
|
+
|
|
8682
|
+
let unserializableFingerprint = 0;
|
|
8683
|
+
function nextUnserializableFingerprint() {
|
|
8684
|
+
unserializableFingerprint += 1;
|
|
8685
|
+
return `__unserializable__${unserializableFingerprint}`;
|
|
8686
|
+
}
|
|
8687
|
+
function serializeJsonLike(value, ancestors) {
|
|
8688
|
+
if (value === null)
|
|
8689
|
+
return 'null';
|
|
8690
|
+
switch (typeof value) {
|
|
8691
|
+
case 'string':
|
|
8692
|
+
return JSON.stringify(value);
|
|
8693
|
+
case 'boolean':
|
|
8694
|
+
return value ? 'true' : 'false';
|
|
8695
|
+
case 'number':
|
|
8696
|
+
if (!Number.isFinite(value))
|
|
8697
|
+
throw new TypeError('Non-finite number');
|
|
8698
|
+
return JSON.stringify(value);
|
|
8699
|
+
case 'object':
|
|
8700
|
+
break;
|
|
8701
|
+
default:
|
|
8702
|
+
throw new TypeError('Unsupported JSON value');
|
|
8703
|
+
}
|
|
8704
|
+
if (ancestors.has(value))
|
|
8705
|
+
throw new TypeError('Circular JSON value');
|
|
8706
|
+
ancestors.add(value);
|
|
8707
|
+
try {
|
|
8708
|
+
if (Array.isArray(value)) {
|
|
8709
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
8710
|
+
throw new TypeError('Unsupported array prototype');
|
|
8711
|
+
}
|
|
8712
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
8713
|
+
const expectedKeys = new Set(['length']);
|
|
8714
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8715
|
+
expectedKeys.add(String(index));
|
|
8716
|
+
}
|
|
8717
|
+
if (ownKeys.some(key => typeof key !== 'string' || !expectedKeys.has(key))
|
|
8718
|
+
|| ownKeys.length !== expectedKeys.size) {
|
|
8719
|
+
throw new TypeError('Unsupported array shape');
|
|
8720
|
+
}
|
|
8721
|
+
const values = [];
|
|
8722
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8723
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
8724
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
8725
|
+
throw new TypeError('Unsupported array item');
|
|
7830
8726
|
}
|
|
7831
|
-
|
|
7832
|
-
el.addEventListener(domEvent, ((event) => {
|
|
7833
|
-
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
7834
|
-
&& VALUE_CONTROL_TYPES$1.has(node.type));
|
|
7835
|
-
if (ownsValueEvent && event.target !== el)
|
|
7836
|
-
return;
|
|
7837
|
-
const eventDetail = (event instanceof CustomEvent && event.detail != null)
|
|
7838
|
-
? event.detail
|
|
7839
|
-
: undefined;
|
|
7840
|
-
enqueueBoundEvent(node.id, eventName, eventDetail);
|
|
7841
|
-
}));
|
|
8727
|
+
values.push(serializeJsonLike(descriptor.value, ancestors));
|
|
7842
8728
|
}
|
|
8729
|
+
return `[${values.join(',')}]`;
|
|
7843
8730
|
}
|
|
7844
|
-
|
|
7845
|
-
|
|
8731
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8732
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
8733
|
+
throw new TypeError('Unsupported object prototype');
|
|
7846
8734
|
}
|
|
7847
|
-
const
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
childrenMap[child.id] = child;
|
|
8735
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
8736
|
+
if (ownKeys.some(key => typeof key !== 'string')) {
|
|
8737
|
+
throw new TypeError('Unsupported symbol key');
|
|
7851
8738
|
}
|
|
7852
|
-
const
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
8739
|
+
const entries = ownKeys.sort().map(key => {
|
|
8740
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
8741
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
8742
|
+
throw new TypeError('Unsupported object property');
|
|
7856
8743
|
}
|
|
8744
|
+
return `${JSON.stringify(key)}:${serializeJsonLike(descriptor.value, ancestors)}`;
|
|
8745
|
+
});
|
|
8746
|
+
return `{${entries.join(',')}}`;
|
|
8747
|
+
}
|
|
8748
|
+
finally {
|
|
8749
|
+
ancestors.delete(value);
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
function stableFingerprint(value) {
|
|
8753
|
+
try {
|
|
8754
|
+
return serializeJsonLike(value, new Set());
|
|
8755
|
+
}
|
|
8756
|
+
catch {
|
|
8757
|
+
return nextUnserializableFingerprint();
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
function disposeWithoutThrow(mounted, hooks) {
|
|
8761
|
+
try {
|
|
8762
|
+
hooks.dispose(mounted);
|
|
8763
|
+
}
|
|
8764
|
+
catch {
|
|
8765
|
+
// DOM ownership has already been decided; cleanup errors cannot change it.
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
function reportUnreconciled(hooks, error) {
|
|
8769
|
+
try {
|
|
8770
|
+
hooks.onUnreconciled?.(error);
|
|
8771
|
+
}
|
|
8772
|
+
catch {
|
|
8773
|
+
// Reporting cannot change which DOM tree is live.
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
function replaceIncrementalNode(current, next, hooks) {
|
|
8777
|
+
const parent = current.element.parentNode;
|
|
8778
|
+
if (!parent) {
|
|
8779
|
+
reportUnreconciled(hooks);
|
|
8780
|
+
return current;
|
|
8781
|
+
}
|
|
8782
|
+
let replacement;
|
|
8783
|
+
try {
|
|
8784
|
+
replacement = hooks.mountReplacement(next);
|
|
8785
|
+
}
|
|
8786
|
+
catch (error) {
|
|
8787
|
+
reportUnreconciled(hooks, error);
|
|
8788
|
+
return current;
|
|
8789
|
+
}
|
|
8790
|
+
if (replacement.element === current.element
|
|
8791
|
+
|| replacement.element.parentNode !== null) {
|
|
8792
|
+
reportUnreconciled(hooks);
|
|
8793
|
+
return current;
|
|
8794
|
+
}
|
|
8795
|
+
let adopted = false;
|
|
8796
|
+
let adoptionError;
|
|
8797
|
+
try {
|
|
8798
|
+
parent.replaceChild(replacement.element, current.element);
|
|
8799
|
+
adopted = replacement.element.parentNode === parent
|
|
8800
|
+
&& current.element.parentNode !== parent;
|
|
8801
|
+
}
|
|
8802
|
+
catch (error) {
|
|
8803
|
+
adoptionError = error;
|
|
8804
|
+
adopted = replacement.element.parentNode === parent
|
|
8805
|
+
&& current.element.parentNode !== parent;
|
|
8806
|
+
}
|
|
8807
|
+
if (!adopted) {
|
|
8808
|
+
if (replacement.element.parentNode === null) {
|
|
8809
|
+
disposeWithoutThrow(replacement, hooks);
|
|
8810
|
+
}
|
|
8811
|
+
reportUnreconciled(hooks, adoptionError);
|
|
8812
|
+
return current;
|
|
8813
|
+
}
|
|
8814
|
+
disposeWithoutThrow(current, hooks);
|
|
8815
|
+
return replacement;
|
|
8816
|
+
}
|
|
8817
|
+
function reconcileIncrementalNode(current, next, hooks) {
|
|
8818
|
+
const parent = current.element.parentNode;
|
|
8819
|
+
const sameChildren = current.children.length === next.children.length
|
|
8820
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
8821
|
+
let decision = 'replace';
|
|
8822
|
+
if (sameChildren) {
|
|
8823
|
+
try {
|
|
8824
|
+
decision = hooks.decide(current, next);
|
|
8825
|
+
}
|
|
8826
|
+
catch {
|
|
8827
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8828
|
+
}
|
|
8829
|
+
if (current.element.parentNode !== parent) {
|
|
8830
|
+
reportUnreconciled(hooks);
|
|
8831
|
+
return current;
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
if (decision === 'replace') {
|
|
8835
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8836
|
+
}
|
|
8837
|
+
if (decision === 'update') {
|
|
8838
|
+
try {
|
|
8839
|
+
hooks.update(current, next);
|
|
8840
|
+
}
|
|
8841
|
+
catch {
|
|
8842
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8843
|
+
}
|
|
8844
|
+
if (current.element.parentNode !== parent) {
|
|
8845
|
+
reportUnreconciled(hooks);
|
|
8846
|
+
return current;
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
try {
|
|
8850
|
+
hooks.syncBindings(current, next);
|
|
8851
|
+
}
|
|
8852
|
+
catch {
|
|
8853
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8854
|
+
}
|
|
8855
|
+
if (current.element.parentNode !== parent) {
|
|
8856
|
+
reportUnreconciled(hooks);
|
|
8857
|
+
return current;
|
|
8858
|
+
}
|
|
8859
|
+
const children = current.children.map((child, index) => (reconcileIncrementalNode(child, next.children[index], hooks)));
|
|
8860
|
+
return { ...next, element: current.element, children };
|
|
8861
|
+
}
|
|
8862
|
+
|
|
8863
|
+
const CARD_NODE_SELECTOR = '[data-card-id][data-card-type]';
|
|
8864
|
+
const FOCUS_TARGET_SELECTOR = [
|
|
8865
|
+
'input:not([disabled])',
|
|
8866
|
+
'textarea:not([disabled])',
|
|
8867
|
+
'select:not([disabled])',
|
|
8868
|
+
'button:not([disabled])',
|
|
8869
|
+
'[contenteditable="true"]',
|
|
8870
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
8871
|
+
].join(',');
|
|
8872
|
+
/**
|
|
8873
|
+
* Add logical node access to one rendered card without depending on whether
|
|
8874
|
+
* its renderer retains or replaces DOM elements between updates.
|
|
8875
|
+
*/
|
|
8876
|
+
function createCardNodeAccess(container) {
|
|
8877
|
+
const handles = new Map();
|
|
8878
|
+
const listeners = new Set();
|
|
8879
|
+
let focusedId = null;
|
|
8880
|
+
let focusSyncScheduled = false;
|
|
8881
|
+
let observing = false;
|
|
8882
|
+
let observationEpoch = 0;
|
|
8883
|
+
let observer = null;
|
|
8884
|
+
let disposed = false;
|
|
8885
|
+
const resolveElement = (id) => {
|
|
8886
|
+
if (disposed)
|
|
8887
|
+
return null;
|
|
8888
|
+
return Array.from(container.querySelectorAll(CARD_NODE_SELECTOR)).find(element => element.getAttribute('data-card-id') === id) ?? null;
|
|
8889
|
+
};
|
|
8890
|
+
const getNode = (id) => {
|
|
8891
|
+
const existing = handles.get(id);
|
|
8892
|
+
if (existing)
|
|
8893
|
+
return existing;
|
|
8894
|
+
const handle = {
|
|
8895
|
+
id,
|
|
8896
|
+
get type() {
|
|
8897
|
+
return resolveElement(id)?.getAttribute('data-card-type') ?? null;
|
|
8898
|
+
},
|
|
8899
|
+
get current() {
|
|
8900
|
+
return resolveElement(id);
|
|
8901
|
+
},
|
|
8902
|
+
getRect() {
|
|
8903
|
+
return resolveElement(id)?.getBoundingClientRect() ?? null;
|
|
8904
|
+
},
|
|
8905
|
+
focus(options) {
|
|
8906
|
+
const element = resolveElement(id);
|
|
8907
|
+
if (!element || element.hasAttribute('data-disabled'))
|
|
8908
|
+
return false;
|
|
8909
|
+
const target = element.shadowRoot
|
|
8910
|
+
?.querySelector(FOCUS_TARGET_SELECTOR) ?? element;
|
|
8911
|
+
target.focus(options);
|
|
8912
|
+
return document.activeElement === element
|
|
8913
|
+
|| element.shadowRoot?.activeElement != null;
|
|
8914
|
+
},
|
|
8915
|
+
blur() {
|
|
8916
|
+
const element = resolveElement(id);
|
|
8917
|
+
if (!element)
|
|
8918
|
+
return false;
|
|
8919
|
+
const active = element.shadowRoot?.activeElement;
|
|
8920
|
+
if (active instanceof HTMLElement) {
|
|
8921
|
+
active.blur();
|
|
8922
|
+
}
|
|
8923
|
+
else {
|
|
8924
|
+
element.blur();
|
|
8925
|
+
}
|
|
8926
|
+
return true;
|
|
8927
|
+
},
|
|
8928
|
+
scrollIntoView(options) {
|
|
8929
|
+
const element = resolveElement(id);
|
|
8930
|
+
if (!element || typeof element.scrollIntoView !== 'function') {
|
|
8931
|
+
return false;
|
|
8932
|
+
}
|
|
8933
|
+
element.scrollIntoView(options);
|
|
8934
|
+
return true;
|
|
8935
|
+
},
|
|
8936
|
+
};
|
|
8937
|
+
handles.set(id, handle);
|
|
8938
|
+
return handle;
|
|
8939
|
+
};
|
|
8940
|
+
const resolveFocusedId = () => {
|
|
8941
|
+
let active = document.activeElement;
|
|
8942
|
+
if (!active || !container.contains(active))
|
|
8943
|
+
return null;
|
|
8944
|
+
while (active) {
|
|
8945
|
+
if (active instanceof HTMLElement
|
|
8946
|
+
&& active.matches(CARD_NODE_SELECTOR)) {
|
|
8947
|
+
return active.getAttribute('data-card-id');
|
|
8948
|
+
}
|
|
8949
|
+
active = active.shadowRoot?.activeElement ?? null;
|
|
8950
|
+
}
|
|
8951
|
+
return null;
|
|
8952
|
+
};
|
|
8953
|
+
const syncFocus = () => {
|
|
8954
|
+
if (disposed || !observing)
|
|
8955
|
+
return;
|
|
8956
|
+
const nextId = resolveFocusedId();
|
|
8957
|
+
if (nextId === focusedId)
|
|
8958
|
+
return;
|
|
8959
|
+
const previousNode = focusedId ? getNode(focusedId) : null;
|
|
8960
|
+
const node = nextId ? getNode(nextId) : null;
|
|
8961
|
+
focusedId = nextId;
|
|
8962
|
+
for (const listener of [...listeners]) {
|
|
8963
|
+
try {
|
|
8964
|
+
listener({ node, previousNode });
|
|
8965
|
+
}
|
|
8966
|
+
catch (error) {
|
|
8967
|
+
console.error('[CardInstance] focus-change listener failed', error);
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
};
|
|
8971
|
+
const scheduleFocusSync = () => {
|
|
8972
|
+
if (disposed || !observing || focusSyncScheduled)
|
|
8973
|
+
return;
|
|
8974
|
+
focusSyncScheduled = true;
|
|
8975
|
+
const scheduledEpoch = observationEpoch;
|
|
8976
|
+
queueMicrotask(() => {
|
|
8977
|
+
if (scheduledEpoch !== observationEpoch)
|
|
8978
|
+
return;
|
|
8979
|
+
focusSyncScheduled = false;
|
|
8980
|
+
syncFocus();
|
|
8981
|
+
});
|
|
8982
|
+
};
|
|
8983
|
+
const startObserving = () => {
|
|
8984
|
+
if (disposed || observing)
|
|
8985
|
+
return;
|
|
8986
|
+
observing = true;
|
|
8987
|
+
observationEpoch += 1;
|
|
8988
|
+
focusedId = resolveFocusedId();
|
|
8989
|
+
container.addEventListener('focusin', scheduleFocusSync, true);
|
|
8990
|
+
container.addEventListener('focusout', scheduleFocusSync, true);
|
|
8991
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
8992
|
+
observer ?? (observer = new MutationObserver(scheduleFocusSync));
|
|
8993
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
const stopObserving = () => {
|
|
8997
|
+
if (!observing)
|
|
8998
|
+
return;
|
|
8999
|
+
observing = false;
|
|
9000
|
+
observationEpoch += 1;
|
|
9001
|
+
focusSyncScheduled = false;
|
|
9002
|
+
const cleanupSteps = [
|
|
9003
|
+
() => container.removeEventListener('focusin', scheduleFocusSync, true),
|
|
9004
|
+
() => container.removeEventListener('focusout', scheduleFocusSync, true),
|
|
9005
|
+
() => observer?.disconnect(),
|
|
9006
|
+
];
|
|
9007
|
+
for (const cleanup of cleanupSteps) {
|
|
9008
|
+
try {
|
|
9009
|
+
cleanup();
|
|
9010
|
+
}
|
|
9011
|
+
catch (error) {
|
|
9012
|
+
console.error('[CardInstance] Focus observation cleanup failed', error);
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
9015
|
+
focusedId = null;
|
|
9016
|
+
};
|
|
9017
|
+
return {
|
|
9018
|
+
getNode,
|
|
9019
|
+
onFocusChange(listener) {
|
|
9020
|
+
if (disposed)
|
|
9021
|
+
return () => { };
|
|
9022
|
+
listeners.add(listener);
|
|
9023
|
+
if (listeners.size === 1)
|
|
9024
|
+
startObserving();
|
|
9025
|
+
let subscribed = true;
|
|
9026
|
+
return () => {
|
|
9027
|
+
if (!subscribed)
|
|
9028
|
+
return;
|
|
9029
|
+
subscribed = false;
|
|
9030
|
+
listeners.delete(listener);
|
|
9031
|
+
if (listeners.size === 0)
|
|
9032
|
+
stopObserving();
|
|
9033
|
+
};
|
|
9034
|
+
},
|
|
9035
|
+
dispose() {
|
|
9036
|
+
if (disposed)
|
|
9037
|
+
return;
|
|
9038
|
+
stopObserving();
|
|
9039
|
+
disposed = true;
|
|
9040
|
+
listeners.clear();
|
|
9041
|
+
handles.clear();
|
|
9042
|
+
},
|
|
9043
|
+
};
|
|
9044
|
+
}
|
|
9045
|
+
|
|
9046
|
+
const FORCE_REPLACE = Symbol('bound-force-replace');
|
|
9047
|
+
/**
|
|
9048
|
+
* Render the scoped/materialized branch of a card.
|
|
9049
|
+
*
|
|
9050
|
+
* This implementation deliberately owns its state independently from the
|
|
9051
|
+
* legacy static renderer. Bound writes happen in isolated drafts and publish
|
|
9052
|
+
* only after materialization and detached DOM construction both succeed.
|
|
9053
|
+
*/
|
|
9054
|
+
function renderBoundCard(container, schema, options) {
|
|
9055
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
9056
|
+
const variables = cloneJsonData({
|
|
9057
|
+
...schema.variables,
|
|
9058
|
+
...options.variables,
|
|
9059
|
+
});
|
|
9060
|
+
const schemaActions = schema.actions ?? {};
|
|
9061
|
+
const lifecycleManager = createLifecycleManager();
|
|
9062
|
+
const abortController = new AbortController();
|
|
9063
|
+
const inflightRequests = new Map();
|
|
9064
|
+
const activeLifecycleNodes = new Map();
|
|
9065
|
+
let currentMaterialized;
|
|
9066
|
+
let currentMountedTree = null;
|
|
9067
|
+
let lastPublishedVariables = cloneJsonData(variables);
|
|
9068
|
+
let revision = 0;
|
|
9069
|
+
let disposed = false;
|
|
9070
|
+
let disposeRequested = false;
|
|
9071
|
+
let publishing = false;
|
|
9072
|
+
const isMobile = options.isMobile === true;
|
|
9073
|
+
let actionQueue = Promise.resolve();
|
|
9074
|
+
let queuedActionCount = 0;
|
|
9075
|
+
let lifecycleQueue = Promise.resolve();
|
|
9076
|
+
const repeatIdentityCountCache = new WeakMap();
|
|
9077
|
+
function expressionContextFor(node) {
|
|
9078
|
+
return isBoundRenderTreeNode(node)
|
|
9079
|
+
? createExpressionContext(node.scope)
|
|
9080
|
+
: variables;
|
|
9081
|
+
}
|
|
9082
|
+
function resolveNodeValue(value, node, renderVariables) {
|
|
9083
|
+
if (node.bindingDialect === 'a2ui') {
|
|
9084
|
+
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
9085
|
+
}
|
|
9086
|
+
if (typeof value === 'string' && hasExpression(value)) {
|
|
9087
|
+
return resolveExpression(value, expressionContextFor(node));
|
|
9088
|
+
}
|
|
9089
|
+
return value;
|
|
9090
|
+
}
|
|
9091
|
+
function resolveNodeProps(node, renderVariables) {
|
|
9092
|
+
const resolvedProps = (node.bindingDialect === 'a2ui'
|
|
9093
|
+
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
9094
|
+
: resolveDeep(node.props, expressionContextFor(node)));
|
|
9095
|
+
if (resolvedProps.content
|
|
9096
|
+
&& typeof resolvedProps.content === 'object'
|
|
9097
|
+
&& 'type' in resolvedProps.content) {
|
|
9098
|
+
resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContextFor(node));
|
|
9099
|
+
}
|
|
9100
|
+
return resolvedProps;
|
|
9101
|
+
}
|
|
9102
|
+
function createPassiveActionContext(renderVariables) {
|
|
9103
|
+
return {
|
|
9104
|
+
...createWebActionContext({
|
|
9105
|
+
...options,
|
|
9106
|
+
abortSignal: abortController.signal,
|
|
9107
|
+
}),
|
|
9108
|
+
variables: renderVariables,
|
|
9109
|
+
botId: options.botId,
|
|
9110
|
+
inflightRequests,
|
|
9111
|
+
};
|
|
9112
|
+
}
|
|
9113
|
+
function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
|
|
9114
|
+
return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
|
|
9115
|
+
}
|
|
9116
|
+
function isNodeDisabled(node, renderVariables) {
|
|
9117
|
+
if (!node.directives?.disabled)
|
|
9118
|
+
return false;
|
|
9119
|
+
const resolved = resolveNodeValue(node.directives.disabled, node, renderVariables);
|
|
9120
|
+
return resolved === true || resolved === 'true' || resolved === 1;
|
|
9121
|
+
}
|
|
9122
|
+
function nodeBindingFingerprint(node, resolvedProps, disabled) {
|
|
9123
|
+
const binding = { disabled };
|
|
9124
|
+
if (resolvedProps?.variableKey !== undefined) {
|
|
9125
|
+
binding.variableKey = resolvedProps.variableKey;
|
|
9126
|
+
}
|
|
9127
|
+
if (!disabled && node.events !== undefined)
|
|
9128
|
+
binding.events = node.events;
|
|
9129
|
+
return stableFingerprint(binding);
|
|
9130
|
+
}
|
|
9131
|
+
function resolveBoundMetadata(node, occurrenceKey, renderVariables) {
|
|
9132
|
+
const visible = isVisible(node, renderVariables);
|
|
9133
|
+
const rendererToken = componentRenderers[node.type]
|
|
9134
|
+
?? componentRenderers._default;
|
|
9135
|
+
if (!visible) {
|
|
9136
|
+
return {
|
|
9137
|
+
occurrenceKey,
|
|
9138
|
+
node,
|
|
9139
|
+
nodeId: node.id,
|
|
9140
|
+
nodeType: node.type,
|
|
9141
|
+
rendererToken,
|
|
9142
|
+
visible: false,
|
|
9143
|
+
disabled: false,
|
|
9144
|
+
resolvedProps: null,
|
|
9145
|
+
propsFingerprint: stableFingerprint(null),
|
|
9146
|
+
layoutFingerprint: stableFingerprint(null),
|
|
9147
|
+
bindingFingerprint: stableFingerprint(null),
|
|
9148
|
+
};
|
|
9149
|
+
}
|
|
9150
|
+
const resolvedProps = resolveNodeProps(node, renderVariables);
|
|
9151
|
+
const disabled = isNodeDisabled(node, renderVariables);
|
|
9152
|
+
return {
|
|
9153
|
+
occurrenceKey,
|
|
9154
|
+
node,
|
|
9155
|
+
nodeId: node.id,
|
|
9156
|
+
nodeType: node.type,
|
|
9157
|
+
rendererToken,
|
|
9158
|
+
visible: true,
|
|
9159
|
+
disabled,
|
|
9160
|
+
resolvedProps,
|
|
9161
|
+
propsFingerprint: stableFingerprint(resolvedProps),
|
|
9162
|
+
layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, child => resolveNodeProps(child, renderVariables))),
|
|
9163
|
+
bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
|
|
9164
|
+
};
|
|
9165
|
+
}
|
|
9166
|
+
function applyDisabledState(element, disabled) {
|
|
9167
|
+
if (!disabled)
|
|
9168
|
+
return;
|
|
9169
|
+
element.setAttribute('data-disabled', 'true');
|
|
9170
|
+
element.style.background = '#F5F5F5';
|
|
9171
|
+
element.style.color = '#C0C0C0';
|
|
9172
|
+
element.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
9173
|
+
element.style.pointerEvents = 'none';
|
|
9174
|
+
element.style.cursor = 'default';
|
|
9175
|
+
}
|
|
9176
|
+
function applyDisabledDescendants(element, disabled) {
|
|
9177
|
+
if (!disabled)
|
|
9178
|
+
return;
|
|
9179
|
+
element.querySelectorAll('*').forEach((child) => {
|
|
9180
|
+
const htmlChild = child;
|
|
9181
|
+
htmlChild.setAttribute('data-disabled', 'true');
|
|
9182
|
+
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
9183
|
+
});
|
|
9184
|
+
}
|
|
9185
|
+
function disposeBoundElement(element) {
|
|
9186
|
+
const cleanupSteps = [
|
|
9187
|
+
() => clearSlotOwnerBehaviorsIn(element),
|
|
9188
|
+
() => clearSlotItemPresentationsIn(element),
|
|
9189
|
+
() => clearElementBindingsIn(element),
|
|
9190
|
+
() => disposeChartsIn(element),
|
|
9191
|
+
];
|
|
9192
|
+
for (const cleanup of cleanupSteps) {
|
|
9193
|
+
try {
|
|
9194
|
+
cleanup();
|
|
9195
|
+
}
|
|
9196
|
+
catch (error) {
|
|
9197
|
+
console.error('[renderCard] Bound element cleanup failed', error);
|
|
9198
|
+
}
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
function disposeUncoveredBoundBranches(current, coverageRoot) {
|
|
9202
|
+
for (const child of current.children) {
|
|
9203
|
+
if (coverageRoot.contains(child.element)) {
|
|
9204
|
+
disposeUncoveredBoundBranches(child, coverageRoot);
|
|
9205
|
+
}
|
|
9206
|
+
else {
|
|
9207
|
+
disposeBoundSubtree(child);
|
|
9208
|
+
}
|
|
9209
|
+
}
|
|
9210
|
+
}
|
|
9211
|
+
function disposeBoundSubtree(current) {
|
|
9212
|
+
disposeBoundElement(current.element);
|
|
9213
|
+
disposeUncoveredBoundBranches(current, current.element);
|
|
9214
|
+
}
|
|
9215
|
+
function disposeBoundBranchesOutsideContainer(current, disposedCoverage = null) {
|
|
9216
|
+
if (container.contains(current.element)) {
|
|
9217
|
+
for (const child of current.children) {
|
|
9218
|
+
disposeBoundBranchesOutsideContainer(child);
|
|
9219
|
+
}
|
|
9220
|
+
return;
|
|
9221
|
+
}
|
|
9222
|
+
let nextCoverage = disposedCoverage;
|
|
9223
|
+
if (!disposedCoverage?.contains(current.element)) {
|
|
9224
|
+
try {
|
|
9225
|
+
disposeBoundElement(current.element);
|
|
9226
|
+
nextCoverage = current.element;
|
|
9227
|
+
}
|
|
9228
|
+
catch {
|
|
9229
|
+
nextCoverage = null;
|
|
9230
|
+
}
|
|
9231
|
+
}
|
|
9232
|
+
for (const child of current.children) {
|
|
9233
|
+
disposeBoundBranchesOutsideContainer(child, nextCoverage);
|
|
9234
|
+
}
|
|
9235
|
+
}
|
|
9236
|
+
function disposeBoundSubtreeWithoutThrow(current) {
|
|
9237
|
+
try {
|
|
9238
|
+
disposeBoundSubtree(current);
|
|
9239
|
+
}
|
|
9240
|
+
catch {
|
|
9241
|
+
// Cleanup cannot change which candidate owns the live DOM.
|
|
9242
|
+
}
|
|
9243
|
+
}
|
|
9244
|
+
function mountBoundSubtree(node, occurrenceKey, renderVariables) {
|
|
9245
|
+
const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
|
|
9246
|
+
if (!metadata.visible) {
|
|
9247
|
+
const placeholder = document.createElement('div');
|
|
9248
|
+
placeholder.style.display = 'none';
|
|
9249
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
9250
|
+
return { ...metadata, element: placeholder, children: [] };
|
|
9251
|
+
}
|
|
9252
|
+
const renderer = metadata.rendererToken;
|
|
9253
|
+
const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
|
|
9254
|
+
const children = [];
|
|
9255
|
+
try {
|
|
9256
|
+
applyDisabledState(element, metadata.disabled);
|
|
9257
|
+
const sameIdOrdinals = new Map();
|
|
9258
|
+
const renderChild = (child) => {
|
|
9259
|
+
const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
|
|
9260
|
+
sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
|
|
9261
|
+
const mounted = mountBoundSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal), renderVariables);
|
|
9262
|
+
children.push(mounted);
|
|
9263
|
+
return mounted.element;
|
|
9264
|
+
};
|
|
9265
|
+
const childrenMap = {};
|
|
9266
|
+
for (const child of node.children)
|
|
9267
|
+
childrenMap[child.id] = child;
|
|
9268
|
+
const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, child => renderChild(child), childrenMap, createPassiveActionContext(renderVariables), child => resolveNodeProps(child, renderVariables));
|
|
9269
|
+
if (!layoutApplied) {
|
|
9270
|
+
for (const child of node.children) {
|
|
9271
|
+
element.appendChild(renderChild(child));
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
applyDisabledDescendants(element, metadata.disabled);
|
|
9275
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
9276
|
+
return { ...metadata, element, children };
|
|
9277
|
+
}
|
|
9278
|
+
catch (error) {
|
|
9279
|
+
disposeBoundSubtreeWithoutThrow({ ...metadata, element, children });
|
|
9280
|
+
throw error;
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
function syncBoundBindings(element, node) {
|
|
9284
|
+
const events = [];
|
|
9285
|
+
if (node.visible && !node.disabled && node.node.events) {
|
|
9286
|
+
const repeatTargetFingerprint = getRepeatTargetFingerprint(node.node);
|
|
9287
|
+
for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
|
|
9288
|
+
if (!eventValue || !resolveActionRef(eventValue, schemaActions)) {
|
|
9289
|
+
continue;
|
|
9290
|
+
}
|
|
9291
|
+
const domEvent = EVENT_MAP[schemaEvent] ?? schemaEvent;
|
|
9292
|
+
events.push({
|
|
9293
|
+
schemaEvent,
|
|
9294
|
+
domEvent,
|
|
9295
|
+
ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
|
|
9296
|
+
&& VALUE_CONTROL_TYPES$1.has(node.nodeType)),
|
|
9297
|
+
dispatch: (event) => {
|
|
9298
|
+
const detail = event instanceof CustomEvent && event.detail != null
|
|
9299
|
+
? event.detail
|
|
9300
|
+
: undefined;
|
|
9301
|
+
enqueueBoundEvent(node.nodeId, schemaEvent, detail, repeatTargetFingerprint);
|
|
9302
|
+
},
|
|
9303
|
+
});
|
|
9304
|
+
}
|
|
9305
|
+
}
|
|
9306
|
+
const variableKey = node.resolvedProps?.variableKey;
|
|
9307
|
+
syncElementBindings(element, {
|
|
9308
|
+
variableKey: typeof variableKey === 'string' ? variableKey : undefined,
|
|
9309
|
+
writeVariable: (key, value) => {
|
|
9310
|
+
if (disposed || publishing || !container.contains(element))
|
|
9311
|
+
return;
|
|
9312
|
+
variables[key] = value;
|
|
9313
|
+
revision += 1;
|
|
9314
|
+
},
|
|
9315
|
+
events,
|
|
9316
|
+
isActive: () => (!disposed
|
|
9317
|
+
&& !publishing
|
|
9318
|
+
&& container.contains(element)),
|
|
9319
|
+
});
|
|
9320
|
+
}
|
|
9321
|
+
function syncMountedBindings(current) {
|
|
9322
|
+
syncBoundBindings(current.element, current);
|
|
9323
|
+
for (const child of current.children)
|
|
9324
|
+
syncMountedBindings(child);
|
|
9325
|
+
}
|
|
9326
|
+
function collectMountedOccurrences(current, occurrences) {
|
|
9327
|
+
occurrences.set(current.occurrenceKey, current);
|
|
9328
|
+
for (const child of current.children) {
|
|
9329
|
+
collectMountedOccurrences(child, occurrences);
|
|
9330
|
+
}
|
|
9331
|
+
}
|
|
9332
|
+
function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, current) {
|
|
9333
|
+
const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
|
|
9334
|
+
const forceReplace = forcedOwnerIds.has(node.id);
|
|
9335
|
+
if (!metadata.visible || forceReplace) {
|
|
9336
|
+
return {
|
|
9337
|
+
...metadata,
|
|
9338
|
+
...(forceReplace ? { [FORCE_REPLACE]: true } : {}),
|
|
9339
|
+
children: [],
|
|
9340
|
+
};
|
|
9341
|
+
}
|
|
9342
|
+
if (!current)
|
|
9343
|
+
return { ...metadata, children: [] };
|
|
9344
|
+
const nextChildrenById = new Map();
|
|
9345
|
+
for (const child of node.children) {
|
|
9346
|
+
const matching = nextChildrenById.get(child.id) ?? [];
|
|
9347
|
+
matching.push(child);
|
|
9348
|
+
nextChildrenById.set(child.id, matching);
|
|
9349
|
+
}
|
|
9350
|
+
const nextChildOrdinals = new Map();
|
|
9351
|
+
const children = [];
|
|
9352
|
+
for (const currentChild of current.children) {
|
|
9353
|
+
const matching = nextChildrenById.get(currentChild.nodeId);
|
|
9354
|
+
if (!matching?.length) {
|
|
9355
|
+
return { ...metadata, [FORCE_REPLACE]: true, children: [] };
|
|
9356
|
+
}
|
|
9357
|
+
const ordinal = nextChildOrdinals.get(currentChild.nodeId) ?? 0;
|
|
9358
|
+
nextChildOrdinals.set(currentChild.nodeId, ordinal + 1);
|
|
9359
|
+
const child = matching[Math.min(ordinal, matching.length - 1)];
|
|
9360
|
+
children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, currentChild));
|
|
9361
|
+
}
|
|
9362
|
+
return {
|
|
9363
|
+
...metadata,
|
|
9364
|
+
children,
|
|
9365
|
+
};
|
|
9366
|
+
}
|
|
9367
|
+
function decideBoundUpdate(current, next) {
|
|
9368
|
+
if (next[FORCE_REPLACE]
|
|
9369
|
+
|| current.occurrenceKey !== next.occurrenceKey
|
|
9370
|
+
|| current.nodeId !== next.nodeId
|
|
9371
|
+
|| current.nodeType !== next.nodeType
|
|
9372
|
+
|| current.rendererToken !== next.rendererToken
|
|
9373
|
+
|| current.visible !== next.visible
|
|
9374
|
+
|| current.disabled !== next.disabled
|
|
9375
|
+
|| current.layoutFingerprint !== next.layoutFingerprint) {
|
|
9376
|
+
return 'replace';
|
|
9377
|
+
}
|
|
9378
|
+
if (current.propsFingerprint === next.propsFingerprint)
|
|
9379
|
+
return 'retain';
|
|
9380
|
+
return (current.element instanceof BaseElement
|
|
9381
|
+
|| current.rendererToken === componentRenderers._default) ? 'update' : 'replace';
|
|
9382
|
+
}
|
|
9383
|
+
function updateBoundNode(current, next) {
|
|
9384
|
+
const element = current.element;
|
|
9385
|
+
if (element instanceof BaseElement) {
|
|
9386
|
+
updateSlotItemShellPresentation(element, () => {
|
|
9387
|
+
element.updateProps(next.resolvedProps, isMobile, options.responsive);
|
|
9388
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
9389
|
+
}, getSlotHostPresentationOwnership(next.resolvedProps));
|
|
9390
|
+
return;
|
|
9391
|
+
}
|
|
9392
|
+
if (current.rendererToken !== componentRenderers._default) {
|
|
9393
|
+
throw new Error('[renderCard] Bound node cannot update in place');
|
|
9394
|
+
}
|
|
9395
|
+
const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
|
|
9396
|
+
const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
|
|
9397
|
+
applyDisabledState(previous, current.disabled);
|
|
9398
|
+
applyDisabledState(desired, next.disabled);
|
|
9399
|
+
const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
|
|
9400
|
+
updateSlotItemShellPresentation(element, () => {
|
|
9401
|
+
patchElementShellPresentation(element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(element));
|
|
9402
|
+
}, hostOwnership);
|
|
9403
|
+
}
|
|
9404
|
+
function collectChangedPointers(before, after, path = '') {
|
|
9405
|
+
if (Object.is(before, after))
|
|
9406
|
+
return [];
|
|
9407
|
+
if (before === null
|
|
9408
|
+
|| after === null
|
|
9409
|
+
|| typeof before !== 'object'
|
|
9410
|
+
|| typeof after !== 'object') {
|
|
9411
|
+
return [path];
|
|
9412
|
+
}
|
|
9413
|
+
if (Array.isArray(before) || Array.isArray(after)) {
|
|
9414
|
+
if (!Array.isArray(before) || !Array.isArray(after))
|
|
9415
|
+
return [path];
|
|
9416
|
+
if (before.length !== after.length)
|
|
9417
|
+
return [path];
|
|
9418
|
+
}
|
|
9419
|
+
const left = before;
|
|
9420
|
+
const right = after;
|
|
9421
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
9422
|
+
const changed = [];
|
|
9423
|
+
for (const key of keys) {
|
|
9424
|
+
const token = key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
9425
|
+
changed.push(...collectChangedPointers(left[key], right[key], `${path}/${token}`));
|
|
9426
|
+
}
|
|
9427
|
+
return changed;
|
|
9428
|
+
}
|
|
9429
|
+
function selectForcedRepeatOwnerIds(before, draft) {
|
|
9430
|
+
if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
|
|
9431
|
+
return new Set([currentMaterialized.root.id]);
|
|
9432
|
+
}
|
|
9433
|
+
const selectedKeys = new Set();
|
|
9434
|
+
for (const pointer of collectChangedPointers(before, draft)) {
|
|
9435
|
+
for (const owner of findAffectedRepeatOwners(currentMaterialized, pointer)) {
|
|
9436
|
+
selectedKeys.add(owner.key);
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
for (const key of [...selectedKeys]) {
|
|
9440
|
+
let parentKey = currentMaterialized.repeatOwners.get(key)?.parentKey;
|
|
9441
|
+
while (parentKey) {
|
|
9442
|
+
if (selectedKeys.has(parentKey)) {
|
|
9443
|
+
selectedKeys.delete(key);
|
|
9444
|
+
break;
|
|
9445
|
+
}
|
|
9446
|
+
parentKey = currentMaterialized.repeatOwners.get(parentKey)?.parentKey;
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9449
|
+
return new Set([...selectedKeys].flatMap((key) => {
|
|
9450
|
+
const id = currentMaterialized.repeatOwners.get(key)?.runtimeOwnerId;
|
|
9451
|
+
return id ? [id] : [];
|
|
9452
|
+
}));
|
|
9453
|
+
}
|
|
9454
|
+
function collectMountedLifecycleIds(root) {
|
|
9455
|
+
const ids = new Set();
|
|
9456
|
+
const visit = (node) => {
|
|
9457
|
+
if (!node.visible)
|
|
9458
|
+
return;
|
|
9459
|
+
if (node.node.lifecycle)
|
|
9460
|
+
ids.add(node.nodeId);
|
|
9461
|
+
for (const child of node.children)
|
|
9462
|
+
visit(child);
|
|
9463
|
+
};
|
|
9464
|
+
if (root)
|
|
9465
|
+
visit(root);
|
|
9466
|
+
return ids;
|
|
9467
|
+
}
|
|
9468
|
+
function prepareReplacementRoots(current, next, draft, replacements) {
|
|
9469
|
+
const sameChildren = current.children.length === next.children.length
|
|
9470
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
9471
|
+
const decision = sameChildren
|
|
9472
|
+
? decideBoundUpdate(current, next)
|
|
9473
|
+
: 'replace';
|
|
9474
|
+
if (decision === 'replace'
|
|
9475
|
+
|| (decision === 'update' && next.children.length === 0)) {
|
|
9476
|
+
replacements.set(next.occurrenceKey, mountBoundSubtree(next.node, next.occurrenceKey, draft));
|
|
9477
|
+
}
|
|
9478
|
+
if (decision === 'replace')
|
|
9479
|
+
return;
|
|
9480
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
9481
|
+
prepareReplacementRoots(current.children[index], next.children[index], draft, replacements);
|
|
7857
9482
|
}
|
|
7858
|
-
if (isDisabled) {
|
|
7859
|
-
el.querySelectorAll('*').forEach((child) => {
|
|
7860
|
-
const htmlChild = child;
|
|
7861
|
-
htmlChild.setAttribute('data-disabled', 'true');
|
|
7862
|
-
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
7863
|
-
});
|
|
7864
|
-
}
|
|
7865
|
-
applyResponsiveStyles(el, createResponsiveContext(isMobile, options.responsive));
|
|
7866
|
-
return el;
|
|
7867
9483
|
}
|
|
7868
|
-
function
|
|
9484
|
+
function prepareIncrementalCandidate(before, draft) {
|
|
7869
9485
|
const materialized = materializeCard(schema, draft);
|
|
7870
|
-
const
|
|
7871
|
-
|
|
7872
|
-
|
|
9486
|
+
const forcedOwnerIds = currentMountedTree
|
|
9487
|
+
? materialized.unresolvedRepeatOwners.size > 0
|
|
9488
|
+
? new Set([materialized.root.id])
|
|
9489
|
+
: selectForcedRepeatOwnerIds(before, draft)
|
|
9490
|
+
: new Set();
|
|
9491
|
+
const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, forcedOwnerIds, currentMountedTree);
|
|
9492
|
+
const replacementRoots = new Map();
|
|
9493
|
+
try {
|
|
9494
|
+
if (currentMountedTree) {
|
|
9495
|
+
prepareReplacementRoots(currentMountedTree, preparedRoot, draft, replacementRoots);
|
|
9496
|
+
}
|
|
9497
|
+
else {
|
|
9498
|
+
replacementRoots.set(preparedRoot.occurrenceKey, mountBoundSubtree(materialized.root, preparedRoot.occurrenceKey, draft));
|
|
9499
|
+
}
|
|
9500
|
+
return {
|
|
9501
|
+
materialized,
|
|
9502
|
+
preparedRoot,
|
|
9503
|
+
replacementRoots,
|
|
9504
|
+
autoFocusIds: currentMountedTree
|
|
9505
|
+
? collectAutoFocusRevealIds(currentMaterialized, materialized, draft)
|
|
9506
|
+
: new Set(),
|
|
9507
|
+
};
|
|
9508
|
+
}
|
|
9509
|
+
catch (error) {
|
|
9510
|
+
for (const replacement of replacementRoots.values()) {
|
|
9511
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9512
|
+
}
|
|
9513
|
+
throw error;
|
|
9514
|
+
}
|
|
7873
9515
|
}
|
|
7874
9516
|
function indexNodes(root) {
|
|
7875
9517
|
const nodes = new Map();
|
|
@@ -7880,6 +9522,93 @@ function renderBoundCard(container, schema, options) {
|
|
|
7880
9522
|
visit(root);
|
|
7881
9523
|
return nodes;
|
|
7882
9524
|
}
|
|
9525
|
+
function getRepeatTargetFingerprint(node) {
|
|
9526
|
+
const identityToken = (key, value) => {
|
|
9527
|
+
if (value === null)
|
|
9528
|
+
return `${key}:null`;
|
|
9529
|
+
if (typeof value === 'string')
|
|
9530
|
+
return `${key}:string:${value}`;
|
|
9531
|
+
if (typeof value === 'boolean')
|
|
9532
|
+
return `${key}:boolean:${value}`;
|
|
9533
|
+
return `${key}:number:${Object.is(value, -0) ? '-0' : String(value)}`;
|
|
9534
|
+
};
|
|
9535
|
+
const identityCountsFor = (root) => {
|
|
9536
|
+
const cached = repeatIdentityCountCache.get(root);
|
|
9537
|
+
if (cached)
|
|
9538
|
+
return cached;
|
|
9539
|
+
const counts = new Map();
|
|
9540
|
+
const visited = new Set();
|
|
9541
|
+
const visit = (candidate) => {
|
|
9542
|
+
if (candidate === null || typeof candidate !== 'object')
|
|
9543
|
+
return;
|
|
9544
|
+
if (visited.has(candidate))
|
|
9545
|
+
return;
|
|
9546
|
+
visited.add(candidate);
|
|
9547
|
+
if (!Array.isArray(candidate)) {
|
|
9548
|
+
for (const key of ['id', 'key']) {
|
|
9549
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
9550
|
+
if (!descriptor || !('value' in descriptor))
|
|
9551
|
+
continue;
|
|
9552
|
+
const identity = descriptor.value;
|
|
9553
|
+
if (identity === null
|
|
9554
|
+
|| (typeof identity !== 'string'
|
|
9555
|
+
&& typeof identity !== 'number'
|
|
9556
|
+
&& typeof identity !== 'boolean')) {
|
|
9557
|
+
continue;
|
|
9558
|
+
}
|
|
9559
|
+
const token = identityToken(key, identity);
|
|
9560
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
9561
|
+
}
|
|
9562
|
+
}
|
|
9563
|
+
for (const nested of Object.values(candidate))
|
|
9564
|
+
visit(nested);
|
|
9565
|
+
};
|
|
9566
|
+
visit(root);
|
|
9567
|
+
repeatIdentityCountCache.set(root, counts);
|
|
9568
|
+
return counts;
|
|
9569
|
+
};
|
|
9570
|
+
const identityFor = (value, root) => {
|
|
9571
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
9572
|
+
return value;
|
|
9573
|
+
}
|
|
9574
|
+
const record = value;
|
|
9575
|
+
for (const key of ['id', 'key']) {
|
|
9576
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
9577
|
+
if (!descriptor || !('value' in descriptor))
|
|
9578
|
+
continue;
|
|
9579
|
+
const identity = descriptor.value;
|
|
9580
|
+
if (identity === null
|
|
9581
|
+
|| (typeof identity !== 'string'
|
|
9582
|
+
&& typeof identity !== 'number'
|
|
9583
|
+
&& typeof identity !== 'boolean')) {
|
|
9584
|
+
continue;
|
|
9585
|
+
}
|
|
9586
|
+
const matches = identityCountsFor(root).get(identityToken(key, identity)) ?? 0;
|
|
9587
|
+
if (matches === 1)
|
|
9588
|
+
return { [key]: identity };
|
|
9589
|
+
}
|
|
9590
|
+
return value;
|
|
9591
|
+
};
|
|
9592
|
+
const targets = [];
|
|
9593
|
+
let scope = node.scope;
|
|
9594
|
+
while (scope) {
|
|
9595
|
+
const scopeRoot = scope.root;
|
|
9596
|
+
const localEntries = Object.entries(scope.locals);
|
|
9597
|
+
if (localEntries.length > 0) {
|
|
9598
|
+
targets.push(Object.fromEntries(localEntries.map(([key, value]) => [
|
|
9599
|
+
key,
|
|
9600
|
+
identityFor(value, scopeRoot),
|
|
9601
|
+
])));
|
|
9602
|
+
}
|
|
9603
|
+
else if (scope.bindingDialect === 'a2ui' && scope.dataPath !== '') {
|
|
9604
|
+
targets.push(identityFor(resolveA2UIDeep({ path: '' }, scopeRoot, scope.dataPath), scopeRoot));
|
|
9605
|
+
}
|
|
9606
|
+
scope = scope.parent;
|
|
9607
|
+
}
|
|
9608
|
+
if (targets.length === 0)
|
|
9609
|
+
return undefined;
|
|
9610
|
+
return stableFingerprint({ sourceId: node.sourceId, targets });
|
|
9611
|
+
}
|
|
7883
9612
|
function selectLifecycleNodes(materialized, ids) {
|
|
7884
9613
|
const indexed = indexNodes(materialized.root);
|
|
7885
9614
|
const selected = new Map();
|
|
@@ -7890,26 +9619,80 @@ function renderBoundCard(container, schema, options) {
|
|
|
7890
9619
|
}
|
|
7891
9620
|
return selected;
|
|
7892
9621
|
}
|
|
7893
|
-
function
|
|
9622
|
+
function isVisible(node, renderVariables) {
|
|
9623
|
+
if (!node.directives?.visible)
|
|
9624
|
+
return true;
|
|
9625
|
+
const resolved = resolveNodeValue(node.directives.visible, node, renderVariables);
|
|
9626
|
+
return !(resolved === false
|
|
9627
|
+
|| resolved === 'false'
|
|
9628
|
+
|| resolved === ''
|
|
9629
|
+
|| resolved === 0);
|
|
9630
|
+
}
|
|
9631
|
+
function collectAutoFocusRevealIds(previous, next, draft) {
|
|
9632
|
+
const previousNodes = indexNodes(previous.root);
|
|
9633
|
+
const requested = new Set();
|
|
9634
|
+
const visit = (nextNode, previousParentVisible, nextParentVisible) => {
|
|
9635
|
+
const previousNode = previousNodes.get(nextNode.id);
|
|
9636
|
+
const wasVisible = Boolean(previousNode
|
|
9637
|
+
&& previousParentVisible
|
|
9638
|
+
&& isVisible(previousNode, variables));
|
|
9639
|
+
const nowVisible = nextParentVisible && isVisible(nextNode, draft);
|
|
9640
|
+
if (nextNode.type === 'Input'
|
|
9641
|
+
&& !wasVisible
|
|
9642
|
+
&& nowVisible
|
|
9643
|
+
&& resolveNodeProps(nextNode, draft).autoFocus === true) {
|
|
9644
|
+
requested.add(nextNode.id);
|
|
9645
|
+
}
|
|
9646
|
+
nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
9647
|
+
};
|
|
9648
|
+
visit(next.root, true, true);
|
|
9649
|
+
return requested;
|
|
9650
|
+
}
|
|
9651
|
+
function applyAutoFocus(candidate) {
|
|
9652
|
+
if (candidate.autoFocusIds.size === 0)
|
|
9653
|
+
return;
|
|
9654
|
+
const inputs = container.querySelectorAll(CardInput.is);
|
|
9655
|
+
for (const input of inputs) {
|
|
9656
|
+
const id = input.getAttribute('data-card-id');
|
|
9657
|
+
if (id && candidate.autoFocusIds.has(id) && input.requestAutoFocus()) {
|
|
9658
|
+
break;
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
function createLifecycleActionSession(node) {
|
|
9663
|
+
const lifecycleVariables = cloneJsonData(variables);
|
|
9664
|
+
const rebaseScope = (scope) => ({
|
|
9665
|
+
...scope,
|
|
9666
|
+
root: lifecycleVariables,
|
|
9667
|
+
parent: scope.parent ? rebaseScope(scope.parent) : undefined,
|
|
9668
|
+
});
|
|
9669
|
+
const lifecycleScope = rebaseScope(node.scope);
|
|
9670
|
+
let authorityRevision = revision;
|
|
7894
9671
|
const writeLiveVariable = (key, value, silent = false) => {
|
|
7895
|
-
|
|
9672
|
+
const nextValue = cloneJsonData(value);
|
|
9673
|
+
writeDraftVariable(lifecycleVariables, key, nextValue);
|
|
9674
|
+
repeatIdentityCountCache.delete(lifecycleVariables);
|
|
9675
|
+
if (disposed || authorityRevision !== revision)
|
|
7896
9676
|
return;
|
|
7897
9677
|
if (silent) {
|
|
7898
|
-
writeDraftVariable(variables, key,
|
|
9678
|
+
writeDraftVariable(variables, key, nextValue);
|
|
7899
9679
|
return;
|
|
7900
9680
|
}
|
|
7901
|
-
|
|
9681
|
+
// Publish the complete lifecycle draft so direct additions, changes,
|
|
9682
|
+
// and deletions join this explicit write in one authoritative render.
|
|
9683
|
+
commitDraft(cloneJsonData(lastPublishedVariables), lifecycleVariables, authorityRevision);
|
|
9684
|
+
authorityRevision = revision;
|
|
7902
9685
|
};
|
|
7903
|
-
|
|
9686
|
+
const context = {
|
|
7904
9687
|
...createWebActionContext({
|
|
7905
9688
|
...options,
|
|
7906
9689
|
setVariable: writeLiveVariable,
|
|
7907
9690
|
abortSignal: abortController.signal,
|
|
7908
9691
|
}),
|
|
7909
|
-
variables,
|
|
7910
|
-
expressionContext: createExpressionContext(
|
|
9692
|
+
variables: lifecycleVariables,
|
|
9693
|
+
expressionContext: createExpressionContext(lifecycleScope),
|
|
7911
9694
|
parameterResolver: node.bindingDialect === 'a2ui'
|
|
7912
|
-
? createA2UIParameterResolver(
|
|
9695
|
+
? createA2UIParameterResolver(lifecycleVariables, node.dataPath)
|
|
7913
9696
|
: undefined,
|
|
7914
9697
|
variableWriter: (key, value, options) => {
|
|
7915
9698
|
writeLiveVariable(key, value, options.silent);
|
|
@@ -7917,6 +9700,20 @@ function renderBoundCard(container, schema, options) {
|
|
|
7917
9700
|
botId: options.botId,
|
|
7918
9701
|
inflightRequests,
|
|
7919
9702
|
};
|
|
9703
|
+
return {
|
|
9704
|
+
context,
|
|
9705
|
+
publishDirectMutations() {
|
|
9706
|
+
if (disposed
|
|
9707
|
+
|| authorityRevision !== revision
|
|
9708
|
+
|| !hasVariableChanges(variables, lifecycleVariables)) {
|
|
9709
|
+
return;
|
|
9710
|
+
}
|
|
9711
|
+
// Direct custom-handler mutations historically updated the live
|
|
9712
|
+
// variables without rendering. Preserve that behavior while keeping
|
|
9713
|
+
// a stale async lifecycle detached after a newer revision wins.
|
|
9714
|
+
replaceRootContents(variables, lifecycleVariables);
|
|
9715
|
+
},
|
|
9716
|
+
};
|
|
7920
9717
|
}
|
|
7921
9718
|
function reconcileLifecycles(nextNodes) {
|
|
7922
9719
|
const removed = [...activeLifecycleNodes.entries()]
|
|
@@ -7929,45 +9726,263 @@ function renderBoundCard(container, schema, options) {
|
|
|
7929
9726
|
}
|
|
7930
9727
|
lifecycleQueue = lifecycleQueue.then(async () => {
|
|
7931
9728
|
for (const [id, node] of removed) {
|
|
7932
|
-
|
|
7933
|
-
|
|
9729
|
+
if (disposed)
|
|
9730
|
+
return;
|
|
9731
|
+
const session = createLifecycleActionSession(node);
|
|
9732
|
+
try {
|
|
9733
|
+
await lifecycleManager.destroy(id, session.context);
|
|
9734
|
+
session.publishDirectMutations();
|
|
9735
|
+
}
|
|
9736
|
+
catch (error) {
|
|
9737
|
+
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
9738
|
+
}
|
|
9739
|
+
finally {
|
|
9740
|
+
lifecycleManager.unregister(id);
|
|
9741
|
+
}
|
|
7934
9742
|
}
|
|
7935
|
-
if (disposed)
|
|
7936
|
-
return;
|
|
7937
9743
|
for (const [id, node] of added) {
|
|
9744
|
+
if (disposed)
|
|
9745
|
+
return;
|
|
7938
9746
|
lifecycleManager.register(id, node.lifecycle);
|
|
7939
|
-
|
|
9747
|
+
const session = createLifecycleActionSession(node);
|
|
9748
|
+
try {
|
|
9749
|
+
await lifecycleManager.mount(id, session.context);
|
|
9750
|
+
session.publishDirectMutations();
|
|
9751
|
+
}
|
|
9752
|
+
catch (error) {
|
|
9753
|
+
lifecycleManager.unregister(id);
|
|
9754
|
+
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
9755
|
+
}
|
|
7940
9756
|
}
|
|
7941
9757
|
}).catch((error) => {
|
|
7942
9758
|
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
7943
9759
|
});
|
|
7944
9760
|
}
|
|
7945
|
-
function
|
|
7946
|
-
const
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
9761
|
+
function disposeCandidate(candidate) {
|
|
9762
|
+
for (const replacement of candidate.replacementRoots.values()) {
|
|
9763
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9764
|
+
}
|
|
9765
|
+
candidate.replacementRoots.clear();
|
|
9766
|
+
}
|
|
9767
|
+
function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
|
|
9768
|
+
if (previous.element !== next.element) {
|
|
9769
|
+
restoreScrollPositions$1(next.element, scrollPositions);
|
|
9770
|
+
restoreMediaStates$1(next.element, mediaStates);
|
|
9771
|
+
return;
|
|
9772
|
+
}
|
|
9773
|
+
if (previous.propsFingerprint !== next.propsFingerprint
|
|
9774
|
+
&& (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
|
|
9775
|
+
restoreMediaStates$1(next.element, mediaStates);
|
|
9776
|
+
}
|
|
9777
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
9778
|
+
const previousChild = previous.children[index];
|
|
9779
|
+
const nextChild = next.children[index];
|
|
9780
|
+
if (previousChild
|
|
9781
|
+
&& previousChild.occurrenceKey === nextChild.occurrenceKey) {
|
|
9782
|
+
restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
9786
|
+
function restoreDisabledDescendantPresentation(current) {
|
|
9787
|
+
if (current.disabled) {
|
|
9788
|
+
applyDisabledState(current.element, true);
|
|
9789
|
+
applyDisabledDescendants(current.element, true);
|
|
9790
|
+
}
|
|
9791
|
+
for (const child of current.children) {
|
|
9792
|
+
restoreDisabledDescendantPresentation(child);
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
function replaceContainerRoot(element, failureMessage) {
|
|
9796
|
+
let adoptionThrew = false;
|
|
9797
|
+
let adoptionError;
|
|
9798
|
+
try {
|
|
9799
|
+
container.replaceChildren(element);
|
|
9800
|
+
}
|
|
9801
|
+
catch (error) {
|
|
9802
|
+
adoptionThrew = true;
|
|
9803
|
+
adoptionError = error;
|
|
9804
|
+
}
|
|
9805
|
+
const adopted = (element.parentNode === container
|
|
9806
|
+
&& container.firstChild === element
|
|
9807
|
+
&& container.childNodes.length === 1);
|
|
9808
|
+
if (adopted)
|
|
9809
|
+
return;
|
|
9810
|
+
if (adoptionThrew)
|
|
9811
|
+
throw adoptionError;
|
|
9812
|
+
throw new Error(failureMessage);
|
|
9813
|
+
}
|
|
9814
|
+
function commitIncrementalCandidate(candidate, renderVariables) {
|
|
9815
|
+
const replacements = candidate.replacementRoots;
|
|
9816
|
+
try {
|
|
9817
|
+
if (!currentMountedTree) {
|
|
9818
|
+
const initial = replacements.get(candidate.preparedRoot.occurrenceKey);
|
|
9819
|
+
if (!initial) {
|
|
9820
|
+
throw new Error('[renderCard] Missing bound root candidate');
|
|
9821
|
+
}
|
|
9822
|
+
syncMountedBindings(initial);
|
|
9823
|
+
disposeChartsIn(container);
|
|
9824
|
+
replaceContainerRoot(initial.element, '[renderCard] Bound root candidate was not adopted');
|
|
9825
|
+
replacements.delete(candidate.preparedRoot.occurrenceKey);
|
|
9826
|
+
restoreDisabledDescendantPresentation(initial);
|
|
9827
|
+
return initial;
|
|
9828
|
+
}
|
|
9829
|
+
const previous = currentMountedTree;
|
|
9830
|
+
const replacementSources = new Map();
|
|
9831
|
+
collectMountedOccurrences(previous, replacementSources);
|
|
9832
|
+
const scrollPositions = captureScrollPositions$1(container);
|
|
9833
|
+
const mediaStates = captureMediaStates$1(container);
|
|
9834
|
+
let unreconciled = false;
|
|
9835
|
+
const next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
|
|
9836
|
+
decide: (current, prepared) => decideBoundUpdate(current, prepared),
|
|
9837
|
+
update: (current, prepared) => updateBoundNode(current, prepared),
|
|
9838
|
+
mountReplacement: (prepared) => {
|
|
9839
|
+
const candidateReplacement = replacements.get(prepared.occurrenceKey);
|
|
9840
|
+
const replacement = candidateReplacement ?? mountBoundSubtree(prepared.node, prepared.occurrenceKey, renderVariables);
|
|
9841
|
+
const current = replacementSources.get(prepared.occurrenceKey);
|
|
9842
|
+
let staged = false;
|
|
9843
|
+
try {
|
|
9844
|
+
if (replacement.element.parentNode !== null) {
|
|
9845
|
+
throw new Error(`[renderCard] Bound replacement "${prepared.occurrenceKey}" is not detached`);
|
|
9846
|
+
}
|
|
9847
|
+
syncMountedBindings(replacement);
|
|
9848
|
+
if (current && replacement.element !== current.element) {
|
|
9849
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
9850
|
+
}
|
|
9851
|
+
staged = true;
|
|
9852
|
+
}
|
|
9853
|
+
finally {
|
|
9854
|
+
if (!staged && !candidateReplacement) {
|
|
9855
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9856
|
+
}
|
|
9857
|
+
}
|
|
9858
|
+
if (candidateReplacement) {
|
|
9859
|
+
replacements.delete(prepared.occurrenceKey);
|
|
9860
|
+
}
|
|
9861
|
+
return replacement;
|
|
9862
|
+
},
|
|
9863
|
+
dispose: disposeBoundSubtree,
|
|
9864
|
+
syncBindings: () => { },
|
|
9865
|
+
onUnreconciled: () => {
|
|
9866
|
+
unreconciled = true;
|
|
9867
|
+
},
|
|
9868
|
+
});
|
|
9869
|
+
if (!unreconciled
|
|
9870
|
+
&& (next.element.parentNode !== container
|
|
9871
|
+
|| container.firstElementChild !== next.element)) {
|
|
9872
|
+
unreconciled = true;
|
|
9873
|
+
}
|
|
9874
|
+
if (!unreconciled) {
|
|
9875
|
+
try {
|
|
9876
|
+
syncMountedBindings(next);
|
|
9877
|
+
}
|
|
9878
|
+
catch {
|
|
9879
|
+
unreconciled = true;
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
if (unreconciled) {
|
|
9883
|
+
let emergency;
|
|
9884
|
+
try {
|
|
9885
|
+
emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
|
|
9886
|
+
}
|
|
9887
|
+
catch (error) {
|
|
9888
|
+
disposeBoundBranchesOutsideContainer(next);
|
|
9889
|
+
throw error;
|
|
9890
|
+
}
|
|
9891
|
+
let adopted = false;
|
|
9892
|
+
try {
|
|
9893
|
+
syncMountedBindings(emergency);
|
|
9894
|
+
replaceContainerRoot(emergency.element, '[renderCard] Bound emergency root was not adopted');
|
|
9895
|
+
adopted = true;
|
|
9896
|
+
}
|
|
9897
|
+
finally {
|
|
9898
|
+
if (!adopted) {
|
|
9899
|
+
disposeBoundSubtreeWithoutThrow(emergency);
|
|
9900
|
+
disposeBoundBranchesOutsideContainer(next);
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
disposeBoundSubtreeWithoutThrow(next);
|
|
9904
|
+
try {
|
|
9905
|
+
restoreChangedSubtreeState(previous, emergency, scrollPositions, mediaStates);
|
|
9906
|
+
}
|
|
9907
|
+
catch {
|
|
9908
|
+
// Presentation restoration cannot roll back an adopted next root.
|
|
9909
|
+
}
|
|
9910
|
+
restoreDisabledDescendantPresentation(emergency);
|
|
9911
|
+
return emergency;
|
|
9912
|
+
}
|
|
9913
|
+
try {
|
|
9914
|
+
restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates);
|
|
9915
|
+
}
|
|
9916
|
+
catch {
|
|
9917
|
+
// Presentation restoration does not affect published data ownership.
|
|
9918
|
+
}
|
|
9919
|
+
restoreDisabledDescendantPresentation(next);
|
|
9920
|
+
return next;
|
|
9921
|
+
}
|
|
9922
|
+
finally {
|
|
9923
|
+
disposeCandidate(candidate);
|
|
9924
|
+
}
|
|
7956
9925
|
}
|
|
7957
9926
|
function assertCurrentRevision(baseRevision) {
|
|
7958
|
-
if (disposed || revision !== baseRevision) {
|
|
9927
|
+
if (disposed || publishing || revision !== baseRevision) {
|
|
7959
9928
|
const error = new Error('[renderCard] BOUND_TRANSACTION_CONFLICT');
|
|
7960
9929
|
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
7961
9930
|
throw error;
|
|
7962
9931
|
}
|
|
7963
9932
|
}
|
|
7964
|
-
function commitDraft(
|
|
9933
|
+
function commitDraft(before, draft, baseRevision) {
|
|
7965
9934
|
assertCurrentRevision(baseRevision);
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
9935
|
+
const validationRoot = cloneJsonData(variables);
|
|
9936
|
+
replaceRootContents(validationRoot, draft);
|
|
9937
|
+
const candidate = prepareIncrementalCandidate(before, draft);
|
|
9938
|
+
try {
|
|
9939
|
+
assertCurrentRevision(baseRevision);
|
|
9940
|
+
}
|
|
9941
|
+
catch (error) {
|
|
9942
|
+
disposeCandidate(candidate);
|
|
9943
|
+
throw error;
|
|
9944
|
+
}
|
|
9945
|
+
publishing = true;
|
|
9946
|
+
let publishingFailed = false;
|
|
9947
|
+
let publishingError;
|
|
9948
|
+
let deferredDisposeFailed = false;
|
|
9949
|
+
let deferredDisposeError;
|
|
9950
|
+
try {
|
|
9951
|
+
const nextMounted = commitIncrementalCandidate(candidate, draft);
|
|
9952
|
+
replaceRootContents(variables, draft);
|
|
9953
|
+
currentMaterialized = materializeCard(schema, variables);
|
|
9954
|
+
currentMountedTree = nextMounted;
|
|
9955
|
+
lastPublishedVariables = cloneJsonData(variables);
|
|
9956
|
+
revision += 1;
|
|
9957
|
+
}
|
|
9958
|
+
catch (error) {
|
|
9959
|
+
publishingFailed = true;
|
|
9960
|
+
publishingError = error;
|
|
9961
|
+
}
|
|
9962
|
+
finally {
|
|
9963
|
+
publishing = false;
|
|
9964
|
+
if (disposeRequested) {
|
|
9965
|
+
try {
|
|
9966
|
+
disposeNow();
|
|
9967
|
+
}
|
|
9968
|
+
catch (error) {
|
|
9969
|
+
deferredDisposeFailed = true;
|
|
9970
|
+
deferredDisposeError = error;
|
|
9971
|
+
}
|
|
9972
|
+
}
|
|
9973
|
+
}
|
|
9974
|
+
if (publishingFailed) {
|
|
9975
|
+
if (deferredDisposeFailed) {
|
|
9976
|
+
console.error('[renderCard] Bound deferred dispose failed', deferredDisposeError);
|
|
9977
|
+
}
|
|
9978
|
+
throw publishingError;
|
|
9979
|
+
}
|
|
9980
|
+
if (deferredDisposeFailed)
|
|
9981
|
+
throw deferredDisposeError;
|
|
9982
|
+
if (disposed)
|
|
9983
|
+
return;
|
|
9984
|
+
reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
|
|
9985
|
+
applyAutoFocus(candidate);
|
|
7971
9986
|
}
|
|
7972
9987
|
function writeDraftVariable(draft, key, value) {
|
|
7973
9988
|
Object.defineProperty(draft, String(key), {
|
|
@@ -7977,6 +9992,33 @@ function renderBoundCard(container, schema, options) {
|
|
|
7977
9992
|
writable: true,
|
|
7978
9993
|
});
|
|
7979
9994
|
}
|
|
9995
|
+
function jsonDataEqual(before, after) {
|
|
9996
|
+
if (Object.is(before, after))
|
|
9997
|
+
return true;
|
|
9998
|
+
if (Array.isArray(before) || Array.isArray(after)) {
|
|
9999
|
+
if (!Array.isArray(before) || !Array.isArray(after))
|
|
10000
|
+
return false;
|
|
10001
|
+
return before.length === after.length
|
|
10002
|
+
&& before.every((value, index) => jsonDataEqual(value, after[index]));
|
|
10003
|
+
}
|
|
10004
|
+
if (before === null
|
|
10005
|
+
|| after === null
|
|
10006
|
+
|| typeof before !== 'object'
|
|
10007
|
+
|| typeof after !== 'object') {
|
|
10008
|
+
return false;
|
|
10009
|
+
}
|
|
10010
|
+
const beforeRecord = before;
|
|
10011
|
+
const afterRecord = after;
|
|
10012
|
+
const beforeKeys = Object.keys(beforeRecord);
|
|
10013
|
+
const afterKeys = Object.keys(afterRecord);
|
|
10014
|
+
return beforeKeys.length === afterKeys.length
|
|
10015
|
+
&& beforeKeys.every(key => Object.prototype.hasOwnProperty.call(afterRecord, key)
|
|
10016
|
+
&& jsonDataEqual(beforeRecord[key], afterRecord[key]));
|
|
10017
|
+
}
|
|
10018
|
+
function hasVariableChanges(before, after) {
|
|
10019
|
+
const validatedAfter = cloneJsonData(after);
|
|
10020
|
+
return !jsonDataEqual(before, validatedAfter);
|
|
10021
|
+
}
|
|
7980
10022
|
function createDraftActionContext(node, draft) {
|
|
7981
10023
|
const write = (key, value) => {
|
|
7982
10024
|
writeDraftVariable(draft, key, value);
|
|
@@ -7997,19 +10039,27 @@ function renderBoundCard(container, schema, options) {
|
|
|
7997
10039
|
inflightRequests,
|
|
7998
10040
|
};
|
|
7999
10041
|
}
|
|
8000
|
-
async function runBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10042
|
+
async function runBoundEvent(runtimeId, eventName, eventDetail, baseRevision, repeatTargetFingerprint) {
|
|
8001
10043
|
if (disposed)
|
|
8002
10044
|
return;
|
|
8003
|
-
|
|
8004
|
-
const
|
|
10045
|
+
assertCurrentRevision(baseRevision);
|
|
10046
|
+
const transactionBefore = cloneJsonData(variables);
|
|
10047
|
+
const draft = cloneJsonData(transactionBefore);
|
|
8005
10048
|
if (eventDetail !== undefined) {
|
|
8006
10049
|
writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
|
|
8007
10050
|
}
|
|
10051
|
+
const actionBaseline = cloneJsonData(draft);
|
|
8008
10052
|
const freshMaterialized = materializeCard(schema, draft);
|
|
8009
10053
|
const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
|
|
8010
10054
|
if (!freshNode) {
|
|
10055
|
+
if (repeatTargetFingerprint !== undefined)
|
|
10056
|
+
return;
|
|
8011
10057
|
throw new Error(`[renderCard] Bound runtime node "${runtimeId}" no longer exists`);
|
|
8012
10058
|
}
|
|
10059
|
+
if (repeatTargetFingerprint !== undefined
|
|
10060
|
+
&& getRepeatTargetFingerprint(freshNode) !== repeatTargetFingerprint) {
|
|
10061
|
+
return;
|
|
10062
|
+
}
|
|
8013
10063
|
const eventValue = freshNode.events?.[eventName];
|
|
8014
10064
|
const steps = eventValue
|
|
8015
10065
|
? resolveActionRef(eventValue, schemaActions)
|
|
@@ -8018,14 +10068,21 @@ function renderBoundCard(container, schema, options) {
|
|
|
8018
10068
|
return;
|
|
8019
10069
|
await runActionSteps(steps, createDraftActionContext(freshNode, draft));
|
|
8020
10070
|
assertCurrentRevision(baseRevision);
|
|
8021
|
-
|
|
8022
|
-
|
|
10071
|
+
if (!hasVariableChanges(actionBaseline, draft))
|
|
10072
|
+
return;
|
|
10073
|
+
// The target check above may have cached identity counts before action
|
|
10074
|
+
// steps changed the draft. Recount once when binding the published tree.
|
|
10075
|
+
repeatIdentityCountCache.delete(draft);
|
|
10076
|
+
commitDraft(cloneJsonData(lastPublishedVariables), draft, baseRevision);
|
|
8023
10077
|
}
|
|
8024
|
-
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10078
|
+
function enqueueBoundEvent(runtimeId, eventName, eventDetail, repeatTargetFingerprint) {
|
|
8025
10079
|
if (disposed)
|
|
8026
10080
|
return;
|
|
10081
|
+
const enqueueRevision = revision;
|
|
10082
|
+
const queuedBehindAnotherAction = queuedActionCount > 0;
|
|
10083
|
+
queuedActionCount += 1;
|
|
8027
10084
|
actionQueue = actionQueue
|
|
8028
|
-
.then(() => runBoundEvent(runtimeId, eventName, eventDetail))
|
|
10085
|
+
.then(() => runBoundEvent(runtimeId, eventName, eventDetail, queuedBehindAnotherAction ? revision : enqueueRevision, repeatTargetFingerprint))
|
|
8029
10086
|
.catch((error) => {
|
|
8030
10087
|
if (error
|
|
8031
10088
|
&& typeof error === 'object'
|
|
@@ -8034,44 +10091,87 @@ function renderBoundCard(container, schema, options) {
|
|
|
8034
10091
|
return;
|
|
8035
10092
|
}
|
|
8036
10093
|
console.error('[renderCard] Bound action failed', error);
|
|
10094
|
+
})
|
|
10095
|
+
.finally(() => {
|
|
10096
|
+
queuedActionCount -= 1;
|
|
8037
10097
|
});
|
|
8038
10098
|
}
|
|
8039
10099
|
function updateVariables(newVariables) {
|
|
8040
10100
|
if (disposed)
|
|
8041
10101
|
return;
|
|
8042
10102
|
const baseRevision = revision;
|
|
10103
|
+
const before = cloneJsonData(lastPublishedVariables);
|
|
8043
10104
|
const draft = cloneJsonData(variables);
|
|
8044
10105
|
const patch = cloneJsonData(newVariables);
|
|
8045
10106
|
for (const key of Object.keys(patch)) {
|
|
8046
10107
|
writeDraftVariable(draft, key, patch[key]);
|
|
8047
10108
|
}
|
|
8048
|
-
|
|
8049
|
-
commitDraft(draft, candidate, baseRevision);
|
|
10109
|
+
commitDraft(before, draft, baseRevision);
|
|
8050
10110
|
}
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
10111
|
+
function disposeNow() {
|
|
10112
|
+
if (disposed)
|
|
10113
|
+
return;
|
|
10114
|
+
disposed = true;
|
|
10115
|
+
disposeRequested = false;
|
|
10116
|
+
nodeAccess.dispose();
|
|
10117
|
+
abortController.abort();
|
|
10118
|
+
if (currentMountedTree) {
|
|
10119
|
+
disposeBoundSubtree(currentMountedTree);
|
|
10120
|
+
currentMountedTree = null;
|
|
10121
|
+
}
|
|
10122
|
+
else {
|
|
8060
10123
|
disposeChartsIn(container);
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
10124
|
+
}
|
|
10125
|
+
container.replaceChildren();
|
|
10126
|
+
const lifecycleNodes = [...activeLifecycleNodes.entries()];
|
|
10127
|
+
activeLifecycleNodes.clear();
|
|
10128
|
+
lifecycleQueue = lifecycleQueue
|
|
10129
|
+
.then(async () => {
|
|
10130
|
+
for (const [id, node] of lifecycleNodes) {
|
|
10131
|
+
const session = createLifecycleActionSession(node);
|
|
10132
|
+
try {
|
|
10133
|
+
await lifecycleManager.destroy(id, session.context);
|
|
10134
|
+
session.publishDirectMutations();
|
|
10135
|
+
}
|
|
10136
|
+
catch (error) {
|
|
10137
|
+
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
10138
|
+
}
|
|
10139
|
+
finally {
|
|
8068
10140
|
lifecycleManager.unregister(id);
|
|
8069
10141
|
}
|
|
10142
|
+
}
|
|
10143
|
+
try {
|
|
8070
10144
|
await lifecycleManager.dispose(createPassiveActionContext(variables));
|
|
8071
|
-
}
|
|
8072
|
-
|
|
10145
|
+
}
|
|
10146
|
+
catch (error) {
|
|
8073
10147
|
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
8074
|
-
}
|
|
10148
|
+
}
|
|
10149
|
+
})
|
|
10150
|
+
.catch((error) => {
|
|
10151
|
+
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
10152
|
+
});
|
|
10153
|
+
}
|
|
10154
|
+
const initialCandidate = prepareIncrementalCandidate(variables, variables);
|
|
10155
|
+
publishing = true;
|
|
10156
|
+
try {
|
|
10157
|
+
currentMountedTree = commitIncrementalCandidate(initialCandidate, variables);
|
|
10158
|
+
currentMaterialized = materializeCard(schema, variables);
|
|
10159
|
+
}
|
|
10160
|
+
finally {
|
|
10161
|
+
publishing = false;
|
|
10162
|
+
}
|
|
10163
|
+
reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
|
|
10164
|
+
return {
|
|
10165
|
+
getNode: nodeAccess.getNode,
|
|
10166
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
10167
|
+
dispose() {
|
|
10168
|
+
if (disposed || disposeRequested)
|
|
10169
|
+
return;
|
|
10170
|
+
if (publishing) {
|
|
10171
|
+
disposeRequested = true;
|
|
10172
|
+
return;
|
|
10173
|
+
}
|
|
10174
|
+
disposeNow();
|
|
8075
10175
|
},
|
|
8076
10176
|
updateVariables,
|
|
8077
10177
|
};
|
|
@@ -8089,7 +10189,11 @@ function captureScrollPositions$1(root) {
|
|
|
8089
10189
|
function restoreScrollPositions$1(root, positions) {
|
|
8090
10190
|
if (positions.size === 0)
|
|
8091
10191
|
return;
|
|
8092
|
-
|
|
10192
|
+
const scrollers = [];
|
|
10193
|
+
if (root.matches('[data-scroll-id]'))
|
|
10194
|
+
scrollers.push(root);
|
|
10195
|
+
scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
|
|
10196
|
+
scrollers.forEach((element) => {
|
|
8093
10197
|
const id = element.getAttribute('data-scroll-id');
|
|
8094
10198
|
const saved = id ? positions.get(id) : undefined;
|
|
8095
10199
|
if (saved == null)
|
|
@@ -8118,7 +10222,11 @@ function captureMediaStates$1(root) {
|
|
|
8118
10222
|
function restoreMediaStates$1(root, states) {
|
|
8119
10223
|
if (states.size === 0)
|
|
8120
10224
|
return;
|
|
8121
|
-
|
|
10225
|
+
const mediaHosts = [];
|
|
10226
|
+
if (root.matches('ai-card-audio, ai-card-video'))
|
|
10227
|
+
mediaHosts.push(root);
|
|
10228
|
+
mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
|
|
10229
|
+
mediaHosts.forEach((element) => {
|
|
8122
10230
|
const id = element.getAttribute('data-card-id');
|
|
8123
10231
|
const snapshot = id ? states.get(id) : undefined;
|
|
8124
10232
|
if (!snapshot)
|
|
@@ -8180,64 +10288,759 @@ function renderCard(container, schemaInput, options = {}) {
|
|
|
8180
10288
|
return renderBoundCard(container, schema, options);
|
|
8181
10289
|
}
|
|
8182
10290
|
function renderStaticCard(container, schema, options) {
|
|
10291
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8183
10292
|
// 2. Parse into render tree
|
|
8184
10293
|
const tree = parseSchema(schema);
|
|
8185
10294
|
// 3. Reactive variables store (mutable copy, merged with external variables)
|
|
8186
10295
|
let variables = { ...schema.variables, ...options.variables };
|
|
8187
|
-
|
|
8188
|
-
|
|
10296
|
+
let disposed = false;
|
|
10297
|
+
let publishing = false;
|
|
10298
|
+
let disposeRequested = false;
|
|
10299
|
+
let hostAuthorityEpoch = 0;
|
|
10300
|
+
let nextStaticEventSequence = 0;
|
|
10301
|
+
const latestStaticEventWriterByKey = new Map();
|
|
10302
|
+
const lifecycleRecords = new Map();
|
|
8189
10303
|
const abortController = new AbortController();
|
|
8190
10304
|
// Per-instance request dedup map (isolated from other cards on the page).
|
|
8191
10305
|
const inflightRequests = new Map();
|
|
8192
|
-
function
|
|
8193
|
-
|
|
10306
|
+
function isVisible(node, renderVariables, parentVisible) {
|
|
10307
|
+
if (!parentVisible)
|
|
10308
|
+
return false;
|
|
10309
|
+
if (!node.directives?.visible)
|
|
10310
|
+
return true;
|
|
10311
|
+
const visible = node.directives.visible;
|
|
10312
|
+
const resolved = hasExpression(visible)
|
|
10313
|
+
? resolveExpression(visible, renderVariables)
|
|
10314
|
+
: visible;
|
|
10315
|
+
return !(resolved === false
|
|
10316
|
+
|| resolved === 'false'
|
|
10317
|
+
|| resolved === ''
|
|
10318
|
+
|| resolved === 0);
|
|
10319
|
+
}
|
|
10320
|
+
function collectAutoFocusRevealIds(previousVariables, nextVariables) {
|
|
10321
|
+
const requested = new Set();
|
|
10322
|
+
const visit = (node, previousParentVisible, nextParentVisible) => {
|
|
10323
|
+
const wasVisible = isVisible(node, previousVariables, previousParentVisible);
|
|
10324
|
+
const nowVisible = isVisible(node, nextVariables, nextParentVisible);
|
|
10325
|
+
if (node.type === 'Input'
|
|
10326
|
+
&& !wasVisible
|
|
10327
|
+
&& nowVisible
|
|
10328
|
+
&& resolveDeep(node.props, nextVariables).autoFocus === true) {
|
|
10329
|
+
requested.add(node.id);
|
|
10330
|
+
}
|
|
10331
|
+
node.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
10332
|
+
};
|
|
10333
|
+
visit(tree, true, true);
|
|
10334
|
+
return requested;
|
|
10335
|
+
}
|
|
10336
|
+
function applyAutoFocus(requested) {
|
|
10337
|
+
if (requested.size === 0)
|
|
10338
|
+
return;
|
|
10339
|
+
for (const input of container.querySelectorAll(CardInput.is)) {
|
|
10340
|
+
const id = input.getAttribute('data-card-id');
|
|
10341
|
+
if (id && requested.has(id) && input.requestAutoFocus())
|
|
10342
|
+
break;
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
function buildActionContext(pendingAutoFocusIds, initialVariables = variables, eventSequence) {
|
|
10346
|
+
const baseAuthorityEpoch = hostAuthorityEpoch;
|
|
10347
|
+
let context;
|
|
10348
|
+
const writeVariable = (key, value, { silent }) => {
|
|
10349
|
+
let actionVariables = context.variables;
|
|
10350
|
+
const latestWriter = latestStaticEventWriterByKey.get(key);
|
|
10351
|
+
if (disposed
|
|
10352
|
+
|| publishing
|
|
10353
|
+
|| baseAuthorityEpoch !== hostAuthorityEpoch
|
|
10354
|
+
|| (eventSequence !== undefined
|
|
10355
|
+
&& latestWriter !== undefined
|
|
10356
|
+
&& latestWriter > eventSequence)) {
|
|
10357
|
+
if (actionVariables === variables) {
|
|
10358
|
+
actionVariables = { ...actionVariables };
|
|
10359
|
+
context.variables = actionVariables;
|
|
10360
|
+
}
|
|
10361
|
+
actionVariables[key] = value;
|
|
10362
|
+
return;
|
|
10363
|
+
}
|
|
10364
|
+
const previousVariables = !silent && pendingAutoFocusIds
|
|
10365
|
+
? { ...actionVariables }
|
|
10366
|
+
: null;
|
|
10367
|
+
const liveVariables = variables;
|
|
10368
|
+
const hadLiveValue = Object.prototype.hasOwnProperty.call(liveVariables, key);
|
|
10369
|
+
const previousLiveValue = liveVariables[key];
|
|
10370
|
+
actionVariables[key] = value;
|
|
10371
|
+
if (silent)
|
|
10372
|
+
return;
|
|
10373
|
+
// Event actions use a private snapshot so a later event cannot replace
|
|
10374
|
+
// their `_event` detail while they await. Publish only the key written by
|
|
10375
|
+
// this step, preserving concurrent actions that update different keys.
|
|
10376
|
+
if (actionVariables !== variables) {
|
|
10377
|
+
variables[key] = value;
|
|
10378
|
+
}
|
|
10379
|
+
if (previousVariables && pendingAutoFocusIds) {
|
|
10380
|
+
for (const id of collectAutoFocusRevealIds(previousVariables, actionVariables)) {
|
|
10381
|
+
pendingAutoFocusIds.add(id);
|
|
10382
|
+
}
|
|
10383
|
+
}
|
|
10384
|
+
try {
|
|
10385
|
+
rerender();
|
|
10386
|
+
}
|
|
10387
|
+
catch (error) {
|
|
10388
|
+
if (variables === liveVariables) {
|
|
10389
|
+
if (hadLiveValue)
|
|
10390
|
+
liveVariables[key] = previousLiveValue;
|
|
10391
|
+
else
|
|
10392
|
+
Reflect.deleteProperty(liveVariables, key);
|
|
10393
|
+
if (eventSequence !== undefined
|
|
10394
|
+
&& latestStaticEventWriterByKey.get(key) === eventSequence) {
|
|
10395
|
+
if (latestWriter === undefined) {
|
|
10396
|
+
latestStaticEventWriterByKey.delete(key);
|
|
10397
|
+
}
|
|
10398
|
+
else {
|
|
10399
|
+
latestStaticEventWriterByKey.set(key, latestWriter);
|
|
10400
|
+
}
|
|
10401
|
+
}
|
|
10402
|
+
pendingAutoFocusIds?.clear();
|
|
10403
|
+
if (!disposed)
|
|
10404
|
+
actionContext = buildActionContext();
|
|
10405
|
+
}
|
|
10406
|
+
throw error;
|
|
10407
|
+
}
|
|
10408
|
+
};
|
|
10409
|
+
context = {
|
|
8194
10410
|
...createWebActionContext({
|
|
8195
10411
|
...options,
|
|
8196
10412
|
setVariable: (key, value) => {
|
|
8197
|
-
|
|
8198
|
-
// Trigger re-render after variable change
|
|
8199
|
-
rerender();
|
|
10413
|
+
writeVariable(key, value, { silent: false});
|
|
8200
10414
|
},
|
|
8201
10415
|
abortSignal: abortController.signal,
|
|
8202
10416
|
}),
|
|
8203
|
-
variables,
|
|
10417
|
+
variables: initialVariables,
|
|
10418
|
+
variableWriter: writeVariable,
|
|
8204
10419
|
botId: options.botId,
|
|
8205
10420
|
inflightRequests,
|
|
8206
10421
|
};
|
|
10422
|
+
return context;
|
|
8207
10423
|
}
|
|
8208
10424
|
let actionContext = buildActionContext();
|
|
8209
10425
|
// 5. Responsive
|
|
8210
10426
|
const isMobile = options.isMobile === true;
|
|
8211
10427
|
// 6. Schema-level action definitions (for string references in events)
|
|
8212
10428
|
const schemaActions = schema.actions ?? {};
|
|
8213
|
-
|
|
10429
|
+
function destroyLifecycle(record) {
|
|
10430
|
+
if (record.state !== 'mounted')
|
|
10431
|
+
return;
|
|
10432
|
+
record.state = 'destroying';
|
|
10433
|
+
void runActionSteps(record.lifecycle.onDestroy ?? [], buildActionContext())
|
|
10434
|
+
.catch((error) => {
|
|
10435
|
+
console.error('[renderCard] Static lifecycle destroy failed', error);
|
|
10436
|
+
})
|
|
10437
|
+
.finally(() => {
|
|
10438
|
+
record.state = 'destroyed';
|
|
10439
|
+
});
|
|
10440
|
+
}
|
|
10441
|
+
function startLifecycleMount(id, lifecycle) {
|
|
10442
|
+
if (disposed || lifecycleRecords.has(id))
|
|
10443
|
+
return;
|
|
10444
|
+
const record = { lifecycle, state: 'mounting' };
|
|
10445
|
+
lifecycleRecords.set(id, record);
|
|
10446
|
+
void runActionSteps(lifecycle.onMount ?? [], buildActionContext())
|
|
10447
|
+
.then(() => {
|
|
10448
|
+
record.state = 'mounted';
|
|
10449
|
+
if (disposed)
|
|
10450
|
+
destroyLifecycle(record);
|
|
10451
|
+
})
|
|
10452
|
+
.catch((error) => {
|
|
10453
|
+
console.error('[renderCard] Static lifecycle mount failed', error);
|
|
10454
|
+
if (disposed) {
|
|
10455
|
+
record.state = 'destroyed';
|
|
10456
|
+
}
|
|
10457
|
+
else if (lifecycleRecords.get(id) === record) {
|
|
10458
|
+
lifecycleRecords.delete(id);
|
|
10459
|
+
}
|
|
10460
|
+
});
|
|
10461
|
+
}
|
|
10462
|
+
let mountedTree = null;
|
|
10463
|
+
let renderGeneration = 0;
|
|
10464
|
+
function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
|
|
10465
|
+
return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
|
|
10466
|
+
}
|
|
10467
|
+
function resolveNodeProps(node) {
|
|
10468
|
+
const resolvedProps = resolveDeep(node.props, variables);
|
|
10469
|
+
if (resolvedProps.content
|
|
10470
|
+
&& typeof resolvedProps.content === 'object'
|
|
10471
|
+
&& 'type' in resolvedProps.content) {
|
|
10472
|
+
resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
|
|
10473
|
+
}
|
|
10474
|
+
return resolvedProps;
|
|
10475
|
+
}
|
|
10476
|
+
function isNodeDisabled(node) {
|
|
10477
|
+
if (!node.directives?.disabled)
|
|
10478
|
+
return false;
|
|
10479
|
+
const disabled = node.directives.disabled;
|
|
10480
|
+
const resolved = hasExpression(disabled)
|
|
10481
|
+
? resolveExpression(disabled, variables)
|
|
10482
|
+
: disabled;
|
|
10483
|
+
return resolved === true || resolved === 'true' || resolved === 1;
|
|
10484
|
+
}
|
|
10485
|
+
function nodeBindingFingerprint(node, resolvedProps, disabled) {
|
|
10486
|
+
const binding = { disabled };
|
|
10487
|
+
if (resolvedProps && resolvedProps.variableKey !== undefined) {
|
|
10488
|
+
binding.variableKey = resolvedProps.variableKey;
|
|
10489
|
+
}
|
|
10490
|
+
if (!disabled && node.events !== undefined) {
|
|
10491
|
+
binding.events = node.events;
|
|
10492
|
+
}
|
|
10493
|
+
return stableFingerprint(binding);
|
|
10494
|
+
}
|
|
10495
|
+
function resolveStaticMetadata(node, occurrenceKey) {
|
|
10496
|
+
const visible = isVisible(node, variables, true);
|
|
10497
|
+
const rendererToken = componentRenderers[node.type]
|
|
10498
|
+
?? componentRenderers._default;
|
|
10499
|
+
if (!visible) {
|
|
10500
|
+
return {
|
|
10501
|
+
occurrenceKey,
|
|
10502
|
+
node,
|
|
10503
|
+
nodeId: node.id,
|
|
10504
|
+
nodeType: node.type,
|
|
10505
|
+
rendererToken,
|
|
10506
|
+
visible: false,
|
|
10507
|
+
disabled: false,
|
|
10508
|
+
resolvedProps: null,
|
|
10509
|
+
propsFingerprint: stableFingerprint(null),
|
|
10510
|
+
layoutFingerprint: stableFingerprint(null),
|
|
10511
|
+
bindingFingerprint: stableFingerprint(null),
|
|
10512
|
+
};
|
|
10513
|
+
}
|
|
10514
|
+
const resolvedProps = resolveNodeProps(node);
|
|
10515
|
+
const disabled = isNodeDisabled(node);
|
|
10516
|
+
return {
|
|
10517
|
+
occurrenceKey,
|
|
10518
|
+
node,
|
|
10519
|
+
nodeId: node.id,
|
|
10520
|
+
nodeType: node.type,
|
|
10521
|
+
rendererToken,
|
|
10522
|
+
visible: true,
|
|
10523
|
+
disabled,
|
|
10524
|
+
resolvedProps,
|
|
10525
|
+
propsFingerprint: stableFingerprint(resolvedProps),
|
|
10526
|
+
layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, resolveNodeProps)),
|
|
10527
|
+
bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
|
|
10528
|
+
};
|
|
10529
|
+
}
|
|
10530
|
+
function applyDisabledState(element, disabled) {
|
|
10531
|
+
if (!disabled)
|
|
10532
|
+
return;
|
|
10533
|
+
element.setAttribute('data-disabled', 'true');
|
|
10534
|
+
element.style.background = '#F5F5F5';
|
|
10535
|
+
element.style.color = '#C0C0C0';
|
|
10536
|
+
element.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
10537
|
+
element.style.pointerEvents = 'none';
|
|
10538
|
+
element.style.cursor = 'default';
|
|
10539
|
+
}
|
|
10540
|
+
function applyDisabledDescendants(element, disabled) {
|
|
10541
|
+
if (!disabled)
|
|
10542
|
+
return;
|
|
10543
|
+
element.querySelectorAll('*').forEach((child) => {
|
|
10544
|
+
const htmlChild = child;
|
|
10545
|
+
htmlChild.setAttribute('data-disabled', 'true');
|
|
10546
|
+
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
10547
|
+
});
|
|
10548
|
+
}
|
|
10549
|
+
function runStaticEventActions(steps, detail) {
|
|
10550
|
+
if (disposed)
|
|
10551
|
+
return;
|
|
10552
|
+
if (detail != null)
|
|
10553
|
+
variables._event = detail;
|
|
10554
|
+
const sourceVariables = variables;
|
|
10555
|
+
const actionAuthorityEpoch = hostAuthorityEpoch;
|
|
10556
|
+
const eventSequence = ++nextStaticEventSequence;
|
|
10557
|
+
const localVariables = Object.create(null);
|
|
10558
|
+
if (Object.prototype.hasOwnProperty.call(sourceVariables, '_event')) {
|
|
10559
|
+
localVariables._event = sourceVariables._event;
|
|
10560
|
+
}
|
|
10561
|
+
const deletedProperties = new Set();
|
|
10562
|
+
let frozenVariables = null;
|
|
10563
|
+
const eventVariables = new Proxy(localVariables, {
|
|
10564
|
+
get(target, property) {
|
|
10565
|
+
if (frozenVariables)
|
|
10566
|
+
return Reflect.get(frozenVariables, property);
|
|
10567
|
+
if (deletedProperties.has(property))
|
|
10568
|
+
return undefined;
|
|
10569
|
+
return Object.prototype.hasOwnProperty.call(target, property)
|
|
10570
|
+
? Reflect.get(target, property)
|
|
10571
|
+
: Reflect.get(sourceVariables, property);
|
|
10572
|
+
},
|
|
10573
|
+
set(target, property, value) {
|
|
10574
|
+
if (frozenVariables) {
|
|
10575
|
+
Reflect.set(frozenVariables, property, value);
|
|
10576
|
+
return true;
|
|
10577
|
+
}
|
|
10578
|
+
deletedProperties.delete(property);
|
|
10579
|
+
Reflect.set(target, property, value);
|
|
10580
|
+
if (property !== '_event'
|
|
10581
|
+
&& !disposed
|
|
10582
|
+
&& !publishing
|
|
10583
|
+
&& variables === sourceVariables
|
|
10584
|
+
&& actionAuthorityEpoch === hostAuthorityEpoch
|
|
10585
|
+
&& (latestStaticEventWriterByKey.get(property) === undefined
|
|
10586
|
+
|| latestStaticEventWriterByKey.get(property) <= eventSequence)) {
|
|
10587
|
+
latestStaticEventWriterByKey.set(property, eventSequence);
|
|
10588
|
+
Reflect.set(sourceVariables, property, value);
|
|
10589
|
+
}
|
|
10590
|
+
return true;
|
|
10591
|
+
},
|
|
10592
|
+
deleteProperty(target, property) {
|
|
10593
|
+
if (frozenVariables) {
|
|
10594
|
+
return Reflect.deleteProperty(frozenVariables, property);
|
|
10595
|
+
}
|
|
10596
|
+
deletedProperties.add(property);
|
|
10597
|
+
Reflect.deleteProperty(target, property);
|
|
10598
|
+
if (property !== '_event'
|
|
10599
|
+
&& !disposed
|
|
10600
|
+
&& !publishing
|
|
10601
|
+
&& variables === sourceVariables
|
|
10602
|
+
&& actionAuthorityEpoch === hostAuthorityEpoch
|
|
10603
|
+
&& (latestStaticEventWriterByKey.get(property) === undefined
|
|
10604
|
+
|| latestStaticEventWriterByKey.get(property) <= eventSequence)) {
|
|
10605
|
+
latestStaticEventWriterByKey.set(property, eventSequence);
|
|
10606
|
+
Reflect.deleteProperty(sourceVariables, property);
|
|
10607
|
+
}
|
|
10608
|
+
return true;
|
|
10609
|
+
},
|
|
10610
|
+
has(target, property) {
|
|
10611
|
+
if (!frozenVariables && deletedProperties.has(property))
|
|
10612
|
+
return false;
|
|
10613
|
+
return frozenVariables
|
|
10614
|
+
? property in frozenVariables
|
|
10615
|
+
: property in target || property in sourceVariables;
|
|
10616
|
+
},
|
|
10617
|
+
ownKeys(target) {
|
|
10618
|
+
return Reflect.ownKeys(frozenVariables ?? { ...sourceVariables, ...target }).filter(property => !deletedProperties.has(property));
|
|
10619
|
+
},
|
|
10620
|
+
getOwnPropertyDescriptor(target, property) {
|
|
10621
|
+
if (!frozenVariables && deletedProperties.has(property)) {
|
|
10622
|
+
return undefined;
|
|
10623
|
+
}
|
|
10624
|
+
const owner = frozenVariables
|
|
10625
|
+
?? (Object.prototype.hasOwnProperty.call(target, property)
|
|
10626
|
+
? target
|
|
10627
|
+
: sourceVariables);
|
|
10628
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, property);
|
|
10629
|
+
return descriptor ? { ...descriptor, configurable: true } : undefined;
|
|
10630
|
+
},
|
|
10631
|
+
});
|
|
10632
|
+
const pendingAutoFocusIds = new Set();
|
|
10633
|
+
void runActionSteps(steps, buildActionContext(pendingAutoFocusIds, eventVariables, eventSequence))
|
|
10634
|
+
.catch((error) => {
|
|
10635
|
+
console.error('[renderCard] Static action failed', error);
|
|
10636
|
+
})
|
|
10637
|
+
.finally(() => {
|
|
10638
|
+
const snapshot = {
|
|
10639
|
+
...sourceVariables,
|
|
10640
|
+
...localVariables,
|
|
10641
|
+
};
|
|
10642
|
+
for (const property of deletedProperties) {
|
|
10643
|
+
Reflect.deleteProperty(snapshot, property);
|
|
10644
|
+
}
|
|
10645
|
+
frozenVariables = snapshot;
|
|
10646
|
+
if (disposed || actionAuthorityEpoch !== hostAuthorityEpoch)
|
|
10647
|
+
return;
|
|
10648
|
+
applyAutoFocus(pendingAutoFocusIds);
|
|
10649
|
+
});
|
|
10650
|
+
}
|
|
10651
|
+
function syncStaticBindings(element, node) {
|
|
10652
|
+
const resolvedEventBindings = [];
|
|
10653
|
+
if (node.visible && !node.disabled && node.node.events) {
|
|
10654
|
+
for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
|
|
10655
|
+
if (!eventValue)
|
|
10656
|
+
continue;
|
|
10657
|
+
const steps = resolveActionRef(eventValue, schemaActions);
|
|
10658
|
+
if (!steps)
|
|
10659
|
+
continue;
|
|
10660
|
+
const domEvent = eventMap[schemaEvent] ?? schemaEvent;
|
|
10661
|
+
resolvedEventBindings.push({
|
|
10662
|
+
schemaEvent,
|
|
10663
|
+
domEvent,
|
|
10664
|
+
ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
|
|
10665
|
+
&& VALUE_CONTROL_TYPES.has(node.nodeType)),
|
|
10666
|
+
dispatch: (event) => {
|
|
10667
|
+
const detail = event instanceof CustomEvent
|
|
10668
|
+
? event.detail
|
|
10669
|
+
: undefined;
|
|
10670
|
+
runStaticEventActions(steps, detail);
|
|
10671
|
+
},
|
|
10672
|
+
});
|
|
10673
|
+
}
|
|
10674
|
+
}
|
|
10675
|
+
const variableKey = node.resolvedProps?.variableKey;
|
|
10676
|
+
syncElementBindings(element, {
|
|
10677
|
+
variableKey: typeof variableKey === 'string' ? variableKey : undefined,
|
|
10678
|
+
writeVariable: (key, value) => {
|
|
10679
|
+
if (disposed
|
|
10680
|
+
|| publishing
|
|
10681
|
+
|| !container.contains(element)) {
|
|
10682
|
+
return;
|
|
10683
|
+
}
|
|
10684
|
+
variables[key] = value;
|
|
10685
|
+
hostAuthorityEpoch += 1;
|
|
10686
|
+
},
|
|
10687
|
+
events: resolvedEventBindings,
|
|
10688
|
+
isActive: () => (!disposed
|
|
10689
|
+
&& !publishing
|
|
10690
|
+
&& container.contains(element)),
|
|
10691
|
+
});
|
|
10692
|
+
}
|
|
10693
|
+
function mountStaticSubtree(node, occurrenceKey) {
|
|
10694
|
+
const metadata = resolveStaticMetadata(node, occurrenceKey);
|
|
10695
|
+
if (!metadata.visible) {
|
|
10696
|
+
const placeholder = document.createElement('div');
|
|
10697
|
+
placeholder.style.display = 'none';
|
|
10698
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
10699
|
+
syncStaticBindings(placeholder, { ...metadata});
|
|
10700
|
+
return { ...metadata, element: placeholder, children: [] };
|
|
10701
|
+
}
|
|
10702
|
+
const renderer = metadata.rendererToken;
|
|
10703
|
+
const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
|
|
10704
|
+
try {
|
|
10705
|
+
applyDisabledState(element, metadata.disabled);
|
|
10706
|
+
syncStaticBindings(element, { ...metadata, children: [] });
|
|
10707
|
+
const children = [];
|
|
10708
|
+
const sameIdOrdinals = new Map();
|
|
10709
|
+
const renderChild = (child) => {
|
|
10710
|
+
const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
|
|
10711
|
+
sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
|
|
10712
|
+
const mounted = mountStaticSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal));
|
|
10713
|
+
children.push(mounted);
|
|
10714
|
+
return mounted.element;
|
|
10715
|
+
};
|
|
10716
|
+
const childrenMap = {};
|
|
10717
|
+
for (const child of node.children)
|
|
10718
|
+
childrenMap[child.id] = child;
|
|
10719
|
+
const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, renderChild, childrenMap, actionContext, resolveNodeProps);
|
|
10720
|
+
if (!layoutApplied) {
|
|
10721
|
+
for (const child of node.children) {
|
|
10722
|
+
element.appendChild(renderChild(child));
|
|
10723
|
+
}
|
|
10724
|
+
}
|
|
10725
|
+
applyDisabledDescendants(element, metadata.disabled);
|
|
10726
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
10727
|
+
return { ...metadata, element, children };
|
|
10728
|
+
}
|
|
10729
|
+
catch (error) {
|
|
10730
|
+
disposeStaticElement(element);
|
|
10731
|
+
throw error;
|
|
10732
|
+
}
|
|
10733
|
+
}
|
|
10734
|
+
function prepareStaticNode(current) {
|
|
10735
|
+
const metadata = resolveStaticMetadata(current.node, current.occurrenceKey);
|
|
10736
|
+
if (current.visible !== metadata.visible
|
|
10737
|
+
|| current.layoutFingerprint !== metadata.layoutFingerprint) {
|
|
10738
|
+
return { ...metadata, children: [] };
|
|
10739
|
+
}
|
|
10740
|
+
return {
|
|
10741
|
+
...metadata,
|
|
10742
|
+
children: current.children.map(prepareStaticNode),
|
|
10743
|
+
};
|
|
10744
|
+
}
|
|
10745
|
+
function mountStaticReplacement(current, next) {
|
|
10746
|
+
const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
|
|
10747
|
+
try {
|
|
10748
|
+
if (replacement.element !== current.element
|
|
10749
|
+
&& replacement.element.parentNode === null) {
|
|
10750
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
10751
|
+
}
|
|
10752
|
+
return replacement;
|
|
10753
|
+
}
|
|
10754
|
+
catch (error) {
|
|
10755
|
+
disposeStaticSubtree(replacement);
|
|
10756
|
+
throw error;
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
/**
|
|
10760
|
+
* Build every renderer-owned replacement before touching the mounted DOM.
|
|
10761
|
+
* This preserves the old committed card when a later business renderer
|
|
10762
|
+
* rejects the same host update. Retained BaseElements are intentionally not
|
|
10763
|
+
* instantiated here: their native controls must stay untouched unless the
|
|
10764
|
+
* live update actually needs its existing replacement fallback.
|
|
10765
|
+
*/
|
|
10766
|
+
function stageStaticReplacements(current, next, staged) {
|
|
10767
|
+
const sameChildren = current.children.length === next.children.length
|
|
10768
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
10769
|
+
const decision = sameChildren
|
|
10770
|
+
? decideStaticUpdate(current, next)
|
|
10771
|
+
: 'replace';
|
|
10772
|
+
if (decision === 'replace') {
|
|
10773
|
+
staged.set(next.occurrenceKey, mountStaticReplacement(current, next));
|
|
10774
|
+
}
|
|
10775
|
+
if (decision === 'replace')
|
|
10776
|
+
return;
|
|
10777
|
+
current.children.forEach((child, index) => {
|
|
10778
|
+
stageStaticReplacements(child, next.children[index], staged);
|
|
10779
|
+
});
|
|
10780
|
+
}
|
|
10781
|
+
function decideStaticUpdate(current, next) {
|
|
10782
|
+
if (current.occurrenceKey !== next.occurrenceKey
|
|
10783
|
+
|| current.nodeId !== next.nodeId
|
|
10784
|
+
|| current.nodeType !== next.nodeType
|
|
10785
|
+
|| current.rendererToken !== next.rendererToken
|
|
10786
|
+
|| current.visible !== next.visible
|
|
10787
|
+
|| current.disabled !== next.disabled
|
|
10788
|
+
|| current.layoutFingerprint !== next.layoutFingerprint) {
|
|
10789
|
+
return 'replace';
|
|
10790
|
+
}
|
|
10791
|
+
if (current.propsFingerprint === next.propsFingerprint
|
|
10792
|
+
&& current.bindingFingerprint === next.bindingFingerprint) {
|
|
10793
|
+
return 'retain';
|
|
10794
|
+
}
|
|
10795
|
+
if (current.element instanceof BaseElement
|
|
10796
|
+
|| current.rendererToken === componentRenderers._default) {
|
|
10797
|
+
return 'update';
|
|
10798
|
+
}
|
|
10799
|
+
return 'replace';
|
|
10800
|
+
}
|
|
10801
|
+
function updateStaticNode(current, next) {
|
|
10802
|
+
const element = current.element;
|
|
10803
|
+
if (element instanceof BaseElement) {
|
|
10804
|
+
updateSlotItemShellPresentation(element, () => {
|
|
10805
|
+
element.updateProps(next.resolvedProps, isMobile, options.responsive);
|
|
10806
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
10807
|
+
}, getSlotHostPresentationOwnership(next.resolvedProps));
|
|
10808
|
+
return;
|
|
10809
|
+
}
|
|
10810
|
+
const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
|
|
10811
|
+
const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
|
|
10812
|
+
applyDisabledState(previous, current.disabled);
|
|
10813
|
+
applyDisabledState(desired, next.disabled);
|
|
10814
|
+
const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
|
|
10815
|
+
updateSlotItemShellPresentation(current.element, () => {
|
|
10816
|
+
patchElementShellPresentation(current.element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(current.element));
|
|
10817
|
+
}, hostOwnership);
|
|
10818
|
+
}
|
|
10819
|
+
function disposeStaticElement(element) {
|
|
10820
|
+
const cleanupSteps = [
|
|
10821
|
+
() => clearSlotOwnerBehaviorsIn(element),
|
|
10822
|
+
() => clearSlotItemPresentationsIn(element),
|
|
10823
|
+
() => clearElementBindingsIn(element),
|
|
10824
|
+
() => disposeChartsIn(element),
|
|
10825
|
+
];
|
|
10826
|
+
for (const cleanup of cleanupSteps) {
|
|
10827
|
+
try {
|
|
10828
|
+
cleanup();
|
|
10829
|
+
}
|
|
10830
|
+
catch (error) {
|
|
10831
|
+
console.error('[renderCard] Static element cleanup failed', error);
|
|
10832
|
+
}
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
function disposeStaticSubtree(current) {
|
|
10836
|
+
disposeStaticElement(current.element);
|
|
10837
|
+
}
|
|
10838
|
+
function collectMountedOccurrences(current, occurrences) {
|
|
10839
|
+
occurrences.set(current.occurrenceKey, current);
|
|
10840
|
+
for (const child of current.children) {
|
|
10841
|
+
collectMountedOccurrences(child, occurrences);
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
function collectMountedLifecycles(current, collection) {
|
|
10845
|
+
if (!current.visible
|
|
10846
|
+
|| !container.contains(current.element)) {
|
|
10847
|
+
return;
|
|
10848
|
+
}
|
|
10849
|
+
if (current.node.lifecycle) {
|
|
10850
|
+
collection.push({
|
|
10851
|
+
id: current.nodeId,
|
|
10852
|
+
lifecycle: current.node.lifecycle,
|
|
10853
|
+
});
|
|
10854
|
+
}
|
|
10855
|
+
for (const child of current.children) {
|
|
10856
|
+
collectMountedLifecycles(child, collection);
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
function restoreDisabledDescendantPresentation(current) {
|
|
10860
|
+
if (current.disabled) {
|
|
10861
|
+
applyDisabledState(current.element, true);
|
|
10862
|
+
applyDisabledDescendants(current.element, true);
|
|
10863
|
+
}
|
|
10864
|
+
for (const child of current.children) {
|
|
10865
|
+
restoreDisabledDescendantPresentation(child);
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
|
|
10869
|
+
if (previous.element !== next.element) {
|
|
10870
|
+
restoreScrollPositions(next.element, scrollPositions);
|
|
10871
|
+
restoreMediaStates(next.element, mediaStates);
|
|
10872
|
+
return;
|
|
10873
|
+
}
|
|
10874
|
+
if (previous.propsFingerprint !== next.propsFingerprint
|
|
10875
|
+
&& (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
|
|
10876
|
+
restoreMediaStates(next.element, mediaStates);
|
|
10877
|
+
}
|
|
10878
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
10879
|
+
const previousChild = previous.children[index];
|
|
10880
|
+
const nextChild = next.children[index];
|
|
10881
|
+
if (previousChild
|
|
10882
|
+
&& previousChild.occurrenceKey === nextChild.occurrenceKey) {
|
|
10883
|
+
restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
8214
10887
|
function render() {
|
|
10888
|
+
if (disposed)
|
|
10889
|
+
return;
|
|
10890
|
+
const generation = ++renderGeneration;
|
|
10891
|
+
const collectedLifecycles = [];
|
|
8215
10892
|
const scrollPositions = captureScrollPositions(container);
|
|
8216
10893
|
const mediaStates = captureMediaStates(container);
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
10894
|
+
let previousTree = null;
|
|
10895
|
+
if (!mountedTree) {
|
|
10896
|
+
const initialTree = mountStaticSubtree(tree, `${tree.id}#0`);
|
|
10897
|
+
if (disposed || generation !== renderGeneration) {
|
|
10898
|
+
disposeStaticSubtree(initialTree);
|
|
10899
|
+
return;
|
|
10900
|
+
}
|
|
10901
|
+
disposeChartsIn(container);
|
|
10902
|
+
container.replaceChildren(initialTree.element);
|
|
10903
|
+
mountedTree = initialTree;
|
|
10904
|
+
}
|
|
10905
|
+
else {
|
|
10906
|
+
previousTree = mountedTree;
|
|
10907
|
+
let unreconciled = false;
|
|
10908
|
+
let unreconciledError;
|
|
10909
|
+
const replacementSources = new Map();
|
|
10910
|
+
collectMountedOccurrences(mountedTree, replacementSources);
|
|
10911
|
+
const preparedTree = prepareStaticNode(mountedTree);
|
|
10912
|
+
if (disposed || generation !== renderGeneration)
|
|
10913
|
+
return;
|
|
10914
|
+
const stagedReplacements = new Map();
|
|
10915
|
+
publishing = true;
|
|
10916
|
+
try {
|
|
10917
|
+
stageStaticReplacements(mountedTree, preparedTree, stagedReplacements);
|
|
10918
|
+
if (disposeRequested)
|
|
10919
|
+
return;
|
|
10920
|
+
mountedTree = reconcileIncrementalNode(mountedTree, preparedTree, {
|
|
10921
|
+
decide: decideStaticUpdate,
|
|
10922
|
+
update: updateStaticNode,
|
|
10923
|
+
mountReplacement: next => {
|
|
10924
|
+
const staged = stagedReplacements.get(next.occurrenceKey);
|
|
10925
|
+
if (staged) {
|
|
10926
|
+
stagedReplacements.delete(next.occurrenceKey);
|
|
10927
|
+
return staged;
|
|
10928
|
+
}
|
|
10929
|
+
const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
|
|
10930
|
+
const current = replacementSources.get(next.occurrenceKey);
|
|
10931
|
+
if (current
|
|
10932
|
+
&& replacement.element !== current.element
|
|
10933
|
+
&& replacement.element.parentNode === null) {
|
|
10934
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
10935
|
+
}
|
|
10936
|
+
return replacement;
|
|
10937
|
+
},
|
|
10938
|
+
dispose: disposeStaticSubtree,
|
|
10939
|
+
syncBindings: (current, next) => {
|
|
10940
|
+
syncStaticBindings(current.element, next);
|
|
10941
|
+
},
|
|
10942
|
+
onUnreconciled: (error) => {
|
|
10943
|
+
if (unreconciled)
|
|
10944
|
+
return;
|
|
10945
|
+
unreconciled = true;
|
|
10946
|
+
unreconciledError = error ?? new Error('[renderCard] Static incremental update could not be reconciled');
|
|
10947
|
+
},
|
|
10948
|
+
});
|
|
10949
|
+
}
|
|
10950
|
+
finally {
|
|
10951
|
+
for (const replacement of stagedReplacements.values()) {
|
|
10952
|
+
disposeStaticSubtree(replacement);
|
|
10953
|
+
}
|
|
10954
|
+
stagedReplacements.clear();
|
|
10955
|
+
publishing = false;
|
|
10956
|
+
if (disposeRequested)
|
|
10957
|
+
disposeNow();
|
|
10958
|
+
}
|
|
10959
|
+
if (unreconciled)
|
|
10960
|
+
throw unreconciledError;
|
|
10961
|
+
if (disposed)
|
|
10962
|
+
return;
|
|
10963
|
+
}
|
|
10964
|
+
restoreDisabledDescendantPresentation(mountedTree);
|
|
10965
|
+
collectMountedLifecycles(mountedTree, collectedLifecycles);
|
|
10966
|
+
if (previousTree) {
|
|
10967
|
+
restoreChangedSubtreeState(previousTree, mountedTree, scrollPositions, mediaStates);
|
|
10968
|
+
}
|
|
10969
|
+
else {
|
|
10970
|
+
restoreScrollPositions(container, scrollPositions);
|
|
10971
|
+
restoreMediaStates(container, mediaStates);
|
|
10972
|
+
}
|
|
10973
|
+
if (disposed || generation !== renderGeneration)
|
|
10974
|
+
return;
|
|
10975
|
+
for (const { id, lifecycle } of collectedLifecycles) {
|
|
10976
|
+
if (disposed || generation !== renderGeneration)
|
|
10977
|
+
break;
|
|
10978
|
+
startLifecycleMount(id, lifecycle);
|
|
10979
|
+
}
|
|
8223
10980
|
}
|
|
8224
10981
|
function rerender() {
|
|
10982
|
+
if (disposed)
|
|
10983
|
+
return;
|
|
8225
10984
|
actionContext = buildActionContext();
|
|
8226
10985
|
render();
|
|
8227
10986
|
}
|
|
10987
|
+
function disposeNow() {
|
|
10988
|
+
if (disposed)
|
|
10989
|
+
return;
|
|
10990
|
+
disposed = true;
|
|
10991
|
+
disposeRequested = false;
|
|
10992
|
+
hostAuthorityEpoch += 1;
|
|
10993
|
+
nodeAccess.dispose();
|
|
10994
|
+
abortController.abort();
|
|
10995
|
+
if (mountedTree) {
|
|
10996
|
+
disposeStaticSubtree(mountedTree);
|
|
10997
|
+
mountedTree = null;
|
|
10998
|
+
}
|
|
10999
|
+
else {
|
|
11000
|
+
disposeChartsIn(container);
|
|
11001
|
+
}
|
|
11002
|
+
container.replaceChildren();
|
|
11003
|
+
for (const record of lifecycleRecords.values())
|
|
11004
|
+
destroyLifecycle(record);
|
|
11005
|
+
}
|
|
8228
11006
|
// 7. Initial render
|
|
8229
11007
|
render();
|
|
8230
11008
|
// 8. Return instance handle
|
|
8231
11009
|
return {
|
|
11010
|
+
getNode: nodeAccess.getNode,
|
|
11011
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
8232
11012
|
dispose() {
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
11013
|
+
if (disposed || disposeRequested)
|
|
11014
|
+
return;
|
|
11015
|
+
if (publishing) {
|
|
11016
|
+
disposeRequested = true;
|
|
11017
|
+
return;
|
|
11018
|
+
}
|
|
11019
|
+
disposeNow();
|
|
8237
11020
|
},
|
|
8238
11021
|
updateVariables(newVars) {
|
|
11022
|
+
if (disposed)
|
|
11023
|
+
return;
|
|
11024
|
+
if (publishing) {
|
|
11025
|
+
const error = new Error('[renderCard] STATIC_TRANSACTION_CONFLICT');
|
|
11026
|
+
error.code = 'STATIC_TRANSACTION_CONFLICT';
|
|
11027
|
+
throw error;
|
|
11028
|
+
}
|
|
11029
|
+
const previousVariables = variables;
|
|
11030
|
+
const previousAuthorityEpoch = hostAuthorityEpoch;
|
|
11031
|
+
hostAuthorityEpoch += 1;
|
|
8239
11032
|
variables = { ...variables, ...newVars };
|
|
8240
|
-
|
|
11033
|
+
try {
|
|
11034
|
+
rerender();
|
|
11035
|
+
}
|
|
11036
|
+
catch (error) {
|
|
11037
|
+
if (!disposed) {
|
|
11038
|
+
variables = previousVariables;
|
|
11039
|
+
hostAuthorityEpoch = previousAuthorityEpoch;
|
|
11040
|
+
actionContext = buildActionContext();
|
|
11041
|
+
}
|
|
11042
|
+
throw error;
|
|
11043
|
+
}
|
|
8241
11044
|
},
|
|
8242
11045
|
};
|
|
8243
11046
|
}
|
|
@@ -8263,7 +11066,11 @@ function captureScrollPositions(root) {
|
|
|
8263
11066
|
function restoreScrollPositions(root, positions) {
|
|
8264
11067
|
if (positions.size === 0)
|
|
8265
11068
|
return;
|
|
8266
|
-
|
|
11069
|
+
const scrollers = [];
|
|
11070
|
+
if (root.matches('[data-scroll-id]'))
|
|
11071
|
+
scrollers.push(root);
|
|
11072
|
+
scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
|
|
11073
|
+
scrollers.forEach((el) => {
|
|
8267
11074
|
const id = el.getAttribute('data-scroll-id');
|
|
8268
11075
|
const saved = id ? positions.get(id) : undefined;
|
|
8269
11076
|
if (saved == null)
|
|
@@ -8303,7 +11110,11 @@ function captureMediaStates(root) {
|
|
|
8303
11110
|
function restoreMediaStates(root, states) {
|
|
8304
11111
|
if (states.size === 0)
|
|
8305
11112
|
return;
|
|
8306
|
-
|
|
11113
|
+
const mediaHosts = [];
|
|
11114
|
+
if (root.matches('ai-card-audio, ai-card-video'))
|
|
11115
|
+
mediaHosts.push(root);
|
|
11116
|
+
mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
|
|
11117
|
+
mediaHosts.forEach((el) => {
|
|
8307
11118
|
const id = el.getAttribute('data-card-id');
|
|
8308
11119
|
const snap = id ? states.get(id) : undefined;
|
|
8309
11120
|
if (!snap)
|
|
@@ -8312,125 +11123,6 @@ function restoreMediaStates(root, states) {
|
|
|
8312
11123
|
restoreMedia(media, snap);
|
|
8313
11124
|
});
|
|
8314
11125
|
}
|
|
8315
|
-
// ─── Recursive Node Renderer ─────────────────────────────────────
|
|
8316
|
-
function renderNode(node, variables, actionContext, isMobile, responsive, lifecycleManager, schemaActions) {
|
|
8317
|
-
// Check directives.visible
|
|
8318
|
-
if (node.directives?.visible) {
|
|
8319
|
-
const visibleExpr = node.directives.visible;
|
|
8320
|
-
const resolved = hasExpression(visibleExpr)
|
|
8321
|
-
? resolveExpression(visibleExpr, variables)
|
|
8322
|
-
: visibleExpr;
|
|
8323
|
-
if (resolved === false || resolved === 'false' || resolved === '' || resolved === 0) {
|
|
8324
|
-
// Hidden element — return empty placeholder
|
|
8325
|
-
const placeholder = document.createElement('div');
|
|
8326
|
-
placeholder.style.display = 'none';
|
|
8327
|
-
placeholder.setAttribute('data-card-id', node.id);
|
|
8328
|
-
return placeholder;
|
|
8329
|
-
}
|
|
8330
|
-
}
|
|
8331
|
-
// Resolve props with variable expressions
|
|
8332
|
-
const resolvedProps = resolveDeep(node.props, variables);
|
|
8333
|
-
// Resolve content if it's an ExpressionValue
|
|
8334
|
-
if (resolvedProps.content && typeof resolvedProps.content === 'object' && 'type' in resolvedProps.content) {
|
|
8335
|
-
resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
|
|
8336
|
-
}
|
|
8337
|
-
// Check directives.disabled
|
|
8338
|
-
let isDisabled = false;
|
|
8339
|
-
if (node.directives?.disabled) {
|
|
8340
|
-
const disabledExpr = node.directives.disabled;
|
|
8341
|
-
const resolved = hasExpression(disabledExpr)
|
|
8342
|
-
? resolveExpression(disabledExpr, variables)
|
|
8343
|
-
: disabledExpr;
|
|
8344
|
-
isDisabled = resolved === true || resolved === 'true' || resolved === 1;
|
|
8345
|
-
}
|
|
8346
|
-
// Lookup component renderer
|
|
8347
|
-
const renderer = componentRenderers[node.type] ?? componentRenderers['_default'];
|
|
8348
|
-
const el = renderer(node, resolvedProps, isMobile, responsive);
|
|
8349
|
-
// Apply disabled styling & attribute
|
|
8350
|
-
if (isDisabled) {
|
|
8351
|
-
el.setAttribute('data-disabled', 'true');
|
|
8352
|
-
el.style.background = '#F5F5F5';
|
|
8353
|
-
el.style.color = '#C0C0C0';
|
|
8354
|
-
el.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
8355
|
-
el.style.pointerEvents = 'none';
|
|
8356
|
-
el.style.cursor = 'default';
|
|
8357
|
-
}
|
|
8358
|
-
// ── variableKey auto-sync for Input components ──────────────
|
|
8359
|
-
// When an Input element declares `variableKey`, its value is
|
|
8360
|
-
// automatically written into the reactive variables store on
|
|
8361
|
-
// every `input` event (without triggering a full re-render).
|
|
8362
|
-
// The value is then available as `${variableKey}` in action params.
|
|
8363
|
-
const variableKey = resolvedProps.variableKey;
|
|
8364
|
-
if (variableKey) {
|
|
8365
|
-
el.addEventListener('input', ((e) => {
|
|
8366
|
-
// Nested value controls own their variableKey. Only a value event
|
|
8367
|
-
// dispatched by this component may update this component's variable.
|
|
8368
|
-
if (e.target !== el)
|
|
8369
|
-
return;
|
|
8370
|
-
const value = e.detail?.value
|
|
8371
|
-
?? e.target?.value;
|
|
8372
|
-
if (value !== undefined) {
|
|
8373
|
-
// Write silently — do NOT trigger rerender (avoids losing focus)
|
|
8374
|
-
variables[variableKey] = value;
|
|
8375
|
-
}
|
|
8376
|
-
}));
|
|
8377
|
-
}
|
|
8378
|
-
// ── Bind events (skip if disabled) ──
|
|
8379
|
-
if (node.events && !isDisabled) {
|
|
8380
|
-
for (const [event, eventValue] of Object.entries(node.events)) {
|
|
8381
|
-
if (!eventValue)
|
|
8382
|
-
continue;
|
|
8383
|
-
const resolvedSteps = resolveActionRef(eventValue, schemaActions);
|
|
8384
|
-
if (!resolvedSteps)
|
|
8385
|
-
continue;
|
|
8386
|
-
const domEvent = eventMap[event] ?? event;
|
|
8387
|
-
el.addEventListener(domEvent, ((e) => {
|
|
8388
|
-
// Value controls own their input/change handlers; a nested control's
|
|
8389
|
-
// same-named event must not trigger the parent's schema action.
|
|
8390
|
-
// Other component/event pairs keep the renderer's existing bubbling
|
|
8391
|
-
// behavior (for example a Container-level click handler).
|
|
8392
|
-
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
8393
|
-
&& VALUE_CONTROL_TYPES.has(node.type));
|
|
8394
|
-
if (ownsValueEvent && e.target !== el)
|
|
8395
|
-
return;
|
|
8396
|
-
// Write event.detail to _event variable so action params can use ${_event.xxx}
|
|
8397
|
-
if (e instanceof CustomEvent && e.detail != null) {
|
|
8398
|
-
variables._event = e.detail;
|
|
8399
|
-
}
|
|
8400
|
-
runActionSteps(resolvedSteps, actionContext);
|
|
8401
|
-
}));
|
|
8402
|
-
}
|
|
8403
|
-
}
|
|
8404
|
-
// Register lifecycle
|
|
8405
|
-
if (node.lifecycle) {
|
|
8406
|
-
lifecycleManager.register(node.id, node.lifecycle);
|
|
8407
|
-
lifecycleManager.mount(node.id, actionContext);
|
|
8408
|
-
}
|
|
8409
|
-
// Render children — use slot layout if applicable, otherwise flat append
|
|
8410
|
-
const renderChild = (child) => renderNode(child, variables, actionContext, isMobile, responsive, lifecycleManager, schemaActions);
|
|
8411
|
-
// Build children-by-id map for layouts that reference IDs (columns groups, float overlays)
|
|
8412
|
-
const childrenMap = {};
|
|
8413
|
-
for (const child of node.children) {
|
|
8414
|
-
childrenMap[child.id] = child;
|
|
8415
|
-
}
|
|
8416
|
-
const layoutApplied = renderSlotLayout(el, node.children, resolvedProps, renderChild, childrenMap, actionContext);
|
|
8417
|
-
if (!layoutApplied) {
|
|
8418
|
-
for (const child of node.children) {
|
|
8419
|
-
el.appendChild(renderChild(child));
|
|
8420
|
-
}
|
|
8421
|
-
}
|
|
8422
|
-
// Apply disabled grey text to all descendants (after children are appended)
|
|
8423
|
-
// Propagate disabled state to child elements (including Shadow DOM custom elements)
|
|
8424
|
-
if (isDisabled) {
|
|
8425
|
-
el.querySelectorAll('*').forEach((child) => {
|
|
8426
|
-
const htmlChild = child;
|
|
8427
|
-
htmlChild.setAttribute('data-disabled', 'true');
|
|
8428
|
-
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
8429
|
-
});
|
|
8430
|
-
}
|
|
8431
|
-
applyResponsiveStyles(el, createResponsiveContext(isMobile, responsive));
|
|
8432
|
-
return el;
|
|
8433
|
-
}
|
|
8434
11126
|
/** Map schema event names → DOM event names */
|
|
8435
11127
|
const eventMap = {
|
|
8436
11128
|
onClick: 'click',
|
|
@@ -8484,6 +11176,7 @@ const VALUE_CONTROL_TYPES = new Set([
|
|
|
8484
11176
|
* ```
|
|
8485
11177
|
*/
|
|
8486
11178
|
function renderStreamingCard(container, options = {}) {
|
|
11179
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8487
11180
|
// ─── State ──────────────────────────────────────────────────────
|
|
8488
11181
|
const parser = new StreamingParser(options.parserOptions);
|
|
8489
11182
|
const elementMap = new Map(); // elementId → DOM element
|
|
@@ -8511,19 +11204,32 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8511
11204
|
let boundActionQueue = Promise.resolve();
|
|
8512
11205
|
let boundLifecycleEpoch = 0;
|
|
8513
11206
|
let disposed = false;
|
|
11207
|
+
let userActionDepth = 0;
|
|
8514
11208
|
const sourceOccurrences = new Map();
|
|
8515
11209
|
const activeBoundLifecycles = new Map();
|
|
8516
11210
|
const mountedBoundLifecycles = new Map();
|
|
8517
11211
|
const boundLifecycleGenerations = new Map();
|
|
11212
|
+
function applyAutoFocusIds(ids) {
|
|
11213
|
+
if (ids.size === 0)
|
|
11214
|
+
return;
|
|
11215
|
+
for (const id of ids) {
|
|
11216
|
+
const input = elementMap.get(id);
|
|
11217
|
+
if (input instanceof CardInput && input.requestAutoFocus())
|
|
11218
|
+
break;
|
|
11219
|
+
}
|
|
11220
|
+
}
|
|
8518
11221
|
// ─── Action Context ─────────────────────────────────────────────
|
|
8519
11222
|
function buildActionContext() {
|
|
8520
11223
|
return {
|
|
8521
11224
|
...createWebActionContext({
|
|
8522
11225
|
...options,
|
|
8523
11226
|
setVariable: (key, value) => {
|
|
11227
|
+
const previousVariables = userActionDepth > 0
|
|
11228
|
+
? { ...variables }
|
|
11229
|
+
: undefined;
|
|
8524
11230
|
variables[key] = value;
|
|
8525
11231
|
// On variable change, patch only affected elements (keyed diff)
|
|
8526
|
-
diffAllElements();
|
|
11232
|
+
diffAllElements(previousVariables);
|
|
8527
11233
|
},
|
|
8528
11234
|
abortSignal: abortController.signal,
|
|
8529
11235
|
}),
|
|
@@ -8606,6 +11312,27 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8606
11312
|
|| resolved === ''
|
|
8607
11313
|
|| resolved === 0);
|
|
8608
11314
|
}
|
|
11315
|
+
function collectBoundAutoFocusRevealIds(previous, next, previousVariables, nextVariables) {
|
|
11316
|
+
const previousNodes = indexBoundNodes(previous.root);
|
|
11317
|
+
const requested = new Set();
|
|
11318
|
+
const visit = (nextNode, previousParentVisible, nextParentVisible) => {
|
|
11319
|
+
const previousNode = previousNodes.get(nextNode.id);
|
|
11320
|
+
const wasVisible = Boolean(previousNode
|
|
11321
|
+
&& previousParentVisible
|
|
11322
|
+
&& computeBoundVisible(previousNode, previousVariables));
|
|
11323
|
+
const nowVisible = nextParentVisible
|
|
11324
|
+
&& computeBoundVisible(nextNode, nextVariables);
|
|
11325
|
+
if (nextNode.type === 'Input'
|
|
11326
|
+
&& !wasVisible
|
|
11327
|
+
&& nowVisible
|
|
11328
|
+
&& resolveBoundNodeProps(nextNode, nextVariables).autoFocus === true) {
|
|
11329
|
+
requested.add(nextNode.id);
|
|
11330
|
+
}
|
|
11331
|
+
nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
11332
|
+
};
|
|
11333
|
+
visit(next.root, true, true);
|
|
11334
|
+
return requested;
|
|
11335
|
+
}
|
|
8609
11336
|
function computeBoundDisabled(node, renderVariables) {
|
|
8610
11337
|
const disabled = node.directives?.disabled;
|
|
8611
11338
|
if (!disabled)
|
|
@@ -8781,7 +11508,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8781
11508
|
if (e instanceof CustomEvent && e.detail != null) {
|
|
8782
11509
|
variables._event = e.detail;
|
|
8783
11510
|
}
|
|
8784
|
-
|
|
11511
|
+
userActionDepth += 1;
|
|
11512
|
+
void runSteps(resolvedSteps, node.id).finally(() => {
|
|
11513
|
+
userActionDepth -= 1;
|
|
11514
|
+
});
|
|
8785
11515
|
}));
|
|
8786
11516
|
}
|
|
8787
11517
|
}
|
|
@@ -8836,6 +11566,43 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8836
11566
|
: visibleExpr;
|
|
8837
11567
|
return !(resolved === false || resolved === 'false' || resolved === '' || resolved === 0);
|
|
8838
11568
|
}
|
|
11569
|
+
function collectLegacyAutoFocusRevealIds(schema, previousVariables) {
|
|
11570
|
+
let tree;
|
|
11571
|
+
try {
|
|
11572
|
+
tree = parseSchema(schema);
|
|
11573
|
+
}
|
|
11574
|
+
catch {
|
|
11575
|
+
return new Set();
|
|
11576
|
+
}
|
|
11577
|
+
const requested = new Set();
|
|
11578
|
+
const ownVisibility = (node, renderVariables) => {
|
|
11579
|
+
const expression = node.directives?.visible;
|
|
11580
|
+
if (!expression)
|
|
11581
|
+
return true;
|
|
11582
|
+
const resolved = hasExpression(expression)
|
|
11583
|
+
? resolveExpression(expression, renderVariables)
|
|
11584
|
+
: expression;
|
|
11585
|
+
return !(resolved === false
|
|
11586
|
+
|| resolved === 'false'
|
|
11587
|
+
|| resolved === ''
|
|
11588
|
+
|| resolved === 0);
|
|
11589
|
+
};
|
|
11590
|
+
const visit = (node, previousParentVisible, nextParentVisible) => {
|
|
11591
|
+
const wasVisible = previousParentVisible
|
|
11592
|
+
&& ownVisibility(node, previousVariables);
|
|
11593
|
+
const nowVisible = nextParentVisible && ownVisibility(node, variables);
|
|
11594
|
+
if (node.type === 'Input'
|
|
11595
|
+
&& !wasVisible
|
|
11596
|
+
&& nowVisible
|
|
11597
|
+
&& resolveDeep(node.props, variables)
|
|
11598
|
+
.autoFocus === true) {
|
|
11599
|
+
requested.add(node.id);
|
|
11600
|
+
}
|
|
11601
|
+
node.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
11602
|
+
};
|
|
11603
|
+
visit(tree, true, true);
|
|
11604
|
+
return requested;
|
|
11605
|
+
}
|
|
8839
11606
|
/** Entrance transition for blocks streamed in incrementally. */
|
|
8840
11607
|
function animateEnter(el) {
|
|
8841
11608
|
if (options.appearTransition === false)
|
|
@@ -8909,11 +11676,14 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8909
11676
|
* (visibility flips / container shape changes) re-render only their subtree.
|
|
8910
11677
|
* Falls back to a full render solely when a subtree can't be rebuilt.
|
|
8911
11678
|
*/
|
|
8912
|
-
function diffAllElements() {
|
|
11679
|
+
function diffAllElements(previousVariables) {
|
|
8913
11680
|
actionContext = buildActionContext();
|
|
8914
11681
|
const schema = (currentSurfaceId ? engine.getSchema(currentSurfaceId) : undefined) ?? currentSchema;
|
|
8915
11682
|
if (!schema)
|
|
8916
11683
|
return;
|
|
11684
|
+
const autoFocusIds = previousVariables
|
|
11685
|
+
? collectLegacyAutoFocusRevealIds(schema, previousVariables)
|
|
11686
|
+
: new Set();
|
|
8917
11687
|
for (const [id, element] of Object.entries(schema.elements)) {
|
|
8918
11688
|
const el = elementMap.get(id);
|
|
8919
11689
|
if (!el)
|
|
@@ -8924,6 +11694,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8924
11694
|
// Visibility flipped (either direction) → rebuild this subtree in place
|
|
8925
11695
|
if (!replaceSubtree(schema, id, el)) {
|
|
8926
11696
|
safeRenderFull();
|
|
11697
|
+
applyAutoFocusIds(autoFocusIds);
|
|
8927
11698
|
return;
|
|
8928
11699
|
}
|
|
8929
11700
|
continue;
|
|
@@ -8940,9 +11711,11 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8940
11711
|
}
|
|
8941
11712
|
else if (!replaceSubtree(schema, id, el)) {
|
|
8942
11713
|
safeRenderFull();
|
|
11714
|
+
applyAutoFocusIds(autoFocusIds);
|
|
8943
11715
|
return;
|
|
8944
11716
|
}
|
|
8945
11717
|
}
|
|
11718
|
+
applyAutoFocusIds(autoFocusIds);
|
|
8946
11719
|
}
|
|
8947
11720
|
function captureBoundScrollPositions(root) {
|
|
8948
11721
|
const positions = new Map();
|
|
@@ -9665,7 +12438,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9665
12438
|
if (!steps)
|
|
9666
12439
|
return;
|
|
9667
12440
|
await runBoundSteps(steps, sourceNode, createBoundActionContext(sourceNode, draft));
|
|
12441
|
+
const nextMaterialized = materializeStreamingCard(currentSchema, draft);
|
|
12442
|
+
const autoFocusIds = collectBoundAutoFocusRevealIds(currentMaterialized, nextMaterialized, before, draft);
|
|
9668
12443
|
commitBoundDraftTransaction(before, draft, baseRevision);
|
|
12444
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9669
12445
|
}
|
|
9670
12446
|
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
9671
12447
|
boundActionQueue = boundActionQueue
|
|
@@ -10247,6 +13023,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10247
13023
|
});
|
|
10248
13024
|
// ─── Public API ─────────────────────────────────────────────────
|
|
10249
13025
|
return {
|
|
13026
|
+
getNode: nodeAccess.getNode,
|
|
13027
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
10250
13028
|
applyCommand(command) {
|
|
10251
13029
|
if (!lockMode('commands'))
|
|
10252
13030
|
return;
|
|
@@ -10323,6 +13101,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10323
13101
|
if (disposed)
|
|
10324
13102
|
return;
|
|
10325
13103
|
disposed = true;
|
|
13104
|
+
nodeAccess.dispose();
|
|
10326
13105
|
boundRevision += 1;
|
|
10327
13106
|
abortController.abort();
|
|
10328
13107
|
teardownBoundLifecycles();
|