@antglobal/copilot-cards-web 1.0.6 → 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 +58 -6
- package/dist/index.js +3075 -479
- package/package.json +2 -2
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,7 @@ function buildHeatmapOption(config) {
|
|
|
706
724
|
*/
|
|
707
725
|
// ─── Slot Layout Constants ──────────────────────────────────────
|
|
708
726
|
const SLOT_LAYOUT = {
|
|
727
|
+
DEFAULT: 'default',
|
|
709
728
|
FLEX: 'flex',
|
|
710
729
|
COLUMNS: 'columns',
|
|
711
730
|
GRID: 'grid',
|
|
@@ -717,6 +736,377 @@ const SLOT_LAYOUT = {
|
|
|
717
736
|
TABLE: 'table',
|
|
718
737
|
CHART: 'chart',
|
|
719
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
|
+
}
|
|
720
1110
|
// ─── Dispatcher ─────────────────────────────────────────────────
|
|
721
1111
|
/**
|
|
722
1112
|
* Apply host-level layout styles based on the slot key.
|
|
@@ -746,11 +1136,11 @@ function resolveFlexGap(value) {
|
|
|
746
1136
|
* Returns true if a special layout was applied (children already appended).
|
|
747
1137
|
* Returns false for `default` — caller uses original append loop.
|
|
748
1138
|
*/
|
|
749
|
-
function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext) {
|
|
1139
|
+
function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext, resolveChildProps) {
|
|
750
1140
|
const slots = props.slots;
|
|
751
1141
|
if (!slots)
|
|
752
1142
|
return false;
|
|
753
|
-
const slotKey =
|
|
1143
|
+
const slotKey = findSpecialSlotKey(props);
|
|
754
1144
|
if (!slotKey)
|
|
755
1145
|
return false;
|
|
756
1146
|
const slotContent = slots[slotKey];
|
|
@@ -759,7 +1149,7 @@ function renderSlotLayout(container, children, props, renderChild, childrenMap,
|
|
|
759
1149
|
renderColumnsSlot(container, slotContent, childrenMap, renderChild);
|
|
760
1150
|
return true;
|
|
761
1151
|
case SLOT_LAYOUT.GRID:
|
|
762
|
-
renderGridSlot(container, children, slotContent, renderChild);
|
|
1152
|
+
renderGridSlot(container, children, slotContent, renderChild, resolveChildProps);
|
|
763
1153
|
return true;
|
|
764
1154
|
case SLOT_LAYOUT.HORIZONTAL_SCROLL:
|
|
765
1155
|
renderHorizontalScrollSlot(container, children, slotContent, renderChild);
|
|
@@ -833,7 +1223,7 @@ function renderColumnsSlot(container, slotContent, childrenMap, renderChild) {
|
|
|
833
1223
|
*
|
|
834
1224
|
* Schema: `slots: { grid: { children: [...], config: { columns: 2, gap: '8px', rows?: 3, rowHeight?: '48px' } } }`
|
|
835
1225
|
*/
|
|
836
|
-
function renderGridSlot(container, children, slotContent, renderChild) {
|
|
1226
|
+
function renderGridSlot(container, children, slotContent, renderChild, resolveChildProps) {
|
|
837
1227
|
const columns = slotContent?.config?.columns ?? 2;
|
|
838
1228
|
const gap = slotContent?.config?.gap ?? '8px';
|
|
839
1229
|
const rows = slotContent?.config?.rows;
|
|
@@ -851,7 +1241,7 @@ function renderGridSlot(container, children, slotContent, renderChild) {
|
|
|
851
1241
|
// regardless of whether the child component applies props.style to its host
|
|
852
1242
|
// or to an inner shadow-DOM node. No-op for children without placement, so
|
|
853
1243
|
// auto-flow cards are unaffected.
|
|
854
|
-
|
|
1244
|
+
bindSlotItemPresentation(el, gridItemDescriptor(resolveChildProps?.(child) ?? child.props));
|
|
855
1245
|
grid.appendChild(el);
|
|
856
1246
|
}
|
|
857
1247
|
container.appendChild(grid);
|
|
@@ -864,16 +1254,31 @@ function renderGridSlot(container, children, slotContent, renderChild) {
|
|
|
864
1254
|
* unresolved expression object) is ignored. Children that declare no placement
|
|
865
1255
|
* are left untouched — preserving auto-flow behaviour for legacy cards.
|
|
866
1256
|
*/
|
|
867
|
-
function
|
|
868
|
-
const style =
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
+
};
|
|
877
1282
|
}
|
|
878
1283
|
function isCssPlacement(value) {
|
|
879
1284
|
return typeof value === 'string' || typeof value === 'number';
|
|
@@ -897,7 +1302,14 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
897
1302
|
const autoScrollLoop = config.autoScrollLoop !== false;
|
|
898
1303
|
const scrollbar = resolveScrollbarMode(config.scrollbar);
|
|
899
1304
|
const hidesScrollbar = scrollbar == null || scrollbar === 'hidden';
|
|
1305
|
+
const itemDescriptor = horizontalScrollItemDescriptor(itemWidth, snap, itemHoverStyle);
|
|
900
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;
|
|
901
1313
|
track.style.cssText = [
|
|
902
1314
|
'display:flex',
|
|
903
1315
|
'overflow-x:auto',
|
|
@@ -917,20 +1329,17 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
917
1329
|
track.dataset.scrollbar = scrollbar;
|
|
918
1330
|
for (const child of children) {
|
|
919
1331
|
const item = renderChild(child);
|
|
920
|
-
item
|
|
921
|
-
if (itemWidth)
|
|
922
|
-
item.style.width = itemWidth;
|
|
923
|
-
if (snap)
|
|
924
|
-
item.style.scrollSnapAlign = 'start';
|
|
925
|
-
if (itemHoverStyle)
|
|
926
|
-
applyHoverStyle(item, itemHoverStyle);
|
|
1332
|
+
bindSlotItemPresentation(item, itemDescriptor);
|
|
927
1333
|
track.appendChild(item);
|
|
928
1334
|
}
|
|
929
1335
|
// Mouse drag (desktop)
|
|
930
1336
|
let isDragging = false;
|
|
931
1337
|
let startX = 0;
|
|
932
1338
|
let scrollLeft = 0;
|
|
933
|
-
|
|
1339
|
+
const handleMouseDown = (event) => {
|
|
1340
|
+
if (ownerDisposed)
|
|
1341
|
+
return;
|
|
1342
|
+
const e = event;
|
|
934
1343
|
if (e.button !== 0)
|
|
935
1344
|
return; // left button only — keep middle/right clicks out of drag state
|
|
936
1345
|
// Nested scrollers: consume the gesture so an enclosing horizontalScroll doesn't
|
|
@@ -944,8 +1353,11 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
944
1353
|
track.style.userSelect = 'none';
|
|
945
1354
|
if (snap)
|
|
946
1355
|
track.style.scrollSnapType = 'none';
|
|
947
|
-
}
|
|
948
|
-
|
|
1356
|
+
};
|
|
1357
|
+
const handleMouseMove = (event) => {
|
|
1358
|
+
if (ownerDisposed)
|
|
1359
|
+
return;
|
|
1360
|
+
const e = event;
|
|
949
1361
|
if (!isDragging)
|
|
950
1362
|
return;
|
|
951
1363
|
// Only swallow the move while THIS track owns the drag — otherwise an in-progress
|
|
@@ -954,7 +1366,7 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
954
1366
|
e.stopPropagation();
|
|
955
1367
|
e.preventDefault();
|
|
956
1368
|
track.scrollLeft = scrollLeft - (e.pageX - startX);
|
|
957
|
-
}
|
|
1369
|
+
};
|
|
958
1370
|
const stopDrag = () => {
|
|
959
1371
|
if (!isDragging)
|
|
960
1372
|
return;
|
|
@@ -964,15 +1376,19 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
964
1376
|
if (snap)
|
|
965
1377
|
track.style.scrollSnapType = 'x mandatory';
|
|
966
1378
|
};
|
|
967
|
-
track
|
|
968
|
-
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);
|
|
969
1383
|
// Wheel / trackpad → horizontal scroll.
|
|
970
1384
|
// Mandatory snap would swallow small wheel deltas (each increment snaps right
|
|
971
1385
|
// back to the same item, so the wheel appears dead) — suspend snap while
|
|
972
1386
|
// wheeling and restore it shortly after the last tick, which also re-aligns
|
|
973
1387
|
// the track to the nearest snap point.
|
|
974
|
-
|
|
975
|
-
|
|
1388
|
+
const handleWheel = (event) => {
|
|
1389
|
+
if (ownerDisposed)
|
|
1390
|
+
return;
|
|
1391
|
+
const e = event;
|
|
976
1392
|
e.preventDefault();
|
|
977
1393
|
// Consume the wheel so a nested horizontalScroll scrolls only itself instead of
|
|
978
1394
|
// also driving an enclosing scroller (simplest nested model — no edge hand-off
|
|
@@ -985,12 +1401,16 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
985
1401
|
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
986
1402
|
track.scrollLeft += delta;
|
|
987
1403
|
if (snap) {
|
|
988
|
-
clearTimeout(wheelTimer);
|
|
1404
|
+
window.clearTimeout(wheelTimer);
|
|
989
1405
|
wheelTimer = window.setTimeout(() => {
|
|
1406
|
+
wheelTimer = 0;
|
|
1407
|
+
if (ownerDisposed)
|
|
1408
|
+
return;
|
|
990
1409
|
track.style.scrollSnapType = 'x mandatory';
|
|
991
1410
|
}, 150);
|
|
992
1411
|
}
|
|
993
|
-
}
|
|
1412
|
+
};
|
|
1413
|
+
addSlotOwnerListener(ownerCleanup, track, 'wheel', handleWheel, { passive: false });
|
|
994
1414
|
// Hover autoplay (desktop): while the pointer is over the track, glide to the next
|
|
995
1415
|
// item every `autoScrollInterval` ms, looping back to the first at the end when
|
|
996
1416
|
// `autoScrollLoop`. Opt-in via `autoScroll`. Manual drag pauses it (resumes on the
|
|
@@ -999,9 +1419,10 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
999
1419
|
// chrome. Targets are item starts (= snap points), so mandatory snap won't fight the
|
|
1000
1420
|
// glide — same reason the arrow buttons can scrollBy smoothly without suspending snap.
|
|
1001
1421
|
if (autoScroll) {
|
|
1002
|
-
let autoTimer = 0;
|
|
1003
1422
|
const glideTo = (left) => track.scrollTo({ left, behavior: 'smooth' });
|
|
1004
1423
|
const advance = () => {
|
|
1424
|
+
if (ownerDisposed)
|
|
1425
|
+
return stopAuto();
|
|
1005
1426
|
// A variable-driven re-render rebuilds the track; bail and clear so the interval
|
|
1006
1427
|
// doesn't keep driving a detached node (mirrors the ResizeObserver self-teardown).
|
|
1007
1428
|
if (!track.isConnected)
|
|
@@ -1022,6 +1443,8 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1022
1443
|
glideTo(track.scrollLeft + (next.getBoundingClientRect().left - trackLeft));
|
|
1023
1444
|
};
|
|
1024
1445
|
const startAuto = () => {
|
|
1446
|
+
if (ownerDisposed)
|
|
1447
|
+
return;
|
|
1025
1448
|
if (autoTimer)
|
|
1026
1449
|
return;
|
|
1027
1450
|
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
|
|
@@ -1031,13 +1454,33 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1031
1454
|
function stopAuto() {
|
|
1032
1455
|
if (!autoTimer)
|
|
1033
1456
|
return;
|
|
1034
|
-
clearInterval(autoTimer);
|
|
1457
|
+
window.clearInterval(autoTimer);
|
|
1035
1458
|
autoTimer = 0;
|
|
1036
1459
|
}
|
|
1037
|
-
track
|
|
1038
|
-
track
|
|
1039
|
-
track
|
|
1460
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseenter', startAuto);
|
|
1461
|
+
addSlotOwnerListener(ownerCleanup, track, 'mouseleave', stopAuto);
|
|
1462
|
+
addSlotOwnerListener(ownerCleanup, track, 'mousedown', stopAuto);
|
|
1040
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
|
+
});
|
|
1041
1484
|
if (hidesScrollbar) {
|
|
1042
1485
|
const trackClass = `card-scroll-${cardId}`;
|
|
1043
1486
|
track.classList.add(trackClass);
|
|
@@ -1082,13 +1525,25 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1082
1525
|
: typeof config.arrowOffset === 'number' ? `${config.arrowOffset}px` : String(config.arrowOffset);
|
|
1083
1526
|
leftArrow = createArrowButton('left', arrowStyle, arrowOffset, config.arrowIconLeft ?? config.arrowIcon, config.arrowIconLeft == null);
|
|
1084
1527
|
rightArrow = createArrowButton('right', arrowStyle, arrowOffset, config.arrowIconRight ?? config.arrowIcon, false);
|
|
1085
|
-
|
|
1086
|
-
|
|
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);
|
|
1087
1540
|
wrapper.appendChild(leftArrow);
|
|
1088
1541
|
wrapper.appendChild(rightArrow);
|
|
1089
1542
|
}
|
|
1090
1543
|
// Show each arrow/mask only while the track can scroll in that direction.
|
|
1091
1544
|
const updateOverlays = () => {
|
|
1545
|
+
if (ownerDisposed)
|
|
1546
|
+
return;
|
|
1092
1547
|
const canLeft = track.scrollLeft > 1;
|
|
1093
1548
|
const canRight = track.scrollLeft + track.clientWidth < track.scrollWidth - 1;
|
|
1094
1549
|
if (leftArrow)
|
|
@@ -1100,27 +1555,54 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
|
|
|
1100
1555
|
if (rightMask)
|
|
1101
1556
|
rightMask.style.opacity = canRight ? maskOpacity : '0';
|
|
1102
1557
|
};
|
|
1103
|
-
track
|
|
1558
|
+
addSlotOwnerListener(ownerCleanup, track, 'scroll', updateOverlays, { passive: true });
|
|
1104
1559
|
// ResizeObserver fires once on observe (covers initial layout) and again on
|
|
1105
1560
|
// any size change; it self-disconnects once the track leaves the DOM.
|
|
1106
1561
|
if (typeof ResizeObserver !== 'undefined') {
|
|
1107
|
-
|
|
1562
|
+
resizeObserver = new ResizeObserver(() => {
|
|
1563
|
+
if (ownerDisposed)
|
|
1564
|
+
return;
|
|
1108
1565
|
if (!track.isConnected) {
|
|
1109
|
-
|
|
1566
|
+
resizeObserver?.disconnect();
|
|
1567
|
+
resizeObserver = null;
|
|
1110
1568
|
return;
|
|
1111
1569
|
}
|
|
1112
1570
|
updateOverlays();
|
|
1113
1571
|
});
|
|
1114
|
-
|
|
1572
|
+
resizeObserver.observe(track);
|
|
1115
1573
|
}
|
|
1116
1574
|
else {
|
|
1117
|
-
requestAnimationFrame(
|
|
1575
|
+
overlayFrame = requestAnimationFrame(() => {
|
|
1576
|
+
overlayFrame = 0;
|
|
1577
|
+
updateOverlays();
|
|
1578
|
+
});
|
|
1118
1579
|
}
|
|
1119
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
|
+
}
|
|
1120
1598
|
/** Apply `hoverStyle` on mouseenter and restore the previous inline values on leave. */
|
|
1121
1599
|
function applyHoverStyle(item, hoverStyle) {
|
|
1122
1600
|
const previous = {};
|
|
1123
|
-
|
|
1601
|
+
let hovered = false;
|
|
1602
|
+
const enter = () => {
|
|
1603
|
+
if (hovered)
|
|
1604
|
+
return;
|
|
1605
|
+
hovered = true;
|
|
1124
1606
|
for (const [key, value] of Object.entries(hoverStyle)) {
|
|
1125
1607
|
if (key.startsWith('--')) {
|
|
1126
1608
|
const previousValue = item.style.getPropertyValue(key);
|
|
@@ -1143,8 +1625,11 @@ function applyHoverStyle(item, hoverStyle) {
|
|
|
1143
1625
|
item.style[key] = value;
|
|
1144
1626
|
}
|
|
1145
1627
|
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1628
|
+
};
|
|
1629
|
+
const leave = () => {
|
|
1630
|
+
if (!hovered)
|
|
1631
|
+
return;
|
|
1632
|
+
hovered = false;
|
|
1148
1633
|
for (const [key, state] of Object.entries(previous)) {
|
|
1149
1634
|
if (state.isCustomProperty) {
|
|
1150
1635
|
if (state.wasPresent) {
|
|
@@ -1157,8 +1642,38 @@ function applyHoverStyle(item, hoverStyle) {
|
|
|
1157
1642
|
else {
|
|
1158
1643
|
item.style[key] = state.value;
|
|
1159
1644
|
}
|
|
1645
|
+
delete previous[key];
|
|
1160
1646
|
}
|
|
1161
|
-
}
|
|
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
|
+
};
|
|
1162
1677
|
}
|
|
1163
1678
|
/** Translucent edge-fade mask hinting that more content is available in that direction. */
|
|
1164
1679
|
function createEdgeMask(side, width, color) {
|
|
@@ -1238,6 +1753,66 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
|
|
|
1238
1753
|
return btn;
|
|
1239
1754
|
}
|
|
1240
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
|
+
}
|
|
1241
1816
|
/**
|
|
1242
1817
|
* Center-focused carousel with scale/opacity/overlay effects and snap.
|
|
1243
1818
|
*
|
|
@@ -1248,30 +1823,40 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
|
|
|
1248
1823
|
* to the first after the last (default false)
|
|
1249
1824
|
* - `autoplayInterval` — ms between auto-advances (default 3000)
|
|
1250
1825
|
*/
|
|
1251
|
-
function renderCarouselSlot(container, children,
|
|
1252
|
-
const
|
|
1253
|
-
const inactiveScale =
|
|
1254
|
-
const inactiveOpacity =
|
|
1255
|
-
const overlayColor =
|
|
1256
|
-
const gap =
|
|
1257
|
-
const itemWidth =
|
|
1258
|
-
const itemWidthPx =
|
|
1259
|
-
const initialIndex =
|
|
1260
|
-
const autoplay =
|
|
1261
|
-
const autoplayInterval =
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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));
|
|
1275
1860
|
// Hide scrollbar
|
|
1276
1861
|
const styleEl = document.createElement('style');
|
|
1277
1862
|
const id = container.getAttribute('data-card-id') ?? '';
|
|
@@ -1279,34 +1864,16 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1279
1864
|
container.appendChild(styleEl);
|
|
1280
1865
|
// The carousel element scrolls itself — tag it so renderCard can snapshot/restore
|
|
1281
1866
|
// its scrollLeft across variable-driven re-renders (see captureScrollPositions/restoreScrollPositions).
|
|
1282
|
-
container.setAttribute(
|
|
1867
|
+
container.setAttribute(CAROUSEL_SCROLL_ID_ATTRIBUTE, id || 'x');
|
|
1283
1868
|
// Render children
|
|
1284
|
-
const items = [];
|
|
1285
1869
|
for (let i = 0; i < children.length; i++) {
|
|
1286
1870
|
const child = children[i];
|
|
1287
1871
|
const item = renderChild(child);
|
|
1288
|
-
item.
|
|
1289
|
-
item.style.scrollSnapAlign = 'center';
|
|
1290
|
-
item.style.transformOrigin = 'center center';
|
|
1291
|
-
item.style.position = 'relative';
|
|
1292
|
-
item.style.overflow = 'hidden';
|
|
1293
|
-
const overlay = document.createElement('div');
|
|
1294
|
-
overlay.className = 'carousel-overlay';
|
|
1295
|
-
Object.assign(overlay.style, {
|
|
1296
|
-
position: 'absolute',
|
|
1297
|
-
inset: '0',
|
|
1298
|
-
background: overlayColor,
|
|
1299
|
-
opacity: '0',
|
|
1300
|
-
pointerEvents: 'none',
|
|
1301
|
-
borderRadius: 'inherit',
|
|
1302
|
-
zIndex: '1',
|
|
1303
|
-
});
|
|
1304
|
-
item.appendChild(overlay);
|
|
1872
|
+
bindSlotItemPresentation(item, carouselItemDescriptor(i, children.length, itemWidthPx ? `${itemWidthPx}px` : `${itemWidth}%`, overlayColor));
|
|
1305
1873
|
container.appendChild(item);
|
|
1306
|
-
items.push(item);
|
|
1307
1874
|
}
|
|
1308
1875
|
// Index of the item whose center is closest to the viewport center
|
|
1309
|
-
const nearestIndex = () => {
|
|
1876
|
+
const nearestIndex = (items) => {
|
|
1310
1877
|
const centerX = container.scrollLeft + container.offsetWidth / 2;
|
|
1311
1878
|
let idx = 0;
|
|
1312
1879
|
let minDist = Infinity;
|
|
@@ -1320,9 +1887,29 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1320
1887
|
});
|
|
1321
1888
|
return idx;
|
|
1322
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
|
+
};
|
|
1323
1907
|
// Center a given item. 'smooth' animates and restores CSS snap afterwards;
|
|
1324
1908
|
// 'auto' jumps instantly (used for initial positioning).
|
|
1325
1909
|
const centerItem = (item, behavior = 'smooth') => {
|
|
1910
|
+
if (ownerDisposed)
|
|
1911
|
+
return;
|
|
1912
|
+
clearPendingSnap();
|
|
1326
1913
|
const target = item.offsetLeft - (container.offsetWidth - item.offsetWidth) / 2;
|
|
1327
1914
|
if (behavior === 'auto') {
|
|
1328
1915
|
// Disable mandatory snap for the instant jump — a mandatory-snap container
|
|
@@ -1330,7 +1917,7 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1330
1917
|
// Restore snap next frame once the scroll offset is committed.
|
|
1331
1918
|
container.style.scrollSnapType = 'none';
|
|
1332
1919
|
container.scrollLeft = target;
|
|
1333
|
-
|
|
1920
|
+
requestOwnerFrame(() => {
|
|
1334
1921
|
container.style.scrollSnapType = 'x mandatory';
|
|
1335
1922
|
});
|
|
1336
1923
|
return;
|
|
@@ -1338,12 +1925,12 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1338
1925
|
// Disable CSS snap during the smooth animation, restore after for pixel-perfect alignment
|
|
1339
1926
|
container.style.scrollSnapType = 'none';
|
|
1340
1927
|
container.scrollTo({ left: target, behavior: 'smooth' });
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}, { once: true });
|
|
1928
|
+
pendingSnapScrollEnd = restoreSnap;
|
|
1929
|
+
container.addEventListener('scrollend', pendingSnapScrollEnd, { once: true });
|
|
1344
1930
|
// Fallback for browsers without scrollend event
|
|
1345
|
-
setTimeout(() => {
|
|
1346
|
-
|
|
1931
|
+
pendingSnapTimer = window.setTimeout(() => {
|
|
1932
|
+
pendingSnapTimer = 0;
|
|
1933
|
+
restoreSnap();
|
|
1347
1934
|
}, 400);
|
|
1348
1935
|
};
|
|
1349
1936
|
// Snap helper
|
|
@@ -1352,7 +1939,10 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1352
1939
|
let isDragging = false;
|
|
1353
1940
|
let startX = 0;
|
|
1354
1941
|
let scrollStart = 0;
|
|
1355
|
-
|
|
1942
|
+
const handleMouseDown = (event) => {
|
|
1943
|
+
if (ownerDisposed)
|
|
1944
|
+
return;
|
|
1945
|
+
const e = event;
|
|
1356
1946
|
if (e.button !== 0)
|
|
1357
1947
|
return; // left button only — keep middle/right clicks out of drag state
|
|
1358
1948
|
isDragging = true;
|
|
@@ -1361,35 +1951,52 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1361
1951
|
container.style.cursor = 'grabbing';
|
|
1362
1952
|
container.style.scrollSnapType = 'none';
|
|
1363
1953
|
e.preventDefault();
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (!isDragging)
|
|
1954
|
+
};
|
|
1955
|
+
const handleMouseMove = (event) => {
|
|
1956
|
+
if (ownerDisposed || !isDragging)
|
|
1367
1957
|
return;
|
|
1958
|
+
const e = event;
|
|
1368
1959
|
container.scrollLeft = scrollStart - (e.pageX - startX);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
if (!isDragging)
|
|
1960
|
+
};
|
|
1961
|
+
const handleMouseUp = () => {
|
|
1962
|
+
if (ownerDisposed || !isDragging)
|
|
1372
1963
|
return;
|
|
1373
1964
|
isDragging = false;
|
|
1374
1965
|
container.style.cursor = 'grab';
|
|
1375
1966
|
snapToNearest();
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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 });
|
|
1976
|
+
// Wheel support
|
|
1381
1977
|
let wheelTimer = 0;
|
|
1382
|
-
|
|
1978
|
+
const handleWheel = (event) => {
|
|
1979
|
+
if (ownerDisposed)
|
|
1980
|
+
return;
|
|
1981
|
+
const e = event;
|
|
1383
1982
|
if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) {
|
|
1384
1983
|
e.preventDefault();
|
|
1385
1984
|
container.style.scrollSnapType = 'none';
|
|
1386
1985
|
container.scrollLeft += e.deltaY;
|
|
1387
|
-
clearTimeout(wheelTimer);
|
|
1388
|
-
wheelTimer = window.setTimeout(() =>
|
|
1986
|
+
window.clearTimeout(wheelTimer);
|
|
1987
|
+
wheelTimer = window.setTimeout(() => {
|
|
1988
|
+
wheelTimer = 0;
|
|
1989
|
+
if (!ownerDisposed)
|
|
1990
|
+
snapToNearest();
|
|
1991
|
+
}, 150);
|
|
1389
1992
|
}
|
|
1390
|
-
}
|
|
1993
|
+
};
|
|
1994
|
+
addSlotOwnerListener(ownerCleanup, container, 'wheel', handleWheel, { passive: false });
|
|
1391
1995
|
// Scale/opacity effects
|
|
1392
1996
|
const updateEffects = () => {
|
|
1997
|
+
if (ownerDisposed)
|
|
1998
|
+
return;
|
|
1999
|
+
const items = carouselItems(container);
|
|
1393
2000
|
const containerWidth = container.offsetWidth;
|
|
1394
2001
|
const scrollCenter = container.scrollLeft + containerWidth / 2;
|
|
1395
2002
|
items.forEach((item) => {
|
|
@@ -1402,20 +2009,27 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1402
2009
|
const opacity = 1 - ratio * (1 - inactiveOpacity);
|
|
1403
2010
|
item.style.transform = `scale3d(${scale},${scale},1)`;
|
|
1404
2011
|
item.style.opacity = `${opacity}`;
|
|
1405
|
-
const overlayEl = item
|
|
2012
|
+
const overlayEl = carouselOverlay(item);
|
|
1406
2013
|
if (overlayEl)
|
|
1407
2014
|
overlayEl.style.opacity = `${ratio}`;
|
|
1408
2015
|
});
|
|
1409
2016
|
};
|
|
1410
2017
|
let rafId = 0;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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 });
|
|
1415
2028
|
// Smooth snap to nearest item center + restore scrollSnapType for pixel-perfect final position
|
|
1416
2029
|
snapToNearest = () => {
|
|
2030
|
+
const items = carouselItems(container);
|
|
1417
2031
|
if (items.length > 0)
|
|
1418
|
-
centerItem(items[nearestIndex()]);
|
|
2032
|
+
centerItem(items[nearestIndex(items)]);
|
|
1419
2033
|
};
|
|
1420
2034
|
// Autoplay — advance to the next item on an interval, looping back to the
|
|
1421
2035
|
// first after the last. Pauses while the user hovers or interacts.
|
|
@@ -1423,33 +2037,64 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1423
2037
|
let autoplayTimer = 0;
|
|
1424
2038
|
const stopAutoplay = () => {
|
|
1425
2039
|
if (autoplayTimer) {
|
|
1426
|
-
clearInterval(autoplayTimer);
|
|
2040
|
+
window.clearInterval(autoplayTimer);
|
|
1427
2041
|
autoplayTimer = 0;
|
|
1428
2042
|
}
|
|
1429
2043
|
};
|
|
1430
2044
|
const startAutoplay = () => {
|
|
1431
|
-
|
|
2045
|
+
const items = carouselItems(container);
|
|
2046
|
+
if (ownerDisposed
|
|
2047
|
+
|| !container.isConnected
|
|
2048
|
+
|| !autoplay
|
|
2049
|
+
|| items.length < 2)
|
|
1432
2050
|
return;
|
|
1433
2051
|
stopAutoplay();
|
|
1434
2052
|
autoplayTimer = window.setInterval(() => {
|
|
1435
|
-
|
|
1436
|
-
if (!container.isConnected) {
|
|
2053
|
+
if (ownerDisposed || !container.isConnected) {
|
|
1437
2054
|
stopAutoplay();
|
|
1438
2055
|
return;
|
|
1439
2056
|
}
|
|
1440
2057
|
if (paused || isDragging)
|
|
1441
2058
|
return;
|
|
1442
|
-
|
|
2059
|
+
const items = carouselItems(container);
|
|
2060
|
+
if (items.length < 2)
|
|
2061
|
+
return;
|
|
2062
|
+
centerItem(items[(nearestIndex(items) + 1) % items.length]);
|
|
1443
2063
|
}, autoplayInterval);
|
|
1444
2064
|
};
|
|
1445
2065
|
if (autoplay) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
});
|
|
1451
2095
|
// Initial position & effects — deferred until element is in DOM and laid out
|
|
1452
|
-
|
|
2096
|
+
requestOwnerFrame(() => {
|
|
2097
|
+
const items = carouselItems(container);
|
|
1453
2098
|
if (items.length > 0) {
|
|
1454
2099
|
// Use ACTUAL rendered width (includes padding in content-box mode) for perfect centering
|
|
1455
2100
|
const containerWidth = container.offsetWidth;
|
|
@@ -1464,13 +2109,13 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
|
|
|
1464
2109
|
delete container.dataset.restoreScrollLeft;
|
|
1465
2110
|
container.style.scrollSnapType = 'none';
|
|
1466
2111
|
container.scrollLeft = parseFloat(saved);
|
|
1467
|
-
|
|
2112
|
+
requestOwnerFrame(() => {
|
|
1468
2113
|
container.style.scrollSnapType = 'x mandatory';
|
|
1469
2114
|
});
|
|
1470
2115
|
}
|
|
1471
2116
|
else {
|
|
1472
2117
|
// Jump to the configured initial item (defaults to the first) — CSS snap maintains alignment
|
|
1473
|
-
centerItem(items[initialIndex], 'auto');
|
|
2118
|
+
centerItem(items[Math.min(initialIndex, items.length - 1)], 'auto');
|
|
1474
2119
|
}
|
|
1475
2120
|
}
|
|
1476
2121
|
updateEffects();
|
|
@@ -1757,6 +2402,97 @@ class BaseElement extends HTMLElement {
|
|
|
1757
2402
|
this.shadowRoot.innerHTML = html;
|
|
1758
2403
|
applyResponsiveStyles(this.shadowRoot, this._responsive);
|
|
1759
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
|
+
}
|
|
1760
2496
|
/**
|
|
1761
2497
|
* Escape a value before interpolating it into a double-quoted HTML
|
|
1762
2498
|
* attribute. Browsers decode the entities before parsing inline CSS, so
|
|
@@ -2745,10 +3481,18 @@ function normalizeNumberAttribute(value, options = {}) {
|
|
|
2745
3481
|
return numeric;
|
|
2746
3482
|
}
|
|
2747
3483
|
class CardInput extends BaseElement {
|
|
3484
|
+
constructor() {
|
|
3485
|
+
super(...arguments);
|
|
3486
|
+
this.wiredInputs = new WeakSet();
|
|
3487
|
+
this.wiredNumberInputs = new WeakSet();
|
|
3488
|
+
}
|
|
2748
3489
|
render() {
|
|
2749
3490
|
if (!this.shadowRoot || !this._node)
|
|
2750
3491
|
return;
|
|
2751
|
-
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;
|
|
2752
3496
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
2753
3497
|
const legacyInputStyle = style && typeof style === 'object' && style.resize != null
|
|
2754
3498
|
? { resize: style.resize }
|
|
@@ -2818,6 +3562,7 @@ class CardInput extends BaseElement {
|
|
|
2818
3562
|
id="${inputId}"
|
|
2819
3563
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2820
3564
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
3565
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2821
3566
|
${describedBy}
|
|
2822
3567
|
${disabled ? 'disabled' : ''}
|
|
2823
3568
|
${readOnly ? 'readonly' : ''}
|
|
@@ -2829,6 +3574,7 @@ class CardInput extends BaseElement {
|
|
|
2829
3574
|
id="${inputId}"
|
|
2830
3575
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2831
3576
|
type="${this.escapeAttr(String(inputType))}"
|
|
3577
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2832
3578
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
2833
3579
|
value="${this.escapeAttr(String(defaultValue))}"
|
|
2834
3580
|
${describedBy}
|
|
@@ -2838,7 +3584,7 @@ class CardInput extends BaseElement {
|
|
|
2838
3584
|
${numberAttributes}
|
|
2839
3585
|
style="${nativeInlineStyle}"
|
|
2840
3586
|
/>`;
|
|
2841
|
-
|
|
3587
|
+
const markup = `
|
|
2842
3588
|
<style>
|
|
2843
3589
|
:host {
|
|
2844
3590
|
display: block;
|
|
@@ -3012,7 +3758,11 @@ class CardInput extends BaseElement {
|
|
|
3012
3758
|
${numberStepperHtml}
|
|
3013
3759
|
</div>
|
|
3014
3760
|
</div>
|
|
3015
|
-
|
|
3761
|
+
`;
|
|
3762
|
+
if (!activeControl
|
|
3763
|
+
|| !this.patchShadowHTMLPreservingElement(markup, activeControl, '.card-input')) {
|
|
3764
|
+
this.setShadowHTML(markup);
|
|
3765
|
+
}
|
|
3016
3766
|
// Wire up native input/change events that bubble out of Shadow DOM.
|
|
3017
3767
|
// renderCard listens for these standard event names (mapped from onInput / onChange).
|
|
3018
3768
|
// The `detail.value` carries the current input value so that:
|
|
@@ -3020,41 +3770,41 @@ class CardInput extends BaseElement {
|
|
|
3020
3770
|
// 2. action handlers can access it if needed
|
|
3021
3771
|
const inputEl = this.shadowRoot.querySelector('.card-input');
|
|
3022
3772
|
if (inputEl) {
|
|
3023
|
-
|
|
3024
|
-
this.
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
queueMicrotask(() => {
|
|
3039
|
-
const control = this.getAutoFocusControl();
|
|
3040
|
-
if (control && this.isFirstAvailableAutoFocusInput()) {
|
|
3041
|
-
control.focus();
|
|
3042
|
-
}
|
|
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
|
+
}));
|
|
3043
3788
|
});
|
|
3044
3789
|
}
|
|
3045
3790
|
if (isNumber &&
|
|
3046
3791
|
showNumberStepper &&
|
|
3047
3792
|
inputEl instanceof HTMLInputElement) {
|
|
3048
|
-
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
3049
|
-
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
3050
3793
|
const refreshStepperState = () => {
|
|
3051
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');
|
|
3052
3797
|
if (increaseButton)
|
|
3053
3798
|
increaseButton.disabled = state.increase;
|
|
3054
3799
|
if (decreaseButton)
|
|
3055
3800
|
decreaseButton.disabled = state.decrease;
|
|
3056
3801
|
};
|
|
3057
|
-
|
|
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');
|
|
3058
3808
|
increaseButton?.addEventListener('click', () => {
|
|
3059
3809
|
stepNumberInput(inputEl, 'increase');
|
|
3060
3810
|
inputEl.focus();
|
|
@@ -3070,21 +3820,25 @@ class CardInput extends BaseElement {
|
|
|
3070
3820
|
}
|
|
3071
3821
|
}
|
|
3072
3822
|
// ─── Helpers ──────────────────────────────────────────────────
|
|
3073
|
-
|
|
3823
|
+
/**
|
|
3824
|
+
* Consume an interaction-driven focus request from the card renderer.
|
|
3825
|
+
* Mounts and ordinary updates never call this method.
|
|
3826
|
+
*/
|
|
3827
|
+
requestAutoFocus() {
|
|
3074
3828
|
if (!this.isConnected
|
|
3075
3829
|
|| !this._props.autoFocus
|
|
3076
3830
|
|| this.hasAttribute('data-disabled')) {
|
|
3077
|
-
return
|
|
3831
|
+
return false;
|
|
3078
3832
|
}
|
|
3079
3833
|
const control = this.shadowRoot?.querySelector('.card-input');
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
if (!('querySelectorAll' in root))
|
|
3834
|
+
if (!control
|
|
3835
|
+
|| control.disabled
|
|
3836
|
+
|| control.readOnly
|
|
3837
|
+
|| control.value !== '') {
|
|
3085
3838
|
return false;
|
|
3086
|
-
|
|
3087
|
-
|
|
3839
|
+
}
|
|
3840
|
+
control.focus({ preventScroll: true });
|
|
3841
|
+
return this.shadowRoot?.activeElement === control;
|
|
3088
3842
|
}
|
|
3089
3843
|
/** Escape HTML entities for safe insertion. */
|
|
3090
3844
|
escapeHtml(str) {
|
|
@@ -3146,15 +3900,37 @@ class CardImage extends BaseElement {
|
|
|
3146
3900
|
if (!this.shadowRoot || !this._node)
|
|
3147
3901
|
return;
|
|
3148
3902
|
const { src, alt = '', width, height, objectFit = 'cover', preview = true, style, isExpressionResultStyle, } = this._props;
|
|
3149
|
-
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
3150
3903
|
const imgSrc = this.resolveContent(src);
|
|
3151
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);
|
|
3152
3924
|
// Build image inline styles
|
|
3153
3925
|
const imgStyles = [];
|
|
3154
|
-
if (
|
|
3155
|
-
imgStyles.push(`width:${
|
|
3156
|
-
if (
|
|
3157
|
-
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');
|
|
3158
3934
|
if (objectFit)
|
|
3159
3935
|
imgStyles.push(`object-fit:${objectFit}`);
|
|
3160
3936
|
if (inlineStyle)
|
|
@@ -3170,9 +3946,12 @@ class CardImage extends BaseElement {
|
|
|
3170
3946
|
position: relative;
|
|
3171
3947
|
display: inline-block;
|
|
3172
3948
|
max-width: 100%;
|
|
3949
|
+
${reservesWidth ? 'width: 100%;' : ''}
|
|
3950
|
+
${reservesHeight ? 'height: 100%;' : ''}
|
|
3173
3951
|
}
|
|
3174
3952
|
.card-image {
|
|
3175
3953
|
display: block;
|
|
3954
|
+
box-sizing: border-box;
|
|
3176
3955
|
max-width: 100%;
|
|
3177
3956
|
height: auto;
|
|
3178
3957
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
@@ -3193,8 +3972,8 @@ class CardImage extends BaseElement {
|
|
|
3193
3972
|
100% { background-position: -200% 0; }
|
|
3194
3973
|
}
|
|
3195
3974
|
.card-image.error {
|
|
3196
|
-
min-width: 100px;
|
|
3197
|
-
min-height: 60px;
|
|
3975
|
+
min-width: ${reservesWidth ? '0' : '100px'};
|
|
3976
|
+
min-height: ${reservesHeight ? '0' : '60px'};
|
|
3198
3977
|
background: #f5f5f5;
|
|
3199
3978
|
display: flex;
|
|
3200
3979
|
align-items: center;
|
|
@@ -3271,6 +4050,26 @@ class CardImage extends BaseElement {
|
|
|
3271
4050
|
`);
|
|
3272
4051
|
this.bindEvents(preview);
|
|
3273
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
|
+
}
|
|
3274
4073
|
bindEvents(preview) {
|
|
3275
4074
|
if (!this.shadowRoot)
|
|
3276
4075
|
return;
|
|
@@ -3385,9 +4184,7 @@ class CardDivider extends BaseElement {
|
|
|
3385
4184
|
<div class="divider-line"></div>
|
|
3386
4185
|
${displayText ? `<span class="divider-text">${displayText}</span><div class="divider-line"></div>` : ''}
|
|
3387
4186
|
`);
|
|
3388
|
-
|
|
3389
|
-
this.style.cssText += ';' + inlineStyle;
|
|
3390
|
-
}
|
|
4187
|
+
this.style.cssText = inlineStyle;
|
|
3391
4188
|
}
|
|
3392
4189
|
}
|
|
3393
4190
|
CardDivider.is = 'ai-card-divider';
|
|
@@ -5316,9 +6113,7 @@ class CardLoading extends BaseElement {
|
|
|
5316
6113
|
<div class="spinner"></div>
|
|
5317
6114
|
${displayText ? `<span class="loading-text">${displayText}</span>` : ''}
|
|
5318
6115
|
`);
|
|
5319
|
-
|
|
5320
|
-
this.style.cssText += ';' + inlineStyle;
|
|
5321
|
-
}
|
|
6116
|
+
this.style.cssText = inlineStyle;
|
|
5322
6117
|
}
|
|
5323
6118
|
}
|
|
5324
6119
|
CardLoading.is = 'ai-card-loading';
|
|
@@ -7787,6 +8582,468 @@ function resolveSizeInStyle(style) {
|
|
|
7787
8582
|
return resolved;
|
|
7788
8583
|
}
|
|
7789
8584
|
|
|
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);
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
function syncElementBindings(element, config) {
|
|
8611
|
+
let state = states.get(element);
|
|
8612
|
+
if (!state) {
|
|
8613
|
+
state = {
|
|
8614
|
+
current: config,
|
|
8615
|
+
listeners: new Map(),
|
|
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);
|
|
8628
|
+
}
|
|
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);
|
|
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);
|
|
8651
|
+
}
|
|
8652
|
+
catch (error) {
|
|
8653
|
+
if (!hasError)
|
|
8654
|
+
firstError = error;
|
|
8655
|
+
hasError = true;
|
|
8656
|
+
}
|
|
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);
|
|
8669
|
+
}
|
|
8670
|
+
catch (error) {
|
|
8671
|
+
if (!hasError)
|
|
8672
|
+
firstError = error;
|
|
8673
|
+
hasError = true;
|
|
8674
|
+
}
|
|
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');
|
|
8726
|
+
}
|
|
8727
|
+
values.push(serializeJsonLike(descriptor.value, ancestors));
|
|
8728
|
+
}
|
|
8729
|
+
return `[${values.join(',')}]`;
|
|
8730
|
+
}
|
|
8731
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8732
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
8733
|
+
throw new TypeError('Unsupported object prototype');
|
|
8734
|
+
}
|
|
8735
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
8736
|
+
if (ownKeys.some(key => typeof key !== 'string')) {
|
|
8737
|
+
throw new TypeError('Unsupported symbol key');
|
|
8738
|
+
}
|
|
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');
|
|
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');
|
|
7790
9047
|
/**
|
|
7791
9048
|
* Render the scoped/materialized branch of a card.
|
|
7792
9049
|
*
|
|
@@ -7795,6 +9052,7 @@ function resolveSizeInStyle(style) {
|
|
|
7795
9052
|
* only after materialization and detached DOM construction both succeed.
|
|
7796
9053
|
*/
|
|
7797
9054
|
function renderBoundCard(container, schema, options) {
|
|
9055
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
7798
9056
|
const variables = cloneJsonData({
|
|
7799
9057
|
...schema.variables,
|
|
7800
9058
|
...options.variables,
|
|
@@ -7805,11 +9063,17 @@ function renderBoundCard(container, schema, options) {
|
|
|
7805
9063
|
const inflightRequests = new Map();
|
|
7806
9064
|
const activeLifecycleNodes = new Map();
|
|
7807
9065
|
let currentMaterialized;
|
|
9066
|
+
let currentMountedTree = null;
|
|
9067
|
+
let lastPublishedVariables = cloneJsonData(variables);
|
|
7808
9068
|
let revision = 0;
|
|
7809
9069
|
let disposed = false;
|
|
9070
|
+
let disposeRequested = false;
|
|
9071
|
+
let publishing = false;
|
|
7810
9072
|
const isMobile = options.isMobile === true;
|
|
7811
9073
|
let actionQueue = Promise.resolve();
|
|
9074
|
+
let queuedActionCount = 0;
|
|
7812
9075
|
let lifecycleQueue = Promise.resolve();
|
|
9076
|
+
const repeatIdentityCountCache = new WeakMap();
|
|
7813
9077
|
function expressionContextFor(node) {
|
|
7814
9078
|
return isBoundRenderTreeNode(node)
|
|
7815
9079
|
? createExpressionContext(node.scope)
|
|
@@ -7846,99 +9110,408 @@ function renderBoundCard(container, schema, options) {
|
|
|
7846
9110
|
inflightRequests,
|
|
7847
9111
|
};
|
|
7848
9112
|
}
|
|
7849
|
-
function
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
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
|
+
};
|
|
7861
9149
|
}
|
|
7862
9150
|
const resolvedProps = resolveNodeProps(node, renderVariables);
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
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
|
+
}
|
|
7879
9199
|
}
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
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));
|
|
7892
9272
|
}
|
|
7893
|
-
}
|
|
9273
|
+
}
|
|
9274
|
+
applyDisabledDescendants(element, metadata.disabled);
|
|
9275
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
9276
|
+
return { ...metadata, element, children };
|
|
7894
9277
|
}
|
|
7895
|
-
|
|
7896
|
-
|
|
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)) {
|
|
7897
9288
|
if (!eventValue || !resolveActionRef(eventValue, schemaActions)) {
|
|
7898
9289
|
continue;
|
|
7899
9290
|
}
|
|
7900
|
-
const domEvent = EVENT_MAP[
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
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
|
+
});
|
|
7911
9304
|
}
|
|
7912
9305
|
}
|
|
7913
|
-
|
|
7914
|
-
|
|
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);
|
|
7915
9330
|
}
|
|
7916
|
-
|
|
7917
|
-
|
|
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();
|
|
7918
9345
|
for (const child of node.children) {
|
|
7919
|
-
|
|
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));
|
|
7920
9361
|
}
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
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);
|
|
7925
9437
|
}
|
|
7926
9438
|
}
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
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);
|
|
7933
9482
|
}
|
|
7934
|
-
applyResponsiveStyles(el, createResponsiveContext(isMobile, options.responsive));
|
|
7935
|
-
return el;
|
|
7936
9483
|
}
|
|
7937
|
-
function
|
|
9484
|
+
function prepareIncrementalCandidate(before, draft) {
|
|
7938
9485
|
const materialized = materializeCard(schema, draft);
|
|
7939
|
-
const
|
|
7940
|
-
|
|
7941
|
-
|
|
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
|
+
}
|
|
7942
9515
|
}
|
|
7943
9516
|
function indexNodes(root) {
|
|
7944
9517
|
const nodes = new Map();
|
|
@@ -7949,6 +9522,93 @@ function renderBoundCard(container, schema, options) {
|
|
|
7949
9522
|
visit(root);
|
|
7950
9523
|
return nodes;
|
|
7951
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
|
+
}
|
|
7952
9612
|
function selectLifecycleNodes(materialized, ids) {
|
|
7953
9613
|
const indexed = indexNodes(materialized.root);
|
|
7954
9614
|
const selected = new Map();
|
|
@@ -7959,26 +9619,80 @@ function renderBoundCard(container, schema, options) {
|
|
|
7959
9619
|
}
|
|
7960
9620
|
return selected;
|
|
7961
9621
|
}
|
|
7962
|
-
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;
|
|
7963
9671
|
const writeLiveVariable = (key, value, silent = false) => {
|
|
7964
|
-
|
|
9672
|
+
const nextValue = cloneJsonData(value);
|
|
9673
|
+
writeDraftVariable(lifecycleVariables, key, nextValue);
|
|
9674
|
+
repeatIdentityCountCache.delete(lifecycleVariables);
|
|
9675
|
+
if (disposed || authorityRevision !== revision)
|
|
7965
9676
|
return;
|
|
7966
9677
|
if (silent) {
|
|
7967
|
-
writeDraftVariable(variables, key,
|
|
9678
|
+
writeDraftVariable(variables, key, nextValue);
|
|
7968
9679
|
return;
|
|
7969
9680
|
}
|
|
7970
|
-
|
|
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;
|
|
7971
9685
|
};
|
|
7972
|
-
|
|
9686
|
+
const context = {
|
|
7973
9687
|
...createWebActionContext({
|
|
7974
9688
|
...options,
|
|
7975
9689
|
setVariable: writeLiveVariable,
|
|
7976
9690
|
abortSignal: abortController.signal,
|
|
7977
9691
|
}),
|
|
7978
|
-
variables,
|
|
7979
|
-
expressionContext: createExpressionContext(
|
|
9692
|
+
variables: lifecycleVariables,
|
|
9693
|
+
expressionContext: createExpressionContext(lifecycleScope),
|
|
7980
9694
|
parameterResolver: node.bindingDialect === 'a2ui'
|
|
7981
|
-
? createA2UIParameterResolver(
|
|
9695
|
+
? createA2UIParameterResolver(lifecycleVariables, node.dataPath)
|
|
7982
9696
|
: undefined,
|
|
7983
9697
|
variableWriter: (key, value, options) => {
|
|
7984
9698
|
writeLiveVariable(key, value, options.silent);
|
|
@@ -7986,6 +9700,20 @@ function renderBoundCard(container, schema, options) {
|
|
|
7986
9700
|
botId: options.botId,
|
|
7987
9701
|
inflightRequests,
|
|
7988
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
|
+
};
|
|
7989
9717
|
}
|
|
7990
9718
|
function reconcileLifecycles(nextNodes) {
|
|
7991
9719
|
const removed = [...activeLifecycleNodes.entries()]
|
|
@@ -7998,45 +9726,263 @@ function renderBoundCard(container, schema, options) {
|
|
|
7998
9726
|
}
|
|
7999
9727
|
lifecycleQueue = lifecycleQueue.then(async () => {
|
|
8000
9728
|
for (const [id, node] of removed) {
|
|
8001
|
-
|
|
8002
|
-
|
|
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
|
+
}
|
|
8003
9742
|
}
|
|
8004
|
-
if (disposed)
|
|
8005
|
-
return;
|
|
8006
9743
|
for (const [id, node] of added) {
|
|
9744
|
+
if (disposed)
|
|
9745
|
+
return;
|
|
8007
9746
|
lifecycleManager.register(id, node.lifecycle);
|
|
8008
|
-
|
|
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
|
+
}
|
|
8009
9756
|
}
|
|
8010
9757
|
}).catch((error) => {
|
|
8011
9758
|
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
8012
9759
|
});
|
|
8013
9760
|
}
|
|
8014
|
-
function
|
|
8015
|
-
const
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
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
|
+
}
|
|
8025
9925
|
}
|
|
8026
9926
|
function assertCurrentRevision(baseRevision) {
|
|
8027
|
-
if (disposed || revision !== baseRevision) {
|
|
9927
|
+
if (disposed || publishing || revision !== baseRevision) {
|
|
8028
9928
|
const error = new Error('[renderCard] BOUND_TRANSACTION_CONFLICT');
|
|
8029
9929
|
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
8030
9930
|
throw error;
|
|
8031
9931
|
}
|
|
8032
9932
|
}
|
|
8033
|
-
function commitDraft(
|
|
9933
|
+
function commitDraft(before, draft, baseRevision) {
|
|
8034
9934
|
assertCurrentRevision(baseRevision);
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
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);
|
|
8040
9986
|
}
|
|
8041
9987
|
function writeDraftVariable(draft, key, value) {
|
|
8042
9988
|
Object.defineProperty(draft, String(key), {
|
|
@@ -8093,11 +10039,12 @@ function renderBoundCard(container, schema, options) {
|
|
|
8093
10039
|
inflightRequests,
|
|
8094
10040
|
};
|
|
8095
10041
|
}
|
|
8096
|
-
async function runBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10042
|
+
async function runBoundEvent(runtimeId, eventName, eventDetail, baseRevision, repeatTargetFingerprint) {
|
|
8097
10043
|
if (disposed)
|
|
8098
10044
|
return;
|
|
8099
|
-
|
|
8100
|
-
const
|
|
10045
|
+
assertCurrentRevision(baseRevision);
|
|
10046
|
+
const transactionBefore = cloneJsonData(variables);
|
|
10047
|
+
const draft = cloneJsonData(transactionBefore);
|
|
8101
10048
|
if (eventDetail !== undefined) {
|
|
8102
10049
|
writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
|
|
8103
10050
|
}
|
|
@@ -8105,8 +10052,14 @@ function renderBoundCard(container, schema, options) {
|
|
|
8105
10052
|
const freshMaterialized = materializeCard(schema, draft);
|
|
8106
10053
|
const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
|
|
8107
10054
|
if (!freshNode) {
|
|
10055
|
+
if (repeatTargetFingerprint !== undefined)
|
|
10056
|
+
return;
|
|
8108
10057
|
throw new Error(`[renderCard] Bound runtime node "${runtimeId}" no longer exists`);
|
|
8109
10058
|
}
|
|
10059
|
+
if (repeatTargetFingerprint !== undefined
|
|
10060
|
+
&& getRepeatTargetFingerprint(freshNode) !== repeatTargetFingerprint) {
|
|
10061
|
+
return;
|
|
10062
|
+
}
|
|
8110
10063
|
const eventValue = freshNode.events?.[eventName];
|
|
8111
10064
|
const steps = eventValue
|
|
8112
10065
|
? resolveActionRef(eventValue, schemaActions)
|
|
@@ -8117,14 +10070,19 @@ function renderBoundCard(container, schema, options) {
|
|
|
8117
10070
|
assertCurrentRevision(baseRevision);
|
|
8118
10071
|
if (!hasVariableChanges(actionBaseline, draft))
|
|
8119
10072
|
return;
|
|
8120
|
-
|
|
8121
|
-
|
|
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);
|
|
8122
10077
|
}
|
|
8123
|
-
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10078
|
+
function enqueueBoundEvent(runtimeId, eventName, eventDetail, repeatTargetFingerprint) {
|
|
8124
10079
|
if (disposed)
|
|
8125
10080
|
return;
|
|
10081
|
+
const enqueueRevision = revision;
|
|
10082
|
+
const queuedBehindAnotherAction = queuedActionCount > 0;
|
|
10083
|
+
queuedActionCount += 1;
|
|
8126
10084
|
actionQueue = actionQueue
|
|
8127
|
-
.then(() => runBoundEvent(runtimeId, eventName, eventDetail))
|
|
10085
|
+
.then(() => runBoundEvent(runtimeId, eventName, eventDetail, queuedBehindAnotherAction ? revision : enqueueRevision, repeatTargetFingerprint))
|
|
8128
10086
|
.catch((error) => {
|
|
8129
10087
|
if (error
|
|
8130
10088
|
&& typeof error === 'object'
|
|
@@ -8133,44 +10091,87 @@ function renderBoundCard(container, schema, options) {
|
|
|
8133
10091
|
return;
|
|
8134
10092
|
}
|
|
8135
10093
|
console.error('[renderCard] Bound action failed', error);
|
|
10094
|
+
})
|
|
10095
|
+
.finally(() => {
|
|
10096
|
+
queuedActionCount -= 1;
|
|
8136
10097
|
});
|
|
8137
10098
|
}
|
|
8138
10099
|
function updateVariables(newVariables) {
|
|
8139
10100
|
if (disposed)
|
|
8140
10101
|
return;
|
|
8141
10102
|
const baseRevision = revision;
|
|
10103
|
+
const before = cloneJsonData(lastPublishedVariables);
|
|
8142
10104
|
const draft = cloneJsonData(variables);
|
|
8143
10105
|
const patch = cloneJsonData(newVariables);
|
|
8144
10106
|
for (const key of Object.keys(patch)) {
|
|
8145
10107
|
writeDraftVariable(draft, key, patch[key]);
|
|
8146
10108
|
}
|
|
8147
|
-
|
|
8148
|
-
commitDraft(draft, candidate, baseRevision);
|
|
10109
|
+
commitDraft(before, draft, baseRevision);
|
|
8149
10110
|
}
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
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 {
|
|
8159
10123
|
disposeChartsIn(container);
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
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 {
|
|
8167
10140
|
lifecycleManager.unregister(id);
|
|
8168
10141
|
}
|
|
10142
|
+
}
|
|
10143
|
+
try {
|
|
8169
10144
|
await lifecycleManager.dispose(createPassiveActionContext(variables));
|
|
8170
|
-
}
|
|
8171
|
-
|
|
10145
|
+
}
|
|
10146
|
+
catch (error) {
|
|
8172
10147
|
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
8173
|
-
}
|
|
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();
|
|
8174
10175
|
},
|
|
8175
10176
|
updateVariables,
|
|
8176
10177
|
};
|
|
@@ -8188,7 +10189,11 @@ function captureScrollPositions$1(root) {
|
|
|
8188
10189
|
function restoreScrollPositions$1(root, positions) {
|
|
8189
10190
|
if (positions.size === 0)
|
|
8190
10191
|
return;
|
|
8191
|
-
|
|
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) => {
|
|
8192
10197
|
const id = element.getAttribute('data-scroll-id');
|
|
8193
10198
|
const saved = id ? positions.get(id) : undefined;
|
|
8194
10199
|
if (saved == null)
|
|
@@ -8217,7 +10222,11 @@ function captureMediaStates$1(root) {
|
|
|
8217
10222
|
function restoreMediaStates$1(root, states) {
|
|
8218
10223
|
if (states.size === 0)
|
|
8219
10224
|
return;
|
|
8220
|
-
|
|
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) => {
|
|
8221
10230
|
const id = element.getAttribute('data-card-id');
|
|
8222
10231
|
const snapshot = id ? states.get(id) : undefined;
|
|
8223
10232
|
if (!snapshot)
|
|
@@ -8279,22 +10288,72 @@ function renderCard(container, schemaInput, options = {}) {
|
|
|
8279
10288
|
return renderBoundCard(container, schema, options);
|
|
8280
10289
|
}
|
|
8281
10290
|
function renderStaticCard(container, schema, options) {
|
|
10291
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8282
10292
|
// 2. Parse into render tree
|
|
8283
10293
|
const tree = parseSchema(schema);
|
|
8284
10294
|
// 3. Reactive variables store (mutable copy, merged with external variables)
|
|
8285
10295
|
let variables = { ...schema.variables, ...options.variables };
|
|
8286
10296
|
let disposed = false;
|
|
10297
|
+
let publishing = false;
|
|
10298
|
+
let disposeRequested = false;
|
|
8287
10299
|
let hostAuthorityEpoch = 0;
|
|
10300
|
+
let nextStaticEventSequence = 0;
|
|
10301
|
+
const latestStaticEventWriterByKey = new Map();
|
|
8288
10302
|
const lifecycleRecords = new Map();
|
|
8289
10303
|
const abortController = new AbortController();
|
|
8290
10304
|
// Per-instance request dedup map (isolated from other cards on the page).
|
|
8291
10305
|
const inflightRequests = new Map();
|
|
8292
|
-
function
|
|
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) {
|
|
8293
10346
|
const baseAuthorityEpoch = hostAuthorityEpoch;
|
|
8294
10347
|
let context;
|
|
8295
10348
|
const writeVariable = (key, value, { silent }) => {
|
|
8296
10349
|
let actionVariables = context.variables;
|
|
8297
|
-
|
|
10350
|
+
const latestWriter = latestStaticEventWriterByKey.get(key);
|
|
10351
|
+
if (disposed
|
|
10352
|
+
|| publishing
|
|
10353
|
+
|| baseAuthorityEpoch !== hostAuthorityEpoch
|
|
10354
|
+
|| (eventSequence !== undefined
|
|
10355
|
+
&& latestWriter !== undefined
|
|
10356
|
+
&& latestWriter > eventSequence)) {
|
|
8298
10357
|
if (actionVariables === variables) {
|
|
8299
10358
|
actionVariables = { ...actionVariables };
|
|
8300
10359
|
context.variables = actionVariables;
|
|
@@ -8302,9 +10361,50 @@ function renderStaticCard(container, schema, options) {
|
|
|
8302
10361
|
actionVariables[key] = value;
|
|
8303
10362
|
return;
|
|
8304
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];
|
|
8305
10370
|
actionVariables[key] = value;
|
|
8306
|
-
if (
|
|
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 {
|
|
8307
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
|
+
}
|
|
8308
10408
|
};
|
|
8309
10409
|
context = {
|
|
8310
10410
|
...createWebActionContext({
|
|
@@ -8314,7 +10414,7 @@ function renderStaticCard(container, schema, options) {
|
|
|
8314
10414
|
},
|
|
8315
10415
|
abortSignal: abortController.signal,
|
|
8316
10416
|
}),
|
|
8317
|
-
variables,
|
|
10417
|
+
variables: initialVariables,
|
|
8318
10418
|
variableWriter: writeVariable,
|
|
8319
10419
|
botId: options.botId,
|
|
8320
10420
|
inflightRequests,
|
|
@@ -8359,8 +10459,431 @@ function renderStaticCard(container, schema, options) {
|
|
|
8359
10459
|
}
|
|
8360
10460
|
});
|
|
8361
10461
|
}
|
|
8362
|
-
|
|
10462
|
+
let mountedTree = null;
|
|
8363
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
|
+
}
|
|
8364
10887
|
function render() {
|
|
8365
10888
|
if (disposed)
|
|
8366
10889
|
return;
|
|
@@ -8368,28 +10891,85 @@ function renderStaticCard(container, schema, options) {
|
|
|
8368
10891
|
const collectedLifecycles = [];
|
|
8369
10892
|
const scrollPositions = captureScrollPositions(container);
|
|
8370
10893
|
const mediaStates = captureMediaStates(container);
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
10894
|
+
let previousTree = null;
|
|
10895
|
+
if (!mountedTree) {
|
|
10896
|
+
const initialTree = mountStaticSubtree(tree, `${tree.id}#0`);
|
|
10897
|
+
if (disposed || generation !== renderGeneration) {
|
|
10898
|
+
disposeStaticSubtree(initialTree);
|
|
8374
10899
|
return;
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
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;
|
|
8378
10961
|
if (disposed)
|
|
8379
10962
|
return;
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
}
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
container.replaceChildren(dom);
|
|
8391
|
-
restoreScrollPositions(container, scrollPositions);
|
|
8392
|
-
restoreMediaStates(container, mediaStates);
|
|
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
|
+
}
|
|
8393
10973
|
if (disposed || generation !== renderGeneration)
|
|
8394
10974
|
return;
|
|
8395
10975
|
for (const { id, lifecycle } of collectedLifecycles) {
|
|
@@ -8404,27 +10984,63 @@ function renderStaticCard(container, schema, options) {
|
|
|
8404
10984
|
actionContext = buildActionContext();
|
|
8405
10985
|
render();
|
|
8406
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
|
+
}
|
|
8407
11006
|
// 7. Initial render
|
|
8408
11007
|
render();
|
|
8409
11008
|
// 8. Return instance handle
|
|
8410
11009
|
return {
|
|
11010
|
+
getNode: nodeAccess.getNode,
|
|
11011
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
8411
11012
|
dispose() {
|
|
8412
|
-
if (disposed)
|
|
11013
|
+
if (disposed || disposeRequested)
|
|
8413
11014
|
return;
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
for (const record of lifecycleRecords.values())
|
|
8420
|
-
destroyLifecycle(record);
|
|
11015
|
+
if (publishing) {
|
|
11016
|
+
disposeRequested = true;
|
|
11017
|
+
return;
|
|
11018
|
+
}
|
|
11019
|
+
disposeNow();
|
|
8421
11020
|
},
|
|
8422
11021
|
updateVariables(newVars) {
|
|
8423
11022
|
if (disposed)
|
|
8424
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;
|
|
8425
11031
|
hostAuthorityEpoch += 1;
|
|
8426
11032
|
variables = { ...variables, ...newVars };
|
|
8427
|
-
|
|
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
|
+
}
|
|
8428
11044
|
},
|
|
8429
11045
|
};
|
|
8430
11046
|
}
|
|
@@ -8450,7 +11066,11 @@ function captureScrollPositions(root) {
|
|
|
8450
11066
|
function restoreScrollPositions(root, positions) {
|
|
8451
11067
|
if (positions.size === 0)
|
|
8452
11068
|
return;
|
|
8453
|
-
|
|
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) => {
|
|
8454
11074
|
const id = el.getAttribute('data-scroll-id');
|
|
8455
11075
|
const saved = id ? positions.get(id) : undefined;
|
|
8456
11076
|
if (saved == null)
|
|
@@ -8490,7 +11110,11 @@ function captureMediaStates(root) {
|
|
|
8490
11110
|
function restoreMediaStates(root, states) {
|
|
8491
11111
|
if (states.size === 0)
|
|
8492
11112
|
return;
|
|
8493
|
-
|
|
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) => {
|
|
8494
11118
|
const id = el.getAttribute('data-card-id');
|
|
8495
11119
|
const snap = id ? states.get(id) : undefined;
|
|
8496
11120
|
if (!snap)
|
|
@@ -8499,121 +11123,6 @@ function restoreMediaStates(root, states) {
|
|
|
8499
11123
|
restoreMedia(media, snap);
|
|
8500
11124
|
});
|
|
8501
11125
|
}
|
|
8502
|
-
// ─── Recursive Node Renderer ─────────────────────────────────────
|
|
8503
|
-
function renderNode(node, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle) {
|
|
8504
|
-
// Check directives.visible
|
|
8505
|
-
if (node.directives?.visible) {
|
|
8506
|
-
const visibleExpr = node.directives.visible;
|
|
8507
|
-
const resolved = hasExpression(visibleExpr)
|
|
8508
|
-
? resolveExpression(visibleExpr, variables)
|
|
8509
|
-
: visibleExpr;
|
|
8510
|
-
if (resolved === false || resolved === 'false' || resolved === '' || resolved === 0) {
|
|
8511
|
-
// Hidden element — return empty placeholder
|
|
8512
|
-
const placeholder = document.createElement('div');
|
|
8513
|
-
placeholder.style.display = 'none';
|
|
8514
|
-
placeholder.setAttribute('data-card-id', node.id);
|
|
8515
|
-
return placeholder;
|
|
8516
|
-
}
|
|
8517
|
-
}
|
|
8518
|
-
// Resolve props with variable expressions
|
|
8519
|
-
const resolvedProps = resolveDeep(node.props, variables);
|
|
8520
|
-
// Resolve content if it's an ExpressionValue
|
|
8521
|
-
if (resolvedProps.content && typeof resolvedProps.content === 'object' && 'type' in resolvedProps.content) {
|
|
8522
|
-
resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
|
|
8523
|
-
}
|
|
8524
|
-
// Check directives.disabled
|
|
8525
|
-
let isDisabled = false;
|
|
8526
|
-
if (node.directives?.disabled) {
|
|
8527
|
-
const disabledExpr = node.directives.disabled;
|
|
8528
|
-
const resolved = hasExpression(disabledExpr)
|
|
8529
|
-
? resolveExpression(disabledExpr, variables)
|
|
8530
|
-
: disabledExpr;
|
|
8531
|
-
isDisabled = resolved === true || resolved === 'true' || resolved === 1;
|
|
8532
|
-
}
|
|
8533
|
-
// Lookup component renderer
|
|
8534
|
-
const renderer = componentRenderers[node.type] ?? componentRenderers['_default'];
|
|
8535
|
-
const el = renderer(node, resolvedProps, isMobile, responsive);
|
|
8536
|
-
// Apply disabled styling & attribute
|
|
8537
|
-
if (isDisabled) {
|
|
8538
|
-
el.setAttribute('data-disabled', 'true');
|
|
8539
|
-
el.style.background = '#F5F5F5';
|
|
8540
|
-
el.style.color = '#C0C0C0';
|
|
8541
|
-
el.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
8542
|
-
el.style.pointerEvents = 'none';
|
|
8543
|
-
el.style.cursor = 'default';
|
|
8544
|
-
}
|
|
8545
|
-
// ── variableKey auto-sync for Input components ──────────────
|
|
8546
|
-
// When an Input element declares `variableKey`, its value is
|
|
8547
|
-
// automatically written into the reactive variables store on
|
|
8548
|
-
// every `input` event (without triggering a full re-render).
|
|
8549
|
-
// The value is then available as `${variableKey}` in action params.
|
|
8550
|
-
const variableKey = resolvedProps.variableKey;
|
|
8551
|
-
if (variableKey) {
|
|
8552
|
-
el.addEventListener('input', ((e) => {
|
|
8553
|
-
// Nested value controls own their variableKey. Only a value event
|
|
8554
|
-
// dispatched by this component may update this component's variable.
|
|
8555
|
-
if (e.target !== el)
|
|
8556
|
-
return;
|
|
8557
|
-
const value = e.detail?.value
|
|
8558
|
-
?? e.target?.value;
|
|
8559
|
-
if (value !== undefined) {
|
|
8560
|
-
// Write silently — do NOT trigger rerender (avoids losing focus)
|
|
8561
|
-
writeInputVariable(variableKey, value, variables);
|
|
8562
|
-
}
|
|
8563
|
-
}));
|
|
8564
|
-
}
|
|
8565
|
-
// ── Bind events (skip if disabled) ──
|
|
8566
|
-
if (node.events && !isDisabled) {
|
|
8567
|
-
for (const [event, eventValue] of Object.entries(node.events)) {
|
|
8568
|
-
if (!eventValue)
|
|
8569
|
-
continue;
|
|
8570
|
-
const resolvedSteps = resolveActionRef(eventValue, schemaActions);
|
|
8571
|
-
if (!resolvedSteps)
|
|
8572
|
-
continue;
|
|
8573
|
-
const domEvent = eventMap[event] ?? event;
|
|
8574
|
-
el.addEventListener(domEvent, ((e) => {
|
|
8575
|
-
// Value controls own their input/change handlers; a nested control's
|
|
8576
|
-
// same-named event must not trigger the parent's schema action.
|
|
8577
|
-
// Other component/event pairs keep the renderer's existing bubbling
|
|
8578
|
-
// behavior (for example a Container-level click handler).
|
|
8579
|
-
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
8580
|
-
&& VALUE_CONTROL_TYPES.has(node.type));
|
|
8581
|
-
if (ownsValueEvent && e.target !== el)
|
|
8582
|
-
return;
|
|
8583
|
-
const detail = e instanceof CustomEvent ? e.detail : undefined;
|
|
8584
|
-
runEventActions(resolvedSteps, detail);
|
|
8585
|
-
}));
|
|
8586
|
-
}
|
|
8587
|
-
}
|
|
8588
|
-
// Register lifecycle
|
|
8589
|
-
if (node.lifecycle) {
|
|
8590
|
-
collectLifecycle(node.id, node.lifecycle);
|
|
8591
|
-
}
|
|
8592
|
-
// Render children — use slot layout if applicable, otherwise flat append
|
|
8593
|
-
const renderChild = (child) => renderNode(child, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle);
|
|
8594
|
-
// Build children-by-id map for layouts that reference IDs (columns groups, float overlays)
|
|
8595
|
-
const childrenMap = {};
|
|
8596
|
-
for (const child of node.children) {
|
|
8597
|
-
childrenMap[child.id] = child;
|
|
8598
|
-
}
|
|
8599
|
-
const layoutApplied = renderSlotLayout(el, node.children, resolvedProps, renderChild, childrenMap, actionContext);
|
|
8600
|
-
if (!layoutApplied) {
|
|
8601
|
-
for (const child of node.children) {
|
|
8602
|
-
el.appendChild(renderChild(child));
|
|
8603
|
-
}
|
|
8604
|
-
}
|
|
8605
|
-
// Apply disabled grey text to all descendants (after children are appended)
|
|
8606
|
-
// Propagate disabled state to child elements (including Shadow DOM custom elements)
|
|
8607
|
-
if (isDisabled) {
|
|
8608
|
-
el.querySelectorAll('*').forEach((child) => {
|
|
8609
|
-
const htmlChild = child;
|
|
8610
|
-
htmlChild.setAttribute('data-disabled', 'true');
|
|
8611
|
-
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
8612
|
-
});
|
|
8613
|
-
}
|
|
8614
|
-
applyResponsiveStyles(el, createResponsiveContext(isMobile, responsive));
|
|
8615
|
-
return el;
|
|
8616
|
-
}
|
|
8617
11126
|
/** Map schema event names → DOM event names */
|
|
8618
11127
|
const eventMap = {
|
|
8619
11128
|
onClick: 'click',
|
|
@@ -8667,6 +11176,7 @@ const VALUE_CONTROL_TYPES = new Set([
|
|
|
8667
11176
|
* ```
|
|
8668
11177
|
*/
|
|
8669
11178
|
function renderStreamingCard(container, options = {}) {
|
|
11179
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8670
11180
|
// ─── State ──────────────────────────────────────────────────────
|
|
8671
11181
|
const parser = new StreamingParser(options.parserOptions);
|
|
8672
11182
|
const elementMap = new Map(); // elementId → DOM element
|
|
@@ -8694,19 +11204,32 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8694
11204
|
let boundActionQueue = Promise.resolve();
|
|
8695
11205
|
let boundLifecycleEpoch = 0;
|
|
8696
11206
|
let disposed = false;
|
|
11207
|
+
let userActionDepth = 0;
|
|
8697
11208
|
const sourceOccurrences = new Map();
|
|
8698
11209
|
const activeBoundLifecycles = new Map();
|
|
8699
11210
|
const mountedBoundLifecycles = new Map();
|
|
8700
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
|
+
}
|
|
8701
11221
|
// ─── Action Context ─────────────────────────────────────────────
|
|
8702
11222
|
function buildActionContext() {
|
|
8703
11223
|
return {
|
|
8704
11224
|
...createWebActionContext({
|
|
8705
11225
|
...options,
|
|
8706
11226
|
setVariable: (key, value) => {
|
|
11227
|
+
const previousVariables = userActionDepth > 0
|
|
11228
|
+
? { ...variables }
|
|
11229
|
+
: undefined;
|
|
8707
11230
|
variables[key] = value;
|
|
8708
11231
|
// On variable change, patch only affected elements (keyed diff)
|
|
8709
|
-
diffAllElements();
|
|
11232
|
+
diffAllElements(previousVariables);
|
|
8710
11233
|
},
|
|
8711
11234
|
abortSignal: abortController.signal,
|
|
8712
11235
|
}),
|
|
@@ -8789,6 +11312,27 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8789
11312
|
|| resolved === ''
|
|
8790
11313
|
|| resolved === 0);
|
|
8791
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
|
+
}
|
|
8792
11336
|
function computeBoundDisabled(node, renderVariables) {
|
|
8793
11337
|
const disabled = node.directives?.disabled;
|
|
8794
11338
|
if (!disabled)
|
|
@@ -8964,7 +11508,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8964
11508
|
if (e instanceof CustomEvent && e.detail != null) {
|
|
8965
11509
|
variables._event = e.detail;
|
|
8966
11510
|
}
|
|
8967
|
-
|
|
11511
|
+
userActionDepth += 1;
|
|
11512
|
+
void runSteps(resolvedSteps, node.id).finally(() => {
|
|
11513
|
+
userActionDepth -= 1;
|
|
11514
|
+
});
|
|
8968
11515
|
}));
|
|
8969
11516
|
}
|
|
8970
11517
|
}
|
|
@@ -9019,6 +11566,43 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9019
11566
|
: visibleExpr;
|
|
9020
11567
|
return !(resolved === false || resolved === 'false' || resolved === '' || resolved === 0);
|
|
9021
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
|
+
}
|
|
9022
11606
|
/** Entrance transition for blocks streamed in incrementally. */
|
|
9023
11607
|
function animateEnter(el) {
|
|
9024
11608
|
if (options.appearTransition === false)
|
|
@@ -9092,11 +11676,14 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9092
11676
|
* (visibility flips / container shape changes) re-render only their subtree.
|
|
9093
11677
|
* Falls back to a full render solely when a subtree can't be rebuilt.
|
|
9094
11678
|
*/
|
|
9095
|
-
function diffAllElements() {
|
|
11679
|
+
function diffAllElements(previousVariables) {
|
|
9096
11680
|
actionContext = buildActionContext();
|
|
9097
11681
|
const schema = (currentSurfaceId ? engine.getSchema(currentSurfaceId) : undefined) ?? currentSchema;
|
|
9098
11682
|
if (!schema)
|
|
9099
11683
|
return;
|
|
11684
|
+
const autoFocusIds = previousVariables
|
|
11685
|
+
? collectLegacyAutoFocusRevealIds(schema, previousVariables)
|
|
11686
|
+
: new Set();
|
|
9100
11687
|
for (const [id, element] of Object.entries(schema.elements)) {
|
|
9101
11688
|
const el = elementMap.get(id);
|
|
9102
11689
|
if (!el)
|
|
@@ -9107,6 +11694,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9107
11694
|
// Visibility flipped (either direction) → rebuild this subtree in place
|
|
9108
11695
|
if (!replaceSubtree(schema, id, el)) {
|
|
9109
11696
|
safeRenderFull();
|
|
11697
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9110
11698
|
return;
|
|
9111
11699
|
}
|
|
9112
11700
|
continue;
|
|
@@ -9123,9 +11711,11 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9123
11711
|
}
|
|
9124
11712
|
else if (!replaceSubtree(schema, id, el)) {
|
|
9125
11713
|
safeRenderFull();
|
|
11714
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9126
11715
|
return;
|
|
9127
11716
|
}
|
|
9128
11717
|
}
|
|
11718
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9129
11719
|
}
|
|
9130
11720
|
function captureBoundScrollPositions(root) {
|
|
9131
11721
|
const positions = new Map();
|
|
@@ -9848,7 +12438,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9848
12438
|
if (!steps)
|
|
9849
12439
|
return;
|
|
9850
12440
|
await runBoundSteps(steps, sourceNode, createBoundActionContext(sourceNode, draft));
|
|
12441
|
+
const nextMaterialized = materializeStreamingCard(currentSchema, draft);
|
|
12442
|
+
const autoFocusIds = collectBoundAutoFocusRevealIds(currentMaterialized, nextMaterialized, before, draft);
|
|
9851
12443
|
commitBoundDraftTransaction(before, draft, baseRevision);
|
|
12444
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9852
12445
|
}
|
|
9853
12446
|
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
9854
12447
|
boundActionQueue = boundActionQueue
|
|
@@ -10430,6 +13023,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10430
13023
|
});
|
|
10431
13024
|
// ─── Public API ─────────────────────────────────────────────────
|
|
10432
13025
|
return {
|
|
13026
|
+
getNode: nodeAccess.getNode,
|
|
13027
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
10433
13028
|
applyCommand(command) {
|
|
10434
13029
|
if (!lockMode('commands'))
|
|
10435
13030
|
return;
|
|
@@ -10506,6 +13101,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10506
13101
|
if (disposed)
|
|
10507
13102
|
return;
|
|
10508
13103
|
disposed = true;
|
|
13104
|
+
nodeAccess.dispose();
|
|
10509
13105
|
boundRevision += 1;
|
|
10510
13106
|
abortController.abort();
|
|
10511
13107
|
teardownBoundLifecycles();
|