@antglobal/copilot-cards-web 0.0.20260828032020-dev.1 → 0.0.20260828032020-dev.5
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 +34 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +721 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -113,10 +113,44 @@ Renders a complete schema and returns a `CardInstance`:
|
|
|
113
113
|
```ts
|
|
114
114
|
interface CardInstance {
|
|
115
115
|
updateVariables(variables: Record<string, unknown>): void;
|
|
116
|
+
onScroll(nodeId: string, listener: CardScrollListener): () => void;
|
|
116
117
|
dispose(): void;
|
|
117
118
|
}
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
`CardScrollEvent` and `CardScrollListener` are top-level package exports. The
|
|
122
|
+
event contains readonly `node`, `scrollTop`, `scrollLeft`, `scrollHeight`,
|
|
123
|
+
`scrollWidth`, `clientHeight`, and `clientWidth` values:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import type {
|
|
127
|
+
CardScrollEvent,
|
|
128
|
+
CardScrollListener,
|
|
129
|
+
} from "@antglobal/copilot-cards-web";
|
|
130
|
+
|
|
131
|
+
const unsubscribe = card.onScroll("friendsList", event => {
|
|
132
|
+
const remaining =
|
|
133
|
+
event.scrollHeight - event.clientHeight - event.scrollTop;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
unsubscribe(); // Idempotent; card.dispose() also removes the subscription.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The subscription follows the logical node ID across physical replacement and
|
|
140
|
+
reports native-frequency values. It does not emit an initial, render, resize,
|
|
141
|
+
or `updateVariables` callback. Generated internal scroll tracks and scroll
|
|
142
|
+
ports inside Shadow DOM are not observed. Thresholds, throttling, requests,
|
|
143
|
+
locks, cursors, retries, and deduplication remain host responsibilities.
|
|
144
|
+
|
|
145
|
+
For pagination, continue passing the complete next array to `updateVariables`.
|
|
146
|
+
The SDK retains the Repeat owner and old prefix only when it can strictly prove
|
|
147
|
+
a structurally equal tail extension whose retained rendering is unchanged, on
|
|
148
|
+
a default-rendered `Container` with an unambiguous direct-child `default` or
|
|
149
|
+
`flex` layout. Every unproven case uses the existing replacement behavior; this
|
|
150
|
+
is not a general keyed diff or public append API. A trailing control object is
|
|
151
|
+
eligible only when it does not change retained rows. Put temporary loading UI
|
|
152
|
+
outside the Repeat owner when scroll identity matters.
|
|
153
|
+
|
|
120
154
|
Common render options include:
|
|
121
155
|
|
|
122
156
|
| Option | Purpose |
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,23 @@ interface CardFocusChangeEvent {
|
|
|
55
55
|
previousNode: CardNodeHandle | null;
|
|
56
56
|
}
|
|
57
57
|
type CardFocusChangeListener = (event: CardFocusChangeEvent) => void;
|
|
58
|
+
interface CardScrollEvent {
|
|
59
|
+
/** Stable logical handle for the scrolling card node. */
|
|
60
|
+
readonly node: CardNodeHandle;
|
|
61
|
+
/** Raw native vertical scroll offset. */
|
|
62
|
+
readonly scrollTop: number;
|
|
63
|
+
/** Raw native horizontal scroll offset, including negative RTL values. */
|
|
64
|
+
readonly scrollLeft: number;
|
|
65
|
+
/** Raw native scrollable content height. */
|
|
66
|
+
readonly scrollHeight: number;
|
|
67
|
+
/** Raw native scrollable content width. */
|
|
68
|
+
readonly scrollWidth: number;
|
|
69
|
+
/** Raw native viewport height. */
|
|
70
|
+
readonly clientHeight: number;
|
|
71
|
+
/** Raw native viewport width. */
|
|
72
|
+
readonly clientWidth: number;
|
|
73
|
+
}
|
|
74
|
+
type CardScrollListener = (event: CardScrollEvent) => void;
|
|
58
75
|
|
|
59
76
|
interface ResponsiveMobileOptions {
|
|
60
77
|
readonly unit: 'rem';
|
|
@@ -108,6 +125,8 @@ interface CardInstance {
|
|
|
108
125
|
getNode: (id: string) => CardNodeHandle;
|
|
109
126
|
/** Observe logical focus changes within this card. */
|
|
110
127
|
onFocusChange: (listener: CardFocusChangeListener) => () => void;
|
|
128
|
+
/** Observe native scroll metrics for one logical card node. */
|
|
129
|
+
onScroll: (nodeId: string, listener: CardScrollListener) => () => void;
|
|
111
130
|
}
|
|
112
131
|
/**
|
|
113
132
|
* Render a card schema into a container element.
|
|
@@ -1841,4 +1860,4 @@ declare const componentRenderers: Record<string, ComponentRenderer>;
|
|
|
1841
1860
|
declare function registerComponent(type: string, renderer: ComponentRenderer): void;
|
|
1842
1861
|
|
|
1843
1862
|
export { BaseElement, BotSDK, CardButton, CardChoiceItem, CardChoiceList, CardCollapse, CardCounter, CardDivider, CardForm, CardHtml, CardIcon, CardImage, CardInput, CardLoading, CardPasscodeInput, CardProgress, CardRate, CardSelect, CardSteps, CardSwitch, CardTag, CardText, DEFAULT_MOBILE_RESPONSIVE, LocalActionConfigProvider, RemoteActionConfigProvider, buildStyleString, componentRenderers, connectSSE, connectStreaming, convertPixelTokens, createResponsiveContext, createWebActionContext, isMobileViewport, onViewportChange, pxToRem, pxToVw, registerComponent, renderCard, renderStreamingCard, resolveSize, sanitizeHtml, trimIncompleteTag };
|
|
1844
|
-
export type { A2UIActionPayload, BotSDKOptions, ButtonProps, CardFocusChangeEvent, CardFocusChangeListener, CardInstance, CardNodeHandle, ChoiceIndicatorIcon, ChoiceItemProps, ChoiceListProps, ChoiceListValue, CollapseProps, ComponentRenderer, ConnectorConfig, CounterIcon, CounterProps, DividerProps, FormField, FormProps, FormRule, HtmlProps, IconProps, ImageProps, InputAffix, InputAffixConfig, InputProps, LoadingProps, PartialFinalizeResult, PasscodeInputProps, ProgressProps, ProgressSegment, RateProps, RenderCardOptions, ResponsiveContext, ResponsiveMobileOptions, ResponsiveOptions, SSEConnectOptions, SelectOption, SelectProps, SelectSize, SelectStyles, SelectVariant, StepIcon, StepItem, StepsProps, StreamingCardInstance, StreamingCardOptions, StreamingConnectOptions, StreamingConnection, SwitchProps, TagProps, TextProps, WebActionContextOptions };
|
|
1863
|
+
export type { A2UIActionPayload, BotSDKOptions, ButtonProps, CardFocusChangeEvent, CardFocusChangeListener, CardInstance, CardNodeHandle, CardScrollEvent, CardScrollListener, ChoiceIndicatorIcon, ChoiceItemProps, ChoiceListProps, ChoiceListValue, CollapseProps, ComponentRenderer, ConnectorConfig, CounterIcon, CounterProps, DividerProps, FormField, FormProps, FormRule, HtmlProps, IconProps, ImageProps, InputAffix, InputAffixConfig, InputProps, LoadingProps, PartialFinalizeResult, PasscodeInputProps, ProgressProps, ProgressSegment, RateProps, RenderCardOptions, ResponsiveContext, ResponsiveMobileOptions, ResponsiveOptions, SSEConnectOptions, SelectOption, SelectProps, SelectSize, SelectStyles, SelectVariant, StepIcon, StepItem, StepsProps, StreamingCardInstance, StreamingCardOptions, StreamingConnectOptions, StreamingConnection, SwitchProps, TagProps, TextProps, WebActionContextOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, findAffectedRepeatOwners, createA2UIParameterResolver, createExpressionContext, replaceRootContents, getByJsonPointer, 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';
|
|
@@ -763,9 +763,33 @@ const CAROUSEL_HOST_PRESENTATION_OWNERSHIP = {
|
|
|
763
763
|
attributes: [CAROUSEL_SCROLL_ID_ATTRIBUTE],
|
|
764
764
|
styleProperties: Object.keys(carouselHostStyles(0)).map(toCSSPropertyName),
|
|
765
765
|
};
|
|
766
|
+
function getNonDefaultSlotKeys(props) {
|
|
767
|
+
const slots = props.slots;
|
|
768
|
+
return slots
|
|
769
|
+
? Object.keys(slots).filter(key => key !== SLOT_LAYOUT.DEFAULT)
|
|
770
|
+
: [];
|
|
771
|
+
}
|
|
766
772
|
function findSpecialSlotKey(props) {
|
|
773
|
+
return getNonDefaultSlotKeys(props)[0];
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Report whether a materialized Repeat slot maps directly onto its owner host.
|
|
777
|
+
* Ambiguous and wrapper/controller layouts deliberately fail closed.
|
|
778
|
+
*/
|
|
779
|
+
function getDirectChildRepeatAppendLayout(props, repeatSlot) {
|
|
767
780
|
const slots = props.slots;
|
|
768
|
-
|
|
781
|
+
if (!slots || slots[repeatSlot] == null)
|
|
782
|
+
return undefined;
|
|
783
|
+
const nonDefaultKeys = getNonDefaultSlotKeys(props);
|
|
784
|
+
if (nonDefaultKeys.length > 1)
|
|
785
|
+
return undefined;
|
|
786
|
+
const effectiveSlot = findSpecialSlotKey(props) ?? SLOT_LAYOUT.DEFAULT;
|
|
787
|
+
if (effectiveSlot !== repeatSlot)
|
|
788
|
+
return undefined;
|
|
789
|
+
return effectiveSlot === SLOT_LAYOUT.DEFAULT
|
|
790
|
+
|| effectiveSlot === SLOT_LAYOUT.FLEX
|
|
791
|
+
? effectiveSlot
|
|
792
|
+
: undefined;
|
|
769
793
|
}
|
|
770
794
|
/** Host attributes/styles owned by the active slot renderer after shell render. */
|
|
771
795
|
function getSlotHostPresentationOwnership(props) {
|
|
@@ -8917,12 +8941,14 @@ const FOCUS_TARGET_SELECTOR = [
|
|
|
8917
8941
|
*/
|
|
8918
8942
|
function createCardNodeAccess(container) {
|
|
8919
8943
|
const handles = new Map();
|
|
8920
|
-
const
|
|
8944
|
+
const focusListeners = new Set();
|
|
8945
|
+
const scrollSubscriptions = new Map();
|
|
8921
8946
|
let focusedId = null;
|
|
8922
8947
|
let focusSyncScheduled = false;
|
|
8923
|
-
let
|
|
8948
|
+
let focusObserving = false;
|
|
8949
|
+
let scrollObserving = false;
|
|
8924
8950
|
let observationEpoch = 0;
|
|
8925
|
-
let
|
|
8951
|
+
let focusObserver = null;
|
|
8926
8952
|
let disposed = false;
|
|
8927
8953
|
const resolveElement = (id) => {
|
|
8928
8954
|
if (disposed)
|
|
@@ -8993,7 +9019,7 @@ function createCardNodeAccess(container) {
|
|
|
8993
9019
|
return null;
|
|
8994
9020
|
};
|
|
8995
9021
|
const syncFocus = () => {
|
|
8996
|
-
if (disposed || !
|
|
9022
|
+
if (disposed || !focusObserving)
|
|
8997
9023
|
return;
|
|
8998
9024
|
const nextId = resolveFocusedId();
|
|
8999
9025
|
if (nextId === focusedId)
|
|
@@ -9001,7 +9027,7 @@ function createCardNodeAccess(container) {
|
|
|
9001
9027
|
const previousNode = focusedId ? getNode(focusedId) : null;
|
|
9002
9028
|
const node = nextId ? getNode(nextId) : null;
|
|
9003
9029
|
focusedId = nextId;
|
|
9004
|
-
for (const listener of [...
|
|
9030
|
+
for (const listener of [...focusListeners]) {
|
|
9005
9031
|
try {
|
|
9006
9032
|
listener({ node, previousNode });
|
|
9007
9033
|
}
|
|
@@ -9011,7 +9037,7 @@ function createCardNodeAccess(container) {
|
|
|
9011
9037
|
}
|
|
9012
9038
|
};
|
|
9013
9039
|
const scheduleFocusSync = () => {
|
|
9014
|
-
if (disposed || !
|
|
9040
|
+
if (disposed || !focusObserving || focusSyncScheduled)
|
|
9015
9041
|
return;
|
|
9016
9042
|
focusSyncScheduled = true;
|
|
9017
9043
|
const scheduledEpoch = observationEpoch;
|
|
@@ -9022,29 +9048,29 @@ function createCardNodeAccess(container) {
|
|
|
9022
9048
|
syncFocus();
|
|
9023
9049
|
});
|
|
9024
9050
|
};
|
|
9025
|
-
const
|
|
9026
|
-
if (disposed ||
|
|
9051
|
+
const startFocusObserving = () => {
|
|
9052
|
+
if (disposed || focusObserving)
|
|
9027
9053
|
return;
|
|
9028
|
-
|
|
9054
|
+
focusObserving = true;
|
|
9029
9055
|
observationEpoch += 1;
|
|
9030
9056
|
focusedId = resolveFocusedId();
|
|
9031
9057
|
container.addEventListener('focusin', scheduleFocusSync, true);
|
|
9032
9058
|
container.addEventListener('focusout', scheduleFocusSync, true);
|
|
9033
9059
|
if (typeof MutationObserver !== 'undefined') {
|
|
9034
|
-
|
|
9035
|
-
|
|
9060
|
+
focusObserver ?? (focusObserver = new MutationObserver(scheduleFocusSync));
|
|
9061
|
+
focusObserver.observe(container, { childList: true, subtree: true });
|
|
9036
9062
|
}
|
|
9037
9063
|
};
|
|
9038
|
-
const
|
|
9039
|
-
if (!
|
|
9064
|
+
const stopFocusObserving = () => {
|
|
9065
|
+
if (!focusObserving)
|
|
9040
9066
|
return;
|
|
9041
|
-
|
|
9067
|
+
focusObserving = false;
|
|
9042
9068
|
observationEpoch += 1;
|
|
9043
9069
|
focusSyncScheduled = false;
|
|
9044
9070
|
const cleanupSteps = [
|
|
9045
9071
|
() => container.removeEventListener('focusin', scheduleFocusSync, true),
|
|
9046
9072
|
() => container.removeEventListener('focusout', scheduleFocusSync, true),
|
|
9047
|
-
() =>
|
|
9073
|
+
() => focusObserver?.disconnect(),
|
|
9048
9074
|
];
|
|
9049
9075
|
for (const cleanup of cleanupSteps) {
|
|
9050
9076
|
try {
|
|
@@ -9056,30 +9082,111 @@ function createCardNodeAccess(container) {
|
|
|
9056
9082
|
}
|
|
9057
9083
|
focusedId = null;
|
|
9058
9084
|
};
|
|
9085
|
+
const handleScroll = (event) => {
|
|
9086
|
+
if (disposed || !scrollObserving)
|
|
9087
|
+
return;
|
|
9088
|
+
const path = typeof event.composedPath === 'function'
|
|
9089
|
+
? event.composedPath()
|
|
9090
|
+
: [];
|
|
9091
|
+
const target = path[0] ?? event.target;
|
|
9092
|
+
if (!(target instanceof HTMLElement)
|
|
9093
|
+
|| !target.matches(CARD_NODE_SELECTOR)) {
|
|
9094
|
+
return;
|
|
9095
|
+
}
|
|
9096
|
+
const nodeId = target.getAttribute('data-card-id');
|
|
9097
|
+
if (!nodeId || resolveElement(nodeId) !== target)
|
|
9098
|
+
return;
|
|
9099
|
+
const subscriptions = scrollSubscriptions.get(nodeId);
|
|
9100
|
+
if (!subscriptions?.size)
|
|
9101
|
+
return;
|
|
9102
|
+
const snapshot = Object.freeze({
|
|
9103
|
+
node: getNode(nodeId),
|
|
9104
|
+
scrollTop: target.scrollTop,
|
|
9105
|
+
scrollLeft: target.scrollLeft,
|
|
9106
|
+
scrollHeight: target.scrollHeight,
|
|
9107
|
+
scrollWidth: target.scrollWidth,
|
|
9108
|
+
clientHeight: target.clientHeight,
|
|
9109
|
+
clientWidth: target.clientWidth,
|
|
9110
|
+
});
|
|
9111
|
+
for (const subscription of [...subscriptions]) {
|
|
9112
|
+
try {
|
|
9113
|
+
subscription.listener(snapshot);
|
|
9114
|
+
}
|
|
9115
|
+
catch (error) {
|
|
9116
|
+
console.error('[CardInstance] scroll listener failed', error);
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
};
|
|
9120
|
+
const startScrollObserving = () => {
|
|
9121
|
+
if (disposed || scrollObserving)
|
|
9122
|
+
return;
|
|
9123
|
+
scrollObserving = true;
|
|
9124
|
+
container.addEventListener('scroll', handleScroll, {
|
|
9125
|
+
capture: true,
|
|
9126
|
+
passive: true,
|
|
9127
|
+
});
|
|
9128
|
+
};
|
|
9129
|
+
const stopScrollObserving = () => {
|
|
9130
|
+
if (!scrollObserving)
|
|
9131
|
+
return;
|
|
9132
|
+
scrollObserving = false;
|
|
9133
|
+
try {
|
|
9134
|
+
container.removeEventListener('scroll', handleScroll, true);
|
|
9135
|
+
}
|
|
9136
|
+
catch (error) {
|
|
9137
|
+
console.error('[CardInstance] Scroll observation cleanup failed', error);
|
|
9138
|
+
}
|
|
9139
|
+
};
|
|
9059
9140
|
return {
|
|
9060
9141
|
getNode,
|
|
9061
9142
|
onFocusChange(listener) {
|
|
9062
9143
|
if (disposed)
|
|
9063
9144
|
return () => { };
|
|
9064
|
-
|
|
9065
|
-
if (
|
|
9066
|
-
|
|
9145
|
+
focusListeners.add(listener);
|
|
9146
|
+
if (focusListeners.size === 1)
|
|
9147
|
+
startFocusObserving();
|
|
9148
|
+
let subscribed = true;
|
|
9149
|
+
return () => {
|
|
9150
|
+
if (!subscribed)
|
|
9151
|
+
return;
|
|
9152
|
+
subscribed = false;
|
|
9153
|
+
focusListeners.delete(listener);
|
|
9154
|
+
if (focusListeners.size === 0)
|
|
9155
|
+
stopFocusObserving();
|
|
9156
|
+
};
|
|
9157
|
+
},
|
|
9158
|
+
onScroll(nodeId, listener) {
|
|
9159
|
+
if (disposed)
|
|
9160
|
+
return () => { };
|
|
9161
|
+
const subscription = { listener };
|
|
9162
|
+
let subscriptions = scrollSubscriptions.get(nodeId);
|
|
9163
|
+
if (!subscriptions) {
|
|
9164
|
+
subscriptions = new Set();
|
|
9165
|
+
scrollSubscriptions.set(nodeId, subscriptions);
|
|
9166
|
+
}
|
|
9167
|
+
subscriptions.add(subscription);
|
|
9168
|
+
startScrollObserving();
|
|
9067
9169
|
let subscribed = true;
|
|
9068
9170
|
return () => {
|
|
9069
9171
|
if (!subscribed)
|
|
9070
9172
|
return;
|
|
9071
9173
|
subscribed = false;
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9174
|
+
const active = scrollSubscriptions.get(nodeId);
|
|
9175
|
+
active?.delete(subscription);
|
|
9176
|
+
if (active?.size === 0)
|
|
9177
|
+
scrollSubscriptions.delete(nodeId);
|
|
9178
|
+
if (scrollSubscriptions.size === 0)
|
|
9179
|
+
stopScrollObserving();
|
|
9075
9180
|
};
|
|
9076
9181
|
},
|
|
9077
9182
|
dispose() {
|
|
9078
9183
|
if (disposed)
|
|
9079
9184
|
return;
|
|
9080
|
-
|
|
9185
|
+
stopFocusObserving();
|
|
9186
|
+
stopScrollObserving();
|
|
9081
9187
|
disposed = true;
|
|
9082
|
-
|
|
9188
|
+
focusListeners.clear();
|
|
9189
|
+
scrollSubscriptions.clear();
|
|
9083
9190
|
handles.clear();
|
|
9084
9191
|
},
|
|
9085
9192
|
};
|
|
@@ -9120,6 +9227,7 @@ function applyDisabledDescendants(element, disabled) {
|
|
|
9120
9227
|
}
|
|
9121
9228
|
|
|
9122
9229
|
const FORCE_REPLACE = Symbol('bound-force-replace');
|
|
9230
|
+
const REPEAT_APPEND = Symbol('bound-repeat-append');
|
|
9123
9231
|
/**
|
|
9124
9232
|
* Render the scoped/materialized branch of a card.
|
|
9125
9233
|
*
|
|
@@ -9386,9 +9494,12 @@ function renderBoundCard(container, schema, options) {
|
|
|
9386
9494
|
collectMountedOccurrences(child, occurrences);
|
|
9387
9495
|
}
|
|
9388
9496
|
}
|
|
9389
|
-
function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, current) {
|
|
9497
|
+
function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, appendPlans, current) {
|
|
9390
9498
|
const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
|
|
9391
9499
|
const forceReplace = forcedOwnerIds.has(node.id);
|
|
9500
|
+
const appendPlan = current
|
|
9501
|
+
? appendPlans.get(current.occurrenceKey)
|
|
9502
|
+
: undefined;
|
|
9392
9503
|
if (!metadata.visible || forceReplace) {
|
|
9393
9504
|
return {
|
|
9394
9505
|
...metadata,
|
|
@@ -9414,14 +9525,17 @@ function renderBoundCard(container, schema, options) {
|
|
|
9414
9525
|
const ordinal = nextChildOrdinals.get(currentChild.nodeId) ?? 0;
|
|
9415
9526
|
nextChildOrdinals.set(currentChild.nodeId, ordinal + 1);
|
|
9416
9527
|
const child = matching[Math.min(ordinal, matching.length - 1)];
|
|
9417
|
-
children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, currentChild));
|
|
9528
|
+
children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, appendPlans, currentChild));
|
|
9418
9529
|
}
|
|
9419
9530
|
return {
|
|
9420
9531
|
...metadata,
|
|
9532
|
+
...(appendPlan ? { [REPEAT_APPEND]: true } : {}),
|
|
9421
9533
|
children,
|
|
9422
9534
|
};
|
|
9423
9535
|
}
|
|
9424
9536
|
function decideBoundUpdate(current, next) {
|
|
9537
|
+
if (next[REPEAT_APPEND])
|
|
9538
|
+
return 'retain';
|
|
9425
9539
|
if (next[FORCE_REPLACE]
|
|
9426
9540
|
|| current.occurrenceKey !== next.occurrenceKey
|
|
9427
9541
|
|| current.nodeId !== next.nodeId
|
|
@@ -9483,11 +9597,308 @@ function renderBoundCard(container, schema, options) {
|
|
|
9483
9597
|
}
|
|
9484
9598
|
return changed;
|
|
9485
9599
|
}
|
|
9486
|
-
function
|
|
9487
|
-
|
|
9488
|
-
|
|
9600
|
+
function pointerOverlaps(left, right) {
|
|
9601
|
+
return left === right
|
|
9602
|
+
|| left === ''
|
|
9603
|
+
|| right === ''
|
|
9604
|
+
|| left.startsWith(`${right}/`)
|
|
9605
|
+
|| right.startsWith(`${left}/`);
|
|
9606
|
+
}
|
|
9607
|
+
function changedPointersOutsideSuffix(before, draft, sourcePath) {
|
|
9608
|
+
const changed = collectChangedPointers(before, draft)
|
|
9609
|
+
.filter(pointer => pointer !== sourcePath);
|
|
9610
|
+
changed.push(`${sourcePath}/length`);
|
|
9611
|
+
return [...new Set(changed)];
|
|
9612
|
+
}
|
|
9613
|
+
function repeatOwnerIdentityEqual(current, next) {
|
|
9614
|
+
return current.key === next.key
|
|
9615
|
+
&& current.parentKey === next.parentKey
|
|
9616
|
+
&& current.runtimeOwnerId === next.runtimeOwnerId
|
|
9617
|
+
&& current.sourceOwnerId === next.sourceOwnerId
|
|
9618
|
+
&& current.slot === next.slot
|
|
9619
|
+
&& current.templateId === next.templateId
|
|
9620
|
+
&& current.emptyTemplateId === next.emptyTemplateId
|
|
9621
|
+
&& current.sourcePath === next.sourcePath
|
|
9622
|
+
&& current.dataPath === next.dataPath
|
|
9623
|
+
&& current.instancePath === next.instancePath
|
|
9624
|
+
&& current.depth === next.depth;
|
|
9625
|
+
}
|
|
9626
|
+
function hasStrictJsonPrefix(previous, next) {
|
|
9627
|
+
return next.length > previous.length
|
|
9628
|
+
&& previous.every((value, index) => jsonDataEqual(value, next[index]));
|
|
9629
|
+
}
|
|
9630
|
+
function childNodeSequenceEqual(parent, expected) {
|
|
9631
|
+
const actual = Array.from(parent.childNodes);
|
|
9632
|
+
return actual.length === expected.length
|
|
9633
|
+
&& actual.every((node, index) => node === expected[index]);
|
|
9634
|
+
}
|
|
9635
|
+
function occurrenceKeysForChildren(parentOccurrenceKey, children) {
|
|
9636
|
+
const ordinals = new Map();
|
|
9637
|
+
return children.map((child) => {
|
|
9638
|
+
const ordinal = ordinals.get(child.id) ?? 0;
|
|
9639
|
+
ordinals.set(child.id, ordinal + 1);
|
|
9640
|
+
return childOccurrenceKey(parentOccurrenceKey, child.id, ordinal);
|
|
9641
|
+
});
|
|
9642
|
+
}
|
|
9643
|
+
function findMountedByOccurrenceKey(root, occurrenceKey) {
|
|
9644
|
+
if (root.occurrenceKey === occurrenceKey)
|
|
9645
|
+
return root;
|
|
9646
|
+
for (const child of root.children) {
|
|
9647
|
+
const match = findMountedByOccurrenceKey(child, occurrenceKey);
|
|
9648
|
+
if (match)
|
|
9649
|
+
return match;
|
|
9650
|
+
}
|
|
9651
|
+
return null;
|
|
9652
|
+
}
|
|
9653
|
+
function replaceMountedByOccurrenceKey(root, occurrenceKey, replacement) {
|
|
9654
|
+
if (root.occurrenceKey === occurrenceKey)
|
|
9655
|
+
return replacement;
|
|
9656
|
+
for (let index = 0; index < root.children.length; index += 1) {
|
|
9657
|
+
const replacedChild = replaceMountedByOccurrenceKey(root.children[index], occurrenceKey, replacement);
|
|
9658
|
+
if (!replacedChild)
|
|
9659
|
+
continue;
|
|
9660
|
+
const children = root.children.slice();
|
|
9661
|
+
children[index] = replacedChild;
|
|
9662
|
+
return { ...root, children };
|
|
9663
|
+
}
|
|
9664
|
+
return null;
|
|
9665
|
+
}
|
|
9666
|
+
function mountedNodesById(root, nodeId) {
|
|
9667
|
+
const matches = [];
|
|
9668
|
+
const visit = (node) => {
|
|
9669
|
+
if (node.nodeId === nodeId)
|
|
9670
|
+
matches.push(node);
|
|
9671
|
+
for (const child of node.children)
|
|
9672
|
+
visit(child);
|
|
9673
|
+
};
|
|
9674
|
+
visit(root);
|
|
9675
|
+
return matches;
|
|
9676
|
+
}
|
|
9677
|
+
function mountedTreeOrderMatchesDom(root) {
|
|
9678
|
+
if (root.element.parentNode !== container
|
|
9679
|
+
|| container.childNodes.length !== 1
|
|
9680
|
+
|| container.firstChild !== root.element) {
|
|
9681
|
+
return false;
|
|
9682
|
+
}
|
|
9683
|
+
const visit = (node) => {
|
|
9684
|
+
let previous = null;
|
|
9685
|
+
for (const child of node.children) {
|
|
9686
|
+
if (!node.element.contains(child.element))
|
|
9687
|
+
return false;
|
|
9688
|
+
if (previous
|
|
9689
|
+
&& previous.compareDocumentPosition(child.element)
|
|
9690
|
+
!== Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
9691
|
+
return false;
|
|
9692
|
+
}
|
|
9693
|
+
previous = child.element;
|
|
9694
|
+
if (!visit(child))
|
|
9695
|
+
return false;
|
|
9696
|
+
}
|
|
9697
|
+
return true;
|
|
9698
|
+
};
|
|
9699
|
+
return visit(root);
|
|
9700
|
+
}
|
|
9701
|
+
function retainedSubtreeEquivalent(current, next) {
|
|
9702
|
+
return current.occurrenceKey === next.occurrenceKey
|
|
9703
|
+
&& current.nodeId === next.nodeId
|
|
9704
|
+
&& current.nodeType === next.nodeType
|
|
9705
|
+
&& current.node.sourceId === next.node.sourceId
|
|
9706
|
+
&& current.node.instancePath === next.node.instancePath
|
|
9707
|
+
&& current.node.dataPath === next.node.dataPath
|
|
9708
|
+
&& current.node.bindingDialect === next.node.bindingDialect
|
|
9709
|
+
&& current.rendererToken === next.rendererToken
|
|
9710
|
+
&& current.visible === next.visible
|
|
9711
|
+
&& current.disabled === next.disabled
|
|
9712
|
+
&& current.propsFingerprint === next.propsFingerprint
|
|
9713
|
+
&& current.layoutFingerprint === next.layoutFingerprint
|
|
9714
|
+
&& current.bindingFingerprint === next.bindingFingerprint
|
|
9715
|
+
&& current.children.length === next.children.length
|
|
9716
|
+
&& current.children.every((child, index) => (retainedSubtreeEquivalent(child, next.children[index])));
|
|
9717
|
+
}
|
|
9718
|
+
function prepareComparableRetainedNode(node, occurrenceKey, renderVariables, current) {
|
|
9719
|
+
const expectedKeys = occurrenceKeysForChildren(occurrenceKey, node.children);
|
|
9720
|
+
if (current.children.length !== node.children.length
|
|
9721
|
+
|| current.children.some((child, index) => (child.occurrenceKey !== expectedKeys[index]))) {
|
|
9722
|
+
return null;
|
|
9723
|
+
}
|
|
9724
|
+
const children = [];
|
|
9725
|
+
for (let index = 0; index < node.children.length; index += 1) {
|
|
9726
|
+
const child = prepareComparableRetainedNode(node.children[index], expectedKeys[index], renderVariables, current.children[index]);
|
|
9727
|
+
if (!child)
|
|
9728
|
+
return null;
|
|
9729
|
+
children.push(child);
|
|
9730
|
+
}
|
|
9731
|
+
return {
|
|
9732
|
+
...resolveBoundMetadata(node, occurrenceKey, renderVariables),
|
|
9733
|
+
children,
|
|
9734
|
+
};
|
|
9735
|
+
}
|
|
9736
|
+
function projectRepeatOwnerPrefix(node, owner, current) {
|
|
9737
|
+
const slots = node.props.slots;
|
|
9738
|
+
const slot = slots?.[owner.slot];
|
|
9739
|
+
const currentSlots = current.node.props.slots;
|
|
9740
|
+
const currentSlot = currentSlots?.[owner.slot];
|
|
9741
|
+
if (!slots
|
|
9742
|
+
|| !slot
|
|
9743
|
+
|| !Array.isArray(slot.children)
|
|
9744
|
+
|| !currentSlot
|
|
9745
|
+
|| !Array.isArray(currentSlot.children)
|
|
9746
|
+
|| currentSlot.children.length >= slot.children.length
|
|
9747
|
+
|| currentSlot.children.some((childId, index) => (childId !== slot.children[index]))) {
|
|
9748
|
+
return null;
|
|
9749
|
+
}
|
|
9750
|
+
const appendedChildIds = new Set(slot.children.slice(currentSlot.children.length));
|
|
9751
|
+
const children = node.children.filter(child => (!appendedChildIds.has(child.id)));
|
|
9752
|
+
if (children.length !== current.children.length
|
|
9753
|
+
|| node.children.length - children.length !== appendedChildIds.size) {
|
|
9754
|
+
return null;
|
|
9755
|
+
}
|
|
9756
|
+
return {
|
|
9757
|
+
...node,
|
|
9758
|
+
props: {
|
|
9759
|
+
...node.props,
|
|
9760
|
+
slots: {
|
|
9761
|
+
...slots,
|
|
9762
|
+
[owner.slot]: {
|
|
9763
|
+
...slot,
|
|
9764
|
+
children: slot.children.slice(0, currentSlot.children.length),
|
|
9765
|
+
},
|
|
9766
|
+
},
|
|
9767
|
+
},
|
|
9768
|
+
children,
|
|
9769
|
+
};
|
|
9770
|
+
}
|
|
9771
|
+
function collectRetainedRepeatOwnerKeys(materialized, outerOwner) {
|
|
9772
|
+
const retained = new Set([outerOwner.key]);
|
|
9773
|
+
for (const candidate of materialized.repeatOwners.values()) {
|
|
9774
|
+
let parentKey = candidate.parentKey;
|
|
9775
|
+
while (parentKey) {
|
|
9776
|
+
if (parentKey === outerOwner.key) {
|
|
9777
|
+
retained.add(candidate.key);
|
|
9778
|
+
break;
|
|
9779
|
+
}
|
|
9780
|
+
parentKey = materialized.repeatOwners.get(parentKey)?.parentKey;
|
|
9781
|
+
}
|
|
9782
|
+
}
|
|
9783
|
+
return retained;
|
|
9784
|
+
}
|
|
9785
|
+
function retainedOwnersConsumeChangedPointer(materialized, retainedOwnerKeys, pointers) {
|
|
9786
|
+
for (const [key, dependencyOwners] of materialized.valueDependencies) {
|
|
9787
|
+
if (!key.startsWith('data:')
|
|
9788
|
+
|| ![...dependencyOwners].some(ownerKey => (retainedOwnerKeys.has(ownerKey)))) {
|
|
9789
|
+
continue;
|
|
9790
|
+
}
|
|
9791
|
+
const dependency = key.slice('data:'.length);
|
|
9792
|
+
if (pointers.some(pointer => pointerOverlaps(pointer, dependency))) {
|
|
9793
|
+
return true;
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
return false;
|
|
9797
|
+
}
|
|
9798
|
+
function createRepeatAppendPlan(currentOwner, nextMaterialized, before, draft) {
|
|
9799
|
+
if (!currentMountedTree || !mountedTreeOrderMatchesDom(currentMountedTree)) {
|
|
9800
|
+
return null;
|
|
9801
|
+
}
|
|
9802
|
+
const nextOwner = nextMaterialized.repeatOwners.get(currentOwner.key);
|
|
9803
|
+
if (!nextOwner || !repeatOwnerIdentityEqual(currentOwner, nextOwner)) {
|
|
9804
|
+
return null;
|
|
9805
|
+
}
|
|
9806
|
+
const currentOwnerCount = [...currentMaterialized.repeatOwners.values()]
|
|
9807
|
+
.filter(owner => owner.runtimeOwnerId === currentOwner.runtimeOwnerId)
|
|
9808
|
+
.length;
|
|
9809
|
+
const nextOwnerCount = [...nextMaterialized.repeatOwners.values()]
|
|
9810
|
+
.filter(owner => owner.runtimeOwnerId === nextOwner.runtimeOwnerId)
|
|
9811
|
+
.length;
|
|
9812
|
+
if (currentOwnerCount !== 1 || nextOwnerCount !== 1)
|
|
9813
|
+
return null;
|
|
9814
|
+
const mountedMatches = mountedNodesById(currentMountedTree, currentOwner.runtimeOwnerId);
|
|
9815
|
+
if (mountedMatches.length !== 1)
|
|
9816
|
+
return null;
|
|
9817
|
+
const current = mountedMatches[0];
|
|
9818
|
+
const nextNode = nextMaterialized.instances
|
|
9819
|
+
.get(nextOwner.runtimeOwnerId)?.node;
|
|
9820
|
+
if (!nextNode
|
|
9821
|
+
|| current.nodeType !== 'Container'
|
|
9822
|
+
|| nextNode.type !== 'Container'
|
|
9823
|
+
|| current.rendererToken !== componentRenderers._default
|
|
9824
|
+
|| !current.visible
|
|
9825
|
+
|| !current.resolvedProps
|
|
9826
|
+
|| !currentOwner.sourcePath
|
|
9827
|
+
|| !nextOwner.sourcePath) {
|
|
9828
|
+
return null;
|
|
9829
|
+
}
|
|
9830
|
+
const nextMetadata = resolveBoundMetadata(nextNode, current.occurrenceKey, draft);
|
|
9831
|
+
const currentLayout = getDirectChildRepeatAppendLayout(current.resolvedProps, currentOwner.slot);
|
|
9832
|
+
const nextLayout = nextMetadata.resolvedProps
|
|
9833
|
+
? getDirectChildRepeatAppendLayout(nextMetadata.resolvedProps, nextOwner.slot)
|
|
9834
|
+
: undefined;
|
|
9835
|
+
if (!currentLayout || currentLayout !== nextLayout)
|
|
9836
|
+
return null;
|
|
9837
|
+
let previousSource;
|
|
9838
|
+
let nextSource;
|
|
9839
|
+
try {
|
|
9840
|
+
previousSource = getByJsonPointer(lastPublishedVariables, currentOwner.sourcePath);
|
|
9841
|
+
nextSource = getByJsonPointer(draft, nextOwner.sourcePath);
|
|
9489
9842
|
}
|
|
9490
|
-
|
|
9843
|
+
catch {
|
|
9844
|
+
return null;
|
|
9845
|
+
}
|
|
9846
|
+
if (!Array.isArray(previousSource)
|
|
9847
|
+
|| !Array.isArray(nextSource)
|
|
9848
|
+
|| !hasStrictJsonPrefix(previousSource, nextSource)) {
|
|
9849
|
+
return null;
|
|
9850
|
+
}
|
|
9851
|
+
const nextOccurrenceKeys = occurrenceKeysForChildren(current.occurrenceKey, nextNode.children);
|
|
9852
|
+
if (current.children.length >= nextNode.children.length
|
|
9853
|
+
|| current.children.some((child, index) => (child.occurrenceKey !== nextOccurrenceKeys[index]))
|
|
9854
|
+
|| !childNodeSequenceEqual(current.element, current.children.map(child => child.element))) {
|
|
9855
|
+
return null;
|
|
9856
|
+
}
|
|
9857
|
+
const projectedOwner = projectRepeatOwnerPrefix(nextNode, nextOwner, current);
|
|
9858
|
+
if (!projectedOwner)
|
|
9859
|
+
return null;
|
|
9860
|
+
const comparableOwner = prepareComparableRetainedNode(projectedOwner, current.occurrenceKey, draft, current);
|
|
9861
|
+
if (!comparableOwner || !retainedSubtreeEquivalent(current, comparableOwner)) {
|
|
9862
|
+
return null;
|
|
9863
|
+
}
|
|
9864
|
+
const retainedOwnerKeys = collectRetainedRepeatOwnerKeys(currentMaterialized, currentOwner);
|
|
9865
|
+
if (retainedOwnersConsumeChangedPointer(currentMaterialized, retainedOwnerKeys, changedPointersOutsideSuffix(before, draft, currentOwner.sourcePath))) {
|
|
9866
|
+
return null;
|
|
9867
|
+
}
|
|
9868
|
+
return { currentOwner, nextOwner, current, nextNode };
|
|
9869
|
+
}
|
|
9870
|
+
function classifyRepeatOwners(before, draft, nextMaterialized) {
|
|
9871
|
+
let isolatedUnresolvedAppendKey;
|
|
9872
|
+
if (currentMaterialized.repeatOwners.size === 1
|
|
9873
|
+
&& nextMaterialized.repeatOwners.size === 1
|
|
9874
|
+
&& currentMaterialized.unresolvedRepeatOwners.size === 1
|
|
9875
|
+
&& nextMaterialized.unresolvedRepeatOwners.size === 1) {
|
|
9876
|
+
const [currentKey] = currentMaterialized.unresolvedRepeatOwners;
|
|
9877
|
+
const [nextKey] = nextMaterialized.unresolvedRepeatOwners;
|
|
9878
|
+
const currentOwner = currentMaterialized.repeatOwners.get(currentKey);
|
|
9879
|
+
const nextOwner = nextMaterialized.repeatOwners.get(nextKey);
|
|
9880
|
+
const changedPointers = collectChangedPointers(before, draft);
|
|
9881
|
+
if (currentKey === nextKey
|
|
9882
|
+
&& currentOwner?.sourcePath
|
|
9883
|
+
&& currentOwner.sourcePath === nextOwner?.sourcePath
|
|
9884
|
+
&& changedPointers.length === 1
|
|
9885
|
+
&& changedPointers[0] === currentOwner.sourcePath) {
|
|
9886
|
+
// Complex expressions inside one Repeat are conservatively unresolved.
|
|
9887
|
+
// A source-only update may still enter the existing append proof: the
|
|
9888
|
+
// strict JSON prefix and retained-subtree comparison below must both
|
|
9889
|
+
// succeed before any live DOM is adopted.
|
|
9890
|
+
isolatedUnresolvedAppendKey = currentKey;
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
if ((currentMaterialized.unresolvedRepeatOwners.size > 0
|
|
9894
|
+
|| nextMaterialized.unresolvedRepeatOwners.size > 0)
|
|
9895
|
+
&& isolatedUnresolvedAppendKey === undefined) {
|
|
9896
|
+
return {
|
|
9897
|
+
forcedOwnerIds: new Set([currentMaterialized.root.id]),
|
|
9898
|
+
appendPlans: new Map(),
|
|
9899
|
+
};
|
|
9900
|
+
}
|
|
9901
|
+
const selectedKeys = new Set(isolatedUnresolvedAppendKey ? [isolatedUnresolvedAppendKey] : []);
|
|
9491
9902
|
for (const pointer of collectChangedPointers(before, draft)) {
|
|
9492
9903
|
for (const owner of findAffectedRepeatOwners(currentMaterialized, pointer)) {
|
|
9493
9904
|
selectedKeys.add(owner.key);
|
|
@@ -9503,10 +9914,19 @@ function renderBoundCard(container, schema, options) {
|
|
|
9503
9914
|
parentKey = currentMaterialized.repeatOwners.get(parentKey)?.parentKey;
|
|
9504
9915
|
}
|
|
9505
9916
|
}
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9917
|
+
const forcedOwnerIds = new Set();
|
|
9918
|
+
const appendPlans = new Map();
|
|
9919
|
+
for (const key of selectedKeys) {
|
|
9920
|
+
const owner = currentMaterialized.repeatOwners.get(key);
|
|
9921
|
+
if (!owner)
|
|
9922
|
+
continue;
|
|
9923
|
+
const plan = createRepeatAppendPlan(owner, nextMaterialized, before, draft);
|
|
9924
|
+
if (plan)
|
|
9925
|
+
appendPlans.set(plan.current.occurrenceKey, plan);
|
|
9926
|
+
else
|
|
9927
|
+
forcedOwnerIds.add(owner.runtimeOwnerId);
|
|
9928
|
+
}
|
|
9929
|
+
return { forcedOwnerIds, appendPlans };
|
|
9510
9930
|
}
|
|
9511
9931
|
function collectMountedLifecycleIds(root) {
|
|
9512
9932
|
const ids = new Set();
|
|
@@ -9538,15 +9958,77 @@ function renderBoundCard(container, schema, options) {
|
|
|
9538
9958
|
prepareReplacementRoots(current.children[index], next.children[index], draft, replacements);
|
|
9539
9959
|
}
|
|
9540
9960
|
}
|
|
9961
|
+
function findPreparedByOccurrenceKey(root, occurrenceKey) {
|
|
9962
|
+
if (root.occurrenceKey === occurrenceKey)
|
|
9963
|
+
return root;
|
|
9964
|
+
for (const child of root.children) {
|
|
9965
|
+
const match = findPreparedByOccurrenceKey(child, occurrenceKey);
|
|
9966
|
+
if (match)
|
|
9967
|
+
return match;
|
|
9968
|
+
}
|
|
9969
|
+
return null;
|
|
9970
|
+
}
|
|
9971
|
+
function disposePreparedRepeatAppend(candidate) {
|
|
9972
|
+
if (candidate.state === 'disposed' || candidate.state === 'adopted')
|
|
9973
|
+
return;
|
|
9974
|
+
for (const suffix of candidate.suffix) {
|
|
9975
|
+
disposeBoundSubtreeWithoutThrow(suffix);
|
|
9976
|
+
}
|
|
9977
|
+
candidate.fragment.replaceChildren();
|
|
9978
|
+
candidate.state = 'disposed';
|
|
9979
|
+
}
|
|
9980
|
+
function prepareRepeatAppendCandidates(preparedRoot, appendPlans, draft) {
|
|
9981
|
+
const candidates = new Map();
|
|
9982
|
+
try {
|
|
9983
|
+
for (const [occurrenceKey, plan] of appendPlans) {
|
|
9984
|
+
const prepared = findPreparedByOccurrenceKey(preparedRoot, occurrenceKey);
|
|
9985
|
+
if (!prepared?.[REPEAT_APPEND]) {
|
|
9986
|
+
throw new Error(`[renderCard] Missing prepared repeat append owner "${occurrenceKey}"`);
|
|
9987
|
+
}
|
|
9988
|
+
const nextOccurrenceKeys = occurrenceKeysForChildren(plan.current.occurrenceKey, plan.nextNode.children);
|
|
9989
|
+
const suffix = [];
|
|
9990
|
+
const fragment = document.createDocumentFragment();
|
|
9991
|
+
const candidate = {
|
|
9992
|
+
occurrenceKey,
|
|
9993
|
+
current: plan.current,
|
|
9994
|
+
next: prepared,
|
|
9995
|
+
suffix,
|
|
9996
|
+
fragment,
|
|
9997
|
+
previousChildNodes: Array.from(plan.current.element.childNodes),
|
|
9998
|
+
expectedChildNodes: [],
|
|
9999
|
+
state: 'prepared',
|
|
10000
|
+
};
|
|
10001
|
+
candidates.set(occurrenceKey, candidate);
|
|
10002
|
+
for (let index = plan.current.children.length; index < plan.nextNode.children.length; index += 1) {
|
|
10003
|
+
const mounted = mountBoundSubtree(plan.nextNode.children[index], nextOccurrenceKeys[index], draft);
|
|
10004
|
+
suffix.push(mounted);
|
|
10005
|
+
fragment.appendChild(mounted.element);
|
|
10006
|
+
}
|
|
10007
|
+
candidate.expectedChildNodes = [
|
|
10008
|
+
...candidate.previousChildNodes,
|
|
10009
|
+
...suffix.map(node => node.element),
|
|
10010
|
+
];
|
|
10011
|
+
}
|
|
10012
|
+
return candidates;
|
|
10013
|
+
}
|
|
10014
|
+
catch (error) {
|
|
10015
|
+
for (const candidate of candidates.values()) {
|
|
10016
|
+
disposePreparedRepeatAppend(candidate);
|
|
10017
|
+
}
|
|
10018
|
+
throw error;
|
|
10019
|
+
}
|
|
10020
|
+
}
|
|
9541
10021
|
function prepareIncrementalCandidate(before, draft) {
|
|
9542
10022
|
const materialized = materializeCard(schema, draft);
|
|
9543
|
-
const
|
|
9544
|
-
?
|
|
9545
|
-
|
|
9546
|
-
:
|
|
9547
|
-
|
|
9548
|
-
|
|
10023
|
+
const classification = currentMountedTree
|
|
10024
|
+
? classifyRepeatOwners(before, draft, materialized)
|
|
10025
|
+
: {
|
|
10026
|
+
forcedOwnerIds: new Set(),
|
|
10027
|
+
appendPlans: new Map(),
|
|
10028
|
+
};
|
|
10029
|
+
const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, classification.forcedOwnerIds, classification.appendPlans, currentMountedTree);
|
|
9549
10030
|
const replacementRoots = new Map();
|
|
10031
|
+
const repeatAppends = prepareRepeatAppendCandidates(preparedRoot, classification.appendPlans, draft);
|
|
9550
10032
|
try {
|
|
9551
10033
|
if (currentMountedTree) {
|
|
9552
10034
|
prepareReplacementRoots(currentMountedTree, preparedRoot, draft, replacementRoots);
|
|
@@ -9558,6 +10040,7 @@ function renderBoundCard(container, schema, options) {
|
|
|
9558
10040
|
materialized,
|
|
9559
10041
|
preparedRoot,
|
|
9560
10042
|
replacementRoots,
|
|
10043
|
+
repeatAppends,
|
|
9561
10044
|
autoFocusIds: currentMountedTree
|
|
9562
10045
|
? collectAutoFocusRevealIds(currentMaterialized, materialized, draft)
|
|
9563
10046
|
: new Set(),
|
|
@@ -9567,6 +10050,9 @@ function renderBoundCard(container, schema, options) {
|
|
|
9567
10050
|
for (const replacement of replacementRoots.values()) {
|
|
9568
10051
|
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9569
10052
|
}
|
|
10053
|
+
for (const append of repeatAppends.values()) {
|
|
10054
|
+
disposePreparedRepeatAppend(append);
|
|
10055
|
+
}
|
|
9570
10056
|
throw error;
|
|
9571
10057
|
}
|
|
9572
10058
|
}
|
|
@@ -9820,6 +10306,10 @@ function renderBoundCard(container, schema, options) {
|
|
|
9820
10306
|
disposeBoundSubtreeWithoutThrow(replacement);
|
|
9821
10307
|
}
|
|
9822
10308
|
candidate.replacementRoots.clear();
|
|
10309
|
+
for (const append of candidate.repeatAppends.values()) {
|
|
10310
|
+
disposePreparedRepeatAppend(append);
|
|
10311
|
+
}
|
|
10312
|
+
candidate.repeatAppends.clear();
|
|
9823
10313
|
}
|
|
9824
10314
|
function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
|
|
9825
10315
|
if (previous.element !== next.element) {
|
|
@@ -9868,6 +10358,111 @@ function renderBoundCard(container, schema, options) {
|
|
|
9868
10358
|
throw adoptionError;
|
|
9869
10359
|
throw new Error(failureMessage);
|
|
9870
10360
|
}
|
|
10361
|
+
function rollbackRepeatAppend(candidate) {
|
|
10362
|
+
for (let index = candidate.suffix.length - 1; index >= 0; index -= 1) {
|
|
10363
|
+
const element = candidate.suffix[index].element;
|
|
10364
|
+
if (element.parentNode !== candidate.current.element)
|
|
10365
|
+
continue;
|
|
10366
|
+
try {
|
|
10367
|
+
candidate.current.element.removeChild(element);
|
|
10368
|
+
}
|
|
10369
|
+
catch {
|
|
10370
|
+
// The exact previous-sequence postcondition below decides success.
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
10373
|
+
const restored = childNodeSequenceEqual(candidate.current.element, candidate.previousChildNodes);
|
|
10374
|
+
candidate.state = restored ? 'rolled-back' : 'rollback-failed';
|
|
10375
|
+
return restored;
|
|
10376
|
+
}
|
|
10377
|
+
function tryAdoptRepeatAppend(candidate) {
|
|
10378
|
+
let adoptionError;
|
|
10379
|
+
try {
|
|
10380
|
+
candidate.current.element.appendChild(candidate.fragment);
|
|
10381
|
+
}
|
|
10382
|
+
catch (error) {
|
|
10383
|
+
adoptionError = error;
|
|
10384
|
+
}
|
|
10385
|
+
if (childNodeSequenceEqual(candidate.current.element, candidate.expectedChildNodes)) {
|
|
10386
|
+
candidate.state = 'adopted';
|
|
10387
|
+
return { status: 'adopted' };
|
|
10388
|
+
}
|
|
10389
|
+
const error = adoptionError ?? new Error('[renderCard] Bound repeat append postcondition failed');
|
|
10390
|
+
if (rollbackRepeatAppend(candidate)) {
|
|
10391
|
+
return { status: 'rolled-back', error };
|
|
10392
|
+
}
|
|
10393
|
+
return { status: 'rollback-failed', error };
|
|
10394
|
+
}
|
|
10395
|
+
function mountedSnapshotStillMatches(previous, working) {
|
|
10396
|
+
const appendOwner = Boolean(working[REPEAT_APPEND]);
|
|
10397
|
+
if (previous.element !== working.element
|
|
10398
|
+
|| previous.occurrenceKey !== working.occurrenceKey
|
|
10399
|
+
|| previous.nodeId !== working.nodeId
|
|
10400
|
+
|| previous.nodeType !== working.nodeType
|
|
10401
|
+
|| previous.rendererToken !== working.rendererToken
|
|
10402
|
+
|| previous.visible !== working.visible
|
|
10403
|
+
|| previous.disabled !== working.disabled
|
|
10404
|
+
|| previous.bindingFingerprint !== working.bindingFingerprint
|
|
10405
|
+
|| previous.children.length !== working.children.length
|
|
10406
|
+
|| (!appendOwner && (previous.propsFingerprint !== working.propsFingerprint
|
|
10407
|
+
|| previous.layoutFingerprint !== working.layoutFingerprint))) {
|
|
10408
|
+
return false;
|
|
10409
|
+
}
|
|
10410
|
+
if (appendOwner
|
|
10411
|
+
&& !childNodeSequenceEqual(previous.element, previous.children.map(child => child.element))) {
|
|
10412
|
+
return false;
|
|
10413
|
+
}
|
|
10414
|
+
return previous.children.every((child, index) => (mountedSnapshotStillMatches(child, working.children[index])));
|
|
10415
|
+
}
|
|
10416
|
+
function adoptCompleteNextEmergencyRoot(candidate, renderVariables, previous, working, scrollPositions, mediaStates) {
|
|
10417
|
+
const emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
|
|
10418
|
+
let adopted = false;
|
|
10419
|
+
try {
|
|
10420
|
+
syncMountedBindings(emergency);
|
|
10421
|
+
replaceContainerRoot(emergency.element, '[renderCard] Bound emergency root was not adopted');
|
|
10422
|
+
adopted = true;
|
|
10423
|
+
}
|
|
10424
|
+
finally {
|
|
10425
|
+
if (!adopted)
|
|
10426
|
+
disposeBoundSubtreeWithoutThrow(emergency);
|
|
10427
|
+
}
|
|
10428
|
+
disposeBoundSubtreeWithoutThrow(working);
|
|
10429
|
+
try {
|
|
10430
|
+
restoreChangedSubtreeState(previous, emergency, scrollPositions, mediaStates);
|
|
10431
|
+
}
|
|
10432
|
+
catch {
|
|
10433
|
+
// Presentation restoration cannot undo an adopted complete next root.
|
|
10434
|
+
}
|
|
10435
|
+
restoreDisabledDescendantPresentation(emergency);
|
|
10436
|
+
return emergency;
|
|
10437
|
+
}
|
|
10438
|
+
function recoverPreviousCommittedRoot(previous, working, scrollPositions, mediaStates) {
|
|
10439
|
+
const recovery = mountBoundSubtree(currentMaterialized.root, previous.occurrenceKey, lastPublishedVariables);
|
|
10440
|
+
let adopted = false;
|
|
10441
|
+
try {
|
|
10442
|
+
syncMountedBindings(recovery);
|
|
10443
|
+
replaceContainerRoot(recovery.element, '[renderCard] Previous bound root recovery was not adopted');
|
|
10444
|
+
adopted = true;
|
|
10445
|
+
}
|
|
10446
|
+
finally {
|
|
10447
|
+
if (!adopted)
|
|
10448
|
+
disposeBoundSubtreeWithoutThrow(recovery);
|
|
10449
|
+
}
|
|
10450
|
+
disposeBoundSubtreeWithoutThrow(working);
|
|
10451
|
+
try {
|
|
10452
|
+
restoreChangedSubtreeState(previous, recovery, scrollPositions, mediaStates);
|
|
10453
|
+
}
|
|
10454
|
+
catch {
|
|
10455
|
+
// Recovery validity is the adopted previous tree, not presentation state.
|
|
10456
|
+
}
|
|
10457
|
+
restoreDisabledDescendantPresentation(recovery);
|
|
10458
|
+
currentMountedTree = recovery;
|
|
10459
|
+
return recovery;
|
|
10460
|
+
}
|
|
10461
|
+
function repeatAppendUnreconciledError(failures) {
|
|
10462
|
+
const error = new Error('[renderCard] Bound repeat append unreconciled');
|
|
10463
|
+
error.failures = failures;
|
|
10464
|
+
return error;
|
|
10465
|
+
}
|
|
9871
10466
|
function commitIncrementalCandidate(candidate, renderVariables) {
|
|
9872
10467
|
const replacements = candidate.replacementRoots;
|
|
9873
10468
|
try {
|
|
@@ -9889,7 +10484,10 @@ function renderBoundCard(container, schema, options) {
|
|
|
9889
10484
|
const scrollPositions = captureScrollPositions$1(container);
|
|
9890
10485
|
const mediaStates = captureMediaStates$1(container);
|
|
9891
10486
|
let unreconciled = false;
|
|
9892
|
-
|
|
10487
|
+
let previousRecoveryRequired = false;
|
|
10488
|
+
let appendFailure;
|
|
10489
|
+
let repeatAppendTransaction = false;
|
|
10490
|
+
let next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
|
|
9893
10491
|
decide: (current, prepared) => decideBoundUpdate(current, prepared),
|
|
9894
10492
|
update: (current, prepared) => updateBoundNode(current, prepared),
|
|
9895
10493
|
mountReplacement: (prepared) => {
|
|
@@ -9923,6 +10521,60 @@ function renderBoundCard(container, schema, options) {
|
|
|
9923
10521
|
unreconciled = true;
|
|
9924
10522
|
},
|
|
9925
10523
|
});
|
|
10524
|
+
if (!unreconciled) {
|
|
10525
|
+
for (const appendCandidate of candidate.repeatAppends.values()) {
|
|
10526
|
+
const reconciledOwner = findMountedByOccurrenceKey(next, appendCandidate.occurrenceKey);
|
|
10527
|
+
if (!reconciledOwner
|
|
10528
|
+
|| reconciledOwner.element !== appendCandidate.current.element) {
|
|
10529
|
+
disposePreparedRepeatAppend(appendCandidate);
|
|
10530
|
+
candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
|
|
10531
|
+
continue;
|
|
10532
|
+
}
|
|
10533
|
+
repeatAppendTransaction = true;
|
|
10534
|
+
const grafted = {
|
|
10535
|
+
...appendCandidate.next,
|
|
10536
|
+
element: reconciledOwner.element,
|
|
10537
|
+
children: [
|
|
10538
|
+
...reconciledOwner.children,
|
|
10539
|
+
...appendCandidate.suffix,
|
|
10540
|
+
],
|
|
10541
|
+
};
|
|
10542
|
+
const graftedRoot = replaceMountedByOccurrenceKey(next, appendCandidate.occurrenceKey, grafted);
|
|
10543
|
+
if (!graftedRoot) {
|
|
10544
|
+
disposePreparedRepeatAppend(appendCandidate);
|
|
10545
|
+
candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
|
|
10546
|
+
unreconciled = true;
|
|
10547
|
+
appendFailure = new Error('[renderCard] Bound repeat append owner disappeared before adoption');
|
|
10548
|
+
break;
|
|
10549
|
+
}
|
|
10550
|
+
const result = tryAdoptRepeatAppend(appendCandidate);
|
|
10551
|
+
if (result.status === 'adopted') {
|
|
10552
|
+
next = graftedRoot;
|
|
10553
|
+
candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
|
|
10554
|
+
continue;
|
|
10555
|
+
}
|
|
10556
|
+
appendFailure = result.error;
|
|
10557
|
+
if (result.status === 'rolled-back') {
|
|
10558
|
+
unreconciled = true;
|
|
10559
|
+
}
|
|
10560
|
+
else {
|
|
10561
|
+
previousRecoveryRequired = true;
|
|
10562
|
+
}
|
|
10563
|
+
break;
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
if (previousRecoveryRequired) {
|
|
10567
|
+
try {
|
|
10568
|
+
recoverPreviousCommittedRoot(previous, next, scrollPositions, mediaStates);
|
|
10569
|
+
}
|
|
10570
|
+
catch (recoveryError) {
|
|
10571
|
+
throw repeatAppendUnreconciledError([
|
|
10572
|
+
appendFailure,
|
|
10573
|
+
recoveryError,
|
|
10574
|
+
]);
|
|
10575
|
+
}
|
|
10576
|
+
throw appendFailure;
|
|
10577
|
+
}
|
|
9926
10578
|
if (!unreconciled
|
|
9927
10579
|
&& (next.element.parentNode !== container
|
|
9928
10580
|
|| container.firstElementChild !== next.element)) {
|
|
@@ -9936,7 +10588,7 @@ function renderBoundCard(container, schema, options) {
|
|
|
9936
10588
|
unreconciled = true;
|
|
9937
10589
|
}
|
|
9938
10590
|
}
|
|
9939
|
-
if (unreconciled) {
|
|
10591
|
+
if (unreconciled && !repeatAppendTransaction) {
|
|
9940
10592
|
let emergency;
|
|
9941
10593
|
try {
|
|
9942
10594
|
emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
|
|
@@ -9967,6 +10619,29 @@ function renderBoundCard(container, schema, options) {
|
|
|
9967
10619
|
restoreDisabledDescendantPresentation(emergency);
|
|
9968
10620
|
return emergency;
|
|
9969
10621
|
}
|
|
10622
|
+
if (unreconciled) {
|
|
10623
|
+
try {
|
|
10624
|
+
return adoptCompleteNextEmergencyRoot(candidate, renderVariables, previous, next, scrollPositions, mediaStates);
|
|
10625
|
+
}
|
|
10626
|
+
catch (emergencyError) {
|
|
10627
|
+
if (mountedTreeOrderMatchesDom(previous)
|
|
10628
|
+
&& mountedSnapshotStillMatches(previous, next)) {
|
|
10629
|
+
syncMountedBindings(previous);
|
|
10630
|
+
throw appendFailure ?? emergencyError;
|
|
10631
|
+
}
|
|
10632
|
+
try {
|
|
10633
|
+
recoverPreviousCommittedRoot(previous, next, scrollPositions, mediaStates);
|
|
10634
|
+
}
|
|
10635
|
+
catch (recoveryError) {
|
|
10636
|
+
throw repeatAppendUnreconciledError([
|
|
10637
|
+
appendFailure,
|
|
10638
|
+
emergencyError,
|
|
10639
|
+
recoveryError,
|
|
10640
|
+
]);
|
|
10641
|
+
}
|
|
10642
|
+
throw appendFailure ?? emergencyError;
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
9970
10645
|
try {
|
|
9971
10646
|
restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates);
|
|
9972
10647
|
}
|
|
@@ -10221,6 +10896,7 @@ function renderBoundCard(container, schema, options) {
|
|
|
10221
10896
|
return {
|
|
10222
10897
|
getNode: nodeAccess.getNode,
|
|
10223
10898
|
onFocusChange: nodeAccess.onFocusChange,
|
|
10899
|
+
onScroll: nodeAccess.onScroll,
|
|
10224
10900
|
dispose() {
|
|
10225
10901
|
if (disposed || disposeRequested)
|
|
10226
10902
|
return;
|
|
@@ -11047,6 +11723,7 @@ function renderStaticCard(container, schema, options) {
|
|
|
11047
11723
|
return {
|
|
11048
11724
|
getNode: nodeAccess.getNode,
|
|
11049
11725
|
onFocusChange: nodeAccess.onFocusChange,
|
|
11726
|
+
onScroll: nodeAccess.onScroll,
|
|
11050
11727
|
dispose() {
|
|
11051
11728
|
if (disposed || disposeRequested)
|
|
11052
11729
|
return;
|
|
@@ -13074,6 +13751,7 @@ function renderStreamingCard(container, options = {}) {
|
|
|
13074
13751
|
return {
|
|
13075
13752
|
getNode: nodeAccess.getNode,
|
|
13076
13753
|
onFocusChange: nodeAccess.onFocusChange,
|
|
13754
|
+
onScroll: nodeAccess.onScroll,
|
|
13077
13755
|
applyCommand(command) {
|
|
13078
13756
|
if (!lockMode('commands'))
|
|
13079
13757
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antglobal/copilot-cards-web",
|
|
3
|
-
"version": "0.0.20260828032020-dev.
|
|
3
|
+
"version": "0.0.20260828032020-dev.5",
|
|
4
4
|
"description": "Web Component renderer for copilot bot card SDK — PC + Mobile + WebView unified rendering via Custom Elements",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@antglobal/copilot-cards-core": "0.0.20260828032020-dev.
|
|
31
|
+
"@antglobal/copilot-cards-core": "0.0.20260828032020-dev.5",
|
|
32
32
|
"echarts": "^5.6.0",
|
|
33
33
|
"marked": "^18.0.5",
|
|
34
34
|
"tslib": "^2.8.1",
|