@jsenv/dom 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_dom.js +59 -1468
- package/package.json +2 -2
package/dist/jsenv_dom.js
CHANGED
|
@@ -74,6 +74,13 @@ const getElementSignature = (element) => {
|
|
|
74
74
|
if (!element) {
|
|
75
75
|
return String(element);
|
|
76
76
|
}
|
|
77
|
+
if (typeof element === "string") {
|
|
78
|
+
return element === ""
|
|
79
|
+
? "empty string"
|
|
80
|
+
: element.length > 10
|
|
81
|
+
? `${element.slice(0, 10)}...`
|
|
82
|
+
: element;
|
|
83
|
+
}
|
|
77
84
|
if (typeof element === "function") {
|
|
78
85
|
const functionName = element.name;
|
|
79
86
|
const functionLabel = functionName
|
|
@@ -85,6 +92,9 @@ const getElementSignature = (element) => {
|
|
|
85
92
|
}
|
|
86
93
|
return `[${functionLabel}]`;
|
|
87
94
|
}
|
|
95
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
|
96
|
+
return `#text(${getElementSignature(element.nodeValue)})`;
|
|
97
|
+
}
|
|
88
98
|
if (element.props) {
|
|
89
99
|
const type = element.type;
|
|
90
100
|
const id = element.props.id;
|
|
@@ -397,10 +407,14 @@ const createSetMany$1 = (setter) => {
|
|
|
397
407
|
const setStyles = createSetMany$1(setStyle);
|
|
398
408
|
const forceStyles = createSetMany$1(forceStyle);
|
|
399
409
|
|
|
400
|
-
// Properties that
|
|
410
|
+
// Properties that can use px units
|
|
401
411
|
const pxPropertySet = new Set([
|
|
402
412
|
"width",
|
|
403
413
|
"height",
|
|
414
|
+
"minWidth",
|
|
415
|
+
"maxWidth",
|
|
416
|
+
"minHeight",
|
|
417
|
+
"maxHeight",
|
|
404
418
|
"top",
|
|
405
419
|
"left",
|
|
406
420
|
"right",
|
|
@@ -8381,9 +8395,11 @@ const createTransition = ({
|
|
|
8381
8395
|
...rest
|
|
8382
8396
|
} = {}) => {
|
|
8383
8397
|
const [updateCallbacks, executeUpdateCallbacks] = createCallbackController();
|
|
8398
|
+
const [cancelCallbacks, executeCancelCallbacks] = createCallbackController();
|
|
8384
8399
|
const [finishCallbacks, executeFinishCallbacks] = createCallbackController();
|
|
8385
8400
|
const channels = {
|
|
8386
8401
|
update: updateCallbacks,
|
|
8402
|
+
cancel: cancelCallbacks,
|
|
8387
8403
|
finish: finishCallbacks,
|
|
8388
8404
|
};
|
|
8389
8405
|
if (onUpdate) {
|
|
@@ -8494,6 +8510,7 @@ const createTransition = ({
|
|
|
8494
8510
|
}
|
|
8495
8511
|
resume = null;
|
|
8496
8512
|
playState = "idle";
|
|
8513
|
+
executeCancelCallbacks();
|
|
8497
8514
|
},
|
|
8498
8515
|
|
|
8499
8516
|
finish: () => {
|
|
@@ -8721,9 +8738,10 @@ const createCallbackController = () => {
|
|
|
8721
8738
|
|
|
8722
8739
|
const transitionStyleController = createStyleController("transition");
|
|
8723
8740
|
|
|
8724
|
-
const createHeightTransition = (element, to, options) => {
|
|
8741
|
+
const createHeightTransition = (element, to, options = {}) => {
|
|
8742
|
+
const { setup, ...rest } = options;
|
|
8725
8743
|
const heightTransition = createTimelineTransition({
|
|
8726
|
-
...
|
|
8744
|
+
...rest,
|
|
8727
8745
|
constructor: createHeightTransition,
|
|
8728
8746
|
key: element,
|
|
8729
8747
|
to,
|
|
@@ -8731,6 +8749,7 @@ const createHeightTransition = (element, to, options) => {
|
|
|
8731
8749
|
minDiff: 10,
|
|
8732
8750
|
lifecycle: {
|
|
8733
8751
|
setup: () => {
|
|
8752
|
+
const teardown = setup?.();
|
|
8734
8753
|
return {
|
|
8735
8754
|
from: getHeight$1(element),
|
|
8736
8755
|
update: ({ value }) => {
|
|
@@ -8738,6 +8757,7 @@ const createHeightTransition = (element, to, options) => {
|
|
|
8738
8757
|
},
|
|
8739
8758
|
teardown: () => {
|
|
8740
8759
|
transitionStyleController.delete(element, "height");
|
|
8760
|
+
teardown?.();
|
|
8741
8761
|
},
|
|
8742
8762
|
};
|
|
8743
8763
|
},
|
|
@@ -8745,9 +8765,10 @@ const createHeightTransition = (element, to, options) => {
|
|
|
8745
8765
|
});
|
|
8746
8766
|
return heightTransition;
|
|
8747
8767
|
};
|
|
8748
|
-
const createWidthTransition = (element, to, options) => {
|
|
8768
|
+
const createWidthTransition = (element, to, options = {}) => {
|
|
8769
|
+
const { setup, ...rest } = options;
|
|
8749
8770
|
const widthTransition = createTimelineTransition({
|
|
8750
|
-
...
|
|
8771
|
+
...rest,
|
|
8751
8772
|
constructor: createWidthTransition,
|
|
8752
8773
|
key: element,
|
|
8753
8774
|
to,
|
|
@@ -8755,6 +8776,7 @@ const createWidthTransition = (element, to, options) => {
|
|
|
8755
8776
|
isVisual: true,
|
|
8756
8777
|
lifecycle: {
|
|
8757
8778
|
setup: () => {
|
|
8779
|
+
const teardown = setup?.();
|
|
8758
8780
|
return {
|
|
8759
8781
|
from: getWidth$1(element),
|
|
8760
8782
|
update: ({ value }) => {
|
|
@@ -8762,6 +8784,7 @@ const createWidthTransition = (element, to, options) => {
|
|
|
8762
8784
|
},
|
|
8763
8785
|
teardown: () => {
|
|
8764
8786
|
transitionStyleController.delete(element, "width");
|
|
8787
|
+
teardown?.();
|
|
8765
8788
|
},
|
|
8766
8789
|
};
|
|
8767
8790
|
},
|
|
@@ -8770,8 +8793,9 @@ const createWidthTransition = (element, to, options) => {
|
|
|
8770
8793
|
return widthTransition;
|
|
8771
8794
|
};
|
|
8772
8795
|
const createOpacityTransition = (element, to, options = {}) => {
|
|
8796
|
+
const { setup, ...rest } = options;
|
|
8773
8797
|
const opacityTransition = createTimelineTransition({
|
|
8774
|
-
...
|
|
8798
|
+
...rest,
|
|
8775
8799
|
constructor: createOpacityTransition,
|
|
8776
8800
|
key: element,
|
|
8777
8801
|
to,
|
|
@@ -8779,6 +8803,7 @@ const createOpacityTransition = (element, to, options = {}) => {
|
|
|
8779
8803
|
isVisual: true,
|
|
8780
8804
|
lifecycle: {
|
|
8781
8805
|
setup: () => {
|
|
8806
|
+
const teardown = setup?.();
|
|
8782
8807
|
return {
|
|
8783
8808
|
from: getOpacity(element),
|
|
8784
8809
|
update: ({ value }) => {
|
|
@@ -8786,6 +8811,7 @@ const createOpacityTransition = (element, to, options = {}) => {
|
|
|
8786
8811
|
},
|
|
8787
8812
|
teardown: () => {
|
|
8788
8813
|
transitionStyleController.delete(element, "opacity");
|
|
8814
|
+
teardown?.();
|
|
8789
8815
|
},
|
|
8790
8816
|
};
|
|
8791
8817
|
},
|
|
@@ -8793,7 +8819,6 @@ const createOpacityTransition = (element, to, options = {}) => {
|
|
|
8793
8819
|
});
|
|
8794
8820
|
return opacityTransition;
|
|
8795
8821
|
};
|
|
8796
|
-
|
|
8797
8822
|
const createTranslateXTransition = (element, to, options = {}) => {
|
|
8798
8823
|
const { setup, ...rest } = options;
|
|
8799
8824
|
const translateXTransition = createTimelineTransition({
|
|
@@ -8816,8 +8841,8 @@ const createTranslateXTransition = (element, to, options = {}) => {
|
|
|
8816
8841
|
});
|
|
8817
8842
|
},
|
|
8818
8843
|
teardown: () => {
|
|
8819
|
-
teardown?.();
|
|
8820
8844
|
transitionStyleController.delete(element, "transform.translateX");
|
|
8845
|
+
teardown?.();
|
|
8821
8846
|
},
|
|
8822
8847
|
};
|
|
8823
8848
|
},
|
|
@@ -8831,6 +8856,10 @@ const getOpacityWithoutTransition = (element) =>
|
|
|
8831
8856
|
getOpacity(element, transitionStyleController);
|
|
8832
8857
|
const getTranslateXWithoutTransition = (element) =>
|
|
8833
8858
|
getTranslateX(element, transitionStyleController);
|
|
8859
|
+
const getWidthWithoutTransition = (element) =>
|
|
8860
|
+
getWidth$1(element, transitionStyleController);
|
|
8861
|
+
const getHeightWithoutTransition = (element) =>
|
|
8862
|
+
getHeight$1(element, transitionStyleController);
|
|
8834
8863
|
|
|
8835
8864
|
// transition that manages multiple transitions
|
|
8836
8865
|
const createGroupTransition = (transitionArray) => {
|
|
@@ -8966,7 +8995,7 @@ const createGroupTransitionController = () => {
|
|
|
8966
8995
|
* @returns {Object} Playback controller with play(), pause(), cancel(), etc.
|
|
8967
8996
|
*/
|
|
8968
8997
|
animate: (transitions, options = {}) => {
|
|
8969
|
-
const { onChange, onFinish } = options;
|
|
8998
|
+
const { onChange, onCancel, onFinish } = options;
|
|
8970
8999
|
|
|
8971
9000
|
if (transitions.length === 0) {
|
|
8972
9001
|
// No transitions to animate, call onFinish immediately
|
|
@@ -8979,7 +9008,11 @@ const createGroupTransitionController = () => {
|
|
|
8979
9008
|
cancel: () => {},
|
|
8980
9009
|
finish: () => {},
|
|
8981
9010
|
playState: "idle",
|
|
8982
|
-
channels: {
|
|
9011
|
+
channels: {
|
|
9012
|
+
update: { add: () => {} },
|
|
9013
|
+
cancel: { add: () => {} },
|
|
9014
|
+
finish: { add: () => {} },
|
|
9015
|
+
},
|
|
8983
9016
|
};
|
|
8984
9017
|
}
|
|
8985
9018
|
|
|
@@ -9033,6 +9066,7 @@ const createGroupTransitionController = () => {
|
|
|
9033
9066
|
playState: "running", // All are already running
|
|
9034
9067
|
channels: {
|
|
9035
9068
|
update: { add: () => {} }, // Update tracking already set up
|
|
9069
|
+
cancel: { add: () => {} },
|
|
9036
9070
|
finish: { add: () => {} },
|
|
9037
9071
|
},
|
|
9038
9072
|
};
|
|
@@ -9057,6 +9091,18 @@ const createGroupTransitionController = () => {
|
|
|
9057
9091
|
});
|
|
9058
9092
|
}
|
|
9059
9093
|
|
|
9094
|
+
if (onCancel) {
|
|
9095
|
+
groupTransition.channels.cancel.add(() => {
|
|
9096
|
+
const changeEntries = [...newTransitions, ...updatedTransitions].map(
|
|
9097
|
+
(transition) => ({
|
|
9098
|
+
transition,
|
|
9099
|
+
value: transition.value,
|
|
9100
|
+
}),
|
|
9101
|
+
);
|
|
9102
|
+
onCancel(changeEntries);
|
|
9103
|
+
});
|
|
9104
|
+
}
|
|
9105
|
+
|
|
9060
9106
|
// Add finish tracking
|
|
9061
9107
|
if (onFinish) {
|
|
9062
9108
|
groupTransition.channels.finish.add(() => {
|
|
@@ -9200,7 +9246,7 @@ const HEIGHT_TRANSITION_DURATION = 300;
|
|
|
9200
9246
|
const ANIMATE_TOGGLE = true;
|
|
9201
9247
|
const ANIMATE_RESIZE_AFTER_MUTATION = true;
|
|
9202
9248
|
const ANIMATION_THRESHOLD_PX = 10; // Don't animate changes smaller than this
|
|
9203
|
-
const DEBUG
|
|
9249
|
+
const DEBUG = false;
|
|
9204
9250
|
|
|
9205
9251
|
const initFlexDetailsSet = (
|
|
9206
9252
|
container,
|
|
@@ -9209,7 +9255,7 @@ const initFlexDetailsSet = (
|
|
|
9209
9255
|
onResizableDetailsChange,
|
|
9210
9256
|
onMouseResizeEnd,
|
|
9211
9257
|
onRequestedSizeChange,
|
|
9212
|
-
debug = DEBUG
|
|
9258
|
+
debug = DEBUG,
|
|
9213
9259
|
} = {},
|
|
9214
9260
|
) => {
|
|
9215
9261
|
const flexDetailsSet = {
|
|
@@ -10415,1459 +10461,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
|
|
|
10415
10461
|
};
|
|
10416
10462
|
};
|
|
10417
10463
|
|
|
10418
|
-
|
|
10419
|
-
import.meta.css = /* css */ `
|
|
10420
|
-
.ui_transition_container[data-transition-overflow] {
|
|
10421
|
-
overflow: hidden;
|
|
10422
|
-
}
|
|
10423
|
-
|
|
10424
|
-
.ui_transition_container,
|
|
10425
|
-
.ui_transition_outer_wrapper,
|
|
10426
|
-
.ui_transition_measure_wrapper,
|
|
10427
|
-
.ui_transition_slot,
|
|
10428
|
-
.ui_transition_phase_overlay,
|
|
10429
|
-
.ui_transition_content_overlay {
|
|
10430
|
-
display: flex;
|
|
10431
|
-
width: fit-content;
|
|
10432
|
-
min-width: 100%;
|
|
10433
|
-
height: fit-content;
|
|
10434
|
-
min-height: 100%;
|
|
10435
|
-
flex-direction: inherit;
|
|
10436
|
-
align-items: inherit;
|
|
10437
|
-
justify-content: inherit;
|
|
10438
|
-
border-radius: inherit;
|
|
10439
|
-
cursor: inherit;
|
|
10440
|
-
}
|
|
10441
|
-
|
|
10442
|
-
.ui_transition_measure_wrapper[data-transition-translate-x] {
|
|
10443
|
-
overflow: hidden;
|
|
10444
|
-
}
|
|
10445
|
-
|
|
10446
|
-
.ui_transition_container,
|
|
10447
|
-
.ui_transition_slot {
|
|
10448
|
-
position: relative;
|
|
10449
|
-
}
|
|
10450
|
-
|
|
10451
|
-
.ui_transition_phase_overlay,
|
|
10452
|
-
.ui_transition_content_overlay {
|
|
10453
|
-
position: absolute;
|
|
10454
|
-
inset: 0;
|
|
10455
|
-
pointer-events: none;
|
|
10456
|
-
}
|
|
10457
|
-
`;
|
|
10458
|
-
|
|
10459
|
-
const DEBUG = {
|
|
10460
|
-
size: false,
|
|
10461
|
-
transition: false,
|
|
10462
|
-
transition_updates: false,
|
|
10463
|
-
};
|
|
10464
|
-
|
|
10465
|
-
// Utility function to format content key states consistently for debug logs
|
|
10466
|
-
const formatContentKeyState = (
|
|
10467
|
-
contentKey,
|
|
10468
|
-
hasChildren,
|
|
10469
|
-
hasTextNode = false,
|
|
10470
|
-
) => {
|
|
10471
|
-
if (hasTextNode) {
|
|
10472
|
-
return "[text]";
|
|
10473
|
-
}
|
|
10474
|
-
if (!hasChildren) {
|
|
10475
|
-
return "[empty]";
|
|
10476
|
-
}
|
|
10477
|
-
if (contentKey === null || contentKey === undefined) {
|
|
10478
|
-
return "[unkeyed]";
|
|
10479
|
-
}
|
|
10480
|
-
return `[data-content-key="${contentKey}"]`;
|
|
10481
|
-
};
|
|
10482
|
-
|
|
10483
|
-
const SIZE_TRANSITION_DURATION = 150; // Default size transition duration
|
|
10484
|
-
const SIZE_DIFF_EPSILON = 0.5; // Ignore size transition when difference below this (px)
|
|
10485
|
-
const CONTENT_TRANSITION = "cross-fade"; // Default content transition type
|
|
10486
|
-
const CONTENT_TRANSITION_DURATION = 300; // Default content transition duration
|
|
10487
|
-
const PHASE_TRANSITION = "cross-fade";
|
|
10488
|
-
const PHASE_TRANSITION_DURATION = 300; // Default phase transition duration
|
|
10489
|
-
|
|
10490
|
-
const initUITransition = (container) => {
|
|
10491
|
-
const localDebug = {
|
|
10492
|
-
...DEBUG,
|
|
10493
|
-
transition: container.hasAttribute("data-debug-transition"),
|
|
10494
|
-
};
|
|
10495
|
-
const debugClones = container.hasAttribute("data-debug-clones");
|
|
10496
|
-
|
|
10497
|
-
const debug = (type, ...args) => {
|
|
10498
|
-
if (localDebug[type]) {
|
|
10499
|
-
console.debug(`[${type}]`, ...args);
|
|
10500
|
-
}
|
|
10501
|
-
};
|
|
10502
|
-
|
|
10503
|
-
if (!container.classList.contains("ui_transition_container")) {
|
|
10504
|
-
console.error("Element must have ui_transition_container class");
|
|
10505
|
-
return { cleanup: () => {} };
|
|
10506
|
-
}
|
|
10507
|
-
|
|
10508
|
-
const outerWrapper = container.querySelector(".ui_transition_outer_wrapper");
|
|
10509
|
-
const measureWrapper = container.querySelector(
|
|
10510
|
-
".ui_transition_measure_wrapper",
|
|
10511
|
-
);
|
|
10512
|
-
const slot = container.querySelector(".ui_transition_slot");
|
|
10513
|
-
let phaseOverlay = measureWrapper.querySelector(
|
|
10514
|
-
".ui_transition_phase_overlay",
|
|
10515
|
-
);
|
|
10516
|
-
let contentOverlay = container.querySelector(
|
|
10517
|
-
".ui_transition_content_overlay",
|
|
10518
|
-
);
|
|
10519
|
-
|
|
10520
|
-
if (
|
|
10521
|
-
!outerWrapper ||
|
|
10522
|
-
!measureWrapper ||
|
|
10523
|
-
!slot ||
|
|
10524
|
-
!phaseOverlay ||
|
|
10525
|
-
!contentOverlay
|
|
10526
|
-
) {
|
|
10527
|
-
console.error("Missing required ui-transition structure");
|
|
10528
|
-
return { cleanup: () => {} };
|
|
10529
|
-
}
|
|
10530
|
-
|
|
10531
|
-
const [teardown, addTeardown] = createPubSub();
|
|
10532
|
-
|
|
10533
|
-
{
|
|
10534
|
-
const transitionOverflowSet = new Set();
|
|
10535
|
-
const updateTransitionOverflowAttribute = () => {
|
|
10536
|
-
if (transitionOverflowSet.size > 0) {
|
|
10537
|
-
container.setAttribute("data-transition-overflow", "");
|
|
10538
|
-
} else {
|
|
10539
|
-
container.removeAttribute("data-transition-overflow");
|
|
10540
|
-
}
|
|
10541
|
-
};
|
|
10542
|
-
const onOverflowStart = (event) => {
|
|
10543
|
-
transitionOverflowSet.add(event.detail.transitionId);
|
|
10544
|
-
updateTransitionOverflowAttribute();
|
|
10545
|
-
};
|
|
10546
|
-
const onOverflowEnd = (event) => {
|
|
10547
|
-
transitionOverflowSet.delete(event.detail.transitionId);
|
|
10548
|
-
updateTransitionOverflowAttribute();
|
|
10549
|
-
};
|
|
10550
|
-
container.addEventListener("ui_transition_overflow_start", onOverflowStart);
|
|
10551
|
-
container.addEventListener("ui_transition_overflow_end", onOverflowEnd);
|
|
10552
|
-
addTeardown(() => {
|
|
10553
|
-
container.removeEventListener(
|
|
10554
|
-
"ui_transition_overflow_start",
|
|
10555
|
-
onOverflowStart,
|
|
10556
|
-
);
|
|
10557
|
-
container.removeEventListener(
|
|
10558
|
-
"ui_transition_overflow_end",
|
|
10559
|
-
onOverflowEnd,
|
|
10560
|
-
);
|
|
10561
|
-
});
|
|
10562
|
-
}
|
|
10563
|
-
|
|
10564
|
-
const transitionController = createGroupTransitionController();
|
|
10565
|
-
|
|
10566
|
-
// Transition state
|
|
10567
|
-
let activeContentTransition = null;
|
|
10568
|
-
let activeContentTransitionType = null;
|
|
10569
|
-
let activePhaseTransition = null;
|
|
10570
|
-
let activePhaseTransitionType = null;
|
|
10571
|
-
let isPaused = false;
|
|
10572
|
-
|
|
10573
|
-
// Size state
|
|
10574
|
-
let naturalContentWidth = 0; // Natural size of actual content (not loading/error states)
|
|
10575
|
-
let naturalContentHeight = 0;
|
|
10576
|
-
let constrainedWidth = 0; // Current constrained dimensions (what outer wrapper is set to)
|
|
10577
|
-
let constrainedHeight = 0;
|
|
10578
|
-
let sizeTransition = null;
|
|
10579
|
-
let resizeObserver = null;
|
|
10580
|
-
let sizeHoldActive = false; // Hold previous dimensions during content transitions when size transitions are disabled
|
|
10581
|
-
|
|
10582
|
-
// Prevent reacting to our own constrained size changes while animating
|
|
10583
|
-
let suppressResizeObserver = false;
|
|
10584
|
-
let pendingResizeSync = false; // ensure one measurement after suppression ends
|
|
10585
|
-
|
|
10586
|
-
// Handle size updates based on content state
|
|
10587
|
-
let hasSizeTransitions = container.hasAttribute("data-size-transition");
|
|
10588
|
-
const initialTransitionEnabled = container.hasAttribute(
|
|
10589
|
-
"data-initial-transition",
|
|
10590
|
-
);
|
|
10591
|
-
let hasPopulatedOnce = false; // track if we've already populated once (null → something)
|
|
10592
|
-
|
|
10593
|
-
// Child state
|
|
10594
|
-
let lastContentKey = null;
|
|
10595
|
-
let previousChildNodes = [];
|
|
10596
|
-
let isContentPhase = false; // Current state: true when showing content phase (loading/error)
|
|
10597
|
-
let wasContentPhase = false; // Previous state for comparison
|
|
10598
|
-
|
|
10599
|
-
const measureContentSize = () => {
|
|
10600
|
-
return [getWidth(measureWrapper), getHeight(measureWrapper)];
|
|
10601
|
-
};
|
|
10602
|
-
|
|
10603
|
-
const updateContentDimensions = () => {
|
|
10604
|
-
const [newWidth, newHeight] = measureContentSize();
|
|
10605
|
-
debug("size", "Content size changed:", {
|
|
10606
|
-
width: `${naturalContentWidth} → ${newWidth}`,
|
|
10607
|
-
height: `${naturalContentHeight} → ${newHeight}`,
|
|
10608
|
-
});
|
|
10609
|
-
|
|
10610
|
-
updateNaturalContentSize(newWidth, newHeight);
|
|
10611
|
-
|
|
10612
|
-
if (sizeTransition) {
|
|
10613
|
-
debug("size", "Updating animation target:", newHeight);
|
|
10614
|
-
updateToSize(newWidth, newHeight);
|
|
10615
|
-
} else {
|
|
10616
|
-
constrainedWidth = newWidth;
|
|
10617
|
-
constrainedHeight = newHeight;
|
|
10618
|
-
}
|
|
10619
|
-
};
|
|
10620
|
-
|
|
10621
|
-
const stopResizeObserver = () => {
|
|
10622
|
-
if (resizeObserver) {
|
|
10623
|
-
resizeObserver.disconnect();
|
|
10624
|
-
resizeObserver = null;
|
|
10625
|
-
}
|
|
10626
|
-
};
|
|
10627
|
-
|
|
10628
|
-
const startResizeObserver = () => {
|
|
10629
|
-
resizeObserver = new ResizeObserver(() => {
|
|
10630
|
-
if (!hasSizeTransitions) {
|
|
10631
|
-
return;
|
|
10632
|
-
}
|
|
10633
|
-
if (suppressResizeObserver) {
|
|
10634
|
-
pendingResizeSync = true;
|
|
10635
|
-
debug("size", "Resize ignored (suppressed during size transition)");
|
|
10636
|
-
return;
|
|
10637
|
-
}
|
|
10638
|
-
updateContentDimensions();
|
|
10639
|
-
});
|
|
10640
|
-
resizeObserver.observe(measureWrapper);
|
|
10641
|
-
};
|
|
10642
|
-
|
|
10643
|
-
const releaseConstraints = (reason) => {
|
|
10644
|
-
debug("size", `Releasing constraints (${reason})`);
|
|
10645
|
-
const [beforeWidth, beforeHeight] = measureContentSize();
|
|
10646
|
-
outerWrapper.style.width = "";
|
|
10647
|
-
outerWrapper.style.height = "";
|
|
10648
|
-
outerWrapper.style.overflow = "";
|
|
10649
|
-
const [afterWidth, afterHeight] = measureContentSize();
|
|
10650
|
-
debug("size", "Size after release:", {
|
|
10651
|
-
width: `${beforeWidth} → ${afterWidth}`,
|
|
10652
|
-
height: `${beforeHeight} → ${afterHeight}`,
|
|
10653
|
-
});
|
|
10654
|
-
constrainedWidth = afterWidth;
|
|
10655
|
-
constrainedHeight = afterHeight;
|
|
10656
|
-
naturalContentWidth = afterWidth;
|
|
10657
|
-
naturalContentHeight = afterHeight;
|
|
10658
|
-
// Defer a sync if suppression just ended; actual dispatch will come from resize observer
|
|
10659
|
-
if (!suppressResizeObserver && pendingResizeSync) {
|
|
10660
|
-
pendingResizeSync = false;
|
|
10661
|
-
updateContentDimensions();
|
|
10662
|
-
}
|
|
10663
|
-
};
|
|
10664
|
-
|
|
10665
|
-
const updateToSize = (targetWidth, targetHeight) => {
|
|
10666
|
-
if (
|
|
10667
|
-
constrainedWidth === targetWidth &&
|
|
10668
|
-
constrainedHeight === targetHeight
|
|
10669
|
-
) {
|
|
10670
|
-
return;
|
|
10671
|
-
}
|
|
10672
|
-
|
|
10673
|
-
const shouldAnimate = container.hasAttribute("data-size-transition");
|
|
10674
|
-
const widthDiff = Math.abs(targetWidth - constrainedWidth);
|
|
10675
|
-
const heightDiff = Math.abs(targetHeight - constrainedHeight);
|
|
10676
|
-
|
|
10677
|
-
if (widthDiff <= SIZE_DIFF_EPSILON && heightDiff <= SIZE_DIFF_EPSILON) {
|
|
10678
|
-
// Both diffs negligible; just sync styles if changed and bail
|
|
10679
|
-
if (widthDiff > 0) {
|
|
10680
|
-
outerWrapper.style.width = `${targetWidth}px`;
|
|
10681
|
-
constrainedWidth = targetWidth;
|
|
10682
|
-
}
|
|
10683
|
-
if (heightDiff > 0) {
|
|
10684
|
-
outerWrapper.style.height = `${targetHeight}px`;
|
|
10685
|
-
constrainedHeight = targetHeight;
|
|
10686
|
-
}
|
|
10687
|
-
debug(
|
|
10688
|
-
"size",
|
|
10689
|
-
`Skip size animation entirely (diffs width:${widthDiff.toFixed(4)}px height:${heightDiff.toFixed(4)}px)`,
|
|
10690
|
-
);
|
|
10691
|
-
return;
|
|
10692
|
-
}
|
|
10693
|
-
|
|
10694
|
-
if (!shouldAnimate) {
|
|
10695
|
-
// No size transitions - just update dimensions instantly
|
|
10696
|
-
debug("size", "Updating size instantly:", {
|
|
10697
|
-
width: `${constrainedWidth} → ${targetWidth}`,
|
|
10698
|
-
height: `${constrainedHeight} → ${targetHeight}`,
|
|
10699
|
-
});
|
|
10700
|
-
suppressResizeObserver = true;
|
|
10701
|
-
outerWrapper.style.width = `${targetWidth}px`;
|
|
10702
|
-
outerWrapper.style.height = `${targetHeight}px`;
|
|
10703
|
-
constrainedWidth = targetWidth;
|
|
10704
|
-
constrainedHeight = targetHeight;
|
|
10705
|
-
// allow any resize notifications to settle then re-enable
|
|
10706
|
-
requestAnimationFrame(() => {
|
|
10707
|
-
suppressResizeObserver = false;
|
|
10708
|
-
if (pendingResizeSync) {
|
|
10709
|
-
pendingResizeSync = false;
|
|
10710
|
-
updateContentDimensions();
|
|
10711
|
-
}
|
|
10712
|
-
});
|
|
10713
|
-
return;
|
|
10714
|
-
}
|
|
10715
|
-
|
|
10716
|
-
// Animated size transition
|
|
10717
|
-
debug("size", "Animating size:", {
|
|
10718
|
-
width: `${constrainedWidth} → ${targetWidth}`,
|
|
10719
|
-
height: `${constrainedHeight} → ${targetHeight}`,
|
|
10720
|
-
});
|
|
10721
|
-
|
|
10722
|
-
const duration = parseInt(
|
|
10723
|
-
container.getAttribute("data-size-transition-duration") ||
|
|
10724
|
-
SIZE_TRANSITION_DURATION,
|
|
10725
|
-
);
|
|
10726
|
-
|
|
10727
|
-
outerWrapper.style.overflow = "hidden";
|
|
10728
|
-
const transitions = [];
|
|
10729
|
-
|
|
10730
|
-
// heightDiff & widthDiff already computed earlier in updateToSize when deciding to skip entirely
|
|
10731
|
-
if (heightDiff <= SIZE_DIFF_EPSILON) {
|
|
10732
|
-
// Treat as identical
|
|
10733
|
-
if (heightDiff > 0) {
|
|
10734
|
-
debug(
|
|
10735
|
-
"size",
|
|
10736
|
-
`Skip height transition (negligible diff ${heightDiff.toFixed(4)}px)`,
|
|
10737
|
-
);
|
|
10738
|
-
}
|
|
10739
|
-
outerWrapper.style.height = `${targetHeight}px`;
|
|
10740
|
-
constrainedHeight = targetHeight;
|
|
10741
|
-
} else if (targetHeight !== constrainedHeight) {
|
|
10742
|
-
transitions.push(
|
|
10743
|
-
createHeightTransition(outerWrapper, targetHeight, {
|
|
10744
|
-
duration,
|
|
10745
|
-
onUpdate: ({ value }) => {
|
|
10746
|
-
constrainedHeight = value;
|
|
10747
|
-
},
|
|
10748
|
-
}),
|
|
10749
|
-
);
|
|
10750
|
-
}
|
|
10751
|
-
|
|
10752
|
-
if (widthDiff <= SIZE_DIFF_EPSILON) {
|
|
10753
|
-
if (widthDiff > 0) {
|
|
10754
|
-
debug(
|
|
10755
|
-
"size",
|
|
10756
|
-
`Skip width transition (negligible diff ${widthDiff.toFixed(4)}px)`,
|
|
10757
|
-
);
|
|
10758
|
-
}
|
|
10759
|
-
outerWrapper.style.width = `${targetWidth}px`;
|
|
10760
|
-
constrainedWidth = targetWidth;
|
|
10761
|
-
} else if (targetWidth !== constrainedWidth) {
|
|
10762
|
-
transitions.push(
|
|
10763
|
-
createWidthTransition(outerWrapper, targetWidth, {
|
|
10764
|
-
duration,
|
|
10765
|
-
onUpdate: ({ value }) => {
|
|
10766
|
-
constrainedWidth = value;
|
|
10767
|
-
},
|
|
10768
|
-
}),
|
|
10769
|
-
);
|
|
10770
|
-
}
|
|
10771
|
-
|
|
10772
|
-
if (transitions.length > 0) {
|
|
10773
|
-
suppressResizeObserver = true;
|
|
10774
|
-
sizeTransition = transitionController.animate(transitions, {
|
|
10775
|
-
onFinish: () => {
|
|
10776
|
-
releaseConstraints("animated size transition completed");
|
|
10777
|
-
// End suppression next frame to avoid RO loop warnings
|
|
10778
|
-
requestAnimationFrame(() => {
|
|
10779
|
-
suppressResizeObserver = false;
|
|
10780
|
-
if (pendingResizeSync) {
|
|
10781
|
-
pendingResizeSync = false;
|
|
10782
|
-
updateContentDimensions();
|
|
10783
|
-
}
|
|
10784
|
-
});
|
|
10785
|
-
},
|
|
10786
|
-
});
|
|
10787
|
-
sizeTransition.play();
|
|
10788
|
-
} else {
|
|
10789
|
-
debug(
|
|
10790
|
-
"size",
|
|
10791
|
-
"No size transitions created (identical or negligible differences)",
|
|
10792
|
-
);
|
|
10793
|
-
}
|
|
10794
|
-
};
|
|
10795
|
-
|
|
10796
|
-
const applySizeConstraints = (targetWidth, targetHeight) => {
|
|
10797
|
-
debug("size", "Applying size constraints:", {
|
|
10798
|
-
width: `${constrainedWidth} → ${targetWidth}`,
|
|
10799
|
-
height: `${constrainedHeight} → ${targetHeight}`,
|
|
10800
|
-
});
|
|
10801
|
-
|
|
10802
|
-
outerWrapper.style.width = `${targetWidth}px`;
|
|
10803
|
-
outerWrapper.style.height = `${targetHeight}px`;
|
|
10804
|
-
outerWrapper.style.overflow = "hidden";
|
|
10805
|
-
constrainedWidth = targetWidth;
|
|
10806
|
-
constrainedHeight = targetHeight;
|
|
10807
|
-
};
|
|
10808
|
-
|
|
10809
|
-
const updateNaturalContentSize = (newWidth, newHeight) => {
|
|
10810
|
-
debug("size", "Updating natural content size:", {
|
|
10811
|
-
width: `${naturalContentWidth} → ${newWidth}`,
|
|
10812
|
-
height: `${naturalContentHeight} → ${newHeight}`,
|
|
10813
|
-
});
|
|
10814
|
-
naturalContentWidth = newWidth;
|
|
10815
|
-
naturalContentHeight = newHeight;
|
|
10816
|
-
};
|
|
10817
|
-
|
|
10818
|
-
let isUpdating = false;
|
|
10819
|
-
|
|
10820
|
-
// Shared transition setup function
|
|
10821
|
-
const setupTransition = ({
|
|
10822
|
-
isPhaseTransition = false,
|
|
10823
|
-
overlay,
|
|
10824
|
-
needsOldChildNodesClone,
|
|
10825
|
-
previousChildNodes,
|
|
10826
|
-
childNodes,
|
|
10827
|
-
attributeToRemove = [],
|
|
10828
|
-
}) => {
|
|
10829
|
-
let cleanup = () => {};
|
|
10830
|
-
let elementToImpact;
|
|
10831
|
-
|
|
10832
|
-
if (overlay.childNodes.length > 0) {
|
|
10833
|
-
elementToImpact = overlay;
|
|
10834
|
-
cleanup = () => {
|
|
10835
|
-
if (!debugClones) {
|
|
10836
|
-
overlay.innerHTML = "";
|
|
10837
|
-
}
|
|
10838
|
-
};
|
|
10839
|
-
debug(
|
|
10840
|
-
"transition",
|
|
10841
|
-
`Continuing from current ${isPhaseTransition ? "phase" : "content"} transition element`,
|
|
10842
|
-
);
|
|
10843
|
-
} else if (needsOldChildNodesClone) {
|
|
10844
|
-
overlay.innerHTML = "";
|
|
10845
|
-
for (const previousChildNode of previousChildNodes) {
|
|
10846
|
-
const previousChildClone = previousChildNode.cloneNode(true);
|
|
10847
|
-
if (previousChildClone.nodeType !== Node.TEXT_NODE) {
|
|
10848
|
-
for (const attrToRemove of attributeToRemove) {
|
|
10849
|
-
previousChildClone.removeAttribute(attrToRemove);
|
|
10850
|
-
}
|
|
10851
|
-
previousChildClone.setAttribute("data-ui-transition-clone", "");
|
|
10852
|
-
}
|
|
10853
|
-
overlay.appendChild(previousChildClone);
|
|
10854
|
-
}
|
|
10855
|
-
elementToImpact = overlay;
|
|
10856
|
-
cleanup = () => {
|
|
10857
|
-
if (!debugClones) {
|
|
10858
|
-
overlay.innerHTML = "";
|
|
10859
|
-
}
|
|
10860
|
-
};
|
|
10861
|
-
debug(
|
|
10862
|
-
"transition",
|
|
10863
|
-
`Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
|
|
10864
|
-
getElementSignature(previousChildNodes),
|
|
10865
|
-
);
|
|
10866
|
-
} else {
|
|
10867
|
-
overlay.innerHTML = "";
|
|
10868
|
-
debug(
|
|
10869
|
-
"transition",
|
|
10870
|
-
`No old child to clone for ${isPhaseTransition ? "phase" : "content"} transition`,
|
|
10871
|
-
);
|
|
10872
|
-
}
|
|
10873
|
-
|
|
10874
|
-
// Determine which elements to return based on transition type:
|
|
10875
|
-
// - Phase transitions: operate on individual elements (cross-fade between specific elements)
|
|
10876
|
-
// - Content transitions: operate at container level (slide entire containers, outlive content phases)
|
|
10877
|
-
let oldElement;
|
|
10878
|
-
let newElement;
|
|
10879
|
-
if (isPhaseTransition) {
|
|
10880
|
-
// Phase transitions work on individual elements
|
|
10881
|
-
oldElement = elementToImpact;
|
|
10882
|
-
newElement = slot;
|
|
10883
|
-
} else {
|
|
10884
|
-
// Content transitions work at container level and can outlive content phase changes
|
|
10885
|
-
oldElement = previousChildNodes.length ? elementToImpact : null;
|
|
10886
|
-
newElement = childNodes.length ? measureWrapper : null;
|
|
10887
|
-
}
|
|
10888
|
-
|
|
10889
|
-
return {
|
|
10890
|
-
cleanup,
|
|
10891
|
-
oldElement,
|
|
10892
|
-
newElement,
|
|
10893
|
-
};
|
|
10894
|
-
};
|
|
10895
|
-
|
|
10896
|
-
// Initialize with current size
|
|
10897
|
-
[constrainedWidth, constrainedHeight] = measureContentSize();
|
|
10898
|
-
|
|
10899
|
-
const handleChildSlotMutation = (reason = "mutation") => {
|
|
10900
|
-
if (isUpdating) {
|
|
10901
|
-
debug("transition", "Preventing recursive update");
|
|
10902
|
-
return;
|
|
10903
|
-
}
|
|
10904
|
-
|
|
10905
|
-
hasSizeTransitions = container.hasAttribute("data-size-transition");
|
|
10906
|
-
|
|
10907
|
-
try {
|
|
10908
|
-
isUpdating = true;
|
|
10909
|
-
const childNodes = Array.from(slot.childNodes);
|
|
10910
|
-
if (localDebug.transition) {
|
|
10911
|
-
const updateLabel =
|
|
10912
|
-
childNodes.length === 0
|
|
10913
|
-
? "cleared/empty"
|
|
10914
|
-
: childNodes.length === 1
|
|
10915
|
-
? getElementSignature(childNodes[0])
|
|
10916
|
-
: getElementSignature(slot);
|
|
10917
|
-
console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
|
|
10918
|
-
}
|
|
10919
|
-
|
|
10920
|
-
// Determine transition scenarios early for early registration check
|
|
10921
|
-
// Prepare phase info early so logging can be unified (even for early return)
|
|
10922
|
-
wasContentPhase = isContentPhase;
|
|
10923
|
-
const hadChild = previousChildNodes.length > 0;
|
|
10924
|
-
const hasChild = childNodes.length > 0;
|
|
10925
|
-
|
|
10926
|
-
// Prefer data-content-key on child, fallback to slot
|
|
10927
|
-
let currentContentKey = null;
|
|
10928
|
-
let slotContentKey = slot.getAttribute("data-content-key");
|
|
10929
|
-
let childContentKey;
|
|
10930
|
-
|
|
10931
|
-
if (childNodes.length === 0) {
|
|
10932
|
-
childContentKey = null;
|
|
10933
|
-
isContentPhase = true; // empty (no child) is treated as content phase
|
|
10934
|
-
} else {
|
|
10935
|
-
for (const childNode of childNodes) {
|
|
10936
|
-
if (childNode.nodeType === Node.TEXT_NODE) {
|
|
10937
|
-
} else if (childNode.hasAttribute("data-content-key")) {
|
|
10938
|
-
childContentKey = childNode.getAttribute("data-content-key");
|
|
10939
|
-
} else if (childNode.hasAttribute("data-content-phase")) {
|
|
10940
|
-
isContentPhase = true;
|
|
10941
|
-
}
|
|
10942
|
-
}
|
|
10943
|
-
}
|
|
10944
|
-
if (childContentKey && slotContentKey) {
|
|
10945
|
-
console.warn(
|
|
10946
|
-
`Slot and slot child both have a [data-content-key]. Slot is ${slotContentKey} and child is ${childContentKey}, using the child.`,
|
|
10947
|
-
);
|
|
10948
|
-
}
|
|
10949
|
-
currentContentKey = childContentKey || slotContentKey || null;
|
|
10950
|
-
// Compute formatted content key states ONCE per mutation (requirement: max 2 calls)
|
|
10951
|
-
const previousContentKeyState = formatContentKeyState(
|
|
10952
|
-
lastContentKey,
|
|
10953
|
-
hadChild,
|
|
10954
|
-
);
|
|
10955
|
-
const currentContentKeyState = formatContentKeyState(
|
|
10956
|
-
currentContentKey,
|
|
10957
|
-
hasChild,
|
|
10958
|
-
);
|
|
10959
|
-
// Track previous key before any potential early registration update
|
|
10960
|
-
const prevKeyBeforeRegistration = lastContentKey;
|
|
10961
|
-
const previousIsContentPhase = !hadChild || wasContentPhase;
|
|
10962
|
-
const currentIsContentPhase = !hasChild || isContentPhase;
|
|
10963
|
-
|
|
10964
|
-
// Early conceptual registration path: empty slot
|
|
10965
|
-
const shouldGiveUpEarlyAndJustRegister = !hadChild && !hasChild;
|
|
10966
|
-
let earlyAction = null;
|
|
10967
|
-
if (shouldGiveUpEarlyAndJustRegister) {
|
|
10968
|
-
const prevKey = prevKeyBeforeRegistration;
|
|
10969
|
-
const keyChanged = prevKey !== currentContentKey;
|
|
10970
|
-
if (!keyChanged) {
|
|
10971
|
-
earlyAction = "unchanged";
|
|
10972
|
-
} else if (prevKey === null && currentContentKey !== null) {
|
|
10973
|
-
earlyAction = "registered";
|
|
10974
|
-
} else if (prevKey !== null && currentContentKey === null) {
|
|
10975
|
-
earlyAction = "cleared";
|
|
10976
|
-
} else {
|
|
10977
|
-
earlyAction = "changed";
|
|
10978
|
-
}
|
|
10979
|
-
// Will update lastContentKey after unified logging
|
|
10980
|
-
}
|
|
10981
|
-
|
|
10982
|
-
// Decide which representation to display for previous/current in early case
|
|
10983
|
-
const conceptualPrevDisplay =
|
|
10984
|
-
prevKeyBeforeRegistration === null
|
|
10985
|
-
? "[unkeyed]"
|
|
10986
|
-
: `[data-content-key="${prevKeyBeforeRegistration}"]`;
|
|
10987
|
-
const conceptualCurrentDisplay =
|
|
10988
|
-
currentContentKey === null
|
|
10989
|
-
? "[unkeyed]"
|
|
10990
|
-
: `[data-content-key="${currentContentKey}"]`;
|
|
10991
|
-
const previousDisplay = shouldGiveUpEarlyAndJustRegister
|
|
10992
|
-
? conceptualPrevDisplay
|
|
10993
|
-
: previousContentKeyState;
|
|
10994
|
-
const currentDisplay = shouldGiveUpEarlyAndJustRegister
|
|
10995
|
-
? conceptualCurrentDisplay
|
|
10996
|
-
: currentContentKeyState;
|
|
10997
|
-
|
|
10998
|
-
// Build a simple descriptive sentence
|
|
10999
|
-
let contentKeysSentence = `Content key: ${previousDisplay} → ${currentDisplay}`;
|
|
11000
|
-
debug("transition", contentKeysSentence);
|
|
11001
|
-
|
|
11002
|
-
if (shouldGiveUpEarlyAndJustRegister) {
|
|
11003
|
-
// Log decision explicitly (was previously embedded)
|
|
11004
|
-
debug("transition", `Decision: EARLY_RETURN (${earlyAction})`);
|
|
11005
|
-
// Register new conceptual key & return early (skip rest of transition logic)
|
|
11006
|
-
lastContentKey = currentContentKey;
|
|
11007
|
-
if (localDebug.transition) {
|
|
11008
|
-
console.groupEnd();
|
|
11009
|
-
}
|
|
11010
|
-
return;
|
|
11011
|
-
}
|
|
11012
|
-
debug(
|
|
11013
|
-
"size",
|
|
11014
|
-
`Update triggered, size: ${constrainedWidth}x${constrainedHeight}`,
|
|
11015
|
-
);
|
|
11016
|
-
|
|
11017
|
-
if (sizeTransition) {
|
|
11018
|
-
sizeTransition.cancel();
|
|
11019
|
-
}
|
|
11020
|
-
|
|
11021
|
-
const [newWidth, newHeight] = measureContentSize();
|
|
11022
|
-
debug("size", `Measured size: ${newWidth}x${newHeight}`);
|
|
11023
|
-
outerWrapper.style.width = `${constrainedWidth}px`;
|
|
11024
|
-
outerWrapper.style.height = `${constrainedHeight}px`;
|
|
11025
|
-
|
|
11026
|
-
// Handle resize observation
|
|
11027
|
-
stopResizeObserver();
|
|
11028
|
-
if (hasChild && !isContentPhase) {
|
|
11029
|
-
startResizeObserver();
|
|
11030
|
-
debug("size", "Observing child resize");
|
|
11031
|
-
}
|
|
11032
|
-
|
|
11033
|
-
// Determine transition scenarios (hadChild/hasChild already computed above for logging)
|
|
11034
|
-
|
|
11035
|
-
/**
|
|
11036
|
-
* Content Phase Logic: Why empty slots are treated as content phases
|
|
11037
|
-
*
|
|
11038
|
-
* When there is no child element (React component returns null), it is considered
|
|
11039
|
-
* that the component does not render anything temporarily. This might be because:
|
|
11040
|
-
* - The component is loading but does not have a loading state
|
|
11041
|
-
* - The component has an error but does not have an error state
|
|
11042
|
-
* - The component is conceptually unloaded (underlying content was deleted/is not accessible)
|
|
11043
|
-
*
|
|
11044
|
-
* This represents a phase of the given content: having nothing to display.
|
|
11045
|
-
*
|
|
11046
|
-
* We support transitions between different contents via the ability to set
|
|
11047
|
-
* [data-content-key] on the ".ui_transition_slot". This is also useful when you want
|
|
11048
|
-
* all children of a React component to inherit the same data-content-key without
|
|
11049
|
-
* explicitly setting the attribute on each child element.
|
|
11050
|
-
*/
|
|
11051
|
-
|
|
11052
|
-
// Content key change when either slot or child has data-content-key and it changed
|
|
11053
|
-
let shouldDoContentTransition = false;
|
|
11054
|
-
if (currentContentKey && lastContentKey !== null) {
|
|
11055
|
-
shouldDoContentTransition = currentContentKey !== lastContentKey;
|
|
11056
|
-
}
|
|
11057
|
-
|
|
11058
|
-
const becomesEmpty = hadChild && !hasChild;
|
|
11059
|
-
const becomesPopulated = !hadChild && hasChild;
|
|
11060
|
-
const isInitialPopulationWithoutTransition =
|
|
11061
|
-
becomesPopulated && !hasPopulatedOnce && !initialTransitionEnabled;
|
|
11062
|
-
|
|
11063
|
-
// Content phase change: any transition between content/content-phase/null except when slot key changes
|
|
11064
|
-
// This includes: null→loading, loading→content, content→loading, loading→null, etc.
|
|
11065
|
-
const shouldDoPhaseTransition =
|
|
11066
|
-
!shouldDoContentTransition &&
|
|
11067
|
-
(becomesPopulated ||
|
|
11068
|
-
becomesEmpty ||
|
|
11069
|
-
(hadChild &&
|
|
11070
|
-
hasChild &&
|
|
11071
|
-
(previousIsContentPhase !== currentIsContentPhase ||
|
|
11072
|
-
(previousIsContentPhase && currentIsContentPhase))));
|
|
11073
|
-
|
|
11074
|
-
const contentChange = hadChild && hasChild && shouldDoContentTransition;
|
|
11075
|
-
const phaseChange = hadChild && hasChild && shouldDoPhaseTransition;
|
|
11076
|
-
|
|
11077
|
-
// Determine if we only need to preserve an existing content transition (no new change)
|
|
11078
|
-
const preserveOnlyContentTransition =
|
|
11079
|
-
activeContentTransition !== null &&
|
|
11080
|
-
!shouldDoContentTransition &&
|
|
11081
|
-
!shouldDoPhaseTransition &&
|
|
11082
|
-
!becomesPopulated &&
|
|
11083
|
-
!becomesEmpty;
|
|
11084
|
-
|
|
11085
|
-
// Include becomesPopulated in content transition only if it's not a phase transition
|
|
11086
|
-
const shouldDoContentTransitionIncludingPopulation =
|
|
11087
|
-
shouldDoContentTransition ||
|
|
11088
|
-
(becomesPopulated && !shouldDoPhaseTransition);
|
|
11089
|
-
|
|
11090
|
-
const decisions = [];
|
|
11091
|
-
if (shouldDoContentTransition) decisions.push("CONTENT TRANSITION");
|
|
11092
|
-
if (shouldDoPhaseTransition) decisions.push("PHASE TRANSITION");
|
|
11093
|
-
if (preserveOnlyContentTransition)
|
|
11094
|
-
decisions.push("PRESERVE CONTENT TRANSITION");
|
|
11095
|
-
if (decisions.length === 0) decisions.push("NO TRANSITION");
|
|
11096
|
-
|
|
11097
|
-
debug("transition", `Decision: ${decisions.join(" + ")}`);
|
|
11098
|
-
if (preserveOnlyContentTransition) {
|
|
11099
|
-
const progress = (activeContentTransition.progress * 100).toFixed(1);
|
|
11100
|
-
debug(
|
|
11101
|
-
"transition",
|
|
11102
|
-
`Preserving existing content transition (progress ${progress}%)`,
|
|
11103
|
-
);
|
|
11104
|
-
}
|
|
11105
|
-
|
|
11106
|
-
// Early return optimization: if no transition decision and we are not continuing
|
|
11107
|
-
// an existing active content transition (animationProgress > 0), we can skip
|
|
11108
|
-
// all transition setup logic below.
|
|
11109
|
-
if (
|
|
11110
|
-
decisions.length === 1 &&
|
|
11111
|
-
decisions[0] === "NO TRANSITION" &&
|
|
11112
|
-
activeContentTransition === null &&
|
|
11113
|
-
activePhaseTransition === null
|
|
11114
|
-
) {
|
|
11115
|
-
debug(
|
|
11116
|
-
"transition",
|
|
11117
|
-
`Early return: no transition or continuation required`,
|
|
11118
|
-
);
|
|
11119
|
-
// Still ensure size logic executes below (so do not return before size alignment)
|
|
11120
|
-
}
|
|
11121
|
-
|
|
11122
|
-
// Handle initial population skip (first null → something): no content or size animations
|
|
11123
|
-
if (isInitialPopulationWithoutTransition) {
|
|
11124
|
-
debug(
|
|
11125
|
-
"transition",
|
|
11126
|
-
"Initial population detected: skipping transitions (opt-in with data-initial-transition)",
|
|
11127
|
-
);
|
|
11128
|
-
|
|
11129
|
-
// Apply sizes instantly, no animation
|
|
11130
|
-
if (isContentPhase) {
|
|
11131
|
-
applySizeConstraints(newWidth, newHeight);
|
|
11132
|
-
} else {
|
|
11133
|
-
updateNaturalContentSize(newWidth, newHeight);
|
|
11134
|
-
releaseConstraints("initial population - skip transitions");
|
|
11135
|
-
}
|
|
11136
|
-
|
|
11137
|
-
// Register state and mark initial population done
|
|
11138
|
-
previousChildNodes = childNodes;
|
|
11139
|
-
lastContentKey = currentContentKey;
|
|
11140
|
-
hasPopulatedOnce = true;
|
|
11141
|
-
if (localDebug.transition) {
|
|
11142
|
-
console.groupEnd();
|
|
11143
|
-
}
|
|
11144
|
-
return;
|
|
11145
|
-
}
|
|
11146
|
-
|
|
11147
|
-
// Plan size transition upfront; execution will happen after content/phase transitions
|
|
11148
|
-
let sizePlan = {
|
|
11149
|
-
action: "none",
|
|
11150
|
-
targetWidth: constrainedWidth,
|
|
11151
|
-
targetHeight: constrainedHeight,
|
|
11152
|
-
};
|
|
11153
|
-
|
|
11154
|
-
size_transition: {
|
|
11155
|
-
const getTargetDimensions = () => {
|
|
11156
|
-
if (!isContentPhase) {
|
|
11157
|
-
return [newWidth, newHeight];
|
|
11158
|
-
}
|
|
11159
|
-
const shouldUseNewDimensions =
|
|
11160
|
-
naturalContentWidth === 0 && naturalContentHeight === 0;
|
|
11161
|
-
const targetWidth = shouldUseNewDimensions
|
|
11162
|
-
? newWidth
|
|
11163
|
-
: naturalContentWidth || newWidth;
|
|
11164
|
-
const targetHeight = shouldUseNewDimensions
|
|
11165
|
-
? newHeight
|
|
11166
|
-
: naturalContentHeight || newHeight;
|
|
11167
|
-
return [targetWidth, targetHeight];
|
|
11168
|
-
};
|
|
11169
|
-
|
|
11170
|
-
const [targetWidth, targetHeight] = getTargetDimensions();
|
|
11171
|
-
sizePlan.targetWidth = targetWidth;
|
|
11172
|
-
sizePlan.targetHeight = targetHeight;
|
|
11173
|
-
|
|
11174
|
-
if (
|
|
11175
|
-
targetWidth === constrainedWidth &&
|
|
11176
|
-
targetHeight === constrainedHeight
|
|
11177
|
-
) {
|
|
11178
|
-
debug("size", "No size change required");
|
|
11179
|
-
// We'll handle potential constraint release in final section (if not holding)
|
|
11180
|
-
break size_transition;
|
|
11181
|
-
}
|
|
11182
|
-
|
|
11183
|
-
debug("size", "Size change needed:", {
|
|
11184
|
-
width: `${constrainedWidth} → ${targetWidth}`,
|
|
11185
|
-
height: `${constrainedHeight} → ${targetHeight}`,
|
|
11186
|
-
});
|
|
11187
|
-
|
|
11188
|
-
if (isContentPhase) {
|
|
11189
|
-
// Content phases (loading/error) always use size constraints for consistent sizing
|
|
11190
|
-
sizePlan.action = hasSizeTransitions ? "animate" : "applyConstraints";
|
|
11191
|
-
} else {
|
|
11192
|
-
// Actual content: update natural content dimensions for future content phases
|
|
11193
|
-
updateNaturalContentSize(targetWidth, targetHeight);
|
|
11194
|
-
sizePlan.action = hasSizeTransitions ? "animate" : "release";
|
|
11195
|
-
}
|
|
11196
|
-
}
|
|
11197
|
-
|
|
11198
|
-
content_transition: {
|
|
11199
|
-
// Handle content transitions (slide-left, cross-fade for content key changes)
|
|
11200
|
-
if (
|
|
11201
|
-
decisions.length === 1 &&
|
|
11202
|
-
decisions[0] === "NO TRANSITION" &&
|
|
11203
|
-
activeContentTransition === null &&
|
|
11204
|
-
activePhaseTransition === null
|
|
11205
|
-
) {
|
|
11206
|
-
// Skip creating any new transitions entirely
|
|
11207
|
-
} else if (
|
|
11208
|
-
shouldDoContentTransitionIncludingPopulation &&
|
|
11209
|
-
!preserveOnlyContentTransition
|
|
11210
|
-
) {
|
|
11211
|
-
const animationProgress = activeContentTransition?.progress || 0;
|
|
11212
|
-
if (animationProgress > 0) {
|
|
11213
|
-
debug(
|
|
11214
|
-
"transition",
|
|
11215
|
-
`Preserving content transition progress: ${(animationProgress * 100).toFixed(1)}%`,
|
|
11216
|
-
);
|
|
11217
|
-
}
|
|
11218
|
-
|
|
11219
|
-
const newTransitionType =
|
|
11220
|
-
container.getAttribute("data-content-transition") ||
|
|
11221
|
-
CONTENT_TRANSITION;
|
|
11222
|
-
const canContinueSmoothly =
|
|
11223
|
-
activeContentTransitionType === newTransitionType &&
|
|
11224
|
-
activeContentTransition;
|
|
11225
|
-
if (canContinueSmoothly) {
|
|
11226
|
-
debug(
|
|
11227
|
-
"transition",
|
|
11228
|
-
"Continuing with same content transition type (restarting due to actual change)",
|
|
11229
|
-
);
|
|
11230
|
-
activeContentTransition.cancel();
|
|
11231
|
-
} else if (
|
|
11232
|
-
activeContentTransition &&
|
|
11233
|
-
activeContentTransitionType !== newTransitionType
|
|
11234
|
-
) {
|
|
11235
|
-
debug(
|
|
11236
|
-
"transition",
|
|
11237
|
-
"Different content transition type, keeping both",
|
|
11238
|
-
`${activeContentTransitionType} → ${newTransitionType}`,
|
|
11239
|
-
);
|
|
11240
|
-
} else if (activeContentTransition) {
|
|
11241
|
-
debug("transition", "Cancelling current content transition");
|
|
11242
|
-
activeContentTransition.cancel();
|
|
11243
|
-
}
|
|
11244
|
-
|
|
11245
|
-
const needsOldChildNodesClone =
|
|
11246
|
-
(contentChange || becomesEmpty) && hadChild;
|
|
11247
|
-
const duration = parseInt(
|
|
11248
|
-
container.getAttribute("data-content-transition-duration") ||
|
|
11249
|
-
CONTENT_TRANSITION_DURATION,
|
|
11250
|
-
);
|
|
11251
|
-
const type =
|
|
11252
|
-
container.getAttribute("data-content-transition") ||
|
|
11253
|
-
CONTENT_TRANSITION;
|
|
11254
|
-
|
|
11255
|
-
const setupContentTransition = () =>
|
|
11256
|
-
setupTransition({
|
|
11257
|
-
isPhaseTransition: false,
|
|
11258
|
-
overlay: contentOverlay,
|
|
11259
|
-
needsOldChildNodesClone,
|
|
11260
|
-
previousChildNodes,
|
|
11261
|
-
childNodes,
|
|
11262
|
-
attributeToRemove: ["data-content-key"],
|
|
11263
|
-
});
|
|
11264
|
-
|
|
11265
|
-
// If size transitions are disabled and the new content is smaller,
|
|
11266
|
-
// hold the previous size to avoid cropping during the transition.
|
|
11267
|
-
if (!hasSizeTransitions) {
|
|
11268
|
-
const willShrinkWidth = constrainedWidth > newWidth;
|
|
11269
|
-
const willShrinkHeight = constrainedHeight > newHeight;
|
|
11270
|
-
sizeHoldActive = willShrinkWidth || willShrinkHeight;
|
|
11271
|
-
if (sizeHoldActive) {
|
|
11272
|
-
debug(
|
|
11273
|
-
"size",
|
|
11274
|
-
`Holding previous size during content transition: ${constrainedWidth}x${constrainedHeight}`,
|
|
11275
|
-
);
|
|
11276
|
-
applySizeConstraints(constrainedWidth, constrainedHeight);
|
|
11277
|
-
}
|
|
11278
|
-
}
|
|
11279
|
-
|
|
11280
|
-
activeContentTransition = applyTransition(
|
|
11281
|
-
transitionController,
|
|
11282
|
-
setupContentTransition,
|
|
11283
|
-
{
|
|
11284
|
-
duration,
|
|
11285
|
-
type,
|
|
11286
|
-
animationProgress,
|
|
11287
|
-
isPhaseTransition: false,
|
|
11288
|
-
fromContentKeyState: previousContentKeyState,
|
|
11289
|
-
toContentKeyState: currentContentKeyState,
|
|
11290
|
-
onComplete: () => {
|
|
11291
|
-
activeContentTransition = null;
|
|
11292
|
-
activeContentTransitionType = null;
|
|
11293
|
-
if (sizeHoldActive) {
|
|
11294
|
-
// Release the hold after the content transition completes
|
|
11295
|
-
releaseConstraints(
|
|
11296
|
-
"content transition completed - release size hold",
|
|
11297
|
-
);
|
|
11298
|
-
sizeHoldActive = false;
|
|
11299
|
-
}
|
|
11300
|
-
},
|
|
11301
|
-
debug,
|
|
11302
|
-
},
|
|
11303
|
-
);
|
|
11304
|
-
|
|
11305
|
-
if (activeContentTransition) {
|
|
11306
|
-
activeContentTransition.play();
|
|
11307
|
-
}
|
|
11308
|
-
activeContentTransitionType = type;
|
|
11309
|
-
} else if (
|
|
11310
|
-
!shouldDoContentTransition &&
|
|
11311
|
-
!preserveOnlyContentTransition
|
|
11312
|
-
) {
|
|
11313
|
-
// Clean up content overlay if no content transition needed and nothing to preserve
|
|
11314
|
-
contentOverlay.innerHTML = "";
|
|
11315
|
-
activeContentTransition = null;
|
|
11316
|
-
activeContentTransitionType = null;
|
|
11317
|
-
}
|
|
11318
|
-
|
|
11319
|
-
// Handle phase transitions (cross-fade for content phase changes)
|
|
11320
|
-
if (shouldDoPhaseTransition) {
|
|
11321
|
-
const phaseTransitionType =
|
|
11322
|
-
container.getAttribute("data-phase-transition") || PHASE_TRANSITION;
|
|
11323
|
-
const phaseAnimationProgress = activePhaseTransition?.progress || 0;
|
|
11324
|
-
if (phaseAnimationProgress > 0) {
|
|
11325
|
-
debug(
|
|
11326
|
-
"transition",
|
|
11327
|
-
`Preserving phase transition progress: ${(phaseAnimationProgress * 100).toFixed(1)}%`,
|
|
11328
|
-
);
|
|
11329
|
-
}
|
|
11330
|
-
|
|
11331
|
-
const canContinueSmoothly =
|
|
11332
|
-
activePhaseTransitionType === phaseTransitionType &&
|
|
11333
|
-
activePhaseTransition;
|
|
11334
|
-
|
|
11335
|
-
if (canContinueSmoothly) {
|
|
11336
|
-
debug("transition", "Continuing with same phase transition type");
|
|
11337
|
-
activePhaseTransition.cancel();
|
|
11338
|
-
} else if (
|
|
11339
|
-
activePhaseTransition &&
|
|
11340
|
-
activePhaseTransitionType !== phaseTransitionType
|
|
11341
|
-
) {
|
|
11342
|
-
debug(
|
|
11343
|
-
"transition",
|
|
11344
|
-
"Different phase transition type, keeping both",
|
|
11345
|
-
`${activePhaseTransitionType} → ${phaseTransitionType}`,
|
|
11346
|
-
);
|
|
11347
|
-
} else if (activePhaseTransition) {
|
|
11348
|
-
debug("transition", "Cancelling current phase transition");
|
|
11349
|
-
activePhaseTransition.cancel();
|
|
11350
|
-
}
|
|
11351
|
-
|
|
11352
|
-
const needsOldPhaseClone =
|
|
11353
|
-
(becomesEmpty || becomesPopulated || phaseChange) && hadChild;
|
|
11354
|
-
const phaseDuration = parseInt(
|
|
11355
|
-
container.getAttribute("data-phase-transition-duration") ||
|
|
11356
|
-
PHASE_TRANSITION_DURATION,
|
|
11357
|
-
);
|
|
11358
|
-
|
|
11359
|
-
const setupPhaseTransition = () =>
|
|
11360
|
-
setupTransition({
|
|
11361
|
-
isPhaseTransition: true,
|
|
11362
|
-
overlay: phaseOverlay,
|
|
11363
|
-
needsOldChildNodesClone: needsOldPhaseClone,
|
|
11364
|
-
previousChildNodes,
|
|
11365
|
-
childNodes,
|
|
11366
|
-
attributeToRemove: ["data-content-key", "data-content-phase"],
|
|
11367
|
-
});
|
|
11368
|
-
|
|
11369
|
-
const fromPhase = !hadChild
|
|
11370
|
-
? "null"
|
|
11371
|
-
: wasContentPhase
|
|
11372
|
-
? "content-phase"
|
|
11373
|
-
: "content";
|
|
11374
|
-
const toPhase = !hasChild
|
|
11375
|
-
? "null"
|
|
11376
|
-
: isContentPhase
|
|
11377
|
-
? "content-phase"
|
|
11378
|
-
: "content";
|
|
11379
|
-
|
|
11380
|
-
debug(
|
|
11381
|
-
"transition",
|
|
11382
|
-
`Starting phase transition: ${fromPhase} → ${toPhase}`,
|
|
11383
|
-
);
|
|
11384
|
-
|
|
11385
|
-
activePhaseTransition = applyTransition(
|
|
11386
|
-
transitionController,
|
|
11387
|
-
setupPhaseTransition,
|
|
11388
|
-
{
|
|
11389
|
-
duration: phaseDuration,
|
|
11390
|
-
type: phaseTransitionType,
|
|
11391
|
-
animationProgress: phaseAnimationProgress,
|
|
11392
|
-
isPhaseTransition: true,
|
|
11393
|
-
fromContentKeyState: previousContentKeyState,
|
|
11394
|
-
toContentKeyState: currentContentKeyState,
|
|
11395
|
-
onComplete: () => {
|
|
11396
|
-
activePhaseTransition = null;
|
|
11397
|
-
activePhaseTransitionType = null;
|
|
11398
|
-
debug("transition", "Phase transition complete");
|
|
11399
|
-
},
|
|
11400
|
-
debug,
|
|
11401
|
-
},
|
|
11402
|
-
);
|
|
11403
|
-
|
|
11404
|
-
if (activePhaseTransition) {
|
|
11405
|
-
activePhaseTransition.play();
|
|
11406
|
-
}
|
|
11407
|
-
activePhaseTransitionType = phaseTransitionType;
|
|
11408
|
-
}
|
|
11409
|
-
}
|
|
11410
|
-
|
|
11411
|
-
// Store current child for next transition
|
|
11412
|
-
previousChildNodes = childNodes;
|
|
11413
|
-
lastContentKey = currentContentKey;
|
|
11414
|
-
if (becomesPopulated) {
|
|
11415
|
-
hasPopulatedOnce = true;
|
|
11416
|
-
}
|
|
11417
|
-
|
|
11418
|
-
// Execute planned size action, unless holding size during a content transition
|
|
11419
|
-
if (!sizeHoldActive) {
|
|
11420
|
-
if (
|
|
11421
|
-
sizePlan.targetWidth === constrainedWidth &&
|
|
11422
|
-
sizePlan.targetHeight === constrainedHeight
|
|
11423
|
-
) {
|
|
11424
|
-
// no size changes planned; possibly release constraints
|
|
11425
|
-
if (!isContentPhase) {
|
|
11426
|
-
releaseConstraints("no size change needed");
|
|
11427
|
-
}
|
|
11428
|
-
} else if (sizePlan.action === "animate") {
|
|
11429
|
-
updateToSize(sizePlan.targetWidth, sizePlan.targetHeight);
|
|
11430
|
-
} else if (sizePlan.action === "applyConstraints") {
|
|
11431
|
-
applySizeConstraints(sizePlan.targetWidth, sizePlan.targetHeight);
|
|
11432
|
-
} else if (sizePlan.action === "release") {
|
|
11433
|
-
releaseConstraints("actual content - no size transitions needed");
|
|
11434
|
-
}
|
|
11435
|
-
}
|
|
11436
|
-
} finally {
|
|
11437
|
-
isUpdating = false;
|
|
11438
|
-
if (localDebug.transition) {
|
|
11439
|
-
console.groupEnd();
|
|
11440
|
-
}
|
|
11441
|
-
}
|
|
11442
|
-
};
|
|
11443
|
-
|
|
11444
|
-
// Run once at init to process current slot content (warnings, sizing, transitions)
|
|
11445
|
-
handleChildSlotMutation("init");
|
|
11446
|
-
|
|
11447
|
-
// Watch for child changes and attribute changes on children
|
|
11448
|
-
const mutationObserver = new MutationObserver((mutations) => {
|
|
11449
|
-
let childListMutation = false;
|
|
11450
|
-
const attributeMutationSet = new Set();
|
|
11451
|
-
|
|
11452
|
-
for (const mutation of mutations) {
|
|
11453
|
-
if (mutation.type === "childList") {
|
|
11454
|
-
childListMutation = true;
|
|
11455
|
-
continue;
|
|
11456
|
-
}
|
|
11457
|
-
if (mutation.type === "attributes") {
|
|
11458
|
-
const { attributeName, target } = mutation;
|
|
11459
|
-
if (
|
|
11460
|
-
attributeName === "data-content-key" ||
|
|
11461
|
-
attributeName === "data-content-phase"
|
|
11462
|
-
) {
|
|
11463
|
-
attributeMutationSet.add(attributeName);
|
|
11464
|
-
debug(
|
|
11465
|
-
"transition",
|
|
11466
|
-
`Attribute change detected: ${attributeName} on`,
|
|
11467
|
-
getElementSignature(target),
|
|
11468
|
-
);
|
|
11469
|
-
}
|
|
11470
|
-
}
|
|
11471
|
-
}
|
|
11472
|
-
|
|
11473
|
-
if (!childListMutation && attributeMutationSet.size === 0) {
|
|
11474
|
-
return;
|
|
11475
|
-
}
|
|
11476
|
-
const reasonParts = [];
|
|
11477
|
-
if (childListMutation) {
|
|
11478
|
-
reasonParts.push("childList change");
|
|
11479
|
-
}
|
|
11480
|
-
if (attributeMutationSet.size) {
|
|
11481
|
-
for (const attr of attributeMutationSet) {
|
|
11482
|
-
reasonParts.push(`[${attr}] change`);
|
|
11483
|
-
}
|
|
11484
|
-
}
|
|
11485
|
-
const reason = reasonParts.join("+");
|
|
11486
|
-
handleChildSlotMutation(reason);
|
|
11487
|
-
});
|
|
11488
|
-
|
|
11489
|
-
mutationObserver.observe(slot, {
|
|
11490
|
-
childList: true,
|
|
11491
|
-
attributes: true,
|
|
11492
|
-
attributeFilter: ["data-content-key", "data-content-phase"],
|
|
11493
|
-
characterData: false,
|
|
11494
|
-
});
|
|
11495
|
-
|
|
11496
|
-
return {
|
|
11497
|
-
slot,
|
|
11498
|
-
|
|
11499
|
-
cleanup: () => {
|
|
11500
|
-
teardown();
|
|
11501
|
-
mutationObserver.disconnect();
|
|
11502
|
-
stopResizeObserver();
|
|
11503
|
-
if (sizeTransition) {
|
|
11504
|
-
sizeTransition.cancel();
|
|
11505
|
-
}
|
|
11506
|
-
if (activeContentTransition) {
|
|
11507
|
-
activeContentTransition.cancel();
|
|
11508
|
-
}
|
|
11509
|
-
if (activePhaseTransition) {
|
|
11510
|
-
activePhaseTransition.cancel();
|
|
11511
|
-
}
|
|
11512
|
-
},
|
|
11513
|
-
pause: () => {
|
|
11514
|
-
if (activeContentTransition?.pause) {
|
|
11515
|
-
activeContentTransition.pause();
|
|
11516
|
-
isPaused = true;
|
|
11517
|
-
}
|
|
11518
|
-
if (activePhaseTransition?.pause) {
|
|
11519
|
-
activePhaseTransition.pause();
|
|
11520
|
-
isPaused = true;
|
|
11521
|
-
}
|
|
11522
|
-
},
|
|
11523
|
-
resume: () => {
|
|
11524
|
-
if (activeContentTransition?.play && isPaused) {
|
|
11525
|
-
activeContentTransition.play();
|
|
11526
|
-
isPaused = false;
|
|
11527
|
-
}
|
|
11528
|
-
if (activePhaseTransition?.play && isPaused) {
|
|
11529
|
-
activePhaseTransition.play();
|
|
11530
|
-
isPaused = false;
|
|
11531
|
-
}
|
|
11532
|
-
},
|
|
11533
|
-
getState: () => ({
|
|
11534
|
-
isPaused,
|
|
11535
|
-
contentTransitionInProgress: activeContentTransition !== null,
|
|
11536
|
-
phaseTransitionInProgress: activePhaseTransition !== null,
|
|
11537
|
-
}),
|
|
11538
|
-
};
|
|
11539
|
-
};
|
|
11540
|
-
|
|
11541
|
-
const applyTransition = (
|
|
11542
|
-
transitionController,
|
|
11543
|
-
setupTransition,
|
|
11544
|
-
{
|
|
11545
|
-
type,
|
|
11546
|
-
duration,
|
|
11547
|
-
animationProgress = 0,
|
|
11548
|
-
isPhaseTransition,
|
|
11549
|
-
onComplete,
|
|
11550
|
-
fromContentKeyState,
|
|
11551
|
-
toContentKeyState,
|
|
11552
|
-
debug,
|
|
11553
|
-
},
|
|
11554
|
-
) => {
|
|
11555
|
-
let transitionType;
|
|
11556
|
-
if (type === "cross-fade") {
|
|
11557
|
-
transitionType = crossFade;
|
|
11558
|
-
} else if (type === "slide-left") {
|
|
11559
|
-
transitionType = slideLeft;
|
|
11560
|
-
} else {
|
|
11561
|
-
return null;
|
|
11562
|
-
}
|
|
11563
|
-
|
|
11564
|
-
const { cleanup, oldElement, newElement, onTeardown } = setupTransition();
|
|
11565
|
-
// Use precomputed content key states (expected to be provided by caller)
|
|
11566
|
-
const fromContentKey = fromContentKeyState;
|
|
11567
|
-
const toContentKey = toContentKeyState;
|
|
11568
|
-
|
|
11569
|
-
debug("transition", "Setting up animation:", {
|
|
11570
|
-
type,
|
|
11571
|
-
from: fromContentKey,
|
|
11572
|
-
to: toContentKey,
|
|
11573
|
-
progress: `${(animationProgress * 100).toFixed(1)}%`,
|
|
11574
|
-
});
|
|
11575
|
-
|
|
11576
|
-
const remainingDuration = Math.max(100, duration * (1 - animationProgress));
|
|
11577
|
-
debug("transition", `Animation duration: ${remainingDuration}ms`);
|
|
11578
|
-
|
|
11579
|
-
const transitions = transitionType.apply(oldElement, newElement, {
|
|
11580
|
-
duration: remainingDuration,
|
|
11581
|
-
startProgress: animationProgress,
|
|
11582
|
-
isPhaseTransition,
|
|
11583
|
-
debug,
|
|
11584
|
-
});
|
|
11585
|
-
|
|
11586
|
-
debug(
|
|
11587
|
-
"transition",
|
|
11588
|
-
`Created ${transitions.length} transition(s) for animation`,
|
|
11589
|
-
);
|
|
11590
|
-
|
|
11591
|
-
if (transitions.length === 0) {
|
|
11592
|
-
debug("transition", "No transitions to animate, cleaning up immediately");
|
|
11593
|
-
cleanup();
|
|
11594
|
-
onTeardown?.();
|
|
11595
|
-
onComplete?.();
|
|
11596
|
-
return null;
|
|
11597
|
-
}
|
|
11598
|
-
|
|
11599
|
-
const groupTransition = transitionController.animate(transitions, {
|
|
11600
|
-
onFinish: () => {
|
|
11601
|
-
groupTransition.cancel();
|
|
11602
|
-
cleanup();
|
|
11603
|
-
onTeardown?.();
|
|
11604
|
-
onComplete?.();
|
|
11605
|
-
},
|
|
11606
|
-
});
|
|
11607
|
-
|
|
11608
|
-
return groupTransition;
|
|
11609
|
-
};
|
|
11610
|
-
|
|
11611
|
-
const slideLeft = {
|
|
11612
|
-
name: "slide-left",
|
|
11613
|
-
apply: (
|
|
11614
|
-
oldElement,
|
|
11615
|
-
newElement,
|
|
11616
|
-
{ duration, startProgress = 0, isPhaseTransition = false, debug },
|
|
11617
|
-
) => {
|
|
11618
|
-
if (!oldElement && !newElement) {
|
|
11619
|
-
return [];
|
|
11620
|
-
}
|
|
11621
|
-
|
|
11622
|
-
if (!newElement) {
|
|
11623
|
-
// Content -> Empty (slide out left only)
|
|
11624
|
-
const currentPosition = getTranslateX(oldElement);
|
|
11625
|
-
const containerWidth = getInnerWidth(oldElement.parentElement);
|
|
11626
|
-
const from = currentPosition;
|
|
11627
|
-
const to = -containerWidth;
|
|
11628
|
-
debug("transition", "Slide out to empty:", { from, to });
|
|
11629
|
-
|
|
11630
|
-
return [
|
|
11631
|
-
createTranslateXTransition(oldElement, to, {
|
|
11632
|
-
setup: () =>
|
|
11633
|
-
notifyTransitionOverflow(newElement, "slide_out_old_content"),
|
|
11634
|
-
from,
|
|
11635
|
-
duration,
|
|
11636
|
-
startProgress,
|
|
11637
|
-
onUpdate: ({ value, timing }) => {
|
|
11638
|
-
debug("transition_updates", "Slide out progress:", value);
|
|
11639
|
-
if (timing === "end") {
|
|
11640
|
-
debug("transition", "Slide out complete");
|
|
11641
|
-
}
|
|
11642
|
-
},
|
|
11643
|
-
}),
|
|
11644
|
-
];
|
|
11645
|
-
}
|
|
11646
|
-
|
|
11647
|
-
if (!oldElement) {
|
|
11648
|
-
// Empty -> Content (slide in from right)
|
|
11649
|
-
const containerWidth = getInnerWidth(newElement.parentElement);
|
|
11650
|
-
const from = containerWidth; // Start from right edge for slide-in effect
|
|
11651
|
-
const to = getTranslateXWithoutTransition(newElement);
|
|
11652
|
-
debug("transition", "Slide in from empty:", { from, to });
|
|
11653
|
-
return [
|
|
11654
|
-
createTranslateXTransition(newElement, to, {
|
|
11655
|
-
setup: () =>
|
|
11656
|
-
notifyTransitionOverflow(newElement, "slice_in_new_content"),
|
|
11657
|
-
from,
|
|
11658
|
-
duration,
|
|
11659
|
-
startProgress,
|
|
11660
|
-
onUpdate: ({ value, timing }) => {
|
|
11661
|
-
debug("transition_updates", "Slide in progress:", value);
|
|
11662
|
-
if (timing === "end") {
|
|
11663
|
-
debug("transition", "Slide in complete");
|
|
11664
|
-
}
|
|
11665
|
-
},
|
|
11666
|
-
}),
|
|
11667
|
-
];
|
|
11668
|
-
}
|
|
11669
|
-
|
|
11670
|
-
// Content -> Content (slide left)
|
|
11671
|
-
// The old content (oldElement) slides OUT to the left
|
|
11672
|
-
// The new content (newElement) slides IN from the right
|
|
11673
|
-
|
|
11674
|
-
// Get positions for the slide animation
|
|
11675
|
-
const containerWidth = getInnerWidth(newElement.parentElement);
|
|
11676
|
-
const oldContentPosition = getTranslateX(oldElement);
|
|
11677
|
-
const currentNewPosition = getTranslateX(newElement);
|
|
11678
|
-
const naturalNewPosition = getTranslateXWithoutTransition(newElement);
|
|
11679
|
-
|
|
11680
|
-
// For smooth continuation: if newElement is mid-transition,
|
|
11681
|
-
// calculate new position to maintain seamless sliding
|
|
11682
|
-
let startNewPosition;
|
|
11683
|
-
if (currentNewPosition !== 0 && naturalNewPosition === 0) {
|
|
11684
|
-
startNewPosition = currentNewPosition + containerWidth;
|
|
11685
|
-
debug(
|
|
11686
|
-
"transition",
|
|
11687
|
-
"Calculated seamless position:",
|
|
11688
|
-
`${currentNewPosition} + ${containerWidth} = ${startNewPosition}`,
|
|
11689
|
-
);
|
|
11690
|
-
} else {
|
|
11691
|
-
startNewPosition = naturalNewPosition || containerWidth;
|
|
11692
|
-
}
|
|
11693
|
-
|
|
11694
|
-
// For phase transitions, force new content to start from right edge for proper slide-in
|
|
11695
|
-
const effectiveFromPosition = isPhaseTransition
|
|
11696
|
-
? containerWidth
|
|
11697
|
-
: startNewPosition;
|
|
11698
|
-
|
|
11699
|
-
debug("transition", "Slide transition:", {
|
|
11700
|
-
oldContent: `${oldContentPosition} → ${-containerWidth}`,
|
|
11701
|
-
newContent: `${effectiveFromPosition} → ${naturalNewPosition}`,
|
|
11702
|
-
});
|
|
11703
|
-
|
|
11704
|
-
const transitions = [];
|
|
11705
|
-
|
|
11706
|
-
// Slide old content out
|
|
11707
|
-
transitions.push(
|
|
11708
|
-
createTranslateXTransition(oldElement, -containerWidth, {
|
|
11709
|
-
setup: () =>
|
|
11710
|
-
notifyTransitionOverflow(newElement, "slide_out_old_content"),
|
|
11711
|
-
from: oldContentPosition,
|
|
11712
|
-
duration,
|
|
11713
|
-
startProgress,
|
|
11714
|
-
onUpdate: ({ value }) => {
|
|
11715
|
-
debug("transition_updates", "Old content slide out:", value);
|
|
11716
|
-
},
|
|
11717
|
-
}),
|
|
11718
|
-
);
|
|
11719
|
-
|
|
11720
|
-
// Slide new content in
|
|
11721
|
-
transitions.push(
|
|
11722
|
-
createTranslateXTransition(newElement, naturalNewPosition, {
|
|
11723
|
-
setup: () =>
|
|
11724
|
-
notifyTransitionOverflow(newElement, "slide_in_new_content"),
|
|
11725
|
-
from: effectiveFromPosition,
|
|
11726
|
-
duration,
|
|
11727
|
-
startProgress,
|
|
11728
|
-
onUpdate: ({ value, timing }) => {
|
|
11729
|
-
debug("transition_updates", "New content slide in:", value);
|
|
11730
|
-
if (timing === "end") {
|
|
11731
|
-
debug("transition", "Slide complete");
|
|
11732
|
-
}
|
|
11733
|
-
},
|
|
11734
|
-
}),
|
|
11735
|
-
);
|
|
11736
|
-
|
|
11737
|
-
return transitions;
|
|
11738
|
-
},
|
|
11739
|
-
};
|
|
11740
|
-
|
|
11741
|
-
const crossFade = {
|
|
11742
|
-
name: "cross-fade",
|
|
11743
|
-
apply: (
|
|
11744
|
-
oldElement,
|
|
11745
|
-
newElement,
|
|
11746
|
-
{ duration, startProgress = 0, isPhaseTransition = false, debug },
|
|
11747
|
-
) => {
|
|
11748
|
-
if (!oldElement && !newElement) {
|
|
11749
|
-
return [];
|
|
11750
|
-
}
|
|
11751
|
-
|
|
11752
|
-
if (!newElement) {
|
|
11753
|
-
// Content -> Empty (fade out only)
|
|
11754
|
-
const from = getOpacity(oldElement);
|
|
11755
|
-
const to = 0;
|
|
11756
|
-
debug("transition", "Fade out to empty:", { from, to });
|
|
11757
|
-
return [
|
|
11758
|
-
createOpacityTransition(oldElement, to, {
|
|
11759
|
-
from,
|
|
11760
|
-
duration,
|
|
11761
|
-
startProgress,
|
|
11762
|
-
onUpdate: ({ value, timing }) => {
|
|
11763
|
-
debug("transition_updates", "Content fade out:", value.toFixed(3));
|
|
11764
|
-
if (timing === "end") {
|
|
11765
|
-
debug("transition", "Fade out complete");
|
|
11766
|
-
}
|
|
11767
|
-
},
|
|
11768
|
-
}),
|
|
11769
|
-
];
|
|
11770
|
-
}
|
|
11771
|
-
|
|
11772
|
-
if (!oldElement) {
|
|
11773
|
-
// Empty -> Content (fade in only)
|
|
11774
|
-
const from = 0;
|
|
11775
|
-
const to = getOpacityWithoutTransition(newElement);
|
|
11776
|
-
debug("transition", "Fade in from empty:", { from, to });
|
|
11777
|
-
return [
|
|
11778
|
-
createOpacityTransition(newElement, to, {
|
|
11779
|
-
from,
|
|
11780
|
-
duration,
|
|
11781
|
-
startProgress,
|
|
11782
|
-
onUpdate: ({ value, timing }) => {
|
|
11783
|
-
debug("transition_updates", "Fade in progress:", value.toFixed(3));
|
|
11784
|
-
if (timing === "end") {
|
|
11785
|
-
debug("transition", "Fade in complete");
|
|
11786
|
-
}
|
|
11787
|
-
},
|
|
11788
|
-
}),
|
|
11789
|
-
];
|
|
11790
|
-
}
|
|
11791
|
-
|
|
11792
|
-
// Content -> Content (cross-fade)
|
|
11793
|
-
// Get current opacity for both elements
|
|
11794
|
-
const oldOpacity = getOpacity(oldElement);
|
|
11795
|
-
const newOpacity = getOpacity(newElement);
|
|
11796
|
-
const newNaturalOpacity = getOpacityWithoutTransition(newElement);
|
|
11797
|
-
|
|
11798
|
-
// For phase transitions, always start new content from 0 for clean visual transition
|
|
11799
|
-
// For content transitions, check for ongoing transitions to continue smoothly
|
|
11800
|
-
let effectiveFromOpacity;
|
|
11801
|
-
if (isPhaseTransition) {
|
|
11802
|
-
effectiveFromOpacity = 0; // Always start fresh for phase transitions (loading → content, etc.)
|
|
11803
|
-
} else {
|
|
11804
|
-
// For content transitions: if new element has ongoing opacity transition
|
|
11805
|
-
// (indicated by non-zero opacity when natural opacity is different),
|
|
11806
|
-
// start from current opacity to continue smoothly, otherwise start from 0
|
|
11807
|
-
const hasOngoingTransition =
|
|
11808
|
-
newOpacity !== newNaturalOpacity && newOpacity > 0;
|
|
11809
|
-
effectiveFromOpacity = hasOngoingTransition ? newOpacity : 0;
|
|
11810
|
-
}
|
|
11811
|
-
|
|
11812
|
-
debug("transition", "Cross-fade transition:", {
|
|
11813
|
-
oldOpacity: `${oldOpacity} → 0`,
|
|
11814
|
-
newOpacity: `${effectiveFromOpacity} → ${newNaturalOpacity}`,
|
|
11815
|
-
isPhaseTransition,
|
|
11816
|
-
});
|
|
11817
|
-
|
|
11818
|
-
return [
|
|
11819
|
-
createOpacityTransition(oldElement, 0, {
|
|
11820
|
-
from: oldOpacity,
|
|
11821
|
-
duration,
|
|
11822
|
-
startProgress,
|
|
11823
|
-
onUpdate: ({ value }) => {
|
|
11824
|
-
if (value > 0) {
|
|
11825
|
-
debug(
|
|
11826
|
-
"transition_updates",
|
|
11827
|
-
"Old content fade out:",
|
|
11828
|
-
value.toFixed(3),
|
|
11829
|
-
);
|
|
11830
|
-
}
|
|
11831
|
-
},
|
|
11832
|
-
}),
|
|
11833
|
-
createOpacityTransition(newElement, newNaturalOpacity, {
|
|
11834
|
-
from: effectiveFromOpacity,
|
|
11835
|
-
duration,
|
|
11836
|
-
startProgress: isPhaseTransition ? 0 : startProgress, // Phase transitions: new content always starts fresh
|
|
11837
|
-
onUpdate: ({ value, timing }) => {
|
|
11838
|
-
debug("transition_updates", "New content fade in:", value.toFixed(3));
|
|
11839
|
-
if (timing === "end") {
|
|
11840
|
-
debug("transition", "Cross-fade complete");
|
|
11841
|
-
}
|
|
11842
|
-
},
|
|
11843
|
-
}),
|
|
11844
|
-
];
|
|
11845
|
-
},
|
|
11846
|
-
};
|
|
11847
|
-
|
|
11848
|
-
const dispatchTransitionOverflowStartCustomEvent = (element, transitionId) => {
|
|
11849
|
-
const customEvent = new CustomEvent("ui_transition_overflow_start", {
|
|
11850
|
-
bubbles: true,
|
|
11851
|
-
detail: {
|
|
11852
|
-
transitionId,
|
|
11853
|
-
},
|
|
11854
|
-
});
|
|
11855
|
-
element.dispatchEvent(customEvent);
|
|
11856
|
-
};
|
|
11857
|
-
const dispatchTransitionOverflowEndCustomEvent = (element, transitionId) => {
|
|
11858
|
-
const customEvent = new CustomEvent("ui_transition_overflow_end", {
|
|
11859
|
-
bubbles: true,
|
|
11860
|
-
detail: {
|
|
11861
|
-
transitionId,
|
|
11862
|
-
},
|
|
11863
|
-
});
|
|
11864
|
-
element.dispatchEvent(customEvent);
|
|
11865
|
-
};
|
|
11866
|
-
const notifyTransitionOverflow = (element, transitionId) => {
|
|
11867
|
-
dispatchTransitionOverflowStartCustomEvent(element, transitionId);
|
|
11868
|
-
return () => {
|
|
11869
|
-
dispatchTransitionOverflowEndCustomEvent(element, transitionId);
|
|
11870
|
-
};
|
|
11871
|
-
};
|
|
11872
|
-
|
|
11873
|
-
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateY, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeTwoStyles, normalizeStyle, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
|
|
10464
|
+
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, initFlexDetailsSet, initFocusGroup, initPositionSticky, isScrollable, mergeTwoStyles, normalizeStyle, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
|