@capgo/capacitor-transitions 8.0.1
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/LICENSE +21 -0
- package/README.md +583 -0
- package/dist/chunk-RLOQDT3I.mjs +2248 -0
- package/dist/chunk-RLOQDT3I.mjs.map +1 -0
- package/dist/components-6C46YYFQ.mjs +15 -0
- package/dist/components-6C46YYFQ.mjs.map +1 -0
- package/dist/index.d.mts +734 -0
- package/dist/index.d.ts +734 -0
- package/dist/index.js +2400 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +75 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +400 -0
- package/dist/react/index.d.ts +400 -0
- package/dist/react/index.js +2244 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +2210 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/solid/index.d.mts +288 -0
- package/dist/solid/index.d.ts +288 -0
- package/dist/solid/index.js +2244 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/solid/index.mjs +2210 -0
- package/dist/solid/index.mjs.map +1 -0
- package/dist/svelte/index.d.mts +298 -0
- package/dist/svelte/index.d.ts +298 -0
- package/dist/svelte/index.js +2274 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/svelte/index.mjs +2239 -0
- package/dist/svelte/index.mjs.map +1 -0
- package/dist/vue/index.d.mts +335 -0
- package/dist/vue/index.d.ts +335 -0
- package/dist/vue/index.js +2244 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/vue/index.mjs +2210 -0
- package/dist/vue/index.mjs.map +1 -0
- package/package.json +127 -0
|
@@ -0,0 +1,2210 @@
|
|
|
1
|
+
// src/core/animations.ts
|
|
2
|
+
var IOS_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
|
|
3
|
+
var ANDROID_EASING = "cubic-bezier(0.36, 0.66, 0.04, 1)";
|
|
4
|
+
var ANDROID_BACK_EASING = "cubic-bezier(0.47, 0, 0.745, 0.715)";
|
|
5
|
+
var IOS_DURATION = 540;
|
|
6
|
+
var ANDROID_DURATION = 280;
|
|
7
|
+
var ANDROID_BACK_DURATION = 200;
|
|
8
|
+
var IOS_OFF_OPACITY = 0.8;
|
|
9
|
+
var IOS_CENTER = "0%";
|
|
10
|
+
var IOS_OFF_RIGHT = "99.5%";
|
|
11
|
+
var IOS_OFF_LEFT = "-33%";
|
|
12
|
+
var IOS_OFF_RIGHT_RTL = "-99.5%";
|
|
13
|
+
var IOS_OFF_LEFT_RTL = "33%";
|
|
14
|
+
var MD_OFF_BOTTOM = "40px";
|
|
15
|
+
var MD_CENTER = "0px";
|
|
16
|
+
function resolveEasing(easing) {
|
|
17
|
+
switch (easing) {
|
|
18
|
+
case "ios":
|
|
19
|
+
return IOS_EASING;
|
|
20
|
+
case "android":
|
|
21
|
+
return ANDROID_EASING;
|
|
22
|
+
case "linear":
|
|
23
|
+
return "linear";
|
|
24
|
+
case "ease":
|
|
25
|
+
return "ease";
|
|
26
|
+
case "ease-in":
|
|
27
|
+
return "ease-in";
|
|
28
|
+
case "ease-out":
|
|
29
|
+
return "ease-out";
|
|
30
|
+
case "ease-in-out":
|
|
31
|
+
return "ease-in-out";
|
|
32
|
+
default:
|
|
33
|
+
return easing;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function detectPlatform() {
|
|
37
|
+
if (typeof navigator === "undefined") return "ios";
|
|
38
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
39
|
+
if (/iphone|ipad|ipod/.test(ua)) return "ios";
|
|
40
|
+
if (/android/.test(ua)) return "android";
|
|
41
|
+
return "ios";
|
|
42
|
+
}
|
|
43
|
+
function getDefaultDuration(platform, direction = "forward") {
|
|
44
|
+
const resolved = platform === "auto" ? detectPlatform() : platform;
|
|
45
|
+
if (resolved === "ios") {
|
|
46
|
+
return IOS_DURATION;
|
|
47
|
+
}
|
|
48
|
+
return direction === "back" ? ANDROID_BACK_DURATION : ANDROID_DURATION;
|
|
49
|
+
}
|
|
50
|
+
function getDefaultEasing(platform, direction = "forward") {
|
|
51
|
+
const resolved = platform === "auto" ? detectPlatform() : platform;
|
|
52
|
+
if (resolved === "ios") {
|
|
53
|
+
return IOS_EASING;
|
|
54
|
+
}
|
|
55
|
+
return direction === "back" ? ANDROID_BACK_EASING : ANDROID_EASING;
|
|
56
|
+
}
|
|
57
|
+
function getDocumentDirection(element) {
|
|
58
|
+
const doc = element.ownerDocument;
|
|
59
|
+
return doc.dir === "rtl" || doc.documentElement.dir === "rtl" ? "rtl" : "ltr";
|
|
60
|
+
}
|
|
61
|
+
function preparePageLayer(element, zIndex) {
|
|
62
|
+
element.classList.add("cap-transition-active");
|
|
63
|
+
element.style.display = "";
|
|
64
|
+
element.style.visibility = "visible";
|
|
65
|
+
element.style.position = "absolute";
|
|
66
|
+
element.style.top = "0";
|
|
67
|
+
element.style.left = "0";
|
|
68
|
+
element.style.width = "100%";
|
|
69
|
+
element.style.height = "100%";
|
|
70
|
+
element.style.zIndex = zIndex;
|
|
71
|
+
element.style.pointerEvents = "none";
|
|
72
|
+
element.style.willChange = "transform, opacity";
|
|
73
|
+
element.style.backfaceVisibility = "hidden";
|
|
74
|
+
element.style.transformStyle = "preserve-3d";
|
|
75
|
+
}
|
|
76
|
+
function createAnimation(element, keyframes, duration, easing) {
|
|
77
|
+
return element.animate(keyframes, {
|
|
78
|
+
duration,
|
|
79
|
+
easing,
|
|
80
|
+
fill: "both"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function resolvePageContents(element) {
|
|
84
|
+
return Array.from(
|
|
85
|
+
element.querySelectorAll(
|
|
86
|
+
':scope > [data-cap-content], :scope > .cap-content, :scope > cap-content, :scope > [slot="content"]'
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
function resolvePageChrome(element) {
|
|
91
|
+
return Array.from(
|
|
92
|
+
element.querySelectorAll(
|
|
93
|
+
':scope > [data-cap-header], :scope > .cap-header, :scope > cap-header, :scope > [slot="header"], :scope > [data-cap-footer], :scope > .cap-footer, :scope > cap-footer, :scope > [slot="footer"]'
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function resolvePageHeaders(element) {
|
|
98
|
+
return Array.from(
|
|
99
|
+
element.querySelectorAll(
|
|
100
|
+
':scope > [data-cap-header], :scope > .cap-header, :scope > cap-header, :scope > [slot="header"]'
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function invertTranslateOffset(offset) {
|
|
105
|
+
if (offset === IOS_CENTER) {
|
|
106
|
+
return IOS_CENTER;
|
|
107
|
+
}
|
|
108
|
+
return offset.startsWith("-") ? offset.slice(1) : `-${offset}`;
|
|
109
|
+
}
|
|
110
|
+
function createPinnedChromeAnimations(elements, fromTransform, toTransform, duration, easing) {
|
|
111
|
+
return elements.map((element) => {
|
|
112
|
+
element.style.willChange = "transform";
|
|
113
|
+
element.style.backfaceVisibility = "hidden";
|
|
114
|
+
return createAnimation(element, [{ transform: fromTransform }, { transform: toTransform }], duration, easing);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function uniqueElements(elements) {
|
|
118
|
+
return Array.from(new Set(elements));
|
|
119
|
+
}
|
|
120
|
+
function resolveToolbarItems(header) {
|
|
121
|
+
const toolbarItems = Array.from(
|
|
122
|
+
header.querySelectorAll(
|
|
123
|
+
':scope > [data-cap-toolbar] > *, :scope > .toolbar > *, :scope > [role="toolbar"] > *, :scope > ion-toolbar > *'
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
const directItems = Array.from(
|
|
127
|
+
header.querySelectorAll(
|
|
128
|
+
":scope > [data-cap-toolbar-item], :scope > .cap-toolbar-item, :scope > h1, :scope > h2, :scope > h3, :scope > button, :scope > a"
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
return uniqueElements(
|
|
132
|
+
[...toolbarItems, ...directItems].filter(
|
|
133
|
+
(element) => !element.matches('[data-cap-toolbar-background], .toolbar-background, [aria-hidden="true"]') && element.tagName !== "STYLE" && element.tagName !== "SCRIPT"
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function createOpacityAnimations(elements, fromOpacity, toOpacity, duration, easing) {
|
|
138
|
+
return elements.map((element) => {
|
|
139
|
+
element.style.willChange = "opacity";
|
|
140
|
+
element.style.backfaceVisibility = "hidden";
|
|
141
|
+
return createAnimation(element, [{ opacity: fromOpacity }, { opacity: toOpacity }], duration, easing);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function createToolbarItemAnimations(headers, fromTransform, toTransform, fromOpacity, toOpacity, duration, easing) {
|
|
145
|
+
return headers.flatMap(
|
|
146
|
+
(header) => resolveToolbarItems(header).map((element) => {
|
|
147
|
+
element.style.willChange = "transform, opacity";
|
|
148
|
+
element.style.backfaceVisibility = "hidden";
|
|
149
|
+
return createAnimation(
|
|
150
|
+
element,
|
|
151
|
+
[
|
|
152
|
+
{ transform: fromTransform, opacity: fromOpacity },
|
|
153
|
+
{ transform: toTransform, opacity: toOpacity }
|
|
154
|
+
],
|
|
155
|
+
duration,
|
|
156
|
+
easing
|
|
157
|
+
);
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
function createIOSTransition(options) {
|
|
162
|
+
const { enteringEl, leavingEl, direction, duration, easing } = options;
|
|
163
|
+
const animations = [];
|
|
164
|
+
const isBack = direction === "back";
|
|
165
|
+
const isRoot = direction === "root";
|
|
166
|
+
const isRTL = getDocumentDirection(enteringEl) === "rtl";
|
|
167
|
+
const offRight = isRTL ? IOS_OFF_RIGHT_RTL : IOS_OFF_RIGHT;
|
|
168
|
+
const offLeft = isRTL ? IOS_OFF_LEFT_RTL : IOS_OFF_LEFT;
|
|
169
|
+
const chromeOffRight = invertTranslateOffset(offRight);
|
|
170
|
+
const chromeOffLeft = invertTranslateOffset(offLeft);
|
|
171
|
+
const leadingEdgeShadow = isRTL ? "8px 0 24px rgba(0, 0, 0, 0.18)" : "-8px 0 24px rgba(0, 0, 0, 0.18)";
|
|
172
|
+
const enteringContent = resolvePageContents(enteringEl);
|
|
173
|
+
const enteringHeaders = resolvePageHeaders(enteringEl);
|
|
174
|
+
const leavingContent = leavingEl ? resolvePageContents(leavingEl) : [];
|
|
175
|
+
const leavingHeaders = leavingEl ? resolvePageHeaders(leavingEl) : [];
|
|
176
|
+
preparePageLayer(enteringEl, isBack ? "99" : "101");
|
|
177
|
+
if (leavingEl) {
|
|
178
|
+
preparePageLayer(leavingEl, "100");
|
|
179
|
+
}
|
|
180
|
+
if (isRoot) {
|
|
181
|
+
animations.push(
|
|
182
|
+
createAnimation(
|
|
183
|
+
enteringEl,
|
|
184
|
+
[
|
|
185
|
+
{ opacity: 0.01, transform: "translate3d(0, 0, 0)" },
|
|
186
|
+
{ opacity: 1, transform: "translate3d(0, 0, 0)" }
|
|
187
|
+
],
|
|
188
|
+
duration,
|
|
189
|
+
easing
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
if (leavingEl) {
|
|
193
|
+
animations.push(
|
|
194
|
+
createAnimation(
|
|
195
|
+
leavingEl,
|
|
196
|
+
[
|
|
197
|
+
{ opacity: 1, transform: "translate3d(0, 0, 0)" },
|
|
198
|
+
{ opacity: 0, transform: "translate3d(0, 0, 0)" }
|
|
199
|
+
],
|
|
200
|
+
Math.min(duration, 240),
|
|
201
|
+
easing
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
} else if (isBack) {
|
|
206
|
+
animations.push(
|
|
207
|
+
...createPinnedChromeAnimations(
|
|
208
|
+
resolvePageChrome(enteringEl),
|
|
209
|
+
`translate3d(${chromeOffLeft}, 0, 0)`,
|
|
210
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
211
|
+
duration,
|
|
212
|
+
easing
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
animations.push(
|
|
216
|
+
createAnimation(
|
|
217
|
+
enteringEl,
|
|
218
|
+
[{ transform: `translate3d(${offLeft}, 0, 0)` }, { transform: `translate3d(${IOS_CENTER}, 0, 0)` }],
|
|
219
|
+
duration,
|
|
220
|
+
easing
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
animations.push(...createOpacityAnimations(enteringContent, IOS_OFF_OPACITY, 1, duration, easing));
|
|
224
|
+
animations.push(
|
|
225
|
+
...createToolbarItemAnimations(
|
|
226
|
+
enteringHeaders,
|
|
227
|
+
`translate3d(${offLeft}, 0, 0)`,
|
|
228
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
229
|
+
0.01,
|
|
230
|
+
1,
|
|
231
|
+
duration,
|
|
232
|
+
easing
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
if (leavingEl) {
|
|
236
|
+
leavingEl.style.boxShadow = leadingEdgeShadow;
|
|
237
|
+
animations.push(
|
|
238
|
+
...createPinnedChromeAnimations(
|
|
239
|
+
resolvePageChrome(leavingEl),
|
|
240
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
241
|
+
`translate3d(${chromeOffRight}, 0, 0)`,
|
|
242
|
+
duration,
|
|
243
|
+
easing
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
animations.push(
|
|
247
|
+
createAnimation(
|
|
248
|
+
leavingEl,
|
|
249
|
+
[{ transform: `translate3d(${IOS_CENTER}, 0, 0)` }, { transform: `translate3d(${offRight}, 0, 0)` }],
|
|
250
|
+
duration,
|
|
251
|
+
easing
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
animations.push(
|
|
255
|
+
...createToolbarItemAnimations(
|
|
256
|
+
leavingHeaders,
|
|
257
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
258
|
+
`translate3d(${offRight}, 0, 0)`,
|
|
259
|
+
0.99,
|
|
260
|
+
0,
|
|
261
|
+
duration,
|
|
262
|
+
easing
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
enteringEl.style.boxShadow = leadingEdgeShadow;
|
|
268
|
+
animations.push(
|
|
269
|
+
...createPinnedChromeAnimations(
|
|
270
|
+
resolvePageChrome(enteringEl),
|
|
271
|
+
`translate3d(${chromeOffRight}, 0, 0)`,
|
|
272
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
273
|
+
duration,
|
|
274
|
+
easing
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
animations.push(
|
|
278
|
+
createAnimation(
|
|
279
|
+
enteringEl,
|
|
280
|
+
[{ transform: `translate3d(${offRight}, 0, 0)` }, { transform: `translate3d(${IOS_CENTER}, 0, 0)` }],
|
|
281
|
+
duration,
|
|
282
|
+
easing
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
animations.push(
|
|
286
|
+
...createToolbarItemAnimations(
|
|
287
|
+
enteringHeaders,
|
|
288
|
+
`translate3d(${offRight}, 0, 0)`,
|
|
289
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
290
|
+
0.01,
|
|
291
|
+
1,
|
|
292
|
+
duration,
|
|
293
|
+
easing
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
if (leavingEl) {
|
|
297
|
+
animations.push(
|
|
298
|
+
...createPinnedChromeAnimations(
|
|
299
|
+
resolvePageChrome(leavingEl),
|
|
300
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
301
|
+
`translate3d(${chromeOffLeft}, 0, 0)`,
|
|
302
|
+
duration,
|
|
303
|
+
easing
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
animations.push(
|
|
307
|
+
createAnimation(
|
|
308
|
+
leavingEl,
|
|
309
|
+
[{ transform: `translate3d(${IOS_CENTER}, 0, 0)` }, { transform: `translate3d(${offLeft}, 0, 0)` }],
|
|
310
|
+
duration,
|
|
311
|
+
easing
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
animations.push(...createOpacityAnimations(leavingContent, 1, IOS_OFF_OPACITY, duration, easing));
|
|
315
|
+
animations.push(
|
|
316
|
+
...createToolbarItemAnimations(
|
|
317
|
+
leavingHeaders,
|
|
318
|
+
`translate3d(${IOS_CENTER}, 0, 0)`,
|
|
319
|
+
`translate3d(${offLeft}, 0, 0)`,
|
|
320
|
+
0.99,
|
|
321
|
+
0,
|
|
322
|
+
duration,
|
|
323
|
+
easing
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return animations;
|
|
329
|
+
}
|
|
330
|
+
function createAndroidTransition(options) {
|
|
331
|
+
const { enteringEl, leavingEl, direction, duration, easing } = options;
|
|
332
|
+
const animations = [];
|
|
333
|
+
const isBack = direction === "back";
|
|
334
|
+
const isRoot = direction === "root";
|
|
335
|
+
preparePageLayer(enteringEl, isBack ? "99" : "101");
|
|
336
|
+
if (leavingEl) {
|
|
337
|
+
preparePageLayer(leavingEl, "100");
|
|
338
|
+
}
|
|
339
|
+
if (isRoot) {
|
|
340
|
+
animations.push(
|
|
341
|
+
createAnimation(
|
|
342
|
+
enteringEl,
|
|
343
|
+
[
|
|
344
|
+
{ opacity: 0.01, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` },
|
|
345
|
+
{ opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` }
|
|
346
|
+
],
|
|
347
|
+
duration,
|
|
348
|
+
easing
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
if (leavingEl) {
|
|
352
|
+
animations.push(
|
|
353
|
+
createAnimation(
|
|
354
|
+
leavingEl,
|
|
355
|
+
[
|
|
356
|
+
{ opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` },
|
|
357
|
+
{ opacity: 0, transform: `translate3d(0, ${MD_CENTER}, 0)` }
|
|
358
|
+
],
|
|
359
|
+
Math.min(duration, ANDROID_BACK_DURATION),
|
|
360
|
+
ANDROID_BACK_EASING
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
} else if (isBack) {
|
|
365
|
+
enteringEl.style.opacity = "1";
|
|
366
|
+
enteringEl.style.transform = `translate3d(0, ${MD_CENTER}, 0)`;
|
|
367
|
+
if (leavingEl) {
|
|
368
|
+
animations.push(
|
|
369
|
+
createAnimation(
|
|
370
|
+
leavingEl,
|
|
371
|
+
[
|
|
372
|
+
{ opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` },
|
|
373
|
+
{ opacity: 0, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` }
|
|
374
|
+
],
|
|
375
|
+
duration,
|
|
376
|
+
easing
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
animations.push(
|
|
382
|
+
createAnimation(
|
|
383
|
+
enteringEl,
|
|
384
|
+
[
|
|
385
|
+
{ opacity: 0.01, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` },
|
|
386
|
+
{ opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` }
|
|
387
|
+
],
|
|
388
|
+
duration,
|
|
389
|
+
easing
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
if (leavingEl) {
|
|
393
|
+
leavingEl.style.opacity = "1";
|
|
394
|
+
leavingEl.style.transform = `translate3d(0, ${MD_CENTER}, 0)`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return animations;
|
|
398
|
+
}
|
|
399
|
+
function createNoneTransition(options) {
|
|
400
|
+
const { enteringEl, leavingEl } = options;
|
|
401
|
+
enteringEl.style.opacity = "1";
|
|
402
|
+
enteringEl.style.transform = "none";
|
|
403
|
+
if (leavingEl) {
|
|
404
|
+
leavingEl.style.opacity = "0";
|
|
405
|
+
leavingEl.style.transform = "none";
|
|
406
|
+
}
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
function createTransition(options, platform = "auto") {
|
|
410
|
+
if (options.direction === "none") {
|
|
411
|
+
return createNoneTransition(options);
|
|
412
|
+
}
|
|
413
|
+
const resolved = platform === "auto" ? detectPlatform() : platform;
|
|
414
|
+
if (resolved === "android") {
|
|
415
|
+
return createAndroidTransition(options);
|
|
416
|
+
}
|
|
417
|
+
return createIOSTransition(options);
|
|
418
|
+
}
|
|
419
|
+
async function waitForAnimations(animations) {
|
|
420
|
+
if (animations.length === 0) return;
|
|
421
|
+
await Promise.all(animations.map((anim) => anim.finished.catch(() => void 0)));
|
|
422
|
+
}
|
|
423
|
+
function cancelAnimations(animations) {
|
|
424
|
+
animations.forEach((anim) => anim.cancel());
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/core/view-transitions.ts
|
|
428
|
+
function supportsViewTransitions() {
|
|
429
|
+
return typeof document !== "undefined" && "startViewTransition" in document;
|
|
430
|
+
}
|
|
431
|
+
async function runViewTransition(options) {
|
|
432
|
+
const { update, direction = "forward", skipAnimation = false } = options;
|
|
433
|
+
if (skipAnimation || !supportsViewTransitions()) {
|
|
434
|
+
await update();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const root = document.documentElement;
|
|
438
|
+
root.dataset.transitionDirection = direction;
|
|
439
|
+
try {
|
|
440
|
+
const transition = document.startViewTransition(async () => {
|
|
441
|
+
await update();
|
|
442
|
+
});
|
|
443
|
+
await transition.finished;
|
|
444
|
+
} finally {
|
|
445
|
+
delete root.dataset.transitionDirection;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function setViewTransitionName(element, name) {
|
|
449
|
+
element.style.viewTransitionName = name;
|
|
450
|
+
}
|
|
451
|
+
function clearViewTransitionName(element) {
|
|
452
|
+
element.style.viewTransitionName = "";
|
|
453
|
+
}
|
|
454
|
+
var VIEW_TRANSITIONS_CSS = `
|
|
455
|
+
/* View Transitions API base styles */
|
|
456
|
+
::view-transition-old(root),
|
|
457
|
+
::view-transition-new(root) {
|
|
458
|
+
animation-duration: 0.4s;
|
|
459
|
+
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* iOS-style forward navigation */
|
|
463
|
+
[data-transition-direction="forward"]::view-transition-old(root) {
|
|
464
|
+
animation-name: cap-slide-out-left;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
[data-transition-direction="forward"]::view-transition-new(root) {
|
|
468
|
+
animation-name: cap-slide-in-right;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* iOS-style back navigation */
|
|
472
|
+
[data-transition-direction="back"]::view-transition-old(root) {
|
|
473
|
+
animation-name: cap-slide-out-right;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
[data-transition-direction="back"]::view-transition-new(root) {
|
|
477
|
+
animation-name: cap-slide-in-left;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* Root/replace navigation - fade */
|
|
481
|
+
[data-transition-direction="root"]::view-transition-old(root) {
|
|
482
|
+
animation-name: cap-fade-out;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
[data-transition-direction="root"]::view-transition-new(root) {
|
|
486
|
+
animation-name: cap-fade-in;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Header transitions */
|
|
490
|
+
::view-transition-old(cap-header),
|
|
491
|
+
::view-transition-new(cap-header) {
|
|
492
|
+
animation-duration: 0.3s;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
[data-transition-direction="forward"]::view-transition-old(cap-header) {
|
|
496
|
+
animation-name: cap-header-out-left;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
[data-transition-direction="forward"]::view-transition-new(cap-header) {
|
|
500
|
+
animation-name: cap-header-in-right;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
[data-transition-direction="back"]::view-transition-old(cap-header) {
|
|
504
|
+
animation-name: cap-header-out-right;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
[data-transition-direction="back"]::view-transition-new(cap-header) {
|
|
508
|
+
animation-name: cap-header-in-left;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* Content transitions */
|
|
512
|
+
::view-transition-old(cap-content),
|
|
513
|
+
::view-transition-new(cap-content) {
|
|
514
|
+
animation-duration: 0.4s;
|
|
515
|
+
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
[data-transition-direction="forward"]::view-transition-old(cap-content) {
|
|
519
|
+
animation-name: cap-slide-out-left;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
[data-transition-direction="forward"]::view-transition-new(cap-content) {
|
|
523
|
+
animation-name: cap-slide-in-right;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
[data-transition-direction="back"]::view-transition-old(cap-content) {
|
|
527
|
+
animation-name: cap-slide-out-right;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
[data-transition-direction="back"]::view-transition-new(cap-content) {
|
|
531
|
+
animation-name: cap-slide-in-left;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* Animation keyframes */
|
|
535
|
+
@keyframes cap-slide-in-right {
|
|
536
|
+
from {
|
|
537
|
+
transform: translateX(100%);
|
|
538
|
+
opacity: 1;
|
|
539
|
+
}
|
|
540
|
+
to {
|
|
541
|
+
transform: translateX(0);
|
|
542
|
+
opacity: 1;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@keyframes cap-slide-out-left {
|
|
547
|
+
from {
|
|
548
|
+
transform: translateX(0);
|
|
549
|
+
opacity: 1;
|
|
550
|
+
}
|
|
551
|
+
to {
|
|
552
|
+
transform: translateX(-30%);
|
|
553
|
+
opacity: 0.8;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@keyframes cap-slide-in-left {
|
|
558
|
+
from {
|
|
559
|
+
transform: translateX(-30%);
|
|
560
|
+
opacity: 0.8;
|
|
561
|
+
}
|
|
562
|
+
to {
|
|
563
|
+
transform: translateX(0);
|
|
564
|
+
opacity: 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@keyframes cap-slide-out-right {
|
|
569
|
+
from {
|
|
570
|
+
transform: translateX(0);
|
|
571
|
+
opacity: 1;
|
|
572
|
+
}
|
|
573
|
+
to {
|
|
574
|
+
transform: translateX(100%);
|
|
575
|
+
opacity: 1;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
@keyframes cap-fade-in {
|
|
580
|
+
from { opacity: 0; }
|
|
581
|
+
to { opacity: 1; }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@keyframes cap-fade-out {
|
|
585
|
+
from { opacity: 1; }
|
|
586
|
+
to { opacity: 0; }
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@keyframes cap-header-in-right {
|
|
590
|
+
from {
|
|
591
|
+
transform: translateX(20px);
|
|
592
|
+
opacity: 0;
|
|
593
|
+
}
|
|
594
|
+
to {
|
|
595
|
+
transform: translateX(0);
|
|
596
|
+
opacity: 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
@keyframes cap-header-out-left {
|
|
601
|
+
from {
|
|
602
|
+
transform: translateX(0);
|
|
603
|
+
opacity: 1;
|
|
604
|
+
}
|
|
605
|
+
to {
|
|
606
|
+
transform: translateX(-20px);
|
|
607
|
+
opacity: 0;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
@keyframes cap-header-in-left {
|
|
612
|
+
from {
|
|
613
|
+
transform: translateX(-20px);
|
|
614
|
+
opacity: 0;
|
|
615
|
+
}
|
|
616
|
+
to {
|
|
617
|
+
transform: translateX(0);
|
|
618
|
+
opacity: 1;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@keyframes cap-header-out-right {
|
|
623
|
+
from {
|
|
624
|
+
transform: translateX(0);
|
|
625
|
+
opacity: 1;
|
|
626
|
+
}
|
|
627
|
+
to {
|
|
628
|
+
transform: translateX(20px);
|
|
629
|
+
opacity: 0;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* Reduced motion support */
|
|
634
|
+
@media (prefers-reduced-motion: reduce) {
|
|
635
|
+
::view-transition-old(root),
|
|
636
|
+
::view-transition-new(root),
|
|
637
|
+
::view-transition-old(cap-header),
|
|
638
|
+
::view-transition-new(cap-header),
|
|
639
|
+
::view-transition-old(cap-content),
|
|
640
|
+
::view-transition-new(cap-content) {
|
|
641
|
+
animation-duration: 0.01ms !important;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
function injectViewTransitionsCSS() {
|
|
646
|
+
if (typeof document === "undefined") return;
|
|
647
|
+
const styleId = "cap-view-transitions-css";
|
|
648
|
+
if (document.getElementById(styleId)) return;
|
|
649
|
+
const style = document.createElement("style");
|
|
650
|
+
style.id = styleId;
|
|
651
|
+
style.textContent = VIEW_TRANSITIONS_CSS;
|
|
652
|
+
document.head.appendChild(style);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/core/transition-controller.ts
|
|
656
|
+
var DEFAULT_CONFIG = {
|
|
657
|
+
platform: "auto",
|
|
658
|
+
duration: 0,
|
|
659
|
+
// Will use platform default
|
|
660
|
+
easing: "",
|
|
661
|
+
// Will use platform default
|
|
662
|
+
useViewTransitions: false,
|
|
663
|
+
detectPlatform
|
|
664
|
+
};
|
|
665
|
+
var TransitionController = class {
|
|
666
|
+
config;
|
|
667
|
+
pageStack = [];
|
|
668
|
+
currentAnimations = [];
|
|
669
|
+
isAnimating = false;
|
|
670
|
+
interactiveBackTransition = null;
|
|
671
|
+
lifecycleCallbacks = /* @__PURE__ */ new Map();
|
|
672
|
+
constructor(config = {}) {
|
|
673
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
674
|
+
if (this.config.useViewTransitions && supportsViewTransitions()) {
|
|
675
|
+
injectViewTransitionsCSS();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Get the resolved platform
|
|
680
|
+
*/
|
|
681
|
+
get platform() {
|
|
682
|
+
if (this.config.platform === "ios") {
|
|
683
|
+
return "ios";
|
|
684
|
+
}
|
|
685
|
+
if (this.config.platform === "android") {
|
|
686
|
+
return "android";
|
|
687
|
+
}
|
|
688
|
+
return this.config.detectPlatform();
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Get the current page state
|
|
692
|
+
*/
|
|
693
|
+
get currentPage() {
|
|
694
|
+
return this.pageStack[this.pageStack.length - 1];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Get the page stack
|
|
698
|
+
*/
|
|
699
|
+
get stack() {
|
|
700
|
+
return this.pageStack;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Check if an animation is in progress
|
|
704
|
+
*/
|
|
705
|
+
get animating() {
|
|
706
|
+
return this.isAnimating;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Update global configuration
|
|
710
|
+
*/
|
|
711
|
+
configure(config) {
|
|
712
|
+
this.config = { ...this.config, ...config };
|
|
713
|
+
if (this.config.useViewTransitions && supportsViewTransitions()) {
|
|
714
|
+
injectViewTransitionsCSS();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Register lifecycle callbacks for a page
|
|
719
|
+
*/
|
|
720
|
+
registerLifecycle(pageId, lifecycle) {
|
|
721
|
+
this.lifecycleCallbacks.set(pageId, lifecycle);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Unregister lifecycle callbacks for a page
|
|
725
|
+
*/
|
|
726
|
+
unregisterLifecycle(pageId) {
|
|
727
|
+
this.lifecycleCallbacks.delete(pageId);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Create a page state from an element
|
|
731
|
+
*/
|
|
732
|
+
createPageState(element, options = {}) {
|
|
733
|
+
const id = options.id || `page-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
734
|
+
const header = element.querySelector(
|
|
735
|
+
'[data-cap-header], .cap-header, cap-header, [slot="header"]'
|
|
736
|
+
);
|
|
737
|
+
const content = element.querySelector(
|
|
738
|
+
'[data-cap-content], .cap-content, cap-content, [slot="content"]'
|
|
739
|
+
);
|
|
740
|
+
const footer = element.querySelector(
|
|
741
|
+
'[data-cap-footer], .cap-footer, cap-footer, [slot="footer"]'
|
|
742
|
+
);
|
|
743
|
+
return {
|
|
744
|
+
id,
|
|
745
|
+
element,
|
|
746
|
+
header: header || void 0,
|
|
747
|
+
content: content || void 0,
|
|
748
|
+
footer: footer || void 0,
|
|
749
|
+
isActive: false,
|
|
750
|
+
data: options.data
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Navigate to a new page (push)
|
|
755
|
+
*/
|
|
756
|
+
async push(enteringEl, config = {}) {
|
|
757
|
+
return this.navigate(enteringEl, { ...config, direction: "forward" });
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Navigate back (pop)
|
|
761
|
+
*/
|
|
762
|
+
async pop(config = {}) {
|
|
763
|
+
if (this.pageStack.length <= 1) {
|
|
764
|
+
return { success: false, duration: 0, error: new Error("Cannot pop: no page to go back to") };
|
|
765
|
+
}
|
|
766
|
+
const leavingState = this.pageStack[this.pageStack.length - 1];
|
|
767
|
+
const enteringState = this.pageStack[this.pageStack.length - 2];
|
|
768
|
+
return this.navigateWithStates(enteringState, leavingState, { ...config, direction: "back" }, () => {
|
|
769
|
+
this.pageStack.pop();
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Start an interactive iOS-style back transition using the cached previous page.
|
|
774
|
+
*/
|
|
775
|
+
beginInteractiveBack(config = {}) {
|
|
776
|
+
if (this.pageStack.length <= 1 || this.isAnimating || this.interactiveBackTransition) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
const leavingState = this.pageStack[this.pageStack.length - 1];
|
|
780
|
+
const enteringState = this.pageStack[this.pageStack.length - 2];
|
|
781
|
+
const direction = "back";
|
|
782
|
+
const duration = config.duration ?? (this.config.duration || getDefaultDuration(this.platform, direction));
|
|
783
|
+
this.isAnimating = true;
|
|
784
|
+
enteringState.element.style.display = "";
|
|
785
|
+
enteringState.element.style.visibility = "visible";
|
|
786
|
+
const animOptions = {
|
|
787
|
+
enteringEl: enteringState.element,
|
|
788
|
+
leavingEl: leavingState.element,
|
|
789
|
+
direction,
|
|
790
|
+
duration,
|
|
791
|
+
easing: "linear",
|
|
792
|
+
isBack: true
|
|
793
|
+
};
|
|
794
|
+
const animations = createTransition(animOptions, this.platform);
|
|
795
|
+
for (const animation of animations) {
|
|
796
|
+
animation.pause();
|
|
797
|
+
animation.currentTime = 0;
|
|
798
|
+
}
|
|
799
|
+
this.currentAnimations = animations;
|
|
800
|
+
this.interactiveBackTransition = {
|
|
801
|
+
enteringState,
|
|
802
|
+
leavingState,
|
|
803
|
+
animations,
|
|
804
|
+
duration
|
|
805
|
+
};
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Move the current interactive back transition to a progress step from 0 to 1.
|
|
810
|
+
*/
|
|
811
|
+
stepInteractiveBack(step) {
|
|
812
|
+
const transition = this.interactiveBackTransition;
|
|
813
|
+
if (!transition) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const progress = Math.max(0, Math.min(step, 0.9999));
|
|
817
|
+
for (const animation of transition.animations) {
|
|
818
|
+
const duration = this.getAnimationDuration(animation, transition.duration);
|
|
819
|
+
animation.pause();
|
|
820
|
+
animation.currentTime = duration * progress;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Complete or cancel the current interactive back transition.
|
|
825
|
+
*/
|
|
826
|
+
async endInteractiveBack(shouldComplete, releaseDuration, commitStack) {
|
|
827
|
+
const transition = this.interactiveBackTransition;
|
|
828
|
+
if (!transition) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
await this.playInteractiveAnimationsTo(shouldComplete ? 1 : 0, releaseDuration);
|
|
833
|
+
if (shouldComplete && commitStack) {
|
|
834
|
+
this.pageStack.pop();
|
|
835
|
+
this.updatePageVisibility(transition.enteringState, transition.leavingState);
|
|
836
|
+
transition.enteringState.isActive = true;
|
|
837
|
+
transition.leavingState.isActive = false;
|
|
838
|
+
cancelAnimations(transition.animations);
|
|
839
|
+
} else if (!shouldComplete) {
|
|
840
|
+
this.updatePageVisibility(transition.leavingState, transition.enteringState);
|
|
841
|
+
transition.leavingState.isActive = true;
|
|
842
|
+
transition.enteringState.isActive = false;
|
|
843
|
+
cancelAnimations(transition.animations);
|
|
844
|
+
}
|
|
845
|
+
} finally {
|
|
846
|
+
this.interactiveBackTransition = null;
|
|
847
|
+
this.currentAnimations = [];
|
|
848
|
+
this.isAnimating = false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Cancel the current interactive back transition immediately.
|
|
853
|
+
*/
|
|
854
|
+
cancelInteractiveBack() {
|
|
855
|
+
const transition = this.interactiveBackTransition;
|
|
856
|
+
if (!transition) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
cancelAnimations(transition.animations);
|
|
860
|
+
this.updatePageVisibility(transition.leavingState, transition.enteringState);
|
|
861
|
+
this.interactiveBackTransition = null;
|
|
862
|
+
this.currentAnimations = [];
|
|
863
|
+
this.isAnimating = false;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Replace all pages with a new root
|
|
867
|
+
*/
|
|
868
|
+
async setRoot(enteringEl, config = {}) {
|
|
869
|
+
return this.navigate(enteringEl, { ...config, direction: "root" });
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Main navigation method
|
|
873
|
+
*/
|
|
874
|
+
async navigate(enteringEl, config = {}) {
|
|
875
|
+
const direction = config.direction || "forward";
|
|
876
|
+
const enteringState = this.createPageState(enteringEl);
|
|
877
|
+
const leavingState = this.currentPage;
|
|
878
|
+
return this.navigateWithStates(enteringState, leavingState, config, () => {
|
|
879
|
+
if (direction === "root") {
|
|
880
|
+
this.pageStack = [enteringState];
|
|
881
|
+
} else if (direction === "back" && this.pageStack.length > 0) {
|
|
882
|
+
this.pageStack.pop();
|
|
883
|
+
const staleEnteringState = this.pageStack.pop();
|
|
884
|
+
if (staleEnteringState && staleEnteringState.element !== enteringState.element) {
|
|
885
|
+
staleEnteringState.element.remove();
|
|
886
|
+
this.lifecycleCallbacks.delete(staleEnteringState.id);
|
|
887
|
+
}
|
|
888
|
+
this.pageStack.push(enteringState);
|
|
889
|
+
} else {
|
|
890
|
+
this.pageStack.push(enteringState);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Navigate between two known page states
|
|
896
|
+
*/
|
|
897
|
+
async navigateWithStates(enteringState, leavingState, config, updateStack) {
|
|
898
|
+
const startTime = performance.now();
|
|
899
|
+
const direction = config.direction || "forward";
|
|
900
|
+
if (this.isAnimating) {
|
|
901
|
+
cancelAnimations(this.currentAnimations);
|
|
902
|
+
}
|
|
903
|
+
this.isAnimating = true;
|
|
904
|
+
const event = {
|
|
905
|
+
direction,
|
|
906
|
+
from: leavingState,
|
|
907
|
+
to: enteringState
|
|
908
|
+
};
|
|
909
|
+
try {
|
|
910
|
+
if (leavingState) {
|
|
911
|
+
const lifecycle = this.lifecycleCallbacks.get(leavingState.id);
|
|
912
|
+
await lifecycle?.onWillLeave?.(event);
|
|
913
|
+
config.onStart?.();
|
|
914
|
+
}
|
|
915
|
+
const enteringLifecycle = this.lifecycleCallbacks.get(enteringState.id);
|
|
916
|
+
await enteringLifecycle?.onWillEnter?.(event);
|
|
917
|
+
const duration = config.duration ?? (this.config.duration || getDefaultDuration(this.platform, direction));
|
|
918
|
+
const easing = this.resolveTransitionEasing(config.easing || this.config.easing, direction);
|
|
919
|
+
const useViewTransitions = config.useViewTransitions !== false && this.config.useViewTransitions && supportsViewTransitions();
|
|
920
|
+
if (useViewTransitions) {
|
|
921
|
+
this.prepareViewTransitionElements(enteringState, leavingState);
|
|
922
|
+
await runViewTransition({
|
|
923
|
+
direction,
|
|
924
|
+
update: () => {
|
|
925
|
+
updateStack();
|
|
926
|
+
this.updatePageVisibility(enteringState, leavingState);
|
|
927
|
+
this.applyViewTransitionNames(enteringState);
|
|
928
|
+
this.clearViewTransitionNames(leavingState);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
this.clearViewTransitionNames(enteringState, leavingState);
|
|
932
|
+
} else {
|
|
933
|
+
updateStack();
|
|
934
|
+
const animOptions = {
|
|
935
|
+
enteringEl: enteringState.element,
|
|
936
|
+
leavingEl: leavingState?.element,
|
|
937
|
+
direction,
|
|
938
|
+
duration,
|
|
939
|
+
easing,
|
|
940
|
+
isBack: direction === "back"
|
|
941
|
+
};
|
|
942
|
+
this.currentAnimations = createTransition(animOptions, this.platform);
|
|
943
|
+
await waitForAnimations(this.currentAnimations);
|
|
944
|
+
this.updatePageVisibility(enteringState, leavingState);
|
|
945
|
+
cancelAnimations(this.currentAnimations);
|
|
946
|
+
}
|
|
947
|
+
enteringState.isActive = true;
|
|
948
|
+
await enteringLifecycle?.onDidEnter?.(event);
|
|
949
|
+
if (leavingState) {
|
|
950
|
+
leavingState.isActive = false;
|
|
951
|
+
const lifecycle = this.lifecycleCallbacks.get(leavingState.id);
|
|
952
|
+
await lifecycle?.onDidLeave?.(event);
|
|
953
|
+
}
|
|
954
|
+
config.onComplete?.();
|
|
955
|
+
const totalDuration = performance.now() - startTime;
|
|
956
|
+
return { success: true, duration: totalDuration };
|
|
957
|
+
} catch (error) {
|
|
958
|
+
return {
|
|
959
|
+
success: false,
|
|
960
|
+
duration: performance.now() - startTime,
|
|
961
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
962
|
+
};
|
|
963
|
+
} finally {
|
|
964
|
+
this.isAnimating = false;
|
|
965
|
+
this.currentAnimations = [];
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Update page visibility after animation
|
|
970
|
+
*/
|
|
971
|
+
updatePageVisibility(enteringState, leavingState) {
|
|
972
|
+
enteringState.element.style.display = "";
|
|
973
|
+
enteringState.element.style.visibility = "visible";
|
|
974
|
+
enteringState.element.style.opacity = "1";
|
|
975
|
+
enteringState.element.style.transform = "none";
|
|
976
|
+
enteringState.element.style.position = "relative";
|
|
977
|
+
this.clearTransitionOnlyStyles(enteringState.element);
|
|
978
|
+
this.clearPagePartTransitionStyles(enteringState);
|
|
979
|
+
if (leavingState) {
|
|
980
|
+
leavingState.element.style.display = "none";
|
|
981
|
+
leavingState.element.style.visibility = "hidden";
|
|
982
|
+
leavingState.element.style.opacity = "1";
|
|
983
|
+
leavingState.element.style.transform = "none";
|
|
984
|
+
this.clearTransitionOnlyStyles(leavingState.element);
|
|
985
|
+
this.clearPagePartTransitionStyles(leavingState);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
getAnimationDuration(animation, fallback) {
|
|
989
|
+
const duration = animation.effect?.getTiming().duration;
|
|
990
|
+
return typeof duration === "number" && Number.isFinite(duration) ? duration : fallback;
|
|
991
|
+
}
|
|
992
|
+
async playInteractiveAnimationsTo(targetProgress, releaseDuration) {
|
|
993
|
+
const transition = this.interactiveBackTransition;
|
|
994
|
+
if (!transition) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (releaseDuration <= 0) {
|
|
998
|
+
for (const animation of transition.animations) {
|
|
999
|
+
const duration = this.getAnimationDuration(animation, transition.duration);
|
|
1000
|
+
animation.pause();
|
|
1001
|
+
animation.currentTime = duration * targetProgress;
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const finished = transition.animations.map((animation) => {
|
|
1006
|
+
const duration = this.getAnimationDuration(animation, transition.duration);
|
|
1007
|
+
const currentTime = typeof animation.currentTime === "number" ? animation.currentTime : 0;
|
|
1008
|
+
const targetTime = duration * targetProgress;
|
|
1009
|
+
const distance = Math.abs(targetTime - currentTime);
|
|
1010
|
+
if (distance < 1) {
|
|
1011
|
+
animation.currentTime = targetTime;
|
|
1012
|
+
return Promise.resolve();
|
|
1013
|
+
}
|
|
1014
|
+
animation.playbackRate = Math.max(distance / releaseDuration, 1e-3) * (targetTime >= currentTime ? 1 : -1);
|
|
1015
|
+
animation.play();
|
|
1016
|
+
return animation.finished.catch(() => void 0);
|
|
1017
|
+
});
|
|
1018
|
+
await Promise.all(finished);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Resolve configured easing presets after platform/direction are known.
|
|
1022
|
+
*/
|
|
1023
|
+
resolveTransitionEasing(easing, direction) {
|
|
1024
|
+
if (!easing) {
|
|
1025
|
+
return getDefaultEasing(this.platform, direction || "forward");
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof easing === "string" && ["ios", "android"].includes(easing)) {
|
|
1028
|
+
return getDefaultEasing(easing, direction || "forward");
|
|
1029
|
+
}
|
|
1030
|
+
return resolveEasing(easing);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Remove styles that should only exist while a page is actively transitioning.
|
|
1034
|
+
*/
|
|
1035
|
+
clearTransitionOnlyStyles(element) {
|
|
1036
|
+
element.classList.remove("cap-transition-active");
|
|
1037
|
+
element.style.removeProperty("z-index");
|
|
1038
|
+
element.style.removeProperty("pointer-events");
|
|
1039
|
+
element.style.removeProperty("will-change");
|
|
1040
|
+
element.style.removeProperty("backface-visibility");
|
|
1041
|
+
element.style.removeProperty("transform-style");
|
|
1042
|
+
element.style.removeProperty("box-shadow");
|
|
1043
|
+
}
|
|
1044
|
+
clearPagePartTransitionStyles(pageState) {
|
|
1045
|
+
const { header, content, footer } = this.resolvePageParts(pageState);
|
|
1046
|
+
for (const element of [header, content, footer]) {
|
|
1047
|
+
if (!element) continue;
|
|
1048
|
+
this.clearTransitionOnlyStyles(element);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Prepare entering/leaving elements for a View Transition capture.
|
|
1053
|
+
* Entering page must be hidden in the "old" snapshot.
|
|
1054
|
+
*/
|
|
1055
|
+
prepareViewTransitionElements(enteringState, leavingState) {
|
|
1056
|
+
this.clearAllKnownViewTransitionNames(enteringState, leavingState);
|
|
1057
|
+
if (leavingState) {
|
|
1058
|
+
this.applyViewTransitionNames(leavingState);
|
|
1059
|
+
enteringState.element.style.display = "none";
|
|
1060
|
+
enteringState.element.style.visibility = "hidden";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Assign view transition names to a page's layout parts.
|
|
1065
|
+
*/
|
|
1066
|
+
applyViewTransitionNames(pageState) {
|
|
1067
|
+
const { header, content, footer } = this.resolvePageParts(pageState);
|
|
1068
|
+
if (header) {
|
|
1069
|
+
setViewTransitionName(header, "cap-header");
|
|
1070
|
+
}
|
|
1071
|
+
if (content) {
|
|
1072
|
+
setViewTransitionName(content, "cap-content");
|
|
1073
|
+
}
|
|
1074
|
+
if (footer) {
|
|
1075
|
+
setViewTransitionName(footer, "cap-footer");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Clear view transition names for one or more page states.
|
|
1080
|
+
*/
|
|
1081
|
+
clearViewTransitionNames(...states) {
|
|
1082
|
+
for (const state of states) {
|
|
1083
|
+
if (!state) continue;
|
|
1084
|
+
const { header, content, footer } = this.resolvePageParts(state);
|
|
1085
|
+
if (header) {
|
|
1086
|
+
clearViewTransitionName(header);
|
|
1087
|
+
}
|
|
1088
|
+
if (content) {
|
|
1089
|
+
clearViewTransitionName(content);
|
|
1090
|
+
}
|
|
1091
|
+
if (footer) {
|
|
1092
|
+
clearViewTransitionName(footer);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Clear view transition names from all known pages plus transient states.
|
|
1098
|
+
*/
|
|
1099
|
+
clearAllKnownViewTransitionNames(...extraStates) {
|
|
1100
|
+
const knownStates = /* @__PURE__ */ new Set();
|
|
1101
|
+
for (const state of this.pageStack) {
|
|
1102
|
+
knownStates.add(state);
|
|
1103
|
+
}
|
|
1104
|
+
for (const state of extraStates) {
|
|
1105
|
+
if (state) {
|
|
1106
|
+
knownStates.add(state);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
this.clearViewTransitionNames(...knownStates);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Resolve page parts lazily to avoid timing issues with custom-element setup.
|
|
1113
|
+
*/
|
|
1114
|
+
resolvePageParts(pageState) {
|
|
1115
|
+
const header = pageState.header || pageState.element.querySelector(
|
|
1116
|
+
'[data-cap-header], .cap-header, cap-header, [slot="header"]'
|
|
1117
|
+
) || void 0;
|
|
1118
|
+
const content = pageState.content || pageState.element.querySelector(
|
|
1119
|
+
'[data-cap-content], .cap-content, cap-content, [slot="content"]'
|
|
1120
|
+
) || void 0;
|
|
1121
|
+
const footer = pageState.footer || pageState.element.querySelector(
|
|
1122
|
+
'[data-cap-footer], .cap-footer, cap-footer, [slot="footer"]'
|
|
1123
|
+
) || void 0;
|
|
1124
|
+
if (header) pageState.header = header;
|
|
1125
|
+
if (content) pageState.content = content;
|
|
1126
|
+
if (footer) pageState.footer = footer;
|
|
1127
|
+
return { header, content, footer };
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Save scroll position for a page
|
|
1131
|
+
*/
|
|
1132
|
+
saveScrollPosition(pageId) {
|
|
1133
|
+
const page = this.pageStack.find((p) => p.id === pageId);
|
|
1134
|
+
if (page?.content) {
|
|
1135
|
+
page.scrollPosition = {
|
|
1136
|
+
x: page.content.scrollLeft,
|
|
1137
|
+
y: page.content.scrollTop
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Restore scroll position for a page
|
|
1143
|
+
*/
|
|
1144
|
+
restoreScrollPosition(pageId) {
|
|
1145
|
+
const page = this.pageStack.find((p) => p.id === pageId);
|
|
1146
|
+
if (page?.content && page.scrollPosition) {
|
|
1147
|
+
page.content.scrollLeft = page.scrollPosition.x;
|
|
1148
|
+
page.content.scrollTop = page.scrollPosition.y;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Remove a page from the stack (used when cleaning up)
|
|
1153
|
+
*/
|
|
1154
|
+
removePage(pageId) {
|
|
1155
|
+
const index = this.pageStack.findIndex((p) => p.id === pageId);
|
|
1156
|
+
if (index !== -1) {
|
|
1157
|
+
this.pageStack.splice(index, 1);
|
|
1158
|
+
this.lifecycleCallbacks.delete(pageId);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Clear all pages
|
|
1163
|
+
*/
|
|
1164
|
+
clear() {
|
|
1165
|
+
this.pageStack = [];
|
|
1166
|
+
this.lifecycleCallbacks.clear();
|
|
1167
|
+
cancelAnimations(this.currentAnimations);
|
|
1168
|
+
this.currentAnimations = [];
|
|
1169
|
+
this.isAnimating = false;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
function createTransitionController(config) {
|
|
1173
|
+
return new TransitionController(config);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/core/native-platform.ts
|
|
1177
|
+
function getCapacitorRuntime() {
|
|
1178
|
+
return globalThis.Capacitor;
|
|
1179
|
+
}
|
|
1180
|
+
function normalizePlatform(platform) {
|
|
1181
|
+
if (platform === "ios" || platform === "android" || platform === "web") {
|
|
1182
|
+
return platform;
|
|
1183
|
+
}
|
|
1184
|
+
return "unknown";
|
|
1185
|
+
}
|
|
1186
|
+
function detectNativePlatform() {
|
|
1187
|
+
const capacitor = getCapacitorRuntime();
|
|
1188
|
+
const platform = normalizePlatform(capacitor?.getPlatform?.());
|
|
1189
|
+
const isNative = capacitor?.isNativePlatform?.() ?? (platform === "ios" || platform === "android");
|
|
1190
|
+
return {
|
|
1191
|
+
platform,
|
|
1192
|
+
isNative
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function isNativeSwipeGesturePlatform() {
|
|
1196
|
+
const { platform, isNative } = detectNativePlatform();
|
|
1197
|
+
return isNative && platform === "ios";
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/components/cap-router-outlet.ts
|
|
1201
|
+
var CapRouterOutlet = class extends HTMLElement {
|
|
1202
|
+
controller;
|
|
1203
|
+
options;
|
|
1204
|
+
observer = null;
|
|
1205
|
+
pendingPage = null;
|
|
1206
|
+
ignoredNodes = /* @__PURE__ */ new WeakSet();
|
|
1207
|
+
swipeGesturePointer = null;
|
|
1208
|
+
swipeGestureListenersActive = false;
|
|
1209
|
+
skipNextHistoryBackTransition = false;
|
|
1210
|
+
swipeBackDepth = 0;
|
|
1211
|
+
lastNavigationHref = null;
|
|
1212
|
+
swipeGestureEdgeWidth = 50;
|
|
1213
|
+
swipeGestureThreshold = 10;
|
|
1214
|
+
swipeGestureMinimumVelocity = 0.2;
|
|
1215
|
+
static get observedAttributes() {
|
|
1216
|
+
return ["platform", "duration", "keep-in-dom", "max-cached", "swipe-gesture"];
|
|
1217
|
+
}
|
|
1218
|
+
constructor() {
|
|
1219
|
+
super();
|
|
1220
|
+
this.options = {
|
|
1221
|
+
keepInDom: true,
|
|
1222
|
+
maxCached: 10,
|
|
1223
|
+
swipeGesture: "auto"
|
|
1224
|
+
};
|
|
1225
|
+
this.controller = createTransitionController();
|
|
1226
|
+
}
|
|
1227
|
+
connectedCallback() {
|
|
1228
|
+
this.style.display = "block";
|
|
1229
|
+
this.style.position = "relative";
|
|
1230
|
+
this.style.width = "100%";
|
|
1231
|
+
this.style.height = "100%";
|
|
1232
|
+
this.style.overflow = "hidden";
|
|
1233
|
+
this.lastNavigationHref = this.getCurrentNavigationHref();
|
|
1234
|
+
this.observer = new MutationObserver((mutations) => {
|
|
1235
|
+
this.handleMutations(mutations);
|
|
1236
|
+
});
|
|
1237
|
+
this.observer.observe(this, {
|
|
1238
|
+
childList: true,
|
|
1239
|
+
subtree: false
|
|
1240
|
+
});
|
|
1241
|
+
const children = Array.from(this.children);
|
|
1242
|
+
if (children.length > 0) {
|
|
1243
|
+
this.initializeFirstPage(children[children.length - 1]);
|
|
1244
|
+
}
|
|
1245
|
+
this.updateSwipeGestureListeners();
|
|
1246
|
+
}
|
|
1247
|
+
disconnectedCallback() {
|
|
1248
|
+
this.observer?.disconnect();
|
|
1249
|
+
this.removeSwipeGestureListeners();
|
|
1250
|
+
this.controller.clear();
|
|
1251
|
+
this.swipeBackDepth = 0;
|
|
1252
|
+
this.lastNavigationHref = null;
|
|
1253
|
+
}
|
|
1254
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1255
|
+
switch (name) {
|
|
1256
|
+
case "platform":
|
|
1257
|
+
this.controller.configure({ platform: newValue });
|
|
1258
|
+
break;
|
|
1259
|
+
case "duration":
|
|
1260
|
+
this.controller.configure({ duration: parseInt(newValue, 10) });
|
|
1261
|
+
break;
|
|
1262
|
+
case "keep-in-dom":
|
|
1263
|
+
this.options.keepInDom = newValue !== "false";
|
|
1264
|
+
break;
|
|
1265
|
+
case "max-cached":
|
|
1266
|
+
this.options.maxCached = parseInt(newValue, 10);
|
|
1267
|
+
break;
|
|
1268
|
+
case "swipe-gesture":
|
|
1269
|
+
this.options.swipeGesture = this.parseSwipeGestureAttribute(newValue);
|
|
1270
|
+
this.updateSwipeGestureListeners();
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Handle DOM mutations (child additions/removals)
|
|
1276
|
+
*/
|
|
1277
|
+
handleMutations(mutations) {
|
|
1278
|
+
const addedNodes = [];
|
|
1279
|
+
const removedNodes = [];
|
|
1280
|
+
for (const mutation of mutations) {
|
|
1281
|
+
for (const node of mutation.removedNodes) {
|
|
1282
|
+
if (node instanceof HTMLElement) {
|
|
1283
|
+
removedNodes.push(node);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
for (const node of mutation.addedNodes) {
|
|
1287
|
+
if (node instanceof HTMLElement) {
|
|
1288
|
+
addedNodes.push(node);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const currentEl = this.controller.currentPage?.element;
|
|
1293
|
+
if (currentEl && removedNodes.includes(currentEl) && addedNodes.length > 0 && !currentEl.isConnected) {
|
|
1294
|
+
this.stylePageForTransition(currentEl);
|
|
1295
|
+
currentEl.style.display = "";
|
|
1296
|
+
currentEl.style.visibility = "visible";
|
|
1297
|
+
const anchor = addedNodes[0];
|
|
1298
|
+
this.ignoredNodes.add(currentEl);
|
|
1299
|
+
if (anchor.parentElement === this) {
|
|
1300
|
+
this.insertBefore(currentEl, anchor);
|
|
1301
|
+
} else {
|
|
1302
|
+
this.appendChild(currentEl);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const node of removedNodes) {
|
|
1306
|
+
if (node === currentEl) continue;
|
|
1307
|
+
const state = this.controller.stack.find((pageState) => pageState.element === node);
|
|
1308
|
+
if (state) {
|
|
1309
|
+
this.controller.removePage(state.id);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (const node of addedNodes) {
|
|
1313
|
+
if (node === this.pendingPage) continue;
|
|
1314
|
+
if (this.ignoredNodes.has(node)) {
|
|
1315
|
+
this.ignoredNodes.delete(node);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
this.handleNewPage(node);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Initialize the first page without animation
|
|
1323
|
+
*/
|
|
1324
|
+
initializeFirstPage(page) {
|
|
1325
|
+
page.style.position = "relative";
|
|
1326
|
+
page.style.width = "100%";
|
|
1327
|
+
page.style.height = "100%";
|
|
1328
|
+
const state = this.controller.createPageState(page);
|
|
1329
|
+
state.isActive = true;
|
|
1330
|
+
this.controller.pageStack.push(state);
|
|
1331
|
+
this.swipeBackDepth = 0;
|
|
1332
|
+
this.lastNavigationHref = this.getCurrentNavigationHref();
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Handle a new page being added
|
|
1336
|
+
*/
|
|
1337
|
+
async handleNewPage(page) {
|
|
1338
|
+
const outletDirection = this.dataset.direction;
|
|
1339
|
+
const direction = page.dataset.direction || outletDirection || "forward";
|
|
1340
|
+
if (outletDirection) {
|
|
1341
|
+
delete this.dataset.direction;
|
|
1342
|
+
}
|
|
1343
|
+
const skipTransition = this.skipNextHistoryBackTransition && direction === "back";
|
|
1344
|
+
this.skipNextHistoryBackTransition = false;
|
|
1345
|
+
const hadPageBefore = this.controller.stack.length > 0;
|
|
1346
|
+
this.stylePageForTransition(page);
|
|
1347
|
+
this.pendingPage = page;
|
|
1348
|
+
try {
|
|
1349
|
+
const result = await this.controller.navigate(page, { direction, duration: skipTransition ? 0 : void 0 });
|
|
1350
|
+
if (result.success) {
|
|
1351
|
+
this.recordCompletedNavigation(direction, { hadPageBefore });
|
|
1352
|
+
}
|
|
1353
|
+
} finally {
|
|
1354
|
+
this.pendingPage = null;
|
|
1355
|
+
}
|
|
1356
|
+
if (!this.options.keepInDom) {
|
|
1357
|
+
this.cleanupOldPages();
|
|
1358
|
+
} else {
|
|
1359
|
+
this.enforceCacheLimit();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Clean up pages that are no longer needed
|
|
1364
|
+
*/
|
|
1365
|
+
cleanupOldPages() {
|
|
1366
|
+
const stack = this.controller.stack;
|
|
1367
|
+
const children = Array.from(this.children);
|
|
1368
|
+
for (const child of children) {
|
|
1369
|
+
const inStack = stack.some((s) => s.element === child);
|
|
1370
|
+
if (!inStack && !child.dataset.keepInDom) {
|
|
1371
|
+
child.remove();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Enforce the cache limit
|
|
1377
|
+
*/
|
|
1378
|
+
enforceCacheLimit() {
|
|
1379
|
+
const stack = this.controller.stack;
|
|
1380
|
+
const maxCached = this.options.maxCached || 10;
|
|
1381
|
+
if (stack.length > maxCached) {
|
|
1382
|
+
const toRemove = stack.slice(0, stack.length - maxCached);
|
|
1383
|
+
for (const page of toRemove) {
|
|
1384
|
+
if (!page.isActive) {
|
|
1385
|
+
page.element.remove();
|
|
1386
|
+
this.controller.removePage(page.id);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Programmatic navigation - push a new page
|
|
1393
|
+
*/
|
|
1394
|
+
async push(page, config = {}) {
|
|
1395
|
+
this.stylePageForTransition(page);
|
|
1396
|
+
const hadPageBefore = this.controller.stack.length > 0;
|
|
1397
|
+
this.pendingPage = page;
|
|
1398
|
+
this.appendChild(page);
|
|
1399
|
+
try {
|
|
1400
|
+
const result = await this.controller.push(page, config);
|
|
1401
|
+
if (result.success) {
|
|
1402
|
+
this.recordCompletedNavigation("forward", { hadPageBefore, forceForward: true });
|
|
1403
|
+
}
|
|
1404
|
+
} finally {
|
|
1405
|
+
this.pendingPage = null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Programmatic navigation - pop current page
|
|
1410
|
+
*/
|
|
1411
|
+
async pop(config = {}) {
|
|
1412
|
+
const result = await this.controller.pop(config);
|
|
1413
|
+
if (result.success) {
|
|
1414
|
+
this.recordCompletedNavigation("back", { hadPageBefore: true });
|
|
1415
|
+
if (!this.options.keepInDom) {
|
|
1416
|
+
const children = Array.from(this.children);
|
|
1417
|
+
const lastChild = children[children.length - 1];
|
|
1418
|
+
if (lastChild) {
|
|
1419
|
+
lastChild.remove();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Programmatic navigation - set root page
|
|
1426
|
+
*/
|
|
1427
|
+
async setRoot(page, config = {}) {
|
|
1428
|
+
const oldChildren = Array.from(this.children);
|
|
1429
|
+
this.stylePageForTransition(page);
|
|
1430
|
+
this.pendingPage = page;
|
|
1431
|
+
this.appendChild(page);
|
|
1432
|
+
try {
|
|
1433
|
+
const result = await this.controller.setRoot(page, config);
|
|
1434
|
+
if (result.success) {
|
|
1435
|
+
this.recordCompletedNavigation("root", { hadPageBefore: true });
|
|
1436
|
+
}
|
|
1437
|
+
} finally {
|
|
1438
|
+
this.pendingPage = null;
|
|
1439
|
+
}
|
|
1440
|
+
for (const child of oldChildren) {
|
|
1441
|
+
child.remove();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Get the current page stack length
|
|
1446
|
+
*/
|
|
1447
|
+
get stackLength() {
|
|
1448
|
+
return this.controller.stack.length;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Check if we can go back
|
|
1452
|
+
*/
|
|
1453
|
+
get canGoBack() {
|
|
1454
|
+
return this.controller.stack.length > 1 && this.swipeBackDepth > 0;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Get whether edge swipe-back gesture is enabled.
|
|
1458
|
+
*/
|
|
1459
|
+
get swipeGesture() {
|
|
1460
|
+
return this.options.swipeGesture ?? "auto";
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Enable, disable, or auto-detect edge swipe-back gesture.
|
|
1464
|
+
*/
|
|
1465
|
+
set swipeGesture(value) {
|
|
1466
|
+
this.setSwipeGesture(value);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Enable, disable, or auto-detect edge swipe-back gesture.
|
|
1470
|
+
*/
|
|
1471
|
+
setSwipeGesture(value) {
|
|
1472
|
+
this.options.swipeGesture = value;
|
|
1473
|
+
const serialized = this.serializeSwipeGesture(value);
|
|
1474
|
+
if (this.getAttribute("swipe-gesture") !== serialized) {
|
|
1475
|
+
this.setAttribute("swipe-gesture", serialized);
|
|
1476
|
+
} else {
|
|
1477
|
+
this.updateSwipeGestureListeners();
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Get the transition controller for advanced usage
|
|
1482
|
+
*/
|
|
1483
|
+
getController() {
|
|
1484
|
+
return this.controller;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Apply layout styles required for transition animations.
|
|
1488
|
+
*/
|
|
1489
|
+
stylePageForTransition(page) {
|
|
1490
|
+
page.style.position = "absolute";
|
|
1491
|
+
page.style.top = "0";
|
|
1492
|
+
page.style.left = "0";
|
|
1493
|
+
page.style.width = "100%";
|
|
1494
|
+
page.style.height = "100%";
|
|
1495
|
+
}
|
|
1496
|
+
parseSwipeGestureAttribute(value) {
|
|
1497
|
+
if (value === null || value === "auto") {
|
|
1498
|
+
return "auto";
|
|
1499
|
+
}
|
|
1500
|
+
if (value === "false") {
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
serializeSwipeGesture(value) {
|
|
1506
|
+
return typeof value === "boolean" ? String(value) : value;
|
|
1507
|
+
}
|
|
1508
|
+
updateSwipeGestureListeners() {
|
|
1509
|
+
if (this.options.swipeGesture === false) {
|
|
1510
|
+
this.removeSwipeGestureListeners();
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (this.swipeGestureListenersActive || typeof PointerEvent === "undefined") {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
this.addEventListener("pointerdown", this.handleSwipeGesturePointerDown);
|
|
1517
|
+
this.addEventListener("pointermove", this.handleSwipeGesturePointerMove, { passive: false });
|
|
1518
|
+
this.addEventListener("pointerup", this.handleSwipeGesturePointerEnd);
|
|
1519
|
+
this.addEventListener("pointercancel", this.handleSwipeGesturePointerCancel);
|
|
1520
|
+
this.swipeGestureListenersActive = true;
|
|
1521
|
+
}
|
|
1522
|
+
removeSwipeGestureListeners() {
|
|
1523
|
+
if (!this.swipeGestureListenersActive) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
this.removeEventListener("pointerdown", this.handleSwipeGesturePointerDown);
|
|
1527
|
+
this.removeEventListener("pointermove", this.handleSwipeGesturePointerMove);
|
|
1528
|
+
this.removeEventListener("pointerup", this.handleSwipeGesturePointerEnd);
|
|
1529
|
+
this.removeEventListener("pointercancel", this.handleSwipeGesturePointerCancel);
|
|
1530
|
+
this.swipeGestureListenersActive = false;
|
|
1531
|
+
this.swipeGesturePointer = null;
|
|
1532
|
+
}
|
|
1533
|
+
isSwipeGestureEnabled() {
|
|
1534
|
+
const option = this.options.swipeGesture ?? "auto";
|
|
1535
|
+
if (option === true) {
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
if (option === false) {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
return isNativeSwipeGesturePlatform();
|
|
1542
|
+
}
|
|
1543
|
+
getCurrentNavigationHref() {
|
|
1544
|
+
return this.ownerDocument.defaultView?.location.href ?? null;
|
|
1545
|
+
}
|
|
1546
|
+
recordCompletedNavigation(direction, options) {
|
|
1547
|
+
const currentHref = this.getCurrentNavigationHref();
|
|
1548
|
+
if (!options.hadPageBefore || direction === "root") {
|
|
1549
|
+
this.swipeBackDepth = 0;
|
|
1550
|
+
this.lastNavigationHref = currentHref;
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (direction === "back") {
|
|
1554
|
+
this.swipeBackDepth = Math.max(0, this.swipeBackDepth - 1);
|
|
1555
|
+
this.lastNavigationHref = currentHref;
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (direction === "forward" || direction === "none") {
|
|
1559
|
+
const hrefChanged = currentHref === null || this.lastNavigationHref === null || currentHref !== this.lastNavigationHref;
|
|
1560
|
+
if (options.forceForward || hrefChanged) {
|
|
1561
|
+
this.swipeBackDepth += 1;
|
|
1562
|
+
}
|
|
1563
|
+
this.lastNavigationHref = currentHref;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
this.lastNavigationHref = currentHref;
|
|
1567
|
+
}
|
|
1568
|
+
canStartSwipeGesture(event) {
|
|
1569
|
+
if (!this.isSwipeGestureEnabled() || this.controller.animating || this.pendingPage || !this.canGoBack) {
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
if (!event.isPrimary || event.pointerType === "mouse" && event.button !== 0) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
if (this.isInteractiveSwipeTarget(event.target) || this.hasScrollableInlineAncestor(event.target)) {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
const rect = this.getBoundingClientRect();
|
|
1579
|
+
const startX = event.clientX - rect.left;
|
|
1580
|
+
if (event.clientY < rect.top || event.clientY > rect.bottom) {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
return this.isRTL() ? startX >= rect.width - this.swipeGestureEdgeWidth : startX <= this.swipeGestureEdgeWidth;
|
|
1584
|
+
}
|
|
1585
|
+
isRTL() {
|
|
1586
|
+
const doc = this.ownerDocument;
|
|
1587
|
+
return doc.dir === "rtl" || doc.documentElement.dir === "rtl" || getComputedStyle(this).direction === "rtl";
|
|
1588
|
+
}
|
|
1589
|
+
getSwipeGestureDeltaX(pointer) {
|
|
1590
|
+
const deltaX = pointer.currentX - pointer.startX;
|
|
1591
|
+
return this.isRTL() ? -deltaX : deltaX;
|
|
1592
|
+
}
|
|
1593
|
+
isInteractiveSwipeTarget(target) {
|
|
1594
|
+
if (!(target instanceof Element)) {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
return Boolean(
|
|
1598
|
+
target.closest(
|
|
1599
|
+
'a, button, input, textarea, select, option, [contenteditable="true"], [data-swipe-gesture-ignore], [data-swipe-back-ignore]'
|
|
1600
|
+
)
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
hasScrollableInlineAncestor(target) {
|
|
1604
|
+
let element = target instanceof Element ? target : null;
|
|
1605
|
+
while (element && element !== this) {
|
|
1606
|
+
if (element instanceof HTMLElement) {
|
|
1607
|
+
const style = getComputedStyle(element);
|
|
1608
|
+
const canScrollInline = /(auto|scroll)/.test(style.overflowX) && element.scrollWidth > element.clientWidth + 1;
|
|
1609
|
+
if (canScrollInline && element.scrollLeft > 0) {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
element = element.parentElement;
|
|
1614
|
+
}
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
handleSwipeGesturePointerDown = (event) => {
|
|
1618
|
+
if (!this.canStartSwipeGesture(event)) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
this.swipeGesturePointer = {
|
|
1622
|
+
pointerId: event.pointerId,
|
|
1623
|
+
startX: event.clientX,
|
|
1624
|
+
startY: event.clientY,
|
|
1625
|
+
currentX: event.clientX,
|
|
1626
|
+
currentY: event.clientY,
|
|
1627
|
+
startTime: performance.now(),
|
|
1628
|
+
dragging: false,
|
|
1629
|
+
transitionStarted: false
|
|
1630
|
+
};
|
|
1631
|
+
try {
|
|
1632
|
+
this.setPointerCapture(event.pointerId);
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
handleSwipeGesturePointerMove = (event) => {
|
|
1637
|
+
const pointer = this.swipeGesturePointer;
|
|
1638
|
+
if (!pointer || pointer.pointerId !== event.pointerId) {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
pointer.currentX = event.clientX;
|
|
1642
|
+
pointer.currentY = event.clientY;
|
|
1643
|
+
const deltaX = this.getSwipeGestureDeltaX(pointer);
|
|
1644
|
+
const deltaY = pointer.currentY - pointer.startY;
|
|
1645
|
+
const absX = Math.abs(deltaX);
|
|
1646
|
+
const absY = Math.abs(deltaY);
|
|
1647
|
+
if (!pointer.dragging && absY > 12 && absY > absX) {
|
|
1648
|
+
this.cancelSwipeGesturePointer(event.pointerId);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (deltaX < -this.swipeGestureThreshold) {
|
|
1652
|
+
this.cancelSwipeGesture(event.pointerId);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (!pointer.dragging && deltaX > this.swipeGestureThreshold && absX > absY) {
|
|
1656
|
+
pointer.dragging = true;
|
|
1657
|
+
pointer.transitionStarted = this.controller.beginInteractiveBack({ direction: "back" });
|
|
1658
|
+
if (!pointer.transitionStarted) {
|
|
1659
|
+
this.cancelSwipeGesturePointer(event.pointerId);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (pointer.dragging && pointer.transitionStarted) {
|
|
1664
|
+
if (event.cancelable) event.preventDefault();
|
|
1665
|
+
const width = Math.max(this.getBoundingClientRect().width, 1);
|
|
1666
|
+
this.controller.stepInteractiveBack(deltaX / width);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
handleSwipeGesturePointerEnd = (event) => {
|
|
1670
|
+
const pointer = this.swipeGesturePointer;
|
|
1671
|
+
if (!pointer || pointer.pointerId !== event.pointerId) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
pointer.currentX = event.clientX;
|
|
1675
|
+
pointer.currentY = event.clientY;
|
|
1676
|
+
const deltaX = this.getSwipeGestureDeltaX(pointer);
|
|
1677
|
+
const elapsed = Math.max(performance.now() - pointer.startTime, 1);
|
|
1678
|
+
const velocityX = deltaX / elapsed;
|
|
1679
|
+
const width = Math.max(this.getBoundingClientRect().width, 1);
|
|
1680
|
+
const step = deltaX / width;
|
|
1681
|
+
const shouldCommit = pointer.dragging && pointer.transitionStarted && velocityX >= 0 && (velocityX > this.swipeGestureMinimumVelocity || deltaX > width / 2);
|
|
1682
|
+
const missing = shouldCommit ? 1 - step : step;
|
|
1683
|
+
const missingDistance = Math.max(missing, 0) * width;
|
|
1684
|
+
const releaseDuration = missingDistance > 5 && Math.abs(velocityX) > 0 ? Math.min(missingDistance / Math.abs(velocityX), 540) : 0;
|
|
1685
|
+
this.releaseSwipeGesturePointer(event.pointerId);
|
|
1686
|
+
void this.finishSwipeGestureBack(shouldCommit, releaseDuration);
|
|
1687
|
+
};
|
|
1688
|
+
handleSwipeGesturePointerCancel = (event) => {
|
|
1689
|
+
this.cancelSwipeGesture(event.pointerId);
|
|
1690
|
+
};
|
|
1691
|
+
cancelSwipeGesturePointer(pointerId) {
|
|
1692
|
+
this.releaseSwipeGesturePointer(pointerId);
|
|
1693
|
+
}
|
|
1694
|
+
releaseSwipeGesturePointer(pointerId) {
|
|
1695
|
+
if (this.swipeGesturePointer?.pointerId !== pointerId) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
try {
|
|
1699
|
+
this.releasePointerCapture(pointerId);
|
|
1700
|
+
} catch {
|
|
1701
|
+
}
|
|
1702
|
+
this.swipeGesturePointer = null;
|
|
1703
|
+
}
|
|
1704
|
+
cancelSwipeGesture(pointerId) {
|
|
1705
|
+
const pointer = this.swipeGesturePointer;
|
|
1706
|
+
if (!pointer || pointer.pointerId !== pointerId) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
this.releaseSwipeGesturePointer(pointerId);
|
|
1710
|
+
if (pointer.transitionStarted) {
|
|
1711
|
+
void this.finishSwipeGestureBack(false, 0);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
async finishSwipeGestureBack(shouldComplete, releaseDuration) {
|
|
1715
|
+
const shouldUseHistory = shouldComplete && typeof window !== "undefined" && window.history.length > 1;
|
|
1716
|
+
await this.controller.endInteractiveBack(shouldComplete, releaseDuration, !shouldUseHistory);
|
|
1717
|
+
if (!shouldComplete) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
if (shouldUseHistory) {
|
|
1721
|
+
this.skipNextHistoryBackTransition = true;
|
|
1722
|
+
this.dataset.direction = "back";
|
|
1723
|
+
window.history.back();
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (!this.options.keepInDom) {
|
|
1727
|
+
this.cleanupOldPages();
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
if (typeof customElements !== "undefined" && !customElements.get("cap-router-outlet")) {
|
|
1732
|
+
customElements.define("cap-router-outlet", CapRouterOutlet);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/components/cap-page.ts
|
|
1736
|
+
var CapPage = class extends HTMLElement {
|
|
1737
|
+
_lifecycle = {};
|
|
1738
|
+
_isActive = false;
|
|
1739
|
+
static get observedAttributes() {
|
|
1740
|
+
return ["key", "cache-scroll"];
|
|
1741
|
+
}
|
|
1742
|
+
constructor() {
|
|
1743
|
+
super();
|
|
1744
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
1745
|
+
shadow.innerHTML = `
|
|
1746
|
+
<style>
|
|
1747
|
+
:host {
|
|
1748
|
+
display: flex;
|
|
1749
|
+
flex-direction: column;
|
|
1750
|
+
width: 100%;
|
|
1751
|
+
height: 100%;
|
|
1752
|
+
position: relative;
|
|
1753
|
+
overflow: hidden;
|
|
1754
|
+
background: var(--cap-page-background, Canvas);
|
|
1755
|
+
color: var(--cap-page-color, CanvasText);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
:host(.cap-transition-active) {
|
|
1759
|
+
overflow: visible;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.header-container {
|
|
1763
|
+
flex-shrink: 0;
|
|
1764
|
+
position: relative;
|
|
1765
|
+
z-index: 10;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
.content-container {
|
|
1769
|
+
flex: 1;
|
|
1770
|
+
position: relative;
|
|
1771
|
+
overflow: hidden;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
.footer-container {
|
|
1775
|
+
flex-shrink: 0;
|
|
1776
|
+
position: relative;
|
|
1777
|
+
z-index: 10;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
::slotted([slot="header"]),
|
|
1781
|
+
::slotted([data-cap-header]) {
|
|
1782
|
+
display: block;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
::slotted([slot="content"]),
|
|
1786
|
+
::slotted([data-cap-content]) {
|
|
1787
|
+
display: block;
|
|
1788
|
+
height: 100%;
|
|
1789
|
+
overflow: auto;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
::slotted([slot="footer"]),
|
|
1793
|
+
::slotted([data-cap-footer]) {
|
|
1794
|
+
display: block;
|
|
1795
|
+
}
|
|
1796
|
+
</style>
|
|
1797
|
+
|
|
1798
|
+
<div class="header-container" part="header">
|
|
1799
|
+
<slot name="header"></slot>
|
|
1800
|
+
</div>
|
|
1801
|
+
|
|
1802
|
+
<div class="content-container" part="content">
|
|
1803
|
+
<slot name="content"></slot>
|
|
1804
|
+
<slot></slot>
|
|
1805
|
+
</div>
|
|
1806
|
+
|
|
1807
|
+
<div class="footer-container" part="footer">
|
|
1808
|
+
<slot name="footer"></slot>
|
|
1809
|
+
</div>
|
|
1810
|
+
`;
|
|
1811
|
+
}
|
|
1812
|
+
connectedCallback() {
|
|
1813
|
+
this.markTransitionElements();
|
|
1814
|
+
this.dispatchEvent(
|
|
1815
|
+
new CustomEvent("cap-page-connected", {
|
|
1816
|
+
bubbles: true,
|
|
1817
|
+
composed: true,
|
|
1818
|
+
detail: { page: this }
|
|
1819
|
+
})
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
disconnectedCallback() {
|
|
1823
|
+
this.dispatchEvent(
|
|
1824
|
+
new CustomEvent("cap-page-disconnected", {
|
|
1825
|
+
bubbles: true,
|
|
1826
|
+
composed: true,
|
|
1827
|
+
detail: { page: this }
|
|
1828
|
+
})
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Mark child elements for transition controller
|
|
1833
|
+
*/
|
|
1834
|
+
markTransitionElements() {
|
|
1835
|
+
const header = this.querySelector('[slot="header"]');
|
|
1836
|
+
if (header) {
|
|
1837
|
+
header.setAttribute("data-cap-header", "");
|
|
1838
|
+
}
|
|
1839
|
+
const content = this.querySelector('[slot="content"]');
|
|
1840
|
+
if (content) {
|
|
1841
|
+
content.setAttribute("data-cap-content", "");
|
|
1842
|
+
}
|
|
1843
|
+
const footer = this.querySelector('[slot="footer"]');
|
|
1844
|
+
if (footer) {
|
|
1845
|
+
footer.setAttribute("data-cap-footer", "");
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Set lifecycle callbacks
|
|
1850
|
+
*/
|
|
1851
|
+
set lifecycle(callbacks) {
|
|
1852
|
+
this._lifecycle = callbacks;
|
|
1853
|
+
}
|
|
1854
|
+
get lifecycle() {
|
|
1855
|
+
return this._lifecycle;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Check if page is active
|
|
1859
|
+
*/
|
|
1860
|
+
get isActive() {
|
|
1861
|
+
return this._isActive;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Called when page will enter view
|
|
1865
|
+
*/
|
|
1866
|
+
async willEnter(event) {
|
|
1867
|
+
await this._lifecycle.onWillEnter?.(event);
|
|
1868
|
+
this.dispatchEvent(
|
|
1869
|
+
new CustomEvent("cap-will-enter", {
|
|
1870
|
+
bubbles: true,
|
|
1871
|
+
detail: event
|
|
1872
|
+
})
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Called when page has entered view
|
|
1877
|
+
*/
|
|
1878
|
+
async didEnter(event) {
|
|
1879
|
+
this._isActive = true;
|
|
1880
|
+
await this._lifecycle.onDidEnter?.(event);
|
|
1881
|
+
this.dispatchEvent(
|
|
1882
|
+
new CustomEvent("cap-did-enter", {
|
|
1883
|
+
bubbles: true,
|
|
1884
|
+
detail: event
|
|
1885
|
+
})
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Called when page will leave view
|
|
1890
|
+
*/
|
|
1891
|
+
async willLeave(event) {
|
|
1892
|
+
await this._lifecycle.onWillLeave?.(event);
|
|
1893
|
+
this.dispatchEvent(
|
|
1894
|
+
new CustomEvent("cap-will-leave", {
|
|
1895
|
+
bubbles: true,
|
|
1896
|
+
detail: event
|
|
1897
|
+
})
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Called when page has left view
|
|
1902
|
+
*/
|
|
1903
|
+
async didLeave(event) {
|
|
1904
|
+
this._isActive = false;
|
|
1905
|
+
await this._lifecycle.onDidLeave?.(event);
|
|
1906
|
+
this.dispatchEvent(
|
|
1907
|
+
new CustomEvent("cap-did-leave", {
|
|
1908
|
+
bubbles: true,
|
|
1909
|
+
detail: event
|
|
1910
|
+
})
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Get the header element
|
|
1915
|
+
*/
|
|
1916
|
+
get headerElement() {
|
|
1917
|
+
return this.querySelector('[slot="header"], [data-cap-header]');
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Get the content element
|
|
1921
|
+
*/
|
|
1922
|
+
get contentElement() {
|
|
1923
|
+
return this.querySelector('[slot="content"], [data-cap-content]');
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Get the footer element
|
|
1927
|
+
*/
|
|
1928
|
+
get footerElement() {
|
|
1929
|
+
return this.querySelector('[slot="footer"], [data-cap-footer]');
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Save scroll position
|
|
1933
|
+
*/
|
|
1934
|
+
saveScrollPosition() {
|
|
1935
|
+
const content = this.contentElement;
|
|
1936
|
+
if (!content) return null;
|
|
1937
|
+
return {
|
|
1938
|
+
x: content.scrollLeft,
|
|
1939
|
+
y: content.scrollTop
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Restore scroll position
|
|
1944
|
+
*/
|
|
1945
|
+
restoreScrollPosition(position) {
|
|
1946
|
+
const content = this.contentElement;
|
|
1947
|
+
if (!content) return;
|
|
1948
|
+
content.scrollLeft = position.x;
|
|
1949
|
+
content.scrollTop = position.y;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
if (typeof customElements !== "undefined" && !customElements.get("cap-page")) {
|
|
1953
|
+
customElements.define("cap-page", CapPage);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/components/cap-header.ts
|
|
1957
|
+
var CapHeader = class extends HTMLElement {
|
|
1958
|
+
static get observedAttributes() {
|
|
1959
|
+
return ["translucent", "collapse"];
|
|
1960
|
+
}
|
|
1961
|
+
constructor() {
|
|
1962
|
+
super();
|
|
1963
|
+
}
|
|
1964
|
+
connectedCallback() {
|
|
1965
|
+
this.style.display = "block";
|
|
1966
|
+
this.style.position = "relative";
|
|
1967
|
+
this.style.zIndex = "10";
|
|
1968
|
+
this.setAttribute("data-cap-header", "");
|
|
1969
|
+
if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
|
|
1970
|
+
this.setAttribute("slot", "header");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1974
|
+
switch (name) {
|
|
1975
|
+
case "translucent":
|
|
1976
|
+
this.dataset.translucent = newValue !== null ? "true" : "false";
|
|
1977
|
+
break;
|
|
1978
|
+
case "collapse":
|
|
1979
|
+
this.dataset.collapse = newValue;
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Get the current height of the header
|
|
1985
|
+
*/
|
|
1986
|
+
get height() {
|
|
1987
|
+
return this.offsetHeight;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
if (typeof customElements !== "undefined" && !customElements.get("cap-header")) {
|
|
1991
|
+
customElements.define("cap-header", CapHeader);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/components/cap-content.ts
|
|
1995
|
+
var CapContent = class extends HTMLElement {
|
|
1996
|
+
_scrollPosition = { x: 0, y: 0 };
|
|
1997
|
+
static get observedAttributes() {
|
|
1998
|
+
return ["fullscreen", "scroll-x", "scroll-y"];
|
|
1999
|
+
}
|
|
2000
|
+
constructor() {
|
|
2001
|
+
super();
|
|
2002
|
+
}
|
|
2003
|
+
connectedCallback() {
|
|
2004
|
+
this.style.display = "block";
|
|
2005
|
+
this.style.position = "relative";
|
|
2006
|
+
this.style.flex = "1";
|
|
2007
|
+
this.style.overflow = "auto";
|
|
2008
|
+
this.style.overscrollBehavior = "contain";
|
|
2009
|
+
this.style.webkitOverflowScrolling = "touch";
|
|
2010
|
+
this.setAttribute("data-cap-content", "");
|
|
2011
|
+
if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
|
|
2012
|
+
this.setAttribute("slot", "content");
|
|
2013
|
+
}
|
|
2014
|
+
this.addEventListener("scroll", this.handleScroll.bind(this), { passive: true });
|
|
2015
|
+
}
|
|
2016
|
+
disconnectedCallback() {
|
|
2017
|
+
this.removeEventListener("scroll", this.handleScroll.bind(this));
|
|
2018
|
+
}
|
|
2019
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
2020
|
+
switch (name) {
|
|
2021
|
+
case "fullscreen":
|
|
2022
|
+
this.dataset.fullscreen = newValue !== null ? "true" : "false";
|
|
2023
|
+
break;
|
|
2024
|
+
case "scroll-x":
|
|
2025
|
+
this.style.overflowX = newValue === "false" ? "hidden" : "auto";
|
|
2026
|
+
break;
|
|
2027
|
+
case "scroll-y":
|
|
2028
|
+
this.style.overflowY = newValue === "false" ? "hidden" : "auto";
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Handle scroll events
|
|
2034
|
+
*/
|
|
2035
|
+
handleScroll() {
|
|
2036
|
+
this._scrollPosition = {
|
|
2037
|
+
x: this.scrollLeft,
|
|
2038
|
+
y: this.scrollTop
|
|
2039
|
+
};
|
|
2040
|
+
this.dispatchEvent(
|
|
2041
|
+
new CustomEvent("cap-scroll", {
|
|
2042
|
+
bubbles: true,
|
|
2043
|
+
detail: this._scrollPosition
|
|
2044
|
+
})
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get current scroll position
|
|
2049
|
+
*/
|
|
2050
|
+
get scrollPosition() {
|
|
2051
|
+
return { ...this._scrollPosition };
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Save current scroll position
|
|
2055
|
+
*/
|
|
2056
|
+
saveScrollPosition() {
|
|
2057
|
+
this._scrollPosition = {
|
|
2058
|
+
x: this.scrollLeft,
|
|
2059
|
+
y: this.scrollTop
|
|
2060
|
+
};
|
|
2061
|
+
return { ...this._scrollPosition };
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Restore scroll position
|
|
2065
|
+
*/
|
|
2066
|
+
restoreScrollPosition(position) {
|
|
2067
|
+
const pos = position || this._scrollPosition;
|
|
2068
|
+
this.scrollLeft = pos.x;
|
|
2069
|
+
this.scrollTop = pos.y;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Scroll to top
|
|
2073
|
+
*/
|
|
2074
|
+
scrollToTop(smooth = true) {
|
|
2075
|
+
this.scrollTo({
|
|
2076
|
+
top: 0,
|
|
2077
|
+
behavior: smooth ? "smooth" : "instant"
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Scroll to bottom
|
|
2082
|
+
*/
|
|
2083
|
+
scrollToBottom(smooth = true) {
|
|
2084
|
+
this.scrollTo({
|
|
2085
|
+
top: this.scrollHeight,
|
|
2086
|
+
behavior: smooth ? "smooth" : "instant"
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Scroll to element
|
|
2091
|
+
*/
|
|
2092
|
+
scrollToElement(element, smooth = true) {
|
|
2093
|
+
element.scrollIntoView({
|
|
2094
|
+
behavior: smooth ? "smooth" : "instant",
|
|
2095
|
+
block: "start"
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
if (typeof customElements !== "undefined" && !customElements.get("cap-content")) {
|
|
2100
|
+
customElements.define("cap-content", CapContent);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/components/cap-footer.ts
|
|
2104
|
+
var CapFooter = class extends HTMLElement {
|
|
2105
|
+
static get observedAttributes() {
|
|
2106
|
+
return ["translucent"];
|
|
2107
|
+
}
|
|
2108
|
+
constructor() {
|
|
2109
|
+
super();
|
|
2110
|
+
}
|
|
2111
|
+
connectedCallback() {
|
|
2112
|
+
this.style.display = "block";
|
|
2113
|
+
this.style.position = "relative";
|
|
2114
|
+
this.style.zIndex = "10";
|
|
2115
|
+
this.setAttribute("data-cap-footer", "");
|
|
2116
|
+
if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
|
|
2117
|
+
this.setAttribute("slot", "footer");
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
2121
|
+
switch (name) {
|
|
2122
|
+
case "translucent":
|
|
2123
|
+
this.dataset.translucent = newValue !== null ? "true" : "false";
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Get the current height of the footer
|
|
2129
|
+
*/
|
|
2130
|
+
get height() {
|
|
2131
|
+
return this.offsetHeight;
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
if (typeof customElements !== "undefined" && !customElements.get("cap-footer")) {
|
|
2135
|
+
customElements.define("cap-footer", CapFooter);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/react/index.ts
|
|
2139
|
+
var globalController = null;
|
|
2140
|
+
var globalDirection = "forward";
|
|
2141
|
+
function initTransitions(config = {}) {
|
|
2142
|
+
globalController = createTransitionController(config);
|
|
2143
|
+
return globalController;
|
|
2144
|
+
}
|
|
2145
|
+
function getController() {
|
|
2146
|
+
if (!globalController) {
|
|
2147
|
+
globalController = createTransitionController();
|
|
2148
|
+
}
|
|
2149
|
+
return globalController;
|
|
2150
|
+
}
|
|
2151
|
+
function getDirection() {
|
|
2152
|
+
return globalDirection;
|
|
2153
|
+
}
|
|
2154
|
+
function setDirection(direction) {
|
|
2155
|
+
globalDirection = direction;
|
|
2156
|
+
if (typeof document !== "undefined") {
|
|
2157
|
+
for (const outlet of document.querySelectorAll("cap-router-outlet")) {
|
|
2158
|
+
outlet.dataset.direction = direction;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function setupRouterOutlet(element, options = {}) {
|
|
2163
|
+
const { keepInDom = true, maxCached = 10, platform = "auto", duration, swipeGesture } = options;
|
|
2164
|
+
element.setAttribute("platform", platform);
|
|
2165
|
+
if (duration !== void 0) element.setAttribute("duration", String(duration));
|
|
2166
|
+
element.setAttribute("keep-in-dom", String(keepInDom));
|
|
2167
|
+
element.setAttribute("max-cached", String(maxCached));
|
|
2168
|
+
if (swipeGesture !== void 0) element.setAttribute("swipe-gesture", String(swipeGesture));
|
|
2169
|
+
}
|
|
2170
|
+
function setupPage(element, callbacks) {
|
|
2171
|
+
const handleWillEnter = (e) => {
|
|
2172
|
+
callbacks?.onWillEnter?.(e.detail);
|
|
2173
|
+
};
|
|
2174
|
+
const handleDidEnter = (e) => {
|
|
2175
|
+
callbacks?.onDidEnter?.(e.detail);
|
|
2176
|
+
};
|
|
2177
|
+
const handleWillLeave = (e) => {
|
|
2178
|
+
callbacks?.onWillLeave?.(e.detail);
|
|
2179
|
+
};
|
|
2180
|
+
const handleDidLeave = (e) => {
|
|
2181
|
+
callbacks?.onDidLeave?.(e.detail);
|
|
2182
|
+
};
|
|
2183
|
+
element.addEventListener("cap-will-enter", handleWillEnter);
|
|
2184
|
+
element.addEventListener("cap-did-enter", handleDidEnter);
|
|
2185
|
+
element.addEventListener("cap-will-leave", handleWillLeave);
|
|
2186
|
+
element.addEventListener("cap-did-leave", handleDidLeave);
|
|
2187
|
+
return () => {
|
|
2188
|
+
element.removeEventListener("cap-will-enter", handleWillEnter);
|
|
2189
|
+
element.removeEventListener("cap-did-enter", handleDidEnter);
|
|
2190
|
+
element.removeEventListener("cap-will-leave", handleWillLeave);
|
|
2191
|
+
element.removeEventListener("cap-did-leave", handleDidLeave);
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
function createTransitionNavigate(navigate) {
|
|
2195
|
+
return (to, direction = "forward") => {
|
|
2196
|
+
setDirection(direction);
|
|
2197
|
+
navigate(to);
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
export {
|
|
2201
|
+
TransitionController,
|
|
2202
|
+
createTransitionNavigate,
|
|
2203
|
+
getController,
|
|
2204
|
+
getDirection,
|
|
2205
|
+
initTransitions,
|
|
2206
|
+
setDirection,
|
|
2207
|
+
setupPage,
|
|
2208
|
+
setupRouterOutlet
|
|
2209
|
+
};
|
|
2210
|
+
//# sourceMappingURL=index.mjs.map
|