@antglobal/copilot-cards-web 1.0.6 → 1.0.8
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/README.md +45 -1
- package/dist/index.d.ts +62 -6
- package/dist/index.js +3213 -580
- 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,99 @@ 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
|
+
if (current.getAttribute(attribute.name) !== attribute.value) {
|
|
2468
|
+
current.setAttribute(attribute.name, attribute.value);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (current === preserved)
|
|
2472
|
+
return;
|
|
2473
|
+
if (current.contains(preserved)) {
|
|
2474
|
+
this.reconcilePreservedTree(current, desired, preserved);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
current.innerHTML = desired.innerHTML;
|
|
2478
|
+
}
|
|
2479
|
+
reconcileKey(element) {
|
|
2480
|
+
if (element.classList.contains('card-input'))
|
|
2481
|
+
return 'card-input';
|
|
2482
|
+
if (element.classList.contains('card-input-wrapper')) {
|
|
2483
|
+
return 'card-input-wrapper';
|
|
2484
|
+
}
|
|
2485
|
+
if (element.classList.contains('card-input-label')) {
|
|
2486
|
+
return 'card-input-label';
|
|
2487
|
+
}
|
|
2488
|
+
if (element.classList.contains('input-control'))
|
|
2489
|
+
return 'input-control';
|
|
2490
|
+
if (element.classList.contains('input-prefix'))
|
|
2491
|
+
return 'input-prefix';
|
|
2492
|
+
if (element.classList.contains('input-suffix'))
|
|
2493
|
+
return 'input-suffix';
|
|
2494
|
+
if (element.classList.contains('number-stepper'))
|
|
2495
|
+
return 'number-stepper';
|
|
2496
|
+
return element.tagName;
|
|
2497
|
+
}
|
|
1760
2498
|
/**
|
|
1761
2499
|
* Escape a value before interpolating it into a double-quoted HTML
|
|
1762
2500
|
* attribute. Browsers decode the entities before parsing inline CSS, so
|
|
@@ -2745,10 +3483,18 @@ function normalizeNumberAttribute(value, options = {}) {
|
|
|
2745
3483
|
return numeric;
|
|
2746
3484
|
}
|
|
2747
3485
|
class CardInput extends BaseElement {
|
|
3486
|
+
constructor() {
|
|
3487
|
+
super(...arguments);
|
|
3488
|
+
this.wiredInputs = new WeakSet();
|
|
3489
|
+
this.wiredNumberInputs = new WeakSet();
|
|
3490
|
+
}
|
|
2748
3491
|
render() {
|
|
2749
3492
|
if (!this.shadowRoot || !this._node)
|
|
2750
3493
|
return;
|
|
2751
|
-
const
|
|
3494
|
+
const activeControl = this.shadowRoot.activeElement?.matches('.card-input')
|
|
3495
|
+
? this.shadowRoot.activeElement
|
|
3496
|
+
: null;
|
|
3497
|
+
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
3498
|
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
2753
3499
|
const legacyInputStyle = style && typeof style === 'object' && style.resize != null
|
|
2754
3500
|
? { resize: style.resize }
|
|
@@ -2818,6 +3564,7 @@ class CardInput extends BaseElement {
|
|
|
2818
3564
|
id="${inputId}"
|
|
2819
3565
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2820
3566
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
3567
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2821
3568
|
${describedBy}
|
|
2822
3569
|
${disabled ? 'disabled' : ''}
|
|
2823
3570
|
${readOnly ? 'readonly' : ''}
|
|
@@ -2829,6 +3576,7 @@ class CardInput extends BaseElement {
|
|
|
2829
3576
|
id="${inputId}"
|
|
2830
3577
|
class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
|
|
2831
3578
|
type="${this.escapeAttr(String(inputType))}"
|
|
3579
|
+
${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
|
|
2832
3580
|
placeholder="${this.escapeAttr(String(placeholder))}"
|
|
2833
3581
|
value="${this.escapeAttr(String(defaultValue))}"
|
|
2834
3582
|
${describedBy}
|
|
@@ -2838,7 +3586,7 @@ class CardInput extends BaseElement {
|
|
|
2838
3586
|
${numberAttributes}
|
|
2839
3587
|
style="${nativeInlineStyle}"
|
|
2840
3588
|
/>`;
|
|
2841
|
-
|
|
3589
|
+
const markup = `
|
|
2842
3590
|
<style>
|
|
2843
3591
|
:host {
|
|
2844
3592
|
display: block;
|
|
@@ -3012,7 +3760,11 @@ class CardInput extends BaseElement {
|
|
|
3012
3760
|
${numberStepperHtml}
|
|
3013
3761
|
</div>
|
|
3014
3762
|
</div>
|
|
3015
|
-
|
|
3763
|
+
`;
|
|
3764
|
+
if (!activeControl
|
|
3765
|
+
|| !this.patchShadowHTMLPreservingElement(markup, activeControl, '.card-input')) {
|
|
3766
|
+
this.setShadowHTML(markup);
|
|
3767
|
+
}
|
|
3016
3768
|
// Wire up native input/change events that bubble out of Shadow DOM.
|
|
3017
3769
|
// renderCard listens for these standard event names (mapped from onInput / onChange).
|
|
3018
3770
|
// The `detail.value` carries the current input value so that:
|
|
@@ -3020,41 +3772,41 @@ class CardInput extends BaseElement {
|
|
|
3020
3772
|
// 2. action handlers can access it if needed
|
|
3021
3773
|
const inputEl = this.shadowRoot.querySelector('.card-input');
|
|
3022
3774
|
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
|
-
}
|
|
3775
|
+
if (!this.wiredInputs.has(inputEl)) {
|
|
3776
|
+
this.wiredInputs.add(inputEl);
|
|
3777
|
+
inputEl.addEventListener('input', () => {
|
|
3778
|
+
this.dispatchEvent(new CustomEvent('input', {
|
|
3779
|
+
bubbles: true,
|
|
3780
|
+
composed: true,
|
|
3781
|
+
detail: { value: inputEl.value },
|
|
3782
|
+
}));
|
|
3783
|
+
});
|
|
3784
|
+
inputEl.addEventListener('change', () => {
|
|
3785
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
3786
|
+
bubbles: true,
|
|
3787
|
+
composed: true,
|
|
3788
|
+
detail: { value: inputEl.value },
|
|
3789
|
+
}));
|
|
3043
3790
|
});
|
|
3044
3791
|
}
|
|
3045
3792
|
if (isNumber &&
|
|
3046
3793
|
showNumberStepper &&
|
|
3047
3794
|
inputEl instanceof HTMLInputElement) {
|
|
3048
|
-
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
3049
|
-
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
3050
3795
|
const refreshStepperState = () => {
|
|
3051
3796
|
const state = getNumberStepperDisabledState(inputEl);
|
|
3797
|
+
const increaseButton = this.shadowRoot?.querySelector('.number-step-button.increase');
|
|
3798
|
+
const decreaseButton = this.shadowRoot?.querySelector('.number-step-button.decrease');
|
|
3052
3799
|
if (increaseButton)
|
|
3053
3800
|
increaseButton.disabled = state.increase;
|
|
3054
3801
|
if (decreaseButton)
|
|
3055
3802
|
decreaseButton.disabled = state.decrease;
|
|
3056
3803
|
};
|
|
3057
|
-
|
|
3804
|
+
if (!this.wiredNumberInputs.has(inputEl)) {
|
|
3805
|
+
this.wiredNumberInputs.add(inputEl);
|
|
3806
|
+
inputEl.addEventListener('input', refreshStepperState);
|
|
3807
|
+
}
|
|
3808
|
+
const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
|
|
3809
|
+
const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
|
|
3058
3810
|
increaseButton?.addEventListener('click', () => {
|
|
3059
3811
|
stepNumberInput(inputEl, 'increase');
|
|
3060
3812
|
inputEl.focus();
|
|
@@ -3070,21 +3822,25 @@ class CardInput extends BaseElement {
|
|
|
3070
3822
|
}
|
|
3071
3823
|
}
|
|
3072
3824
|
// ─── Helpers ──────────────────────────────────────────────────
|
|
3073
|
-
|
|
3825
|
+
/**
|
|
3826
|
+
* Consume an interaction-driven focus request from the card renderer.
|
|
3827
|
+
* Mounts and ordinary updates never call this method.
|
|
3828
|
+
*/
|
|
3829
|
+
requestAutoFocus() {
|
|
3074
3830
|
if (!this.isConnected
|
|
3075
3831
|
|| !this._props.autoFocus
|
|
3076
3832
|
|| this.hasAttribute('data-disabled')) {
|
|
3077
|
-
return
|
|
3833
|
+
return false;
|
|
3078
3834
|
}
|
|
3079
3835
|
const control = this.shadowRoot?.querySelector('.card-input');
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
if (!('querySelectorAll' in root))
|
|
3836
|
+
if (!control
|
|
3837
|
+
|| control.disabled
|
|
3838
|
+
|| control.readOnly
|
|
3839
|
+
|| control.value !== '') {
|
|
3085
3840
|
return false;
|
|
3086
|
-
|
|
3087
|
-
|
|
3841
|
+
}
|
|
3842
|
+
control.focus({ preventScroll: true });
|
|
3843
|
+
return this.shadowRoot?.activeElement === control;
|
|
3088
3844
|
}
|
|
3089
3845
|
/** Escape HTML entities for safe insertion. */
|
|
3090
3846
|
escapeHtml(str) {
|
|
@@ -3141,25 +3897,55 @@ class CardImage extends BaseElement {
|
|
|
3141
3897
|
constructor() {
|
|
3142
3898
|
super(...arguments);
|
|
3143
3899
|
this._lightbox = null;
|
|
3900
|
+
this._wiredImages = new WeakSet();
|
|
3901
|
+
this._wiredLightboxes = new WeakSet();
|
|
3902
|
+
this._wiredCloseButtons = new WeakSet();
|
|
3903
|
+
this._handleEscape = (event) => {
|
|
3904
|
+
if (event.key === 'Escape' && this._lightbox) {
|
|
3905
|
+
this.closeLightbox(this._lightbox);
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3144
3908
|
}
|
|
3145
3909
|
render() {
|
|
3146
3910
|
if (!this.shadowRoot || !this._node)
|
|
3147
3911
|
return;
|
|
3148
3912
|
const { src, alt = '', width, height, objectFit = 'cover', preview = true, style, isExpressionResultStyle, } = this._props;
|
|
3149
|
-
const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
|
|
3150
3913
|
const imgSrc = this.resolveContent(src);
|
|
3151
3914
|
const imgAlt = this.resolveContent(alt);
|
|
3915
|
+
// Keep the custom-element box independent from the image's intrinsic
|
|
3916
|
+
// dimensions. A failed image has different intrinsic sizing rules, so
|
|
3917
|
+
// sizing only the inner <img> makes flex/grid layouts jump on error.
|
|
3918
|
+
const hostWidth = this.applyHostDimension('width', style?.width, width);
|
|
3919
|
+
const hostHeight = this.applyHostDimension('height', style?.height, height);
|
|
3920
|
+
const ownsWidth = hostWidth != null;
|
|
3921
|
+
const ownsHeight = hostHeight != null;
|
|
3922
|
+
const reservesWidth = ownsWidth && hostWidth !== 'auto';
|
|
3923
|
+
const reservesHeight = ownsHeight && hostHeight !== 'auto';
|
|
3924
|
+
// Width and height belong to the host layout box. Remove them from the
|
|
3925
|
+
// inner style so relative lengths (for example 50%) are not applied twice.
|
|
3926
|
+
const imageStyle = style && typeof style === 'object' ? { ...style } : style;
|
|
3927
|
+
if (imageStyle && typeof imageStyle === 'object') {
|
|
3928
|
+
if (ownsWidth)
|
|
3929
|
+
delete imageStyle.width;
|
|
3930
|
+
if (ownsHeight)
|
|
3931
|
+
delete imageStyle.height;
|
|
3932
|
+
}
|
|
3933
|
+
const inlineStyle = this.buildInlineStyle(imageStyle, isExpressionResultStyle);
|
|
3152
3934
|
// Build image inline styles
|
|
3153
3935
|
const imgStyles = [];
|
|
3154
|
-
if (
|
|
3155
|
-
imgStyles.push(`width:${
|
|
3156
|
-
if (
|
|
3157
|
-
imgStyles.push(`height:${
|
|
3936
|
+
if (ownsWidth)
|
|
3937
|
+
imgStyles.push(`width:${reservesWidth ? '100%' : 'auto'}`);
|
|
3938
|
+
if (ownsHeight)
|
|
3939
|
+
imgStyles.push(`height:${reservesHeight ? '100%' : 'auto'}`);
|
|
3940
|
+
if (reservesWidth)
|
|
3941
|
+
imgStyles.push('min-width:0');
|
|
3942
|
+
if (reservesHeight)
|
|
3943
|
+
imgStyles.push('min-height:0');
|
|
3158
3944
|
if (objectFit)
|
|
3159
3945
|
imgStyles.push(`object-fit:${objectFit}`);
|
|
3160
3946
|
if (inlineStyle)
|
|
3161
3947
|
imgStyles.push(inlineStyle);
|
|
3162
|
-
|
|
3948
|
+
const markup = `
|
|
3163
3949
|
<style>
|
|
3164
3950
|
:host {
|
|
3165
3951
|
display: inline-block;
|
|
@@ -3170,9 +3956,12 @@ class CardImage extends BaseElement {
|
|
|
3170
3956
|
position: relative;
|
|
3171
3957
|
display: inline-block;
|
|
3172
3958
|
max-width: 100%;
|
|
3959
|
+
${reservesWidth ? 'width: 100%;' : ''}
|
|
3960
|
+
${reservesHeight ? 'height: 100%;' : ''}
|
|
3173
3961
|
}
|
|
3174
3962
|
.card-image {
|
|
3175
3963
|
display: block;
|
|
3964
|
+
box-sizing: border-box;
|
|
3176
3965
|
max-width: 100%;
|
|
3177
3966
|
height: auto;
|
|
3178
3967
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
@@ -3193,8 +3982,8 @@ class CardImage extends BaseElement {
|
|
|
3193
3982
|
100% { background-position: -200% 0; }
|
|
3194
3983
|
}
|
|
3195
3984
|
.card-image.error {
|
|
3196
|
-
min-width: 100px;
|
|
3197
|
-
min-height: 60px;
|
|
3985
|
+
min-width: ${reservesWidth ? '0' : '100px'};
|
|
3986
|
+
min-height: ${reservesHeight ? '0' : '60px'};
|
|
3198
3987
|
background: #f5f5f5;
|
|
3199
3988
|
display: flex;
|
|
3200
3989
|
align-items: center;
|
|
@@ -3264,66 +4053,113 @@ class CardImage extends BaseElement {
|
|
|
3264
4053
|
loading="lazy"
|
|
3265
4054
|
/>
|
|
3266
4055
|
</div>
|
|
3267
|
-
|
|
3268
|
-
<
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
4056
|
+
${preview ? `
|
|
4057
|
+
<div class="lightbox">
|
|
4058
|
+
<button class="lightbox-close" aria-label="Close">×</button>
|
|
4059
|
+
<img class="lightbox-image" src="${imgSrc}" alt="${imgAlt}" />
|
|
4060
|
+
</div>
|
|
4061
|
+
` : ''}
|
|
4062
|
+
`;
|
|
4063
|
+
const currentImage = this.shadowRoot.querySelector('.card-image');
|
|
4064
|
+
const keepError = currentImage?.classList.contains('error') === true
|
|
4065
|
+
&& currentImage.getAttribute('src') === imgSrc;
|
|
4066
|
+
if (this._lightbox?.classList.contains('open')) {
|
|
4067
|
+
this.closeLightbox(this._lightbox);
|
|
4068
|
+
}
|
|
4069
|
+
if (!currentImage
|
|
4070
|
+
|| !this.patchShadowHTMLPreservingElement(markup, currentImage, '.card-image')) {
|
|
4071
|
+
this.setShadowHTML(markup);
|
|
4072
|
+
}
|
|
4073
|
+
if (keepError)
|
|
4074
|
+
currentImage.classList.add('error');
|
|
3272
4075
|
this.bindEvents(preview);
|
|
3273
4076
|
}
|
|
4077
|
+
applyHostDimension(property, styleValue, propValue) {
|
|
4078
|
+
this.style.removeProperty(property);
|
|
4079
|
+
for (const value of [styleValue, propValue]) {
|
|
4080
|
+
const resolved = this.resolveDeclaredDimension(value);
|
|
4081
|
+
if (resolved == null)
|
|
4082
|
+
continue;
|
|
4083
|
+
this.style.setProperty(property, resolved);
|
|
4084
|
+
const applied = this.style.getPropertyValue(property);
|
|
4085
|
+
if (applied)
|
|
4086
|
+
return applied;
|
|
4087
|
+
}
|
|
4088
|
+
return undefined;
|
|
4089
|
+
}
|
|
4090
|
+
resolveDeclaredDimension(value) {
|
|
4091
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
4092
|
+
return this.toCSS(value);
|
|
4093
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
4094
|
+
return this.toCSS(value.trim());
|
|
4095
|
+
return undefined;
|
|
4096
|
+
}
|
|
3274
4097
|
bindEvents(preview) {
|
|
3275
4098
|
if (!this.shadowRoot)
|
|
3276
4099
|
return;
|
|
3277
4100
|
const img = this.shadowRoot.querySelector('.card-image');
|
|
3278
4101
|
const lightbox = this.shadowRoot.querySelector('.lightbox');
|
|
3279
4102
|
const closeBtn = this.shadowRoot.querySelector('.lightbox-close');
|
|
3280
|
-
if (!img
|
|
4103
|
+
if (!img)
|
|
3281
4104
|
return;
|
|
3282
4105
|
// Handle image load/error states
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
4106
|
+
if (!this._wiredImages.has(img)) {
|
|
4107
|
+
this._wiredImages.add(img);
|
|
4108
|
+
img.addEventListener('load', () => {
|
|
4109
|
+
img.classList.remove('loading');
|
|
4110
|
+
img.classList.remove('error');
|
|
4111
|
+
});
|
|
4112
|
+
img.addEventListener('error', () => {
|
|
4113
|
+
img.classList.remove('loading');
|
|
4114
|
+
img.classList.add('error');
|
|
4115
|
+
});
|
|
4116
|
+
img.addEventListener('click', (event) => {
|
|
4117
|
+
if (this._props.preview === false)
|
|
4118
|
+
return;
|
|
4119
|
+
const activeLightbox = this.shadowRoot?.querySelector('.lightbox');
|
|
4120
|
+
if (!activeLightbox)
|
|
4121
|
+
return;
|
|
4122
|
+
event.stopPropagation();
|
|
4123
|
+
this.openLightbox(activeLightbox);
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
if (!preview || !lightbox || !closeBtn) {
|
|
4127
|
+
this._lightbox = null;
|
|
3291
4128
|
return;
|
|
3292
|
-
|
|
3293
|
-
img.addEventListener('click', (e) => {
|
|
3294
|
-
e.stopPropagation();
|
|
3295
|
-
this.openLightbox(lightbox);
|
|
3296
|
-
});
|
|
4129
|
+
}
|
|
3297
4130
|
// Close lightbox on overlay/close button click
|
|
3298
|
-
|
|
3299
|
-
this.
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
4131
|
+
if (!this._wiredLightboxes.has(lightbox)) {
|
|
4132
|
+
this._wiredLightboxes.add(lightbox);
|
|
4133
|
+
lightbox.addEventListener('click', () => {
|
|
4134
|
+
this.closeLightbox(lightbox);
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
4137
|
+
if (!this._wiredCloseButtons.has(closeBtn)) {
|
|
4138
|
+
this._wiredCloseButtons.add(closeBtn);
|
|
4139
|
+
closeBtn.addEventListener('click', (event) => {
|
|
4140
|
+
event.stopPropagation();
|
|
4141
|
+
this.closeLightbox(lightbox);
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
3305
4144
|
// Close on Escape key
|
|
3306
4145
|
this._lightbox = lightbox;
|
|
3307
4146
|
}
|
|
3308
4147
|
openLightbox(lightbox) {
|
|
4148
|
+
this._lightbox = lightbox;
|
|
3309
4149
|
lightbox.classList.add('open');
|
|
3310
4150
|
document.body.style.overflow = 'hidden';
|
|
3311
|
-
|
|
3312
|
-
const handleEscape = (e) => {
|
|
3313
|
-
if (e.key === 'Escape') {
|
|
3314
|
-
this.closeLightbox(lightbox);
|
|
3315
|
-
document.removeEventListener('keydown', handleEscape);
|
|
3316
|
-
}
|
|
3317
|
-
};
|
|
3318
|
-
document.addEventListener('keydown', handleEscape);
|
|
4151
|
+
document.addEventListener('keydown', this._handleEscape);
|
|
3319
4152
|
}
|
|
3320
4153
|
closeLightbox(lightbox) {
|
|
3321
4154
|
lightbox.classList.remove('open');
|
|
3322
4155
|
document.body.style.overflow = '';
|
|
4156
|
+
document.removeEventListener('keydown', this._handleEscape);
|
|
3323
4157
|
}
|
|
3324
4158
|
disconnectedCallback() {
|
|
3325
4159
|
// Ensure body overflow is restored when element is removed
|
|
3326
4160
|
document.body.style.overflow = '';
|
|
4161
|
+
document.removeEventListener('keydown', this._handleEscape);
|
|
4162
|
+
this._lightbox = null;
|
|
3327
4163
|
}
|
|
3328
4164
|
}
|
|
3329
4165
|
CardImage.is = 'ai-card-image';
|
|
@@ -3385,9 +4221,7 @@ class CardDivider extends BaseElement {
|
|
|
3385
4221
|
<div class="divider-line"></div>
|
|
3386
4222
|
${displayText ? `<span class="divider-text">${displayText}</span><div class="divider-line"></div>` : ''}
|
|
3387
4223
|
`);
|
|
3388
|
-
|
|
3389
|
-
this.style.cssText += ';' + inlineStyle;
|
|
3390
|
-
}
|
|
4224
|
+
this.style.cssText = inlineStyle;
|
|
3391
4225
|
}
|
|
3392
4226
|
}
|
|
3393
4227
|
CardDivider.is = 'ai-card-divider';
|
|
@@ -5316,9 +6150,7 @@ class CardLoading extends BaseElement {
|
|
|
5316
6150
|
<div class="spinner"></div>
|
|
5317
6151
|
${displayText ? `<span class="loading-text">${displayText}</span>` : ''}
|
|
5318
6152
|
`);
|
|
5319
|
-
|
|
5320
|
-
this.style.cssText += ';' + inlineStyle;
|
|
5321
|
-
}
|
|
6153
|
+
this.style.cssText = inlineStyle;
|
|
5322
6154
|
}
|
|
5323
6155
|
}
|
|
5324
6156
|
CardLoading.is = 'ai-card-loading';
|
|
@@ -7787,168 +8619,1033 @@ function resolveSizeInStyle(style) {
|
|
|
7787
8619
|
return resolved;
|
|
7788
8620
|
}
|
|
7789
8621
|
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
function expressionContextFor(node) {
|
|
7814
|
-
return isBoundRenderTreeNode(node)
|
|
7815
|
-
? createExpressionContext(node.scope)
|
|
7816
|
-
: variables;
|
|
7817
|
-
}
|
|
7818
|
-
function resolveNodeValue(value, node, renderVariables) {
|
|
7819
|
-
if (node.bindingDialect === 'a2ui') {
|
|
7820
|
-
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
7821
|
-
}
|
|
7822
|
-
if (typeof value === 'string' && hasExpression(value)) {
|
|
7823
|
-
return resolveExpression(value, expressionContextFor(node));
|
|
7824
|
-
}
|
|
7825
|
-
return value;
|
|
7826
|
-
}
|
|
7827
|
-
function resolveNodeProps(node, renderVariables) {
|
|
7828
|
-
const resolvedProps = (node.bindingDialect === 'a2ui'
|
|
7829
|
-
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
7830
|
-
: resolveDeep(node.props, expressionContextFor(node)));
|
|
7831
|
-
if (resolvedProps.content
|
|
7832
|
-
&& typeof resolvedProps.content === 'object'
|
|
7833
|
-
&& 'type' in resolvedProps.content) {
|
|
7834
|
-
resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContextFor(node));
|
|
7835
|
-
}
|
|
7836
|
-
return resolvedProps;
|
|
8622
|
+
const states = new WeakMap();
|
|
8623
|
+
function dispatchCurrent(element, state, domEvent, event) {
|
|
8624
|
+
const config = state.current;
|
|
8625
|
+
if (!config.isActive())
|
|
8626
|
+
return;
|
|
8627
|
+
if (domEvent === 'input'
|
|
8628
|
+
&& config.variableKey
|
|
8629
|
+
&& config.writeVariable
|
|
8630
|
+
&& event.target === element) {
|
|
8631
|
+
const value = event instanceof CustomEvent
|
|
8632
|
+
&& event.detail?.value !== undefined
|
|
8633
|
+
? event.detail.value
|
|
8634
|
+
: event.target?.value;
|
|
8635
|
+
if (value !== undefined) {
|
|
8636
|
+
config.writeVariable(config.variableKey, value);
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
for (const binding of config.events) {
|
|
8640
|
+
if (binding.domEvent !== domEvent)
|
|
8641
|
+
continue;
|
|
8642
|
+
if (binding.ownsValueEvent && event.target !== element)
|
|
8643
|
+
continue;
|
|
8644
|
+
binding.dispatch(event);
|
|
7837
8645
|
}
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
botId: options.botId,
|
|
7846
|
-
inflightRequests,
|
|
8646
|
+
}
|
|
8647
|
+
function syncElementBindings(element, config) {
|
|
8648
|
+
let state = states.get(element);
|
|
8649
|
+
if (!state) {
|
|
8650
|
+
state = {
|
|
8651
|
+
current: config,
|
|
8652
|
+
listeners: new Map(),
|
|
7847
8653
|
};
|
|
8654
|
+
states.set(element, state);
|
|
8655
|
+
}
|
|
8656
|
+
state.current = config;
|
|
8657
|
+
const desiredEvents = new Set(config.events.map(event => event.domEvent));
|
|
8658
|
+
if (config.variableKey)
|
|
8659
|
+
desiredEvents.add('input');
|
|
8660
|
+
for (const [name, listener] of state.listeners) {
|
|
8661
|
+
if (desiredEvents.has(name))
|
|
8662
|
+
continue;
|
|
8663
|
+
element.removeEventListener(name, listener);
|
|
8664
|
+
state.listeners.delete(name);
|
|
7848
8665
|
}
|
|
7849
|
-
|
|
7850
|
-
if (
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
const placeholder = document.createElement('div');
|
|
7857
|
-
placeholder.style.display = 'none';
|
|
7858
|
-
placeholder.setAttribute('data-card-id', node.id);
|
|
7859
|
-
return placeholder;
|
|
8666
|
+
for (const name of desiredEvents) {
|
|
8667
|
+
if (state.listeners.has(name))
|
|
8668
|
+
continue;
|
|
8669
|
+
const listener = event => {
|
|
8670
|
+
const latest = states.get(element);
|
|
8671
|
+
if (latest) {
|
|
8672
|
+
dispatchCurrent(element, latest, name, event);
|
|
7860
8673
|
}
|
|
8674
|
+
};
|
|
8675
|
+
element.addEventListener(name, listener);
|
|
8676
|
+
state.listeners.set(name, listener);
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
function clearElementBindings(element) {
|
|
8680
|
+
const state = states.get(element);
|
|
8681
|
+
if (!state)
|
|
8682
|
+
return;
|
|
8683
|
+
let hasError = false;
|
|
8684
|
+
let firstError;
|
|
8685
|
+
for (const [name, listener] of state.listeners) {
|
|
8686
|
+
try {
|
|
8687
|
+
element.removeEventListener(name, listener);
|
|
7861
8688
|
}
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
isDisabled = (resolved === true
|
|
7867
|
-
|| resolved === 'true'
|
|
7868
|
-
|| resolved === 1);
|
|
8689
|
+
catch (error) {
|
|
8690
|
+
if (!hasError)
|
|
8691
|
+
firstError = error;
|
|
8692
|
+
hasError = true;
|
|
7869
8693
|
}
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
8694
|
+
}
|
|
8695
|
+
state.listeners.clear();
|
|
8696
|
+
states.delete(element);
|
|
8697
|
+
if (hasError)
|
|
8698
|
+
throw firstError;
|
|
8699
|
+
}
|
|
8700
|
+
function clearElementBindingsIn(root) {
|
|
8701
|
+
let hasError = false;
|
|
8702
|
+
let firstError;
|
|
8703
|
+
const clear = (element) => {
|
|
8704
|
+
try {
|
|
8705
|
+
clearElementBindings(element);
|
|
7879
8706
|
}
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
return;
|
|
7885
|
-
const value = event.detail?.value
|
|
7886
|
-
?? event.target?.value;
|
|
7887
|
-
if (value !== undefined) {
|
|
7888
|
-
// Keep the existing focus-preserving, no-rerender behavior. Bumping
|
|
7889
|
-
// the revision prevents an older async action from overwriting it.
|
|
7890
|
-
variables[variableKey] = value;
|
|
7891
|
-
revision += 1;
|
|
7892
|
-
}
|
|
7893
|
-
}));
|
|
8707
|
+
catch (error) {
|
|
8708
|
+
if (!hasError)
|
|
8709
|
+
firstError = error;
|
|
8710
|
+
hasError = true;
|
|
7894
8711
|
}
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
8712
|
+
};
|
|
8713
|
+
clear(root);
|
|
8714
|
+
root.querySelectorAll('*').forEach(clear);
|
|
8715
|
+
if (hasError)
|
|
8716
|
+
throw firstError;
|
|
8717
|
+
}
|
|
8718
|
+
|
|
8719
|
+
let unserializableFingerprint = 0;
|
|
8720
|
+
function nextUnserializableFingerprint() {
|
|
8721
|
+
unserializableFingerprint += 1;
|
|
8722
|
+
return `__unserializable__${unserializableFingerprint}`;
|
|
8723
|
+
}
|
|
8724
|
+
function serializeJsonLike(value, ancestors) {
|
|
8725
|
+
if (value === null)
|
|
8726
|
+
return 'null';
|
|
8727
|
+
switch (typeof value) {
|
|
8728
|
+
case 'string':
|
|
8729
|
+
return JSON.stringify(value);
|
|
8730
|
+
case 'boolean':
|
|
8731
|
+
return value ? 'true' : 'false';
|
|
8732
|
+
case 'number':
|
|
8733
|
+
if (!Number.isFinite(value))
|
|
8734
|
+
throw new TypeError('Non-finite number');
|
|
8735
|
+
return JSON.stringify(value);
|
|
8736
|
+
case 'object':
|
|
8737
|
+
break;
|
|
8738
|
+
default:
|
|
8739
|
+
throw new TypeError('Unsupported JSON value');
|
|
8740
|
+
}
|
|
8741
|
+
if (ancestors.has(value))
|
|
8742
|
+
throw new TypeError('Circular JSON value');
|
|
8743
|
+
ancestors.add(value);
|
|
8744
|
+
try {
|
|
8745
|
+
if (Array.isArray(value)) {
|
|
8746
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
8747
|
+
throw new TypeError('Unsupported array prototype');
|
|
8748
|
+
}
|
|
8749
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
8750
|
+
const expectedKeys = new Set(['length']);
|
|
8751
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8752
|
+
expectedKeys.add(String(index));
|
|
8753
|
+
}
|
|
8754
|
+
if (ownKeys.some(key => typeof key !== 'string' || !expectedKeys.has(key))
|
|
8755
|
+
|| ownKeys.length !== expectedKeys.size) {
|
|
8756
|
+
throw new TypeError('Unsupported array shape');
|
|
8757
|
+
}
|
|
8758
|
+
const values = [];
|
|
8759
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8760
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
8761
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
8762
|
+
throw new TypeError('Unsupported array item');
|
|
7899
8763
|
}
|
|
7900
|
-
|
|
7901
|
-
el.addEventListener(domEvent, ((event) => {
|
|
7902
|
-
const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
|
|
7903
|
-
&& VALUE_CONTROL_TYPES$1.has(node.type));
|
|
7904
|
-
if (ownsValueEvent && event.target !== el)
|
|
7905
|
-
return;
|
|
7906
|
-
const eventDetail = (event instanceof CustomEvent && event.detail != null)
|
|
7907
|
-
? event.detail
|
|
7908
|
-
: undefined;
|
|
7909
|
-
enqueueBoundEvent(node.id, eventName, eventDetail);
|
|
7910
|
-
}));
|
|
8764
|
+
values.push(serializeJsonLike(descriptor.value, ancestors));
|
|
7911
8765
|
}
|
|
8766
|
+
return `[${values.join(',')}]`;
|
|
7912
8767
|
}
|
|
7913
|
-
|
|
7914
|
-
|
|
8768
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8769
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
8770
|
+
throw new TypeError('Unsupported object prototype');
|
|
7915
8771
|
}
|
|
7916
|
-
const
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
childrenMap[child.id] = child;
|
|
8772
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
8773
|
+
if (ownKeys.some(key => typeof key !== 'string')) {
|
|
8774
|
+
throw new TypeError('Unsupported symbol key');
|
|
7920
8775
|
}
|
|
7921
|
-
const
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
8776
|
+
const entries = ownKeys.sort().map(key => {
|
|
8777
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
8778
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
8779
|
+
throw new TypeError('Unsupported object property');
|
|
7925
8780
|
}
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
const htmlChild = child;
|
|
7930
|
-
htmlChild.setAttribute('data-disabled', 'true');
|
|
7931
|
-
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
7932
|
-
});
|
|
7933
|
-
}
|
|
7934
|
-
applyResponsiveStyles(el, createResponsiveContext(isMobile, options.responsive));
|
|
7935
|
-
return el;
|
|
8781
|
+
return `${JSON.stringify(key)}:${serializeJsonLike(descriptor.value, ancestors)}`;
|
|
8782
|
+
});
|
|
8783
|
+
return `{${entries.join(',')}}`;
|
|
7936
8784
|
}
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
const lifecycleIds = new Set();
|
|
7940
|
-
const dom = renderNode(materialized.root, draft, lifecycleIds);
|
|
7941
|
-
return { materialized, dom, lifecycleIds };
|
|
8785
|
+
finally {
|
|
8786
|
+
ancestors.delete(value);
|
|
7942
8787
|
}
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
8788
|
+
}
|
|
8789
|
+
function stableFingerprint(value) {
|
|
8790
|
+
try {
|
|
8791
|
+
return serializeJsonLike(value, new Set());
|
|
8792
|
+
}
|
|
8793
|
+
catch {
|
|
8794
|
+
return nextUnserializableFingerprint();
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
function disposeWithoutThrow(mounted, hooks) {
|
|
8798
|
+
try {
|
|
8799
|
+
hooks.dispose(mounted);
|
|
8800
|
+
}
|
|
8801
|
+
catch {
|
|
8802
|
+
// DOM ownership has already been decided; cleanup errors cannot change it.
|
|
8803
|
+
}
|
|
8804
|
+
}
|
|
8805
|
+
function reportUnreconciled(hooks, error) {
|
|
8806
|
+
try {
|
|
8807
|
+
hooks.onUnreconciled?.(error);
|
|
8808
|
+
}
|
|
8809
|
+
catch {
|
|
8810
|
+
// Reporting cannot change which DOM tree is live.
|
|
8811
|
+
}
|
|
8812
|
+
}
|
|
8813
|
+
function replaceIncrementalNode(current, next, hooks) {
|
|
8814
|
+
const parent = current.element.parentNode;
|
|
8815
|
+
if (!parent) {
|
|
8816
|
+
reportUnreconciled(hooks);
|
|
8817
|
+
return current;
|
|
8818
|
+
}
|
|
8819
|
+
let replacement;
|
|
8820
|
+
try {
|
|
8821
|
+
replacement = hooks.mountReplacement(next);
|
|
8822
|
+
}
|
|
8823
|
+
catch (error) {
|
|
8824
|
+
reportUnreconciled(hooks, error);
|
|
8825
|
+
return current;
|
|
8826
|
+
}
|
|
8827
|
+
if (replacement.element === current.element
|
|
8828
|
+
|| replacement.element.parentNode !== null) {
|
|
8829
|
+
reportUnreconciled(hooks);
|
|
8830
|
+
return current;
|
|
8831
|
+
}
|
|
8832
|
+
let adopted = false;
|
|
8833
|
+
let adoptionError;
|
|
8834
|
+
try {
|
|
8835
|
+
parent.replaceChild(replacement.element, current.element);
|
|
8836
|
+
adopted = replacement.element.parentNode === parent
|
|
8837
|
+
&& current.element.parentNode !== parent;
|
|
8838
|
+
}
|
|
8839
|
+
catch (error) {
|
|
8840
|
+
adoptionError = error;
|
|
8841
|
+
adopted = replacement.element.parentNode === parent
|
|
8842
|
+
&& current.element.parentNode !== parent;
|
|
8843
|
+
}
|
|
8844
|
+
if (!adopted) {
|
|
8845
|
+
if (replacement.element.parentNode === null) {
|
|
8846
|
+
disposeWithoutThrow(replacement, hooks);
|
|
8847
|
+
}
|
|
8848
|
+
reportUnreconciled(hooks, adoptionError);
|
|
8849
|
+
return current;
|
|
8850
|
+
}
|
|
8851
|
+
disposeWithoutThrow(current, hooks);
|
|
8852
|
+
return replacement;
|
|
8853
|
+
}
|
|
8854
|
+
function reconcileIncrementalNode(current, next, hooks) {
|
|
8855
|
+
const parent = current.element.parentNode;
|
|
8856
|
+
const sameChildren = current.children.length === next.children.length
|
|
8857
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
8858
|
+
let decision = 'replace';
|
|
8859
|
+
if (sameChildren) {
|
|
8860
|
+
try {
|
|
8861
|
+
decision = hooks.decide(current, next);
|
|
8862
|
+
}
|
|
8863
|
+
catch {
|
|
8864
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8865
|
+
}
|
|
8866
|
+
if (current.element.parentNode !== parent) {
|
|
8867
|
+
reportUnreconciled(hooks);
|
|
8868
|
+
return current;
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
if (decision === 'replace') {
|
|
8872
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8873
|
+
}
|
|
8874
|
+
if (decision === 'update') {
|
|
8875
|
+
try {
|
|
8876
|
+
hooks.update(current, next);
|
|
8877
|
+
}
|
|
8878
|
+
catch {
|
|
8879
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8880
|
+
}
|
|
8881
|
+
if (current.element.parentNode !== parent) {
|
|
8882
|
+
reportUnreconciled(hooks);
|
|
8883
|
+
return current;
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
try {
|
|
8887
|
+
hooks.syncBindings(current, next);
|
|
8888
|
+
}
|
|
8889
|
+
catch {
|
|
8890
|
+
return replaceIncrementalNode(current, next, hooks);
|
|
8891
|
+
}
|
|
8892
|
+
if (current.element.parentNode !== parent) {
|
|
8893
|
+
reportUnreconciled(hooks);
|
|
8894
|
+
return current;
|
|
8895
|
+
}
|
|
8896
|
+
const children = current.children.map((child, index) => (reconcileIncrementalNode(child, next.children[index], hooks)));
|
|
8897
|
+
return { ...next, element: current.element, children };
|
|
8898
|
+
}
|
|
8899
|
+
|
|
8900
|
+
const CARD_NODE_SELECTOR = '[data-card-id][data-card-type]';
|
|
8901
|
+
const FOCUS_TARGET_SELECTOR = [
|
|
8902
|
+
'input:not([disabled])',
|
|
8903
|
+
'textarea:not([disabled])',
|
|
8904
|
+
'select:not([disabled])',
|
|
8905
|
+
'button:not([disabled])',
|
|
8906
|
+
'[contenteditable="true"]',
|
|
8907
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
8908
|
+
].join(',');
|
|
8909
|
+
/**
|
|
8910
|
+
* Add logical node access to one rendered card without depending on whether
|
|
8911
|
+
* its renderer retains or replaces DOM elements between updates.
|
|
8912
|
+
*/
|
|
8913
|
+
function createCardNodeAccess(container) {
|
|
8914
|
+
const handles = new Map();
|
|
8915
|
+
const listeners = new Set();
|
|
8916
|
+
let focusedId = null;
|
|
8917
|
+
let focusSyncScheduled = false;
|
|
8918
|
+
let observing = false;
|
|
8919
|
+
let observationEpoch = 0;
|
|
8920
|
+
let observer = null;
|
|
8921
|
+
let disposed = false;
|
|
8922
|
+
const resolveElement = (id) => {
|
|
8923
|
+
if (disposed)
|
|
8924
|
+
return null;
|
|
8925
|
+
return Array.from(container.querySelectorAll(CARD_NODE_SELECTOR)).find(element => element.getAttribute('data-card-id') === id) ?? null;
|
|
8926
|
+
};
|
|
8927
|
+
const getNode = (id) => {
|
|
8928
|
+
const existing = handles.get(id);
|
|
8929
|
+
if (existing)
|
|
8930
|
+
return existing;
|
|
8931
|
+
const handle = {
|
|
8932
|
+
id,
|
|
8933
|
+
get type() {
|
|
8934
|
+
return resolveElement(id)?.getAttribute('data-card-type') ?? null;
|
|
8935
|
+
},
|
|
8936
|
+
get current() {
|
|
8937
|
+
return resolveElement(id);
|
|
8938
|
+
},
|
|
8939
|
+
getRect() {
|
|
8940
|
+
return resolveElement(id)?.getBoundingClientRect() ?? null;
|
|
8941
|
+
},
|
|
8942
|
+
focus(options) {
|
|
8943
|
+
const element = resolveElement(id);
|
|
8944
|
+
if (!element || element.hasAttribute('data-disabled'))
|
|
8945
|
+
return false;
|
|
8946
|
+
const target = element.shadowRoot
|
|
8947
|
+
?.querySelector(FOCUS_TARGET_SELECTOR) ?? element;
|
|
8948
|
+
target.focus(options);
|
|
8949
|
+
return document.activeElement === element
|
|
8950
|
+
|| element.shadowRoot?.activeElement != null;
|
|
8951
|
+
},
|
|
8952
|
+
blur() {
|
|
8953
|
+
const element = resolveElement(id);
|
|
8954
|
+
if (!element)
|
|
8955
|
+
return false;
|
|
8956
|
+
const active = element.shadowRoot?.activeElement;
|
|
8957
|
+
if (active instanceof HTMLElement) {
|
|
8958
|
+
active.blur();
|
|
8959
|
+
}
|
|
8960
|
+
else {
|
|
8961
|
+
element.blur();
|
|
8962
|
+
}
|
|
8963
|
+
return true;
|
|
8964
|
+
},
|
|
8965
|
+
scrollIntoView(options) {
|
|
8966
|
+
const element = resolveElement(id);
|
|
8967
|
+
if (!element || typeof element.scrollIntoView !== 'function') {
|
|
8968
|
+
return false;
|
|
8969
|
+
}
|
|
8970
|
+
element.scrollIntoView(options);
|
|
8971
|
+
return true;
|
|
8972
|
+
},
|
|
8973
|
+
};
|
|
8974
|
+
handles.set(id, handle);
|
|
8975
|
+
return handle;
|
|
8976
|
+
};
|
|
8977
|
+
const resolveFocusedId = () => {
|
|
8978
|
+
let active = document.activeElement;
|
|
8979
|
+
if (!active || !container.contains(active))
|
|
8980
|
+
return null;
|
|
8981
|
+
while (active) {
|
|
8982
|
+
if (active instanceof HTMLElement
|
|
8983
|
+
&& active.matches(CARD_NODE_SELECTOR)) {
|
|
8984
|
+
return active.getAttribute('data-card-id');
|
|
8985
|
+
}
|
|
8986
|
+
active = active.shadowRoot?.activeElement ?? null;
|
|
8987
|
+
}
|
|
8988
|
+
return null;
|
|
8989
|
+
};
|
|
8990
|
+
const syncFocus = () => {
|
|
8991
|
+
if (disposed || !observing)
|
|
8992
|
+
return;
|
|
8993
|
+
const nextId = resolveFocusedId();
|
|
8994
|
+
if (nextId === focusedId)
|
|
8995
|
+
return;
|
|
8996
|
+
const previousNode = focusedId ? getNode(focusedId) : null;
|
|
8997
|
+
const node = nextId ? getNode(nextId) : null;
|
|
8998
|
+
focusedId = nextId;
|
|
8999
|
+
for (const listener of [...listeners]) {
|
|
9000
|
+
try {
|
|
9001
|
+
listener({ node, previousNode });
|
|
9002
|
+
}
|
|
9003
|
+
catch (error) {
|
|
9004
|
+
console.error('[CardInstance] focus-change listener failed', error);
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
};
|
|
9008
|
+
const scheduleFocusSync = () => {
|
|
9009
|
+
if (disposed || !observing || focusSyncScheduled)
|
|
9010
|
+
return;
|
|
9011
|
+
focusSyncScheduled = true;
|
|
9012
|
+
const scheduledEpoch = observationEpoch;
|
|
9013
|
+
queueMicrotask(() => {
|
|
9014
|
+
if (scheduledEpoch !== observationEpoch)
|
|
9015
|
+
return;
|
|
9016
|
+
focusSyncScheduled = false;
|
|
9017
|
+
syncFocus();
|
|
9018
|
+
});
|
|
9019
|
+
};
|
|
9020
|
+
const startObserving = () => {
|
|
9021
|
+
if (disposed || observing)
|
|
9022
|
+
return;
|
|
9023
|
+
observing = true;
|
|
9024
|
+
observationEpoch += 1;
|
|
9025
|
+
focusedId = resolveFocusedId();
|
|
9026
|
+
container.addEventListener('focusin', scheduleFocusSync, true);
|
|
9027
|
+
container.addEventListener('focusout', scheduleFocusSync, true);
|
|
9028
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
9029
|
+
observer ?? (observer = new MutationObserver(scheduleFocusSync));
|
|
9030
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
9031
|
+
}
|
|
9032
|
+
};
|
|
9033
|
+
const stopObserving = () => {
|
|
9034
|
+
if (!observing)
|
|
9035
|
+
return;
|
|
9036
|
+
observing = false;
|
|
9037
|
+
observationEpoch += 1;
|
|
9038
|
+
focusSyncScheduled = false;
|
|
9039
|
+
const cleanupSteps = [
|
|
9040
|
+
() => container.removeEventListener('focusin', scheduleFocusSync, true),
|
|
9041
|
+
() => container.removeEventListener('focusout', scheduleFocusSync, true),
|
|
9042
|
+
() => observer?.disconnect(),
|
|
9043
|
+
];
|
|
9044
|
+
for (const cleanup of cleanupSteps) {
|
|
9045
|
+
try {
|
|
9046
|
+
cleanup();
|
|
9047
|
+
}
|
|
9048
|
+
catch (error) {
|
|
9049
|
+
console.error('[CardInstance] Focus observation cleanup failed', error);
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
focusedId = null;
|
|
9053
|
+
};
|
|
9054
|
+
return {
|
|
9055
|
+
getNode,
|
|
9056
|
+
onFocusChange(listener) {
|
|
9057
|
+
if (disposed)
|
|
9058
|
+
return () => { };
|
|
9059
|
+
listeners.add(listener);
|
|
9060
|
+
if (listeners.size === 1)
|
|
9061
|
+
startObserving();
|
|
9062
|
+
let subscribed = true;
|
|
9063
|
+
return () => {
|
|
9064
|
+
if (!subscribed)
|
|
9065
|
+
return;
|
|
9066
|
+
subscribed = false;
|
|
9067
|
+
listeners.delete(listener);
|
|
9068
|
+
if (listeners.size === 0)
|
|
9069
|
+
stopObserving();
|
|
9070
|
+
};
|
|
9071
|
+
},
|
|
9072
|
+
dispose() {
|
|
9073
|
+
if (disposed)
|
|
9074
|
+
return;
|
|
9075
|
+
stopObserving();
|
|
9076
|
+
disposed = true;
|
|
9077
|
+
listeners.clear();
|
|
9078
|
+
handles.clear();
|
|
9079
|
+
},
|
|
9080
|
+
};
|
|
9081
|
+
}
|
|
9082
|
+
|
|
9083
|
+
const FORCE_REPLACE = Symbol('bound-force-replace');
|
|
9084
|
+
/**
|
|
9085
|
+
* Render the scoped/materialized branch of a card.
|
|
9086
|
+
*
|
|
9087
|
+
* This implementation deliberately owns its state independently from the
|
|
9088
|
+
* legacy static renderer. Bound writes happen in isolated drafts and publish
|
|
9089
|
+
* only after materialization and detached DOM construction both succeed.
|
|
9090
|
+
*/
|
|
9091
|
+
function renderBoundCard(container, schema, options) {
|
|
9092
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
9093
|
+
const variables = cloneJsonData({
|
|
9094
|
+
...schema.variables,
|
|
9095
|
+
...options.variables,
|
|
9096
|
+
});
|
|
9097
|
+
const schemaActions = schema.actions ?? {};
|
|
9098
|
+
const lifecycleManager = createLifecycleManager();
|
|
9099
|
+
const abortController = new AbortController();
|
|
9100
|
+
const inflightRequests = new Map();
|
|
9101
|
+
const activeLifecycleNodes = new Map();
|
|
9102
|
+
let currentMaterialized;
|
|
9103
|
+
let currentMountedTree = null;
|
|
9104
|
+
let lastPublishedVariables = cloneJsonData(variables);
|
|
9105
|
+
let revision = 0;
|
|
9106
|
+
let disposed = false;
|
|
9107
|
+
let disposeRequested = false;
|
|
9108
|
+
let publishing = false;
|
|
9109
|
+
const isMobile = options.isMobile === true;
|
|
9110
|
+
let actionQueue = Promise.resolve();
|
|
9111
|
+
let queuedActionCount = 0;
|
|
9112
|
+
let lifecycleQueue = Promise.resolve();
|
|
9113
|
+
const repeatIdentityCountCache = new WeakMap();
|
|
9114
|
+
function expressionContextFor(node) {
|
|
9115
|
+
return isBoundRenderTreeNode(node)
|
|
9116
|
+
? createExpressionContext(node.scope)
|
|
9117
|
+
: variables;
|
|
9118
|
+
}
|
|
9119
|
+
function resolveNodeValue(value, node, renderVariables) {
|
|
9120
|
+
if (node.bindingDialect === 'a2ui') {
|
|
9121
|
+
return resolveA2UIDeep(value, renderVariables, node.dataPath);
|
|
9122
|
+
}
|
|
9123
|
+
if (typeof value === 'string' && hasExpression(value)) {
|
|
9124
|
+
return resolveExpression(value, expressionContextFor(node));
|
|
9125
|
+
}
|
|
9126
|
+
return value;
|
|
9127
|
+
}
|
|
9128
|
+
function resolveNodeProps(node, renderVariables) {
|
|
9129
|
+
const resolvedProps = (node.bindingDialect === 'a2ui'
|
|
9130
|
+
? resolveA2UIDeep(node.props, renderVariables, node.dataPath)
|
|
9131
|
+
: resolveDeep(node.props, expressionContextFor(node)));
|
|
9132
|
+
if (resolvedProps.content
|
|
9133
|
+
&& typeof resolvedProps.content === 'object'
|
|
9134
|
+
&& 'type' in resolvedProps.content) {
|
|
9135
|
+
resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContextFor(node));
|
|
9136
|
+
}
|
|
9137
|
+
return resolvedProps;
|
|
9138
|
+
}
|
|
9139
|
+
function createPassiveActionContext(renderVariables) {
|
|
9140
|
+
return {
|
|
9141
|
+
...createWebActionContext({
|
|
9142
|
+
...options,
|
|
9143
|
+
abortSignal: abortController.signal,
|
|
9144
|
+
}),
|
|
9145
|
+
variables: renderVariables,
|
|
9146
|
+
botId: options.botId,
|
|
9147
|
+
inflightRequests,
|
|
9148
|
+
};
|
|
9149
|
+
}
|
|
9150
|
+
function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
|
|
9151
|
+
return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
|
|
9152
|
+
}
|
|
9153
|
+
function isNodeDisabled(node, renderVariables) {
|
|
9154
|
+
if (!node.directives?.disabled)
|
|
9155
|
+
return false;
|
|
9156
|
+
const resolved = resolveNodeValue(node.directives.disabled, node, renderVariables);
|
|
9157
|
+
return resolved === true || resolved === 'true' || resolved === 1;
|
|
9158
|
+
}
|
|
9159
|
+
function nodeBindingFingerprint(node, resolvedProps, disabled) {
|
|
9160
|
+
const binding = { disabled };
|
|
9161
|
+
if (resolvedProps?.variableKey !== undefined) {
|
|
9162
|
+
binding.variableKey = resolvedProps.variableKey;
|
|
9163
|
+
}
|
|
9164
|
+
if (!disabled && node.events !== undefined)
|
|
9165
|
+
binding.events = node.events;
|
|
9166
|
+
return stableFingerprint(binding);
|
|
9167
|
+
}
|
|
9168
|
+
function resolveBoundMetadata(node, occurrenceKey, renderVariables) {
|
|
9169
|
+
const visible = isVisible(node, renderVariables);
|
|
9170
|
+
const rendererToken = componentRenderers[node.type]
|
|
9171
|
+
?? componentRenderers._default;
|
|
9172
|
+
if (!visible) {
|
|
9173
|
+
return {
|
|
9174
|
+
occurrenceKey,
|
|
9175
|
+
node,
|
|
9176
|
+
nodeId: node.id,
|
|
9177
|
+
nodeType: node.type,
|
|
9178
|
+
rendererToken,
|
|
9179
|
+
visible: false,
|
|
9180
|
+
disabled: false,
|
|
9181
|
+
resolvedProps: null,
|
|
9182
|
+
propsFingerprint: stableFingerprint(null),
|
|
9183
|
+
layoutFingerprint: stableFingerprint(null),
|
|
9184
|
+
bindingFingerprint: stableFingerprint(null),
|
|
9185
|
+
};
|
|
9186
|
+
}
|
|
9187
|
+
const resolvedProps = resolveNodeProps(node, renderVariables);
|
|
9188
|
+
const disabled = isNodeDisabled(node, renderVariables);
|
|
9189
|
+
return {
|
|
9190
|
+
occurrenceKey,
|
|
9191
|
+
node,
|
|
9192
|
+
nodeId: node.id,
|
|
9193
|
+
nodeType: node.type,
|
|
9194
|
+
rendererToken,
|
|
9195
|
+
visible: true,
|
|
9196
|
+
disabled,
|
|
9197
|
+
resolvedProps,
|
|
9198
|
+
propsFingerprint: stableFingerprint(resolvedProps),
|
|
9199
|
+
layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, child => resolveNodeProps(child, renderVariables))),
|
|
9200
|
+
bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
function applyDisabledState(element, disabled) {
|
|
9204
|
+
if (!disabled)
|
|
9205
|
+
return;
|
|
9206
|
+
element.setAttribute('data-disabled', 'true');
|
|
9207
|
+
element.style.background = '#F5F5F5';
|
|
9208
|
+
element.style.color = '#C0C0C0';
|
|
9209
|
+
element.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
9210
|
+
element.style.pointerEvents = 'none';
|
|
9211
|
+
element.style.cursor = 'default';
|
|
9212
|
+
}
|
|
9213
|
+
function applyDisabledDescendants(element, disabled) {
|
|
9214
|
+
if (!disabled)
|
|
9215
|
+
return;
|
|
9216
|
+
element.querySelectorAll('*').forEach((child) => {
|
|
9217
|
+
const htmlChild = child;
|
|
9218
|
+
htmlChild.setAttribute('data-disabled', 'true');
|
|
9219
|
+
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
9220
|
+
});
|
|
9221
|
+
}
|
|
9222
|
+
function disposeBoundElement(element) {
|
|
9223
|
+
const cleanupSteps = [
|
|
9224
|
+
() => clearSlotOwnerBehaviorsIn(element),
|
|
9225
|
+
() => clearSlotItemPresentationsIn(element),
|
|
9226
|
+
() => clearElementBindingsIn(element),
|
|
9227
|
+
() => disposeChartsIn(element),
|
|
9228
|
+
];
|
|
9229
|
+
for (const cleanup of cleanupSteps) {
|
|
9230
|
+
try {
|
|
9231
|
+
cleanup();
|
|
9232
|
+
}
|
|
9233
|
+
catch (error) {
|
|
9234
|
+
console.error('[renderCard] Bound element cleanup failed', error);
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
}
|
|
9238
|
+
function disposeUncoveredBoundBranches(current, coverageRoot) {
|
|
9239
|
+
for (const child of current.children) {
|
|
9240
|
+
if (coverageRoot.contains(child.element)) {
|
|
9241
|
+
disposeUncoveredBoundBranches(child, coverageRoot);
|
|
9242
|
+
}
|
|
9243
|
+
else {
|
|
9244
|
+
disposeBoundSubtree(child);
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
}
|
|
9248
|
+
function disposeBoundSubtree(current) {
|
|
9249
|
+
disposeBoundElement(current.element);
|
|
9250
|
+
disposeUncoveredBoundBranches(current, current.element);
|
|
9251
|
+
}
|
|
9252
|
+
function disposeBoundBranchesOutsideContainer(current, disposedCoverage = null) {
|
|
9253
|
+
if (container.contains(current.element)) {
|
|
9254
|
+
for (const child of current.children) {
|
|
9255
|
+
disposeBoundBranchesOutsideContainer(child);
|
|
9256
|
+
}
|
|
9257
|
+
return;
|
|
9258
|
+
}
|
|
9259
|
+
let nextCoverage = disposedCoverage;
|
|
9260
|
+
if (!disposedCoverage?.contains(current.element)) {
|
|
9261
|
+
try {
|
|
9262
|
+
disposeBoundElement(current.element);
|
|
9263
|
+
nextCoverage = current.element;
|
|
9264
|
+
}
|
|
9265
|
+
catch {
|
|
9266
|
+
nextCoverage = null;
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
for (const child of current.children) {
|
|
9270
|
+
disposeBoundBranchesOutsideContainer(child, nextCoverage);
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
function disposeBoundSubtreeWithoutThrow(current) {
|
|
9274
|
+
try {
|
|
9275
|
+
disposeBoundSubtree(current);
|
|
9276
|
+
}
|
|
9277
|
+
catch {
|
|
9278
|
+
// Cleanup cannot change which candidate owns the live DOM.
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
9281
|
+
function mountBoundSubtree(node, occurrenceKey, renderVariables) {
|
|
9282
|
+
const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
|
|
9283
|
+
if (!metadata.visible) {
|
|
9284
|
+
const placeholder = document.createElement('div');
|
|
9285
|
+
placeholder.style.display = 'none';
|
|
9286
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
9287
|
+
return { ...metadata, element: placeholder, children: [] };
|
|
9288
|
+
}
|
|
9289
|
+
const renderer = metadata.rendererToken;
|
|
9290
|
+
const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
|
|
9291
|
+
const children = [];
|
|
9292
|
+
try {
|
|
9293
|
+
applyDisabledState(element, metadata.disabled);
|
|
9294
|
+
const sameIdOrdinals = new Map();
|
|
9295
|
+
const renderChild = (child) => {
|
|
9296
|
+
const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
|
|
9297
|
+
sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
|
|
9298
|
+
const mounted = mountBoundSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal), renderVariables);
|
|
9299
|
+
children.push(mounted);
|
|
9300
|
+
return mounted.element;
|
|
9301
|
+
};
|
|
9302
|
+
const childrenMap = {};
|
|
9303
|
+
for (const child of node.children)
|
|
9304
|
+
childrenMap[child.id] = child;
|
|
9305
|
+
const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, child => renderChild(child), childrenMap, createPassiveActionContext(renderVariables), child => resolveNodeProps(child, renderVariables));
|
|
9306
|
+
if (!layoutApplied) {
|
|
9307
|
+
for (const child of node.children) {
|
|
9308
|
+
element.appendChild(renderChild(child));
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
applyDisabledDescendants(element, metadata.disabled);
|
|
9312
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
9313
|
+
return { ...metadata, element, children };
|
|
9314
|
+
}
|
|
9315
|
+
catch (error) {
|
|
9316
|
+
disposeBoundSubtreeWithoutThrow({ ...metadata, element, children });
|
|
9317
|
+
throw error;
|
|
9318
|
+
}
|
|
9319
|
+
}
|
|
9320
|
+
function syncBoundBindings(element, node) {
|
|
9321
|
+
const events = [];
|
|
9322
|
+
if (node.visible && !node.disabled && node.node.events) {
|
|
9323
|
+
const repeatTargetFingerprint = getRepeatTargetFingerprint(node.node);
|
|
9324
|
+
for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
|
|
9325
|
+
if (!eventValue || !resolveActionRef(eventValue, schemaActions)) {
|
|
9326
|
+
continue;
|
|
9327
|
+
}
|
|
9328
|
+
const domEvent = EVENT_MAP[schemaEvent] ?? schemaEvent;
|
|
9329
|
+
events.push({
|
|
9330
|
+
schemaEvent,
|
|
9331
|
+
domEvent,
|
|
9332
|
+
ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
|
|
9333
|
+
&& VALUE_CONTROL_TYPES$1.has(node.nodeType)),
|
|
9334
|
+
dispatch: (event) => {
|
|
9335
|
+
const detail = event instanceof CustomEvent && event.detail != null
|
|
9336
|
+
? event.detail
|
|
9337
|
+
: undefined;
|
|
9338
|
+
enqueueBoundEvent(node.nodeId, schemaEvent, detail, repeatTargetFingerprint);
|
|
9339
|
+
},
|
|
9340
|
+
});
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9343
|
+
const variableKey = node.resolvedProps?.variableKey;
|
|
9344
|
+
syncElementBindings(element, {
|
|
9345
|
+
variableKey: typeof variableKey === 'string' ? variableKey : undefined,
|
|
9346
|
+
writeVariable: (key, value) => {
|
|
9347
|
+
if (disposed || publishing || !container.contains(element))
|
|
9348
|
+
return;
|
|
9349
|
+
variables[key] = value;
|
|
9350
|
+
revision += 1;
|
|
9351
|
+
},
|
|
9352
|
+
events,
|
|
9353
|
+
isActive: () => (!disposed
|
|
9354
|
+
&& !publishing
|
|
9355
|
+
&& container.contains(element)),
|
|
9356
|
+
});
|
|
9357
|
+
}
|
|
9358
|
+
function syncMountedBindings(current) {
|
|
9359
|
+
syncBoundBindings(current.element, current);
|
|
9360
|
+
for (const child of current.children)
|
|
9361
|
+
syncMountedBindings(child);
|
|
9362
|
+
}
|
|
9363
|
+
function collectMountedOccurrences(current, occurrences) {
|
|
9364
|
+
occurrences.set(current.occurrenceKey, current);
|
|
9365
|
+
for (const child of current.children) {
|
|
9366
|
+
collectMountedOccurrences(child, occurrences);
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, current) {
|
|
9370
|
+
const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
|
|
9371
|
+
const forceReplace = forcedOwnerIds.has(node.id);
|
|
9372
|
+
if (!metadata.visible || forceReplace) {
|
|
9373
|
+
return {
|
|
9374
|
+
...metadata,
|
|
9375
|
+
...(forceReplace ? { [FORCE_REPLACE]: true } : {}),
|
|
9376
|
+
children: [],
|
|
9377
|
+
};
|
|
9378
|
+
}
|
|
9379
|
+
if (!current)
|
|
9380
|
+
return { ...metadata, children: [] };
|
|
9381
|
+
const nextChildrenById = new Map();
|
|
9382
|
+
for (const child of node.children) {
|
|
9383
|
+
const matching = nextChildrenById.get(child.id) ?? [];
|
|
9384
|
+
matching.push(child);
|
|
9385
|
+
nextChildrenById.set(child.id, matching);
|
|
9386
|
+
}
|
|
9387
|
+
const nextChildOrdinals = new Map();
|
|
9388
|
+
const children = [];
|
|
9389
|
+
for (const currentChild of current.children) {
|
|
9390
|
+
const matching = nextChildrenById.get(currentChild.nodeId);
|
|
9391
|
+
if (!matching?.length) {
|
|
9392
|
+
return { ...metadata, [FORCE_REPLACE]: true, children: [] };
|
|
9393
|
+
}
|
|
9394
|
+
const ordinal = nextChildOrdinals.get(currentChild.nodeId) ?? 0;
|
|
9395
|
+
nextChildOrdinals.set(currentChild.nodeId, ordinal + 1);
|
|
9396
|
+
const child = matching[Math.min(ordinal, matching.length - 1)];
|
|
9397
|
+
children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, currentChild));
|
|
9398
|
+
}
|
|
9399
|
+
return {
|
|
9400
|
+
...metadata,
|
|
9401
|
+
children,
|
|
9402
|
+
};
|
|
9403
|
+
}
|
|
9404
|
+
function decideBoundUpdate(current, next) {
|
|
9405
|
+
if (next[FORCE_REPLACE]
|
|
9406
|
+
|| current.occurrenceKey !== next.occurrenceKey
|
|
9407
|
+
|| current.nodeId !== next.nodeId
|
|
9408
|
+
|| current.nodeType !== next.nodeType
|
|
9409
|
+
|| current.rendererToken !== next.rendererToken
|
|
9410
|
+
|| current.visible !== next.visible
|
|
9411
|
+
|| current.disabled !== next.disabled
|
|
9412
|
+
|| current.layoutFingerprint !== next.layoutFingerprint) {
|
|
9413
|
+
return 'replace';
|
|
9414
|
+
}
|
|
9415
|
+
if (current.propsFingerprint === next.propsFingerprint)
|
|
9416
|
+
return 'retain';
|
|
9417
|
+
return (current.element instanceof BaseElement
|
|
9418
|
+
|| current.rendererToken === componentRenderers._default) ? 'update' : 'replace';
|
|
9419
|
+
}
|
|
9420
|
+
function updateBoundNode(current, next) {
|
|
9421
|
+
const element = current.element;
|
|
9422
|
+
if (element instanceof BaseElement) {
|
|
9423
|
+
updateSlotItemShellPresentation(element, () => {
|
|
9424
|
+
element.updateProps(next.resolvedProps, isMobile, options.responsive);
|
|
9425
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
9426
|
+
}, getSlotHostPresentationOwnership(next.resolvedProps));
|
|
9427
|
+
return;
|
|
9428
|
+
}
|
|
9429
|
+
if (current.rendererToken !== componentRenderers._default) {
|
|
9430
|
+
throw new Error('[renderCard] Bound node cannot update in place');
|
|
9431
|
+
}
|
|
9432
|
+
const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
|
|
9433
|
+
const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
|
|
9434
|
+
applyDisabledState(previous, current.disabled);
|
|
9435
|
+
applyDisabledState(desired, next.disabled);
|
|
9436
|
+
const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
|
|
9437
|
+
updateSlotItemShellPresentation(element, () => {
|
|
9438
|
+
patchElementShellPresentation(element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(element));
|
|
9439
|
+
}, hostOwnership);
|
|
9440
|
+
}
|
|
9441
|
+
function collectChangedPointers(before, after, path = '') {
|
|
9442
|
+
if (Object.is(before, after))
|
|
9443
|
+
return [];
|
|
9444
|
+
if (before === null
|
|
9445
|
+
|| after === null
|
|
9446
|
+
|| typeof before !== 'object'
|
|
9447
|
+
|| typeof after !== 'object') {
|
|
9448
|
+
return [path];
|
|
9449
|
+
}
|
|
9450
|
+
if (Array.isArray(before) || Array.isArray(after)) {
|
|
9451
|
+
if (!Array.isArray(before) || !Array.isArray(after))
|
|
9452
|
+
return [path];
|
|
9453
|
+
if (before.length !== after.length)
|
|
9454
|
+
return [path];
|
|
9455
|
+
}
|
|
9456
|
+
const left = before;
|
|
9457
|
+
const right = after;
|
|
9458
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
9459
|
+
const changed = [];
|
|
9460
|
+
for (const key of keys) {
|
|
9461
|
+
const token = key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
9462
|
+
changed.push(...collectChangedPointers(left[key], right[key], `${path}/${token}`));
|
|
9463
|
+
}
|
|
9464
|
+
return changed;
|
|
9465
|
+
}
|
|
9466
|
+
function selectForcedRepeatOwnerIds(before, draft) {
|
|
9467
|
+
if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
|
|
9468
|
+
return new Set([currentMaterialized.root.id]);
|
|
9469
|
+
}
|
|
9470
|
+
const selectedKeys = new Set();
|
|
9471
|
+
for (const pointer of collectChangedPointers(before, draft)) {
|
|
9472
|
+
for (const owner of findAffectedRepeatOwners(currentMaterialized, pointer)) {
|
|
9473
|
+
selectedKeys.add(owner.key);
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
for (const key of [...selectedKeys]) {
|
|
9477
|
+
let parentKey = currentMaterialized.repeatOwners.get(key)?.parentKey;
|
|
9478
|
+
while (parentKey) {
|
|
9479
|
+
if (selectedKeys.has(parentKey)) {
|
|
9480
|
+
selectedKeys.delete(key);
|
|
9481
|
+
break;
|
|
9482
|
+
}
|
|
9483
|
+
parentKey = currentMaterialized.repeatOwners.get(parentKey)?.parentKey;
|
|
9484
|
+
}
|
|
9485
|
+
}
|
|
9486
|
+
return new Set([...selectedKeys].flatMap((key) => {
|
|
9487
|
+
const id = currentMaterialized.repeatOwners.get(key)?.runtimeOwnerId;
|
|
9488
|
+
return id ? [id] : [];
|
|
9489
|
+
}));
|
|
9490
|
+
}
|
|
9491
|
+
function collectMountedLifecycleIds(root) {
|
|
9492
|
+
const ids = new Set();
|
|
9493
|
+
const visit = (node) => {
|
|
9494
|
+
if (!node.visible)
|
|
9495
|
+
return;
|
|
9496
|
+
if (node.node.lifecycle)
|
|
9497
|
+
ids.add(node.nodeId);
|
|
9498
|
+
for (const child of node.children)
|
|
9499
|
+
visit(child);
|
|
9500
|
+
};
|
|
9501
|
+
if (root)
|
|
9502
|
+
visit(root);
|
|
9503
|
+
return ids;
|
|
9504
|
+
}
|
|
9505
|
+
function prepareReplacementRoots(current, next, draft, replacements) {
|
|
9506
|
+
const sameChildren = current.children.length === next.children.length
|
|
9507
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
9508
|
+
const decision = sameChildren
|
|
9509
|
+
? decideBoundUpdate(current, next)
|
|
9510
|
+
: 'replace';
|
|
9511
|
+
if (decision === 'replace'
|
|
9512
|
+
|| (decision === 'update' && next.children.length === 0)) {
|
|
9513
|
+
replacements.set(next.occurrenceKey, mountBoundSubtree(next.node, next.occurrenceKey, draft));
|
|
9514
|
+
}
|
|
9515
|
+
if (decision === 'replace')
|
|
9516
|
+
return;
|
|
9517
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
9518
|
+
prepareReplacementRoots(current.children[index], next.children[index], draft, replacements);
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
function prepareIncrementalCandidate(before, draft) {
|
|
9522
|
+
const materialized = materializeCard(schema, draft);
|
|
9523
|
+
const forcedOwnerIds = currentMountedTree
|
|
9524
|
+
? materialized.unresolvedRepeatOwners.size > 0
|
|
9525
|
+
? new Set([materialized.root.id])
|
|
9526
|
+
: selectForcedRepeatOwnerIds(before, draft)
|
|
9527
|
+
: new Set();
|
|
9528
|
+
const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, forcedOwnerIds, currentMountedTree);
|
|
9529
|
+
const replacementRoots = new Map();
|
|
9530
|
+
try {
|
|
9531
|
+
if (currentMountedTree) {
|
|
9532
|
+
prepareReplacementRoots(currentMountedTree, preparedRoot, draft, replacementRoots);
|
|
9533
|
+
}
|
|
9534
|
+
else {
|
|
9535
|
+
replacementRoots.set(preparedRoot.occurrenceKey, mountBoundSubtree(materialized.root, preparedRoot.occurrenceKey, draft));
|
|
9536
|
+
}
|
|
9537
|
+
return {
|
|
9538
|
+
materialized,
|
|
9539
|
+
preparedRoot,
|
|
9540
|
+
replacementRoots,
|
|
9541
|
+
autoFocusIds: currentMountedTree
|
|
9542
|
+
? collectAutoFocusRevealIds(currentMaterialized, materialized, draft)
|
|
9543
|
+
: new Set(),
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
catch (error) {
|
|
9547
|
+
for (const replacement of replacementRoots.values()) {
|
|
9548
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9549
|
+
}
|
|
9550
|
+
throw error;
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
function indexNodes(root) {
|
|
9554
|
+
const nodes = new Map();
|
|
9555
|
+
const visit = (node) => {
|
|
9556
|
+
nodes.set(node.id, node);
|
|
7947
9557
|
node.children.forEach(visit);
|
|
7948
9558
|
};
|
|
7949
9559
|
visit(root);
|
|
7950
9560
|
return nodes;
|
|
7951
9561
|
}
|
|
9562
|
+
function getRepeatTargetFingerprint(node) {
|
|
9563
|
+
const identityToken = (key, value) => {
|
|
9564
|
+
if (value === null)
|
|
9565
|
+
return `${key}:null`;
|
|
9566
|
+
if (typeof value === 'string')
|
|
9567
|
+
return `${key}:string:${value}`;
|
|
9568
|
+
if (typeof value === 'boolean')
|
|
9569
|
+
return `${key}:boolean:${value}`;
|
|
9570
|
+
return `${key}:number:${Object.is(value, -0) ? '-0' : String(value)}`;
|
|
9571
|
+
};
|
|
9572
|
+
const identityCountsFor = (root) => {
|
|
9573
|
+
const cached = repeatIdentityCountCache.get(root);
|
|
9574
|
+
if (cached)
|
|
9575
|
+
return cached;
|
|
9576
|
+
const counts = new Map();
|
|
9577
|
+
const visited = new Set();
|
|
9578
|
+
const visit = (candidate) => {
|
|
9579
|
+
if (candidate === null || typeof candidate !== 'object')
|
|
9580
|
+
return;
|
|
9581
|
+
if (visited.has(candidate))
|
|
9582
|
+
return;
|
|
9583
|
+
visited.add(candidate);
|
|
9584
|
+
if (!Array.isArray(candidate)) {
|
|
9585
|
+
for (const key of ['id', 'key']) {
|
|
9586
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
9587
|
+
if (!descriptor || !('value' in descriptor))
|
|
9588
|
+
continue;
|
|
9589
|
+
const identity = descriptor.value;
|
|
9590
|
+
if (identity === null
|
|
9591
|
+
|| (typeof identity !== 'string'
|
|
9592
|
+
&& typeof identity !== 'number'
|
|
9593
|
+
&& typeof identity !== 'boolean')) {
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
const token = identityToken(key, identity);
|
|
9597
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9600
|
+
for (const nested of Object.values(candidate))
|
|
9601
|
+
visit(nested);
|
|
9602
|
+
};
|
|
9603
|
+
visit(root);
|
|
9604
|
+
repeatIdentityCountCache.set(root, counts);
|
|
9605
|
+
return counts;
|
|
9606
|
+
};
|
|
9607
|
+
const identityFor = (value, root) => {
|
|
9608
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
9609
|
+
return value;
|
|
9610
|
+
}
|
|
9611
|
+
const record = value;
|
|
9612
|
+
for (const key of ['id', 'key']) {
|
|
9613
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
9614
|
+
if (!descriptor || !('value' in descriptor))
|
|
9615
|
+
continue;
|
|
9616
|
+
const identity = descriptor.value;
|
|
9617
|
+
if (identity === null
|
|
9618
|
+
|| (typeof identity !== 'string'
|
|
9619
|
+
&& typeof identity !== 'number'
|
|
9620
|
+
&& typeof identity !== 'boolean')) {
|
|
9621
|
+
continue;
|
|
9622
|
+
}
|
|
9623
|
+
const matches = identityCountsFor(root).get(identityToken(key, identity)) ?? 0;
|
|
9624
|
+
if (matches === 1)
|
|
9625
|
+
return { [key]: identity };
|
|
9626
|
+
}
|
|
9627
|
+
return value;
|
|
9628
|
+
};
|
|
9629
|
+
const targets = [];
|
|
9630
|
+
let scope = node.scope;
|
|
9631
|
+
while (scope) {
|
|
9632
|
+
const scopeRoot = scope.root;
|
|
9633
|
+
const localEntries = Object.entries(scope.locals);
|
|
9634
|
+
if (localEntries.length > 0) {
|
|
9635
|
+
targets.push(Object.fromEntries(localEntries.map(([key, value]) => [
|
|
9636
|
+
key,
|
|
9637
|
+
identityFor(value, scopeRoot),
|
|
9638
|
+
])));
|
|
9639
|
+
}
|
|
9640
|
+
else if (scope.bindingDialect === 'a2ui' && scope.dataPath !== '') {
|
|
9641
|
+
targets.push(identityFor(resolveA2UIDeep({ path: '' }, scopeRoot, scope.dataPath), scopeRoot));
|
|
9642
|
+
}
|
|
9643
|
+
scope = scope.parent;
|
|
9644
|
+
}
|
|
9645
|
+
if (targets.length === 0)
|
|
9646
|
+
return undefined;
|
|
9647
|
+
return stableFingerprint({ sourceId: node.sourceId, targets });
|
|
9648
|
+
}
|
|
7952
9649
|
function selectLifecycleNodes(materialized, ids) {
|
|
7953
9650
|
const indexed = indexNodes(materialized.root);
|
|
7954
9651
|
const selected = new Map();
|
|
@@ -7959,26 +9656,80 @@ function renderBoundCard(container, schema, options) {
|
|
|
7959
9656
|
}
|
|
7960
9657
|
return selected;
|
|
7961
9658
|
}
|
|
7962
|
-
function
|
|
9659
|
+
function isVisible(node, renderVariables) {
|
|
9660
|
+
if (!node.directives?.visible)
|
|
9661
|
+
return true;
|
|
9662
|
+
const resolved = resolveNodeValue(node.directives.visible, node, renderVariables);
|
|
9663
|
+
return !(resolved === false
|
|
9664
|
+
|| resolved === 'false'
|
|
9665
|
+
|| resolved === ''
|
|
9666
|
+
|| resolved === 0);
|
|
9667
|
+
}
|
|
9668
|
+
function collectAutoFocusRevealIds(previous, next, draft) {
|
|
9669
|
+
const previousNodes = indexNodes(previous.root);
|
|
9670
|
+
const requested = new Set();
|
|
9671
|
+
const visit = (nextNode, previousParentVisible, nextParentVisible) => {
|
|
9672
|
+
const previousNode = previousNodes.get(nextNode.id);
|
|
9673
|
+
const wasVisible = Boolean(previousNode
|
|
9674
|
+
&& previousParentVisible
|
|
9675
|
+
&& isVisible(previousNode, variables));
|
|
9676
|
+
const nowVisible = nextParentVisible && isVisible(nextNode, draft);
|
|
9677
|
+
if (nextNode.type === 'Input'
|
|
9678
|
+
&& !wasVisible
|
|
9679
|
+
&& nowVisible
|
|
9680
|
+
&& resolveNodeProps(nextNode, draft).autoFocus === true) {
|
|
9681
|
+
requested.add(nextNode.id);
|
|
9682
|
+
}
|
|
9683
|
+
nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
9684
|
+
};
|
|
9685
|
+
visit(next.root, true, true);
|
|
9686
|
+
return requested;
|
|
9687
|
+
}
|
|
9688
|
+
function applyAutoFocus(candidate) {
|
|
9689
|
+
if (candidate.autoFocusIds.size === 0)
|
|
9690
|
+
return;
|
|
9691
|
+
const inputs = container.querySelectorAll(CardInput.is);
|
|
9692
|
+
for (const input of inputs) {
|
|
9693
|
+
const id = input.getAttribute('data-card-id');
|
|
9694
|
+
if (id && candidate.autoFocusIds.has(id) && input.requestAutoFocus()) {
|
|
9695
|
+
break;
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
function createLifecycleActionSession(node) {
|
|
9700
|
+
const lifecycleVariables = cloneJsonData(variables);
|
|
9701
|
+
const rebaseScope = (scope) => ({
|
|
9702
|
+
...scope,
|
|
9703
|
+
root: lifecycleVariables,
|
|
9704
|
+
parent: scope.parent ? rebaseScope(scope.parent) : undefined,
|
|
9705
|
+
});
|
|
9706
|
+
const lifecycleScope = rebaseScope(node.scope);
|
|
9707
|
+
let authorityRevision = revision;
|
|
7963
9708
|
const writeLiveVariable = (key, value, silent = false) => {
|
|
7964
|
-
|
|
9709
|
+
const nextValue = cloneJsonData(value);
|
|
9710
|
+
writeDraftVariable(lifecycleVariables, key, nextValue);
|
|
9711
|
+
repeatIdentityCountCache.delete(lifecycleVariables);
|
|
9712
|
+
if (disposed || authorityRevision !== revision)
|
|
7965
9713
|
return;
|
|
7966
9714
|
if (silent) {
|
|
7967
|
-
writeDraftVariable(variables, key,
|
|
9715
|
+
writeDraftVariable(variables, key, nextValue);
|
|
7968
9716
|
return;
|
|
7969
9717
|
}
|
|
7970
|
-
|
|
9718
|
+
// Publish the complete lifecycle draft so direct additions, changes,
|
|
9719
|
+
// and deletions join this explicit write in one authoritative render.
|
|
9720
|
+
commitDraft(cloneJsonData(lastPublishedVariables), lifecycleVariables, authorityRevision);
|
|
9721
|
+
authorityRevision = revision;
|
|
7971
9722
|
};
|
|
7972
|
-
|
|
9723
|
+
const context = {
|
|
7973
9724
|
...createWebActionContext({
|
|
7974
9725
|
...options,
|
|
7975
9726
|
setVariable: writeLiveVariable,
|
|
7976
9727
|
abortSignal: abortController.signal,
|
|
7977
9728
|
}),
|
|
7978
|
-
variables,
|
|
7979
|
-
expressionContext: createExpressionContext(
|
|
9729
|
+
variables: lifecycleVariables,
|
|
9730
|
+
expressionContext: createExpressionContext(lifecycleScope),
|
|
7980
9731
|
parameterResolver: node.bindingDialect === 'a2ui'
|
|
7981
|
-
? createA2UIParameterResolver(
|
|
9732
|
+
? createA2UIParameterResolver(lifecycleVariables, node.dataPath)
|
|
7982
9733
|
: undefined,
|
|
7983
9734
|
variableWriter: (key, value, options) => {
|
|
7984
9735
|
writeLiveVariable(key, value, options.silent);
|
|
@@ -7986,6 +9737,20 @@ function renderBoundCard(container, schema, options) {
|
|
|
7986
9737
|
botId: options.botId,
|
|
7987
9738
|
inflightRequests,
|
|
7988
9739
|
};
|
|
9740
|
+
return {
|
|
9741
|
+
context,
|
|
9742
|
+
publishDirectMutations() {
|
|
9743
|
+
if (disposed
|
|
9744
|
+
|| authorityRevision !== revision
|
|
9745
|
+
|| !hasVariableChanges(variables, lifecycleVariables)) {
|
|
9746
|
+
return;
|
|
9747
|
+
}
|
|
9748
|
+
// Direct custom-handler mutations historically updated the live
|
|
9749
|
+
// variables without rendering. Preserve that behavior while keeping
|
|
9750
|
+
// a stale async lifecycle detached after a newer revision wins.
|
|
9751
|
+
replaceRootContents(variables, lifecycleVariables);
|
|
9752
|
+
},
|
|
9753
|
+
};
|
|
7989
9754
|
}
|
|
7990
9755
|
function reconcileLifecycles(nextNodes) {
|
|
7991
9756
|
const removed = [...activeLifecycleNodes.entries()]
|
|
@@ -7998,45 +9763,263 @@ function renderBoundCard(container, schema, options) {
|
|
|
7998
9763
|
}
|
|
7999
9764
|
lifecycleQueue = lifecycleQueue.then(async () => {
|
|
8000
9765
|
for (const [id, node] of removed) {
|
|
8001
|
-
|
|
8002
|
-
|
|
9766
|
+
if (disposed)
|
|
9767
|
+
return;
|
|
9768
|
+
const session = createLifecycleActionSession(node);
|
|
9769
|
+
try {
|
|
9770
|
+
await lifecycleManager.destroy(id, session.context);
|
|
9771
|
+
session.publishDirectMutations();
|
|
9772
|
+
}
|
|
9773
|
+
catch (error) {
|
|
9774
|
+
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
9775
|
+
}
|
|
9776
|
+
finally {
|
|
9777
|
+
lifecycleManager.unregister(id);
|
|
9778
|
+
}
|
|
8003
9779
|
}
|
|
8004
|
-
if (disposed)
|
|
8005
|
-
return;
|
|
8006
9780
|
for (const [id, node] of added) {
|
|
9781
|
+
if (disposed)
|
|
9782
|
+
return;
|
|
8007
9783
|
lifecycleManager.register(id, node.lifecycle);
|
|
8008
|
-
|
|
9784
|
+
const session = createLifecycleActionSession(node);
|
|
9785
|
+
try {
|
|
9786
|
+
await lifecycleManager.mount(id, session.context);
|
|
9787
|
+
session.publishDirectMutations();
|
|
9788
|
+
}
|
|
9789
|
+
catch (error) {
|
|
9790
|
+
lifecycleManager.unregister(id);
|
|
9791
|
+
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
9792
|
+
}
|
|
8009
9793
|
}
|
|
8010
9794
|
}).catch((error) => {
|
|
8011
9795
|
console.error('[renderCard] Bound lifecycle action failed', error);
|
|
8012
9796
|
});
|
|
8013
9797
|
}
|
|
8014
|
-
function
|
|
8015
|
-
const
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
9798
|
+
function disposeCandidate(candidate) {
|
|
9799
|
+
for (const replacement of candidate.replacementRoots.values()) {
|
|
9800
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9801
|
+
}
|
|
9802
|
+
candidate.replacementRoots.clear();
|
|
9803
|
+
}
|
|
9804
|
+
function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
|
|
9805
|
+
if (previous.element !== next.element) {
|
|
9806
|
+
restoreScrollPositions$1(next.element, scrollPositions);
|
|
9807
|
+
restoreMediaStates$1(next.element, mediaStates);
|
|
9808
|
+
return;
|
|
9809
|
+
}
|
|
9810
|
+
if (previous.propsFingerprint !== next.propsFingerprint
|
|
9811
|
+
&& (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
|
|
9812
|
+
restoreMediaStates$1(next.element, mediaStates);
|
|
9813
|
+
}
|
|
9814
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
9815
|
+
const previousChild = previous.children[index];
|
|
9816
|
+
const nextChild = next.children[index];
|
|
9817
|
+
if (previousChild
|
|
9818
|
+
&& previousChild.occurrenceKey === nextChild.occurrenceKey) {
|
|
9819
|
+
restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
}
|
|
9823
|
+
function restoreDisabledDescendantPresentation(current) {
|
|
9824
|
+
if (current.disabled) {
|
|
9825
|
+
applyDisabledState(current.element, true);
|
|
9826
|
+
applyDisabledDescendants(current.element, true);
|
|
9827
|
+
}
|
|
9828
|
+
for (const child of current.children) {
|
|
9829
|
+
restoreDisabledDescendantPresentation(child);
|
|
9830
|
+
}
|
|
9831
|
+
}
|
|
9832
|
+
function replaceContainerRoot(element, failureMessage) {
|
|
9833
|
+
let adoptionThrew = false;
|
|
9834
|
+
let adoptionError;
|
|
9835
|
+
try {
|
|
9836
|
+
container.replaceChildren(element);
|
|
9837
|
+
}
|
|
9838
|
+
catch (error) {
|
|
9839
|
+
adoptionThrew = true;
|
|
9840
|
+
adoptionError = error;
|
|
9841
|
+
}
|
|
9842
|
+
const adopted = (element.parentNode === container
|
|
9843
|
+
&& container.firstChild === element
|
|
9844
|
+
&& container.childNodes.length === 1);
|
|
9845
|
+
if (adopted)
|
|
9846
|
+
return;
|
|
9847
|
+
if (adoptionThrew)
|
|
9848
|
+
throw adoptionError;
|
|
9849
|
+
throw new Error(failureMessage);
|
|
9850
|
+
}
|
|
9851
|
+
function commitIncrementalCandidate(candidate, renderVariables) {
|
|
9852
|
+
const replacements = candidate.replacementRoots;
|
|
9853
|
+
try {
|
|
9854
|
+
if (!currentMountedTree) {
|
|
9855
|
+
const initial = replacements.get(candidate.preparedRoot.occurrenceKey);
|
|
9856
|
+
if (!initial) {
|
|
9857
|
+
throw new Error('[renderCard] Missing bound root candidate');
|
|
9858
|
+
}
|
|
9859
|
+
syncMountedBindings(initial);
|
|
9860
|
+
disposeChartsIn(container);
|
|
9861
|
+
replaceContainerRoot(initial.element, '[renderCard] Bound root candidate was not adopted');
|
|
9862
|
+
replacements.delete(candidate.preparedRoot.occurrenceKey);
|
|
9863
|
+
restoreDisabledDescendantPresentation(initial);
|
|
9864
|
+
return initial;
|
|
9865
|
+
}
|
|
9866
|
+
const previous = currentMountedTree;
|
|
9867
|
+
const replacementSources = new Map();
|
|
9868
|
+
collectMountedOccurrences(previous, replacementSources);
|
|
9869
|
+
const scrollPositions = captureScrollPositions$1(container);
|
|
9870
|
+
const mediaStates = captureMediaStates$1(container);
|
|
9871
|
+
let unreconciled = false;
|
|
9872
|
+
const next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
|
|
9873
|
+
decide: (current, prepared) => decideBoundUpdate(current, prepared),
|
|
9874
|
+
update: (current, prepared) => updateBoundNode(current, prepared),
|
|
9875
|
+
mountReplacement: (prepared) => {
|
|
9876
|
+
const candidateReplacement = replacements.get(prepared.occurrenceKey);
|
|
9877
|
+
const replacement = candidateReplacement ?? mountBoundSubtree(prepared.node, prepared.occurrenceKey, renderVariables);
|
|
9878
|
+
const current = replacementSources.get(prepared.occurrenceKey);
|
|
9879
|
+
let staged = false;
|
|
9880
|
+
try {
|
|
9881
|
+
if (replacement.element.parentNode !== null) {
|
|
9882
|
+
throw new Error(`[renderCard] Bound replacement "${prepared.occurrenceKey}" is not detached`);
|
|
9883
|
+
}
|
|
9884
|
+
syncMountedBindings(replacement);
|
|
9885
|
+
if (current && replacement.element !== current.element) {
|
|
9886
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
9887
|
+
}
|
|
9888
|
+
staged = true;
|
|
9889
|
+
}
|
|
9890
|
+
finally {
|
|
9891
|
+
if (!staged && !candidateReplacement) {
|
|
9892
|
+
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
if (candidateReplacement) {
|
|
9896
|
+
replacements.delete(prepared.occurrenceKey);
|
|
9897
|
+
}
|
|
9898
|
+
return replacement;
|
|
9899
|
+
},
|
|
9900
|
+
dispose: disposeBoundSubtree,
|
|
9901
|
+
syncBindings: () => { },
|
|
9902
|
+
onUnreconciled: () => {
|
|
9903
|
+
unreconciled = true;
|
|
9904
|
+
},
|
|
9905
|
+
});
|
|
9906
|
+
if (!unreconciled
|
|
9907
|
+
&& (next.element.parentNode !== container
|
|
9908
|
+
|| container.firstElementChild !== next.element)) {
|
|
9909
|
+
unreconciled = true;
|
|
9910
|
+
}
|
|
9911
|
+
if (!unreconciled) {
|
|
9912
|
+
try {
|
|
9913
|
+
syncMountedBindings(next);
|
|
9914
|
+
}
|
|
9915
|
+
catch {
|
|
9916
|
+
unreconciled = true;
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
if (unreconciled) {
|
|
9920
|
+
let emergency;
|
|
9921
|
+
try {
|
|
9922
|
+
emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
|
|
9923
|
+
}
|
|
9924
|
+
catch (error) {
|
|
9925
|
+
disposeBoundBranchesOutsideContainer(next);
|
|
9926
|
+
throw error;
|
|
9927
|
+
}
|
|
9928
|
+
let adopted = false;
|
|
9929
|
+
try {
|
|
9930
|
+
syncMountedBindings(emergency);
|
|
9931
|
+
replaceContainerRoot(emergency.element, '[renderCard] Bound emergency root was not adopted');
|
|
9932
|
+
adopted = true;
|
|
9933
|
+
}
|
|
9934
|
+
finally {
|
|
9935
|
+
if (!adopted) {
|
|
9936
|
+
disposeBoundSubtreeWithoutThrow(emergency);
|
|
9937
|
+
disposeBoundBranchesOutsideContainer(next);
|
|
9938
|
+
}
|
|
9939
|
+
}
|
|
9940
|
+
disposeBoundSubtreeWithoutThrow(next);
|
|
9941
|
+
try {
|
|
9942
|
+
restoreChangedSubtreeState(previous, emergency, scrollPositions, mediaStates);
|
|
9943
|
+
}
|
|
9944
|
+
catch {
|
|
9945
|
+
// Presentation restoration cannot roll back an adopted next root.
|
|
9946
|
+
}
|
|
9947
|
+
restoreDisabledDescendantPresentation(emergency);
|
|
9948
|
+
return emergency;
|
|
9949
|
+
}
|
|
9950
|
+
try {
|
|
9951
|
+
restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates);
|
|
9952
|
+
}
|
|
9953
|
+
catch {
|
|
9954
|
+
// Presentation restoration does not affect published data ownership.
|
|
9955
|
+
}
|
|
9956
|
+
restoreDisabledDescendantPresentation(next);
|
|
9957
|
+
return next;
|
|
9958
|
+
}
|
|
9959
|
+
finally {
|
|
9960
|
+
disposeCandidate(candidate);
|
|
9961
|
+
}
|
|
8025
9962
|
}
|
|
8026
9963
|
function assertCurrentRevision(baseRevision) {
|
|
8027
|
-
if (disposed || revision !== baseRevision) {
|
|
9964
|
+
if (disposed || publishing || revision !== baseRevision) {
|
|
8028
9965
|
const error = new Error('[renderCard] BOUND_TRANSACTION_CONFLICT');
|
|
8029
9966
|
error.code = 'BOUND_TRANSACTION_CONFLICT';
|
|
8030
9967
|
throw error;
|
|
8031
9968
|
}
|
|
8032
9969
|
}
|
|
8033
|
-
function commitDraft(
|
|
9970
|
+
function commitDraft(before, draft, baseRevision) {
|
|
8034
9971
|
assertCurrentRevision(baseRevision);
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
9972
|
+
const validationRoot = cloneJsonData(variables);
|
|
9973
|
+
replaceRootContents(validationRoot, draft);
|
|
9974
|
+
const candidate = prepareIncrementalCandidate(before, draft);
|
|
9975
|
+
try {
|
|
9976
|
+
assertCurrentRevision(baseRevision);
|
|
9977
|
+
}
|
|
9978
|
+
catch (error) {
|
|
9979
|
+
disposeCandidate(candidate);
|
|
9980
|
+
throw error;
|
|
9981
|
+
}
|
|
9982
|
+
publishing = true;
|
|
9983
|
+
let publishingFailed = false;
|
|
9984
|
+
let publishingError;
|
|
9985
|
+
let deferredDisposeFailed = false;
|
|
9986
|
+
let deferredDisposeError;
|
|
9987
|
+
try {
|
|
9988
|
+
const nextMounted = commitIncrementalCandidate(candidate, draft);
|
|
9989
|
+
replaceRootContents(variables, draft);
|
|
9990
|
+
currentMaterialized = materializeCard(schema, variables);
|
|
9991
|
+
currentMountedTree = nextMounted;
|
|
9992
|
+
lastPublishedVariables = cloneJsonData(variables);
|
|
9993
|
+
revision += 1;
|
|
9994
|
+
}
|
|
9995
|
+
catch (error) {
|
|
9996
|
+
publishingFailed = true;
|
|
9997
|
+
publishingError = error;
|
|
9998
|
+
}
|
|
9999
|
+
finally {
|
|
10000
|
+
publishing = false;
|
|
10001
|
+
if (disposeRequested) {
|
|
10002
|
+
try {
|
|
10003
|
+
disposeNow();
|
|
10004
|
+
}
|
|
10005
|
+
catch (error) {
|
|
10006
|
+
deferredDisposeFailed = true;
|
|
10007
|
+
deferredDisposeError = error;
|
|
10008
|
+
}
|
|
10009
|
+
}
|
|
10010
|
+
}
|
|
10011
|
+
if (publishingFailed) {
|
|
10012
|
+
if (deferredDisposeFailed) {
|
|
10013
|
+
console.error('[renderCard] Bound deferred dispose failed', deferredDisposeError);
|
|
10014
|
+
}
|
|
10015
|
+
throw publishingError;
|
|
10016
|
+
}
|
|
10017
|
+
if (deferredDisposeFailed)
|
|
10018
|
+
throw deferredDisposeError;
|
|
10019
|
+
if (disposed)
|
|
10020
|
+
return;
|
|
10021
|
+
reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
|
|
10022
|
+
applyAutoFocus(candidate);
|
|
8040
10023
|
}
|
|
8041
10024
|
function writeDraftVariable(draft, key, value) {
|
|
8042
10025
|
Object.defineProperty(draft, String(key), {
|
|
@@ -8093,11 +10076,12 @@ function renderBoundCard(container, schema, options) {
|
|
|
8093
10076
|
inflightRequests,
|
|
8094
10077
|
};
|
|
8095
10078
|
}
|
|
8096
|
-
async function runBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10079
|
+
async function runBoundEvent(runtimeId, eventName, eventDetail, baseRevision, repeatTargetFingerprint) {
|
|
8097
10080
|
if (disposed)
|
|
8098
10081
|
return;
|
|
8099
|
-
|
|
8100
|
-
const
|
|
10082
|
+
assertCurrentRevision(baseRevision);
|
|
10083
|
+
const transactionBefore = cloneJsonData(variables);
|
|
10084
|
+
const draft = cloneJsonData(transactionBefore);
|
|
8101
10085
|
if (eventDetail !== undefined) {
|
|
8102
10086
|
writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
|
|
8103
10087
|
}
|
|
@@ -8105,8 +10089,14 @@ function renderBoundCard(container, schema, options) {
|
|
|
8105
10089
|
const freshMaterialized = materializeCard(schema, draft);
|
|
8106
10090
|
const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
|
|
8107
10091
|
if (!freshNode) {
|
|
10092
|
+
if (repeatTargetFingerprint !== undefined)
|
|
10093
|
+
return;
|
|
8108
10094
|
throw new Error(`[renderCard] Bound runtime node "${runtimeId}" no longer exists`);
|
|
8109
10095
|
}
|
|
10096
|
+
if (repeatTargetFingerprint !== undefined
|
|
10097
|
+
&& getRepeatTargetFingerprint(freshNode) !== repeatTargetFingerprint) {
|
|
10098
|
+
return;
|
|
10099
|
+
}
|
|
8110
10100
|
const eventValue = freshNode.events?.[eventName];
|
|
8111
10101
|
const steps = eventValue
|
|
8112
10102
|
? resolveActionRef(eventValue, schemaActions)
|
|
@@ -8117,14 +10107,19 @@ function renderBoundCard(container, schema, options) {
|
|
|
8117
10107
|
assertCurrentRevision(baseRevision);
|
|
8118
10108
|
if (!hasVariableChanges(actionBaseline, draft))
|
|
8119
10109
|
return;
|
|
8120
|
-
|
|
8121
|
-
|
|
10110
|
+
// The target check above may have cached identity counts before action
|
|
10111
|
+
// steps changed the draft. Recount once when binding the published tree.
|
|
10112
|
+
repeatIdentityCountCache.delete(draft);
|
|
10113
|
+
commitDraft(cloneJsonData(lastPublishedVariables), draft, baseRevision);
|
|
8122
10114
|
}
|
|
8123
|
-
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
10115
|
+
function enqueueBoundEvent(runtimeId, eventName, eventDetail, repeatTargetFingerprint) {
|
|
8124
10116
|
if (disposed)
|
|
8125
10117
|
return;
|
|
10118
|
+
const enqueueRevision = revision;
|
|
10119
|
+
const queuedBehindAnotherAction = queuedActionCount > 0;
|
|
10120
|
+
queuedActionCount += 1;
|
|
8126
10121
|
actionQueue = actionQueue
|
|
8127
|
-
.then(() => runBoundEvent(runtimeId, eventName, eventDetail))
|
|
10122
|
+
.then(() => runBoundEvent(runtimeId, eventName, eventDetail, queuedBehindAnotherAction ? revision : enqueueRevision, repeatTargetFingerprint))
|
|
8128
10123
|
.catch((error) => {
|
|
8129
10124
|
if (error
|
|
8130
10125
|
&& typeof error === 'object'
|
|
@@ -8133,44 +10128,87 @@ function renderBoundCard(container, schema, options) {
|
|
|
8133
10128
|
return;
|
|
8134
10129
|
}
|
|
8135
10130
|
console.error('[renderCard] Bound action failed', error);
|
|
10131
|
+
})
|
|
10132
|
+
.finally(() => {
|
|
10133
|
+
queuedActionCount -= 1;
|
|
8136
10134
|
});
|
|
8137
10135
|
}
|
|
8138
10136
|
function updateVariables(newVariables) {
|
|
8139
10137
|
if (disposed)
|
|
8140
10138
|
return;
|
|
8141
10139
|
const baseRevision = revision;
|
|
10140
|
+
const before = cloneJsonData(lastPublishedVariables);
|
|
8142
10141
|
const draft = cloneJsonData(variables);
|
|
8143
10142
|
const patch = cloneJsonData(newVariables);
|
|
8144
10143
|
for (const key of Object.keys(patch)) {
|
|
8145
10144
|
writeDraftVariable(draft, key, patch[key]);
|
|
8146
10145
|
}
|
|
8147
|
-
|
|
8148
|
-
commitDraft(draft, candidate, baseRevision);
|
|
10146
|
+
commitDraft(before, draft, baseRevision);
|
|
8149
10147
|
}
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
10148
|
+
function disposeNow() {
|
|
10149
|
+
if (disposed)
|
|
10150
|
+
return;
|
|
10151
|
+
disposed = true;
|
|
10152
|
+
disposeRequested = false;
|
|
10153
|
+
nodeAccess.dispose();
|
|
10154
|
+
abortController.abort();
|
|
10155
|
+
if (currentMountedTree) {
|
|
10156
|
+
disposeBoundSubtree(currentMountedTree);
|
|
10157
|
+
currentMountedTree = null;
|
|
10158
|
+
}
|
|
10159
|
+
else {
|
|
8159
10160
|
disposeChartsIn(container);
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
10161
|
+
}
|
|
10162
|
+
container.replaceChildren();
|
|
10163
|
+
const lifecycleNodes = [...activeLifecycleNodes.entries()];
|
|
10164
|
+
activeLifecycleNodes.clear();
|
|
10165
|
+
lifecycleQueue = lifecycleQueue
|
|
10166
|
+
.then(async () => {
|
|
10167
|
+
for (const [id, node] of lifecycleNodes) {
|
|
10168
|
+
const session = createLifecycleActionSession(node);
|
|
10169
|
+
try {
|
|
10170
|
+
await lifecycleManager.destroy(id, session.context);
|
|
10171
|
+
session.publishDirectMutations();
|
|
10172
|
+
}
|
|
10173
|
+
catch (error) {
|
|
10174
|
+
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
10175
|
+
}
|
|
10176
|
+
finally {
|
|
8167
10177
|
lifecycleManager.unregister(id);
|
|
8168
10178
|
}
|
|
10179
|
+
}
|
|
10180
|
+
try {
|
|
8169
10181
|
await lifecycleManager.dispose(createPassiveActionContext(variables));
|
|
8170
|
-
}
|
|
8171
|
-
|
|
10182
|
+
}
|
|
10183
|
+
catch (error) {
|
|
8172
10184
|
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
8173
|
-
}
|
|
10185
|
+
}
|
|
10186
|
+
})
|
|
10187
|
+
.catch((error) => {
|
|
10188
|
+
console.error('[renderCard] Bound lifecycle dispose failed', error);
|
|
10189
|
+
});
|
|
10190
|
+
}
|
|
10191
|
+
const initialCandidate = prepareIncrementalCandidate(variables, variables);
|
|
10192
|
+
publishing = true;
|
|
10193
|
+
try {
|
|
10194
|
+
currentMountedTree = commitIncrementalCandidate(initialCandidate, variables);
|
|
10195
|
+
currentMaterialized = materializeCard(schema, variables);
|
|
10196
|
+
}
|
|
10197
|
+
finally {
|
|
10198
|
+
publishing = false;
|
|
10199
|
+
}
|
|
10200
|
+
reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
|
|
10201
|
+
return {
|
|
10202
|
+
getNode: nodeAccess.getNode,
|
|
10203
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
10204
|
+
dispose() {
|
|
10205
|
+
if (disposed || disposeRequested)
|
|
10206
|
+
return;
|
|
10207
|
+
if (publishing) {
|
|
10208
|
+
disposeRequested = true;
|
|
10209
|
+
return;
|
|
10210
|
+
}
|
|
10211
|
+
disposeNow();
|
|
8174
10212
|
},
|
|
8175
10213
|
updateVariables,
|
|
8176
10214
|
};
|
|
@@ -8188,7 +10226,11 @@ function captureScrollPositions$1(root) {
|
|
|
8188
10226
|
function restoreScrollPositions$1(root, positions) {
|
|
8189
10227
|
if (positions.size === 0)
|
|
8190
10228
|
return;
|
|
8191
|
-
|
|
10229
|
+
const scrollers = [];
|
|
10230
|
+
if (root.matches('[data-scroll-id]'))
|
|
10231
|
+
scrollers.push(root);
|
|
10232
|
+
scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
|
|
10233
|
+
scrollers.forEach((element) => {
|
|
8192
10234
|
const id = element.getAttribute('data-scroll-id');
|
|
8193
10235
|
const saved = id ? positions.get(id) : undefined;
|
|
8194
10236
|
if (saved == null)
|
|
@@ -8217,7 +10259,11 @@ function captureMediaStates$1(root) {
|
|
|
8217
10259
|
function restoreMediaStates$1(root, states) {
|
|
8218
10260
|
if (states.size === 0)
|
|
8219
10261
|
return;
|
|
8220
|
-
|
|
10262
|
+
const mediaHosts = [];
|
|
10263
|
+
if (root.matches('ai-card-audio, ai-card-video'))
|
|
10264
|
+
mediaHosts.push(root);
|
|
10265
|
+
mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
|
|
10266
|
+
mediaHosts.forEach((element) => {
|
|
8221
10267
|
const id = element.getAttribute('data-card-id');
|
|
8222
10268
|
const snapshot = id ? states.get(id) : undefined;
|
|
8223
10269
|
if (!snapshot)
|
|
@@ -8279,22 +10325,72 @@ function renderCard(container, schemaInput, options = {}) {
|
|
|
8279
10325
|
return renderBoundCard(container, schema, options);
|
|
8280
10326
|
}
|
|
8281
10327
|
function renderStaticCard(container, schema, options) {
|
|
10328
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8282
10329
|
// 2. Parse into render tree
|
|
8283
10330
|
const tree = parseSchema(schema);
|
|
8284
10331
|
// 3. Reactive variables store (mutable copy, merged with external variables)
|
|
8285
10332
|
let variables = { ...schema.variables, ...options.variables };
|
|
8286
10333
|
let disposed = false;
|
|
10334
|
+
let publishing = false;
|
|
10335
|
+
let disposeRequested = false;
|
|
8287
10336
|
let hostAuthorityEpoch = 0;
|
|
10337
|
+
let nextStaticEventSequence = 0;
|
|
10338
|
+
const latestStaticEventWriterByKey = new Map();
|
|
8288
10339
|
const lifecycleRecords = new Map();
|
|
8289
10340
|
const abortController = new AbortController();
|
|
8290
10341
|
// Per-instance request dedup map (isolated from other cards on the page).
|
|
8291
10342
|
const inflightRequests = new Map();
|
|
8292
|
-
function
|
|
10343
|
+
function isVisible(node, renderVariables, parentVisible) {
|
|
10344
|
+
if (!parentVisible)
|
|
10345
|
+
return false;
|
|
10346
|
+
if (!node.directives?.visible)
|
|
10347
|
+
return true;
|
|
10348
|
+
const visible = node.directives.visible;
|
|
10349
|
+
const resolved = hasExpression(visible)
|
|
10350
|
+
? resolveExpression(visible, renderVariables)
|
|
10351
|
+
: visible;
|
|
10352
|
+
return !(resolved === false
|
|
10353
|
+
|| resolved === 'false'
|
|
10354
|
+
|| resolved === ''
|
|
10355
|
+
|| resolved === 0);
|
|
10356
|
+
}
|
|
10357
|
+
function collectAutoFocusRevealIds(previousVariables, nextVariables) {
|
|
10358
|
+
const requested = new Set();
|
|
10359
|
+
const visit = (node, previousParentVisible, nextParentVisible) => {
|
|
10360
|
+
const wasVisible = isVisible(node, previousVariables, previousParentVisible);
|
|
10361
|
+
const nowVisible = isVisible(node, nextVariables, nextParentVisible);
|
|
10362
|
+
if (node.type === 'Input'
|
|
10363
|
+
&& !wasVisible
|
|
10364
|
+
&& nowVisible
|
|
10365
|
+
&& resolveDeep(node.props, nextVariables).autoFocus === true) {
|
|
10366
|
+
requested.add(node.id);
|
|
10367
|
+
}
|
|
10368
|
+
node.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
10369
|
+
};
|
|
10370
|
+
visit(tree, true, true);
|
|
10371
|
+
return requested;
|
|
10372
|
+
}
|
|
10373
|
+
function applyAutoFocus(requested) {
|
|
10374
|
+
if (requested.size === 0)
|
|
10375
|
+
return;
|
|
10376
|
+
for (const input of container.querySelectorAll(CardInput.is)) {
|
|
10377
|
+
const id = input.getAttribute('data-card-id');
|
|
10378
|
+
if (id && requested.has(id) && input.requestAutoFocus())
|
|
10379
|
+
break;
|
|
10380
|
+
}
|
|
10381
|
+
}
|
|
10382
|
+
function buildActionContext(pendingAutoFocusIds, initialVariables = variables, eventSequence) {
|
|
8293
10383
|
const baseAuthorityEpoch = hostAuthorityEpoch;
|
|
8294
10384
|
let context;
|
|
8295
10385
|
const writeVariable = (key, value, { silent }) => {
|
|
8296
10386
|
let actionVariables = context.variables;
|
|
8297
|
-
|
|
10387
|
+
const latestWriter = latestStaticEventWriterByKey.get(key);
|
|
10388
|
+
if (disposed
|
|
10389
|
+
|| publishing
|
|
10390
|
+
|| baseAuthorityEpoch !== hostAuthorityEpoch
|
|
10391
|
+
|| (eventSequence !== undefined
|
|
10392
|
+
&& latestWriter !== undefined
|
|
10393
|
+
&& latestWriter > eventSequence)) {
|
|
8298
10394
|
if (actionVariables === variables) {
|
|
8299
10395
|
actionVariables = { ...actionVariables };
|
|
8300
10396
|
context.variables = actionVariables;
|
|
@@ -8302,9 +10398,50 @@ function renderStaticCard(container, schema, options) {
|
|
|
8302
10398
|
actionVariables[key] = value;
|
|
8303
10399
|
return;
|
|
8304
10400
|
}
|
|
10401
|
+
const previousVariables = !silent && pendingAutoFocusIds
|
|
10402
|
+
? { ...actionVariables }
|
|
10403
|
+
: null;
|
|
10404
|
+
const liveVariables = variables;
|
|
10405
|
+
const hadLiveValue = Object.prototype.hasOwnProperty.call(liveVariables, key);
|
|
10406
|
+
const previousLiveValue = liveVariables[key];
|
|
8305
10407
|
actionVariables[key] = value;
|
|
8306
|
-
if (
|
|
10408
|
+
if (silent)
|
|
10409
|
+
return;
|
|
10410
|
+
// Event actions use a private snapshot so a later event cannot replace
|
|
10411
|
+
// their `_event` detail while they await. Publish only the key written by
|
|
10412
|
+
// this step, preserving concurrent actions that update different keys.
|
|
10413
|
+
if (actionVariables !== variables) {
|
|
10414
|
+
variables[key] = value;
|
|
10415
|
+
}
|
|
10416
|
+
if (previousVariables && pendingAutoFocusIds) {
|
|
10417
|
+
for (const id of collectAutoFocusRevealIds(previousVariables, actionVariables)) {
|
|
10418
|
+
pendingAutoFocusIds.add(id);
|
|
10419
|
+
}
|
|
10420
|
+
}
|
|
10421
|
+
try {
|
|
8307
10422
|
rerender();
|
|
10423
|
+
}
|
|
10424
|
+
catch (error) {
|
|
10425
|
+
if (variables === liveVariables) {
|
|
10426
|
+
if (hadLiveValue)
|
|
10427
|
+
liveVariables[key] = previousLiveValue;
|
|
10428
|
+
else
|
|
10429
|
+
Reflect.deleteProperty(liveVariables, key);
|
|
10430
|
+
if (eventSequence !== undefined
|
|
10431
|
+
&& latestStaticEventWriterByKey.get(key) === eventSequence) {
|
|
10432
|
+
if (latestWriter === undefined) {
|
|
10433
|
+
latestStaticEventWriterByKey.delete(key);
|
|
10434
|
+
}
|
|
10435
|
+
else {
|
|
10436
|
+
latestStaticEventWriterByKey.set(key, latestWriter);
|
|
10437
|
+
}
|
|
10438
|
+
}
|
|
10439
|
+
pendingAutoFocusIds?.clear();
|
|
10440
|
+
if (!disposed)
|
|
10441
|
+
actionContext = buildActionContext();
|
|
10442
|
+
}
|
|
10443
|
+
throw error;
|
|
10444
|
+
}
|
|
8308
10445
|
};
|
|
8309
10446
|
context = {
|
|
8310
10447
|
...createWebActionContext({
|
|
@@ -8314,7 +10451,7 @@ function renderStaticCard(container, schema, options) {
|
|
|
8314
10451
|
},
|
|
8315
10452
|
abortSignal: abortController.signal,
|
|
8316
10453
|
}),
|
|
8317
|
-
variables,
|
|
10454
|
+
variables: initialVariables,
|
|
8318
10455
|
variableWriter: writeVariable,
|
|
8319
10456
|
botId: options.botId,
|
|
8320
10457
|
inflightRequests,
|
|
@@ -8359,8 +10496,431 @@ function renderStaticCard(container, schema, options) {
|
|
|
8359
10496
|
}
|
|
8360
10497
|
});
|
|
8361
10498
|
}
|
|
8362
|
-
|
|
10499
|
+
let mountedTree = null;
|
|
8363
10500
|
let renderGeneration = 0;
|
|
10501
|
+
function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
|
|
10502
|
+
return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
|
|
10503
|
+
}
|
|
10504
|
+
function resolveNodeProps(node) {
|
|
10505
|
+
const resolvedProps = resolveDeep(node.props, variables);
|
|
10506
|
+
if (resolvedProps.content
|
|
10507
|
+
&& typeof resolvedProps.content === 'object'
|
|
10508
|
+
&& 'type' in resolvedProps.content) {
|
|
10509
|
+
resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
|
|
10510
|
+
}
|
|
10511
|
+
return resolvedProps;
|
|
10512
|
+
}
|
|
10513
|
+
function isNodeDisabled(node) {
|
|
10514
|
+
if (!node.directives?.disabled)
|
|
10515
|
+
return false;
|
|
10516
|
+
const disabled = node.directives.disabled;
|
|
10517
|
+
const resolved = hasExpression(disabled)
|
|
10518
|
+
? resolveExpression(disabled, variables)
|
|
10519
|
+
: disabled;
|
|
10520
|
+
return resolved === true || resolved === 'true' || resolved === 1;
|
|
10521
|
+
}
|
|
10522
|
+
function nodeBindingFingerprint(node, resolvedProps, disabled) {
|
|
10523
|
+
const binding = { disabled };
|
|
10524
|
+
if (resolvedProps && resolvedProps.variableKey !== undefined) {
|
|
10525
|
+
binding.variableKey = resolvedProps.variableKey;
|
|
10526
|
+
}
|
|
10527
|
+
if (!disabled && node.events !== undefined) {
|
|
10528
|
+
binding.events = node.events;
|
|
10529
|
+
}
|
|
10530
|
+
return stableFingerprint(binding);
|
|
10531
|
+
}
|
|
10532
|
+
function resolveStaticMetadata(node, occurrenceKey) {
|
|
10533
|
+
const visible = isVisible(node, variables, true);
|
|
10534
|
+
const rendererToken = componentRenderers[node.type]
|
|
10535
|
+
?? componentRenderers._default;
|
|
10536
|
+
if (!visible) {
|
|
10537
|
+
return {
|
|
10538
|
+
occurrenceKey,
|
|
10539
|
+
node,
|
|
10540
|
+
nodeId: node.id,
|
|
10541
|
+
nodeType: node.type,
|
|
10542
|
+
rendererToken,
|
|
10543
|
+
visible: false,
|
|
10544
|
+
disabled: false,
|
|
10545
|
+
resolvedProps: null,
|
|
10546
|
+
propsFingerprint: stableFingerprint(null),
|
|
10547
|
+
layoutFingerprint: stableFingerprint(null),
|
|
10548
|
+
bindingFingerprint: stableFingerprint(null),
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
const resolvedProps = resolveNodeProps(node);
|
|
10552
|
+
const disabled = isNodeDisabled(node);
|
|
10553
|
+
return {
|
|
10554
|
+
occurrenceKey,
|
|
10555
|
+
node,
|
|
10556
|
+
nodeId: node.id,
|
|
10557
|
+
nodeType: node.type,
|
|
10558
|
+
rendererToken,
|
|
10559
|
+
visible: true,
|
|
10560
|
+
disabled,
|
|
10561
|
+
resolvedProps,
|
|
10562
|
+
propsFingerprint: stableFingerprint(resolvedProps),
|
|
10563
|
+
layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, resolveNodeProps)),
|
|
10564
|
+
bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
|
|
10565
|
+
};
|
|
10566
|
+
}
|
|
10567
|
+
function applyDisabledState(element, disabled) {
|
|
10568
|
+
if (!disabled)
|
|
10569
|
+
return;
|
|
10570
|
+
element.setAttribute('data-disabled', 'true');
|
|
10571
|
+
element.style.background = '#F5F5F5';
|
|
10572
|
+
element.style.color = '#C0C0C0';
|
|
10573
|
+
element.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
10574
|
+
element.style.pointerEvents = 'none';
|
|
10575
|
+
element.style.cursor = 'default';
|
|
10576
|
+
}
|
|
10577
|
+
function applyDisabledDescendants(element, disabled) {
|
|
10578
|
+
if (!disabled)
|
|
10579
|
+
return;
|
|
10580
|
+
element.querySelectorAll('*').forEach((child) => {
|
|
10581
|
+
const htmlChild = child;
|
|
10582
|
+
htmlChild.setAttribute('data-disabled', 'true');
|
|
10583
|
+
htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
|
|
10584
|
+
});
|
|
10585
|
+
}
|
|
10586
|
+
function runStaticEventActions(steps, detail) {
|
|
10587
|
+
if (disposed)
|
|
10588
|
+
return;
|
|
10589
|
+
if (detail != null)
|
|
10590
|
+
variables._event = detail;
|
|
10591
|
+
const sourceVariables = variables;
|
|
10592
|
+
const actionAuthorityEpoch = hostAuthorityEpoch;
|
|
10593
|
+
const eventSequence = ++nextStaticEventSequence;
|
|
10594
|
+
const localVariables = Object.create(null);
|
|
10595
|
+
if (Object.prototype.hasOwnProperty.call(sourceVariables, '_event')) {
|
|
10596
|
+
localVariables._event = sourceVariables._event;
|
|
10597
|
+
}
|
|
10598
|
+
const deletedProperties = new Set();
|
|
10599
|
+
let frozenVariables = null;
|
|
10600
|
+
const eventVariables = new Proxy(localVariables, {
|
|
10601
|
+
get(target, property) {
|
|
10602
|
+
if (frozenVariables)
|
|
10603
|
+
return Reflect.get(frozenVariables, property);
|
|
10604
|
+
if (deletedProperties.has(property))
|
|
10605
|
+
return undefined;
|
|
10606
|
+
return Object.prototype.hasOwnProperty.call(target, property)
|
|
10607
|
+
? Reflect.get(target, property)
|
|
10608
|
+
: Reflect.get(sourceVariables, property);
|
|
10609
|
+
},
|
|
10610
|
+
set(target, property, value) {
|
|
10611
|
+
if (frozenVariables) {
|
|
10612
|
+
Reflect.set(frozenVariables, property, value);
|
|
10613
|
+
return true;
|
|
10614
|
+
}
|
|
10615
|
+
deletedProperties.delete(property);
|
|
10616
|
+
Reflect.set(target, property, value);
|
|
10617
|
+
if (property !== '_event'
|
|
10618
|
+
&& !disposed
|
|
10619
|
+
&& !publishing
|
|
10620
|
+
&& variables === sourceVariables
|
|
10621
|
+
&& actionAuthorityEpoch === hostAuthorityEpoch
|
|
10622
|
+
&& (latestStaticEventWriterByKey.get(property) === undefined
|
|
10623
|
+
|| latestStaticEventWriterByKey.get(property) <= eventSequence)) {
|
|
10624
|
+
latestStaticEventWriterByKey.set(property, eventSequence);
|
|
10625
|
+
Reflect.set(sourceVariables, property, value);
|
|
10626
|
+
}
|
|
10627
|
+
return true;
|
|
10628
|
+
},
|
|
10629
|
+
deleteProperty(target, property) {
|
|
10630
|
+
if (frozenVariables) {
|
|
10631
|
+
return Reflect.deleteProperty(frozenVariables, property);
|
|
10632
|
+
}
|
|
10633
|
+
deletedProperties.add(property);
|
|
10634
|
+
Reflect.deleteProperty(target, property);
|
|
10635
|
+
if (property !== '_event'
|
|
10636
|
+
&& !disposed
|
|
10637
|
+
&& !publishing
|
|
10638
|
+
&& variables === sourceVariables
|
|
10639
|
+
&& actionAuthorityEpoch === hostAuthorityEpoch
|
|
10640
|
+
&& (latestStaticEventWriterByKey.get(property) === undefined
|
|
10641
|
+
|| latestStaticEventWriterByKey.get(property) <= eventSequence)) {
|
|
10642
|
+
latestStaticEventWriterByKey.set(property, eventSequence);
|
|
10643
|
+
Reflect.deleteProperty(sourceVariables, property);
|
|
10644
|
+
}
|
|
10645
|
+
return true;
|
|
10646
|
+
},
|
|
10647
|
+
has(target, property) {
|
|
10648
|
+
if (!frozenVariables && deletedProperties.has(property))
|
|
10649
|
+
return false;
|
|
10650
|
+
return frozenVariables
|
|
10651
|
+
? property in frozenVariables
|
|
10652
|
+
: property in target || property in sourceVariables;
|
|
10653
|
+
},
|
|
10654
|
+
ownKeys(target) {
|
|
10655
|
+
return Reflect.ownKeys(frozenVariables ?? { ...sourceVariables, ...target }).filter(property => !deletedProperties.has(property));
|
|
10656
|
+
},
|
|
10657
|
+
getOwnPropertyDescriptor(target, property) {
|
|
10658
|
+
if (!frozenVariables && deletedProperties.has(property)) {
|
|
10659
|
+
return undefined;
|
|
10660
|
+
}
|
|
10661
|
+
const owner = frozenVariables
|
|
10662
|
+
?? (Object.prototype.hasOwnProperty.call(target, property)
|
|
10663
|
+
? target
|
|
10664
|
+
: sourceVariables);
|
|
10665
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, property);
|
|
10666
|
+
return descriptor ? { ...descriptor, configurable: true } : undefined;
|
|
10667
|
+
},
|
|
10668
|
+
});
|
|
10669
|
+
const pendingAutoFocusIds = new Set();
|
|
10670
|
+
void runActionSteps(steps, buildActionContext(pendingAutoFocusIds, eventVariables, eventSequence))
|
|
10671
|
+
.catch((error) => {
|
|
10672
|
+
console.error('[renderCard] Static action failed', error);
|
|
10673
|
+
})
|
|
10674
|
+
.finally(() => {
|
|
10675
|
+
const snapshot = {
|
|
10676
|
+
...sourceVariables,
|
|
10677
|
+
...localVariables,
|
|
10678
|
+
};
|
|
10679
|
+
for (const property of deletedProperties) {
|
|
10680
|
+
Reflect.deleteProperty(snapshot, property);
|
|
10681
|
+
}
|
|
10682
|
+
frozenVariables = snapshot;
|
|
10683
|
+
if (disposed || actionAuthorityEpoch !== hostAuthorityEpoch)
|
|
10684
|
+
return;
|
|
10685
|
+
applyAutoFocus(pendingAutoFocusIds);
|
|
10686
|
+
});
|
|
10687
|
+
}
|
|
10688
|
+
function syncStaticBindings(element, node) {
|
|
10689
|
+
const resolvedEventBindings = [];
|
|
10690
|
+
if (node.visible && !node.disabled && node.node.events) {
|
|
10691
|
+
for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
|
|
10692
|
+
if (!eventValue)
|
|
10693
|
+
continue;
|
|
10694
|
+
const steps = resolveActionRef(eventValue, schemaActions);
|
|
10695
|
+
if (!steps)
|
|
10696
|
+
continue;
|
|
10697
|
+
const domEvent = eventMap[schemaEvent] ?? schemaEvent;
|
|
10698
|
+
resolvedEventBindings.push({
|
|
10699
|
+
schemaEvent,
|
|
10700
|
+
domEvent,
|
|
10701
|
+
ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
|
|
10702
|
+
&& VALUE_CONTROL_TYPES.has(node.nodeType)),
|
|
10703
|
+
dispatch: (event) => {
|
|
10704
|
+
const detail = event instanceof CustomEvent
|
|
10705
|
+
? event.detail
|
|
10706
|
+
: undefined;
|
|
10707
|
+
runStaticEventActions(steps, detail);
|
|
10708
|
+
},
|
|
10709
|
+
});
|
|
10710
|
+
}
|
|
10711
|
+
}
|
|
10712
|
+
const variableKey = node.resolvedProps?.variableKey;
|
|
10713
|
+
syncElementBindings(element, {
|
|
10714
|
+
variableKey: typeof variableKey === 'string' ? variableKey : undefined,
|
|
10715
|
+
writeVariable: (key, value) => {
|
|
10716
|
+
if (disposed
|
|
10717
|
+
|| publishing
|
|
10718
|
+
|| !container.contains(element)) {
|
|
10719
|
+
return;
|
|
10720
|
+
}
|
|
10721
|
+
variables[key] = value;
|
|
10722
|
+
hostAuthorityEpoch += 1;
|
|
10723
|
+
},
|
|
10724
|
+
events: resolvedEventBindings,
|
|
10725
|
+
isActive: () => (!disposed
|
|
10726
|
+
&& !publishing
|
|
10727
|
+
&& container.contains(element)),
|
|
10728
|
+
});
|
|
10729
|
+
}
|
|
10730
|
+
function mountStaticSubtree(node, occurrenceKey) {
|
|
10731
|
+
const metadata = resolveStaticMetadata(node, occurrenceKey);
|
|
10732
|
+
if (!metadata.visible) {
|
|
10733
|
+
const placeholder = document.createElement('div');
|
|
10734
|
+
placeholder.style.display = 'none';
|
|
10735
|
+
placeholder.setAttribute('data-card-id', node.id);
|
|
10736
|
+
syncStaticBindings(placeholder, { ...metadata});
|
|
10737
|
+
return { ...metadata, element: placeholder, children: [] };
|
|
10738
|
+
}
|
|
10739
|
+
const renderer = metadata.rendererToken;
|
|
10740
|
+
const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
|
|
10741
|
+
try {
|
|
10742
|
+
applyDisabledState(element, metadata.disabled);
|
|
10743
|
+
syncStaticBindings(element, { ...metadata, children: [] });
|
|
10744
|
+
const children = [];
|
|
10745
|
+
const sameIdOrdinals = new Map();
|
|
10746
|
+
const renderChild = (child) => {
|
|
10747
|
+
const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
|
|
10748
|
+
sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
|
|
10749
|
+
const mounted = mountStaticSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal));
|
|
10750
|
+
children.push(mounted);
|
|
10751
|
+
return mounted.element;
|
|
10752
|
+
};
|
|
10753
|
+
const childrenMap = {};
|
|
10754
|
+
for (const child of node.children)
|
|
10755
|
+
childrenMap[child.id] = child;
|
|
10756
|
+
const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, renderChild, childrenMap, actionContext, resolveNodeProps);
|
|
10757
|
+
if (!layoutApplied) {
|
|
10758
|
+
for (const child of node.children) {
|
|
10759
|
+
element.appendChild(renderChild(child));
|
|
10760
|
+
}
|
|
10761
|
+
}
|
|
10762
|
+
applyDisabledDescendants(element, metadata.disabled);
|
|
10763
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
10764
|
+
return { ...metadata, element, children };
|
|
10765
|
+
}
|
|
10766
|
+
catch (error) {
|
|
10767
|
+
disposeStaticElement(element);
|
|
10768
|
+
throw error;
|
|
10769
|
+
}
|
|
10770
|
+
}
|
|
10771
|
+
function prepareStaticNode(current) {
|
|
10772
|
+
const metadata = resolveStaticMetadata(current.node, current.occurrenceKey);
|
|
10773
|
+
if (current.visible !== metadata.visible
|
|
10774
|
+
|| current.layoutFingerprint !== metadata.layoutFingerprint) {
|
|
10775
|
+
return { ...metadata, children: [] };
|
|
10776
|
+
}
|
|
10777
|
+
return {
|
|
10778
|
+
...metadata,
|
|
10779
|
+
children: current.children.map(prepareStaticNode),
|
|
10780
|
+
};
|
|
10781
|
+
}
|
|
10782
|
+
function mountStaticReplacement(current, next) {
|
|
10783
|
+
const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
|
|
10784
|
+
try {
|
|
10785
|
+
if (replacement.element !== current.element
|
|
10786
|
+
&& replacement.element.parentNode === null) {
|
|
10787
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
10788
|
+
}
|
|
10789
|
+
return replacement;
|
|
10790
|
+
}
|
|
10791
|
+
catch (error) {
|
|
10792
|
+
disposeStaticSubtree(replacement);
|
|
10793
|
+
throw error;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
/**
|
|
10797
|
+
* Build every renderer-owned replacement before touching the mounted DOM.
|
|
10798
|
+
* This preserves the old committed card when a later business renderer
|
|
10799
|
+
* rejects the same host update. Retained BaseElements are intentionally not
|
|
10800
|
+
* instantiated here: their native controls must stay untouched unless the
|
|
10801
|
+
* live update actually needs its existing replacement fallback.
|
|
10802
|
+
*/
|
|
10803
|
+
function stageStaticReplacements(current, next, staged) {
|
|
10804
|
+
const sameChildren = current.children.length === next.children.length
|
|
10805
|
+
&& current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
|
|
10806
|
+
const decision = sameChildren
|
|
10807
|
+
? decideStaticUpdate(current, next)
|
|
10808
|
+
: 'replace';
|
|
10809
|
+
if (decision === 'replace') {
|
|
10810
|
+
staged.set(next.occurrenceKey, mountStaticReplacement(current, next));
|
|
10811
|
+
}
|
|
10812
|
+
if (decision === 'replace')
|
|
10813
|
+
return;
|
|
10814
|
+
current.children.forEach((child, index) => {
|
|
10815
|
+
stageStaticReplacements(child, next.children[index], staged);
|
|
10816
|
+
});
|
|
10817
|
+
}
|
|
10818
|
+
function decideStaticUpdate(current, next) {
|
|
10819
|
+
if (current.occurrenceKey !== next.occurrenceKey
|
|
10820
|
+
|| current.nodeId !== next.nodeId
|
|
10821
|
+
|| current.nodeType !== next.nodeType
|
|
10822
|
+
|| current.rendererToken !== next.rendererToken
|
|
10823
|
+
|| current.visible !== next.visible
|
|
10824
|
+
|| current.disabled !== next.disabled
|
|
10825
|
+
|| current.layoutFingerprint !== next.layoutFingerprint) {
|
|
10826
|
+
return 'replace';
|
|
10827
|
+
}
|
|
10828
|
+
if (current.propsFingerprint === next.propsFingerprint
|
|
10829
|
+
&& current.bindingFingerprint === next.bindingFingerprint) {
|
|
10830
|
+
return 'retain';
|
|
10831
|
+
}
|
|
10832
|
+
if (current.element instanceof BaseElement
|
|
10833
|
+
|| current.rendererToken === componentRenderers._default) {
|
|
10834
|
+
return 'update';
|
|
10835
|
+
}
|
|
10836
|
+
return 'replace';
|
|
10837
|
+
}
|
|
10838
|
+
function updateStaticNode(current, next) {
|
|
10839
|
+
const element = current.element;
|
|
10840
|
+
if (element instanceof BaseElement) {
|
|
10841
|
+
updateSlotItemShellPresentation(element, () => {
|
|
10842
|
+
element.updateProps(next.resolvedProps, isMobile, options.responsive);
|
|
10843
|
+
applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
|
|
10844
|
+
}, getSlotHostPresentationOwnership(next.resolvedProps));
|
|
10845
|
+
return;
|
|
10846
|
+
}
|
|
10847
|
+
const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
|
|
10848
|
+
const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
|
|
10849
|
+
applyDisabledState(previous, current.disabled);
|
|
10850
|
+
applyDisabledState(desired, next.disabled);
|
|
10851
|
+
const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
|
|
10852
|
+
updateSlotItemShellPresentation(current.element, () => {
|
|
10853
|
+
patchElementShellPresentation(current.element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(current.element));
|
|
10854
|
+
}, hostOwnership);
|
|
10855
|
+
}
|
|
10856
|
+
function disposeStaticElement(element) {
|
|
10857
|
+
const cleanupSteps = [
|
|
10858
|
+
() => clearSlotOwnerBehaviorsIn(element),
|
|
10859
|
+
() => clearSlotItemPresentationsIn(element),
|
|
10860
|
+
() => clearElementBindingsIn(element),
|
|
10861
|
+
() => disposeChartsIn(element),
|
|
10862
|
+
];
|
|
10863
|
+
for (const cleanup of cleanupSteps) {
|
|
10864
|
+
try {
|
|
10865
|
+
cleanup();
|
|
10866
|
+
}
|
|
10867
|
+
catch (error) {
|
|
10868
|
+
console.error('[renderCard] Static element cleanup failed', error);
|
|
10869
|
+
}
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10872
|
+
function disposeStaticSubtree(current) {
|
|
10873
|
+
disposeStaticElement(current.element);
|
|
10874
|
+
}
|
|
10875
|
+
function collectMountedOccurrences(current, occurrences) {
|
|
10876
|
+
occurrences.set(current.occurrenceKey, current);
|
|
10877
|
+
for (const child of current.children) {
|
|
10878
|
+
collectMountedOccurrences(child, occurrences);
|
|
10879
|
+
}
|
|
10880
|
+
}
|
|
10881
|
+
function collectMountedLifecycles(current, collection) {
|
|
10882
|
+
if (!current.visible
|
|
10883
|
+
|| !container.contains(current.element)) {
|
|
10884
|
+
return;
|
|
10885
|
+
}
|
|
10886
|
+
if (current.node.lifecycle) {
|
|
10887
|
+
collection.push({
|
|
10888
|
+
id: current.nodeId,
|
|
10889
|
+
lifecycle: current.node.lifecycle,
|
|
10890
|
+
});
|
|
10891
|
+
}
|
|
10892
|
+
for (const child of current.children) {
|
|
10893
|
+
collectMountedLifecycles(child, collection);
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10896
|
+
function restoreDisabledDescendantPresentation(current) {
|
|
10897
|
+
if (current.disabled) {
|
|
10898
|
+
applyDisabledState(current.element, true);
|
|
10899
|
+
applyDisabledDescendants(current.element, true);
|
|
10900
|
+
}
|
|
10901
|
+
for (const child of current.children) {
|
|
10902
|
+
restoreDisabledDescendantPresentation(child);
|
|
10903
|
+
}
|
|
10904
|
+
}
|
|
10905
|
+
function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
|
|
10906
|
+
if (previous.element !== next.element) {
|
|
10907
|
+
restoreScrollPositions(next.element, scrollPositions);
|
|
10908
|
+
restoreMediaStates(next.element, mediaStates);
|
|
10909
|
+
return;
|
|
10910
|
+
}
|
|
10911
|
+
if (previous.propsFingerprint !== next.propsFingerprint
|
|
10912
|
+
&& (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
|
|
10913
|
+
restoreMediaStates(next.element, mediaStates);
|
|
10914
|
+
}
|
|
10915
|
+
for (let index = 0; index < next.children.length; index += 1) {
|
|
10916
|
+
const previousChild = previous.children[index];
|
|
10917
|
+
const nextChild = next.children[index];
|
|
10918
|
+
if (previousChild
|
|
10919
|
+
&& previousChild.occurrenceKey === nextChild.occurrenceKey) {
|
|
10920
|
+
restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
|
|
10921
|
+
}
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
8364
10924
|
function render() {
|
|
8365
10925
|
if (disposed)
|
|
8366
10926
|
return;
|
|
@@ -8368,28 +10928,85 @@ function renderStaticCard(container, schema, options) {
|
|
|
8368
10928
|
const collectedLifecycles = [];
|
|
8369
10929
|
const scrollPositions = captureScrollPositions(container);
|
|
8370
10930
|
const mediaStates = captureMediaStates(container);
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
10931
|
+
let previousTree = null;
|
|
10932
|
+
if (!mountedTree) {
|
|
10933
|
+
const initialTree = mountStaticSubtree(tree, `${tree.id}#0`);
|
|
10934
|
+
if (disposed || generation !== renderGeneration) {
|
|
10935
|
+
disposeStaticSubtree(initialTree);
|
|
8374
10936
|
return;
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
10937
|
+
}
|
|
10938
|
+
disposeChartsIn(container);
|
|
10939
|
+
container.replaceChildren(initialTree.element);
|
|
10940
|
+
mountedTree = initialTree;
|
|
10941
|
+
}
|
|
10942
|
+
else {
|
|
10943
|
+
previousTree = mountedTree;
|
|
10944
|
+
let unreconciled = false;
|
|
10945
|
+
let unreconciledError;
|
|
10946
|
+
const replacementSources = new Map();
|
|
10947
|
+
collectMountedOccurrences(mountedTree, replacementSources);
|
|
10948
|
+
const preparedTree = prepareStaticNode(mountedTree);
|
|
10949
|
+
if (disposed || generation !== renderGeneration)
|
|
10950
|
+
return;
|
|
10951
|
+
const stagedReplacements = new Map();
|
|
10952
|
+
publishing = true;
|
|
10953
|
+
try {
|
|
10954
|
+
stageStaticReplacements(mountedTree, preparedTree, stagedReplacements);
|
|
10955
|
+
if (disposeRequested)
|
|
10956
|
+
return;
|
|
10957
|
+
mountedTree = reconcileIncrementalNode(mountedTree, preparedTree, {
|
|
10958
|
+
decide: decideStaticUpdate,
|
|
10959
|
+
update: updateStaticNode,
|
|
10960
|
+
mountReplacement: next => {
|
|
10961
|
+
const staged = stagedReplacements.get(next.occurrenceKey);
|
|
10962
|
+
if (staged) {
|
|
10963
|
+
stagedReplacements.delete(next.occurrenceKey);
|
|
10964
|
+
return staged;
|
|
10965
|
+
}
|
|
10966
|
+
const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
|
|
10967
|
+
const current = replacementSources.get(next.occurrenceKey);
|
|
10968
|
+
if (current
|
|
10969
|
+
&& replacement.element !== current.element
|
|
10970
|
+
&& replacement.element.parentNode === null) {
|
|
10971
|
+
transferSlotItemPresentation(current.element, replacement.element);
|
|
10972
|
+
}
|
|
10973
|
+
return replacement;
|
|
10974
|
+
},
|
|
10975
|
+
dispose: disposeStaticSubtree,
|
|
10976
|
+
syncBindings: (current, next) => {
|
|
10977
|
+
syncStaticBindings(current.element, next);
|
|
10978
|
+
},
|
|
10979
|
+
onUnreconciled: (error) => {
|
|
10980
|
+
if (unreconciled)
|
|
10981
|
+
return;
|
|
10982
|
+
unreconciled = true;
|
|
10983
|
+
unreconciledError = error ?? new Error('[renderCard] Static incremental update could not be reconciled');
|
|
10984
|
+
},
|
|
10985
|
+
});
|
|
10986
|
+
}
|
|
10987
|
+
finally {
|
|
10988
|
+
for (const replacement of stagedReplacements.values()) {
|
|
10989
|
+
disposeStaticSubtree(replacement);
|
|
10990
|
+
}
|
|
10991
|
+
stagedReplacements.clear();
|
|
10992
|
+
publishing = false;
|
|
10993
|
+
if (disposeRequested)
|
|
10994
|
+
disposeNow();
|
|
10995
|
+
}
|
|
10996
|
+
if (unreconciled)
|
|
10997
|
+
throw unreconciledError;
|
|
8378
10998
|
if (disposed)
|
|
8379
10999
|
return;
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
}
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
container.replaceChildren(dom);
|
|
8391
|
-
restoreScrollPositions(container, scrollPositions);
|
|
8392
|
-
restoreMediaStates(container, mediaStates);
|
|
11000
|
+
}
|
|
11001
|
+
restoreDisabledDescendantPresentation(mountedTree);
|
|
11002
|
+
collectMountedLifecycles(mountedTree, collectedLifecycles);
|
|
11003
|
+
if (previousTree) {
|
|
11004
|
+
restoreChangedSubtreeState(previousTree, mountedTree, scrollPositions, mediaStates);
|
|
11005
|
+
}
|
|
11006
|
+
else {
|
|
11007
|
+
restoreScrollPositions(container, scrollPositions);
|
|
11008
|
+
restoreMediaStates(container, mediaStates);
|
|
11009
|
+
}
|
|
8393
11010
|
if (disposed || generation !== renderGeneration)
|
|
8394
11011
|
return;
|
|
8395
11012
|
for (const { id, lifecycle } of collectedLifecycles) {
|
|
@@ -8404,27 +11021,63 @@ function renderStaticCard(container, schema, options) {
|
|
|
8404
11021
|
actionContext = buildActionContext();
|
|
8405
11022
|
render();
|
|
8406
11023
|
}
|
|
11024
|
+
function disposeNow() {
|
|
11025
|
+
if (disposed)
|
|
11026
|
+
return;
|
|
11027
|
+
disposed = true;
|
|
11028
|
+
disposeRequested = false;
|
|
11029
|
+
hostAuthorityEpoch += 1;
|
|
11030
|
+
nodeAccess.dispose();
|
|
11031
|
+
abortController.abort();
|
|
11032
|
+
if (mountedTree) {
|
|
11033
|
+
disposeStaticSubtree(mountedTree);
|
|
11034
|
+
mountedTree = null;
|
|
11035
|
+
}
|
|
11036
|
+
else {
|
|
11037
|
+
disposeChartsIn(container);
|
|
11038
|
+
}
|
|
11039
|
+
container.replaceChildren();
|
|
11040
|
+
for (const record of lifecycleRecords.values())
|
|
11041
|
+
destroyLifecycle(record);
|
|
11042
|
+
}
|
|
8407
11043
|
// 7. Initial render
|
|
8408
11044
|
render();
|
|
8409
11045
|
// 8. Return instance handle
|
|
8410
11046
|
return {
|
|
11047
|
+
getNode: nodeAccess.getNode,
|
|
11048
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
8411
11049
|
dispose() {
|
|
8412
|
-
if (disposed)
|
|
11050
|
+
if (disposed || disposeRequested)
|
|
8413
11051
|
return;
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
for (const record of lifecycleRecords.values())
|
|
8420
|
-
destroyLifecycle(record);
|
|
11052
|
+
if (publishing) {
|
|
11053
|
+
disposeRequested = true;
|
|
11054
|
+
return;
|
|
11055
|
+
}
|
|
11056
|
+
disposeNow();
|
|
8421
11057
|
},
|
|
8422
11058
|
updateVariables(newVars) {
|
|
8423
11059
|
if (disposed)
|
|
8424
11060
|
return;
|
|
11061
|
+
if (publishing) {
|
|
11062
|
+
const error = new Error('[renderCard] STATIC_TRANSACTION_CONFLICT');
|
|
11063
|
+
error.code = 'STATIC_TRANSACTION_CONFLICT';
|
|
11064
|
+
throw error;
|
|
11065
|
+
}
|
|
11066
|
+
const previousVariables = variables;
|
|
11067
|
+
const previousAuthorityEpoch = hostAuthorityEpoch;
|
|
8425
11068
|
hostAuthorityEpoch += 1;
|
|
8426
11069
|
variables = { ...variables, ...newVars };
|
|
8427
|
-
|
|
11070
|
+
try {
|
|
11071
|
+
rerender();
|
|
11072
|
+
}
|
|
11073
|
+
catch (error) {
|
|
11074
|
+
if (!disposed) {
|
|
11075
|
+
variables = previousVariables;
|
|
11076
|
+
hostAuthorityEpoch = previousAuthorityEpoch;
|
|
11077
|
+
actionContext = buildActionContext();
|
|
11078
|
+
}
|
|
11079
|
+
throw error;
|
|
11080
|
+
}
|
|
8428
11081
|
},
|
|
8429
11082
|
};
|
|
8430
11083
|
}
|
|
@@ -8450,7 +11103,11 @@ function captureScrollPositions(root) {
|
|
|
8450
11103
|
function restoreScrollPositions(root, positions) {
|
|
8451
11104
|
if (positions.size === 0)
|
|
8452
11105
|
return;
|
|
8453
|
-
|
|
11106
|
+
const scrollers = [];
|
|
11107
|
+
if (root.matches('[data-scroll-id]'))
|
|
11108
|
+
scrollers.push(root);
|
|
11109
|
+
scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
|
|
11110
|
+
scrollers.forEach((el) => {
|
|
8454
11111
|
const id = el.getAttribute('data-scroll-id');
|
|
8455
11112
|
const saved = id ? positions.get(id) : undefined;
|
|
8456
11113
|
if (saved == null)
|
|
@@ -8490,7 +11147,11 @@ function captureMediaStates(root) {
|
|
|
8490
11147
|
function restoreMediaStates(root, states) {
|
|
8491
11148
|
if (states.size === 0)
|
|
8492
11149
|
return;
|
|
8493
|
-
|
|
11150
|
+
const mediaHosts = [];
|
|
11151
|
+
if (root.matches('ai-card-audio, ai-card-video'))
|
|
11152
|
+
mediaHosts.push(root);
|
|
11153
|
+
mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
|
|
11154
|
+
mediaHosts.forEach((el) => {
|
|
8494
11155
|
const id = el.getAttribute('data-card-id');
|
|
8495
11156
|
const snap = id ? states.get(id) : undefined;
|
|
8496
11157
|
if (!snap)
|
|
@@ -8499,121 +11160,6 @@ function restoreMediaStates(root, states) {
|
|
|
8499
11160
|
restoreMedia(media, snap);
|
|
8500
11161
|
});
|
|
8501
11162
|
}
|
|
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
11163
|
/** Map schema event names → DOM event names */
|
|
8618
11164
|
const eventMap = {
|
|
8619
11165
|
onClick: 'click',
|
|
@@ -8667,6 +11213,7 @@ const VALUE_CONTROL_TYPES = new Set([
|
|
|
8667
11213
|
* ```
|
|
8668
11214
|
*/
|
|
8669
11215
|
function renderStreamingCard(container, options = {}) {
|
|
11216
|
+
const nodeAccess = createCardNodeAccess(container);
|
|
8670
11217
|
// ─── State ──────────────────────────────────────────────────────
|
|
8671
11218
|
const parser = new StreamingParser(options.parserOptions);
|
|
8672
11219
|
const elementMap = new Map(); // elementId → DOM element
|
|
@@ -8694,19 +11241,32 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8694
11241
|
let boundActionQueue = Promise.resolve();
|
|
8695
11242
|
let boundLifecycleEpoch = 0;
|
|
8696
11243
|
let disposed = false;
|
|
11244
|
+
let userActionDepth = 0;
|
|
8697
11245
|
const sourceOccurrences = new Map();
|
|
8698
11246
|
const activeBoundLifecycles = new Map();
|
|
8699
11247
|
const mountedBoundLifecycles = new Map();
|
|
8700
11248
|
const boundLifecycleGenerations = new Map();
|
|
11249
|
+
function applyAutoFocusIds(ids) {
|
|
11250
|
+
if (ids.size === 0)
|
|
11251
|
+
return;
|
|
11252
|
+
for (const id of ids) {
|
|
11253
|
+
const input = elementMap.get(id);
|
|
11254
|
+
if (input instanceof CardInput && input.requestAutoFocus())
|
|
11255
|
+
break;
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
8701
11258
|
// ─── Action Context ─────────────────────────────────────────────
|
|
8702
11259
|
function buildActionContext() {
|
|
8703
11260
|
return {
|
|
8704
11261
|
...createWebActionContext({
|
|
8705
11262
|
...options,
|
|
8706
11263
|
setVariable: (key, value) => {
|
|
11264
|
+
const previousVariables = userActionDepth > 0
|
|
11265
|
+
? { ...variables }
|
|
11266
|
+
: undefined;
|
|
8707
11267
|
variables[key] = value;
|
|
8708
11268
|
// On variable change, patch only affected elements (keyed diff)
|
|
8709
|
-
diffAllElements();
|
|
11269
|
+
diffAllElements(previousVariables);
|
|
8710
11270
|
},
|
|
8711
11271
|
abortSignal: abortController.signal,
|
|
8712
11272
|
}),
|
|
@@ -8789,6 +11349,27 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8789
11349
|
|| resolved === ''
|
|
8790
11350
|
|| resolved === 0);
|
|
8791
11351
|
}
|
|
11352
|
+
function collectBoundAutoFocusRevealIds(previous, next, previousVariables, nextVariables) {
|
|
11353
|
+
const previousNodes = indexBoundNodes(previous.root);
|
|
11354
|
+
const requested = new Set();
|
|
11355
|
+
const visit = (nextNode, previousParentVisible, nextParentVisible) => {
|
|
11356
|
+
const previousNode = previousNodes.get(nextNode.id);
|
|
11357
|
+
const wasVisible = Boolean(previousNode
|
|
11358
|
+
&& previousParentVisible
|
|
11359
|
+
&& computeBoundVisible(previousNode, previousVariables));
|
|
11360
|
+
const nowVisible = nextParentVisible
|
|
11361
|
+
&& computeBoundVisible(nextNode, nextVariables);
|
|
11362
|
+
if (nextNode.type === 'Input'
|
|
11363
|
+
&& !wasVisible
|
|
11364
|
+
&& nowVisible
|
|
11365
|
+
&& resolveBoundNodeProps(nextNode, nextVariables).autoFocus === true) {
|
|
11366
|
+
requested.add(nextNode.id);
|
|
11367
|
+
}
|
|
11368
|
+
nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
11369
|
+
};
|
|
11370
|
+
visit(next.root, true, true);
|
|
11371
|
+
return requested;
|
|
11372
|
+
}
|
|
8792
11373
|
function computeBoundDisabled(node, renderVariables) {
|
|
8793
11374
|
const disabled = node.directives?.disabled;
|
|
8794
11375
|
if (!disabled)
|
|
@@ -8964,7 +11545,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
8964
11545
|
if (e instanceof CustomEvent && e.detail != null) {
|
|
8965
11546
|
variables._event = e.detail;
|
|
8966
11547
|
}
|
|
8967
|
-
|
|
11548
|
+
userActionDepth += 1;
|
|
11549
|
+
void runSteps(resolvedSteps, node.id).finally(() => {
|
|
11550
|
+
userActionDepth -= 1;
|
|
11551
|
+
});
|
|
8968
11552
|
}));
|
|
8969
11553
|
}
|
|
8970
11554
|
}
|
|
@@ -9019,6 +11603,43 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9019
11603
|
: visibleExpr;
|
|
9020
11604
|
return !(resolved === false || resolved === 'false' || resolved === '' || resolved === 0);
|
|
9021
11605
|
}
|
|
11606
|
+
function collectLegacyAutoFocusRevealIds(schema, previousVariables) {
|
|
11607
|
+
let tree;
|
|
11608
|
+
try {
|
|
11609
|
+
tree = parseSchema(schema);
|
|
11610
|
+
}
|
|
11611
|
+
catch {
|
|
11612
|
+
return new Set();
|
|
11613
|
+
}
|
|
11614
|
+
const requested = new Set();
|
|
11615
|
+
const ownVisibility = (node, renderVariables) => {
|
|
11616
|
+
const expression = node.directives?.visible;
|
|
11617
|
+
if (!expression)
|
|
11618
|
+
return true;
|
|
11619
|
+
const resolved = hasExpression(expression)
|
|
11620
|
+
? resolveExpression(expression, renderVariables)
|
|
11621
|
+
: expression;
|
|
11622
|
+
return !(resolved === false
|
|
11623
|
+
|| resolved === 'false'
|
|
11624
|
+
|| resolved === ''
|
|
11625
|
+
|| resolved === 0);
|
|
11626
|
+
};
|
|
11627
|
+
const visit = (node, previousParentVisible, nextParentVisible) => {
|
|
11628
|
+
const wasVisible = previousParentVisible
|
|
11629
|
+
&& ownVisibility(node, previousVariables);
|
|
11630
|
+
const nowVisible = nextParentVisible && ownVisibility(node, variables);
|
|
11631
|
+
if (node.type === 'Input'
|
|
11632
|
+
&& !wasVisible
|
|
11633
|
+
&& nowVisible
|
|
11634
|
+
&& resolveDeep(node.props, variables)
|
|
11635
|
+
.autoFocus === true) {
|
|
11636
|
+
requested.add(node.id);
|
|
11637
|
+
}
|
|
11638
|
+
node.children.forEach(child => visit(child, wasVisible, nowVisible));
|
|
11639
|
+
};
|
|
11640
|
+
visit(tree, true, true);
|
|
11641
|
+
return requested;
|
|
11642
|
+
}
|
|
9022
11643
|
/** Entrance transition for blocks streamed in incrementally. */
|
|
9023
11644
|
function animateEnter(el) {
|
|
9024
11645
|
if (options.appearTransition === false)
|
|
@@ -9092,11 +11713,14 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9092
11713
|
* (visibility flips / container shape changes) re-render only their subtree.
|
|
9093
11714
|
* Falls back to a full render solely when a subtree can't be rebuilt.
|
|
9094
11715
|
*/
|
|
9095
|
-
function diffAllElements() {
|
|
11716
|
+
function diffAllElements(previousVariables) {
|
|
9096
11717
|
actionContext = buildActionContext();
|
|
9097
11718
|
const schema = (currentSurfaceId ? engine.getSchema(currentSurfaceId) : undefined) ?? currentSchema;
|
|
9098
11719
|
if (!schema)
|
|
9099
11720
|
return;
|
|
11721
|
+
const autoFocusIds = previousVariables
|
|
11722
|
+
? collectLegacyAutoFocusRevealIds(schema, previousVariables)
|
|
11723
|
+
: new Set();
|
|
9100
11724
|
for (const [id, element] of Object.entries(schema.elements)) {
|
|
9101
11725
|
const el = elementMap.get(id);
|
|
9102
11726
|
if (!el)
|
|
@@ -9107,6 +11731,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9107
11731
|
// Visibility flipped (either direction) → rebuild this subtree in place
|
|
9108
11732
|
if (!replaceSubtree(schema, id, el)) {
|
|
9109
11733
|
safeRenderFull();
|
|
11734
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9110
11735
|
return;
|
|
9111
11736
|
}
|
|
9112
11737
|
continue;
|
|
@@ -9123,9 +11748,11 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9123
11748
|
}
|
|
9124
11749
|
else if (!replaceSubtree(schema, id, el)) {
|
|
9125
11750
|
safeRenderFull();
|
|
11751
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9126
11752
|
return;
|
|
9127
11753
|
}
|
|
9128
11754
|
}
|
|
11755
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9129
11756
|
}
|
|
9130
11757
|
function captureBoundScrollPositions(root) {
|
|
9131
11758
|
const positions = new Map();
|
|
@@ -9848,7 +12475,10 @@ function renderStreamingCard(container, options = {}) {
|
|
|
9848
12475
|
if (!steps)
|
|
9849
12476
|
return;
|
|
9850
12477
|
await runBoundSteps(steps, sourceNode, createBoundActionContext(sourceNode, draft));
|
|
12478
|
+
const nextMaterialized = materializeStreamingCard(currentSchema, draft);
|
|
12479
|
+
const autoFocusIds = collectBoundAutoFocusRevealIds(currentMaterialized, nextMaterialized, before, draft);
|
|
9851
12480
|
commitBoundDraftTransaction(before, draft, baseRevision);
|
|
12481
|
+
applyAutoFocusIds(autoFocusIds);
|
|
9852
12482
|
}
|
|
9853
12483
|
function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
|
|
9854
12484
|
boundActionQueue = boundActionQueue
|
|
@@ -10430,6 +13060,8 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10430
13060
|
});
|
|
10431
13061
|
// ─── Public API ─────────────────────────────────────────────────
|
|
10432
13062
|
return {
|
|
13063
|
+
getNode: nodeAccess.getNode,
|
|
13064
|
+
onFocusChange: nodeAccess.onFocusChange,
|
|
10433
13065
|
applyCommand(command) {
|
|
10434
13066
|
if (!lockMode('commands'))
|
|
10435
13067
|
return;
|
|
@@ -10506,6 +13138,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
10506
13138
|
if (disposed)
|
|
10507
13139
|
return;
|
|
10508
13140
|
disposed = true;
|
|
13141
|
+
nodeAccess.dispose();
|
|
10509
13142
|
boundRevision += 1;
|
|
10510
13143
|
abortController.abort();
|
|
10511
13144
|
teardownBoundLifecycles();
|