zumly 0.92.5 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -18
- package/dist/zumly.cjs +2 -2
- package/dist/zumly.css +15 -0
- package/dist/zumly.js +2 -2
- package/dist/zumly.min.css +1 -1
- package/dist/zumly.mjs +2 -2
- package/package.json +9 -5
- package/src/drivers/anime-transition.js +5 -5
- package/src/drivers/css-transition.js +82 -23
- package/src/drivers/driver-helpers.js +21 -6
- package/src/drivers/motion-transition.js +5 -5
- package/src/drivers/transform-matrix.js +34 -0
- package/src/view-lifecycle.js +70 -0
- package/src/view-visibility.js +43 -0
- package/types/zumly.d.ts +13 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { showViewContent } from '../view-visibility.js'
|
|
2
|
+
import { disposeView } from '../view-lifecycle.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Shared helpers for Zumly transition drivers.
|
|
@@ -129,11 +130,15 @@ export function applyZoomOutLastState (element, backwardState) {
|
|
|
129
130
|
* @param {HTMLElement} canvas - The canvas container
|
|
130
131
|
*/
|
|
131
132
|
export function removeViewFromCanvas (element, canvas) {
|
|
133
|
+
disposeView(element)
|
|
132
134
|
try {
|
|
133
135
|
if (canvas) canvas.removeChild(element)
|
|
134
136
|
} catch (e) {
|
|
135
137
|
try {
|
|
136
|
-
if (element
|
|
138
|
+
if (element?.parentElement && canvas) {
|
|
139
|
+
disposeView(element.parentElement)
|
|
140
|
+
canvas.removeChild(element.parentElement)
|
|
141
|
+
}
|
|
137
142
|
} catch (e2) {
|
|
138
143
|
// Element already removed or re-parented — safe to ignore.
|
|
139
144
|
}
|
|
@@ -211,10 +216,10 @@ export const SAFETY_BUFFER_MS = 150
|
|
|
211
216
|
*
|
|
212
217
|
* @param {function} cleanup - The actual cleanup + onComplete work
|
|
213
218
|
* @param {number} timeoutMs - Safety timeout duration
|
|
214
|
-
* @returns {{ finish: function, safetyTimer: number }}
|
|
219
|
+
* @returns {{ finish: function, extend: function, safetyTimer: number }}
|
|
215
220
|
*
|
|
216
221
|
* @example
|
|
217
|
-
* const { finish,
|
|
222
|
+
* const { finish, extend } = createFinishGuard(() => {
|
|
218
223
|
* cancelAnimations()
|
|
219
224
|
* applyFinalState()
|
|
220
225
|
* onComplete()
|
|
@@ -224,10 +229,13 @@ export const SAFETY_BUFFER_MS = 150
|
|
|
224
229
|
* animation.onfinish = finish
|
|
225
230
|
*
|
|
226
231
|
* // The safetyTimer ensures finish() runs even if onfinish never fires.
|
|
232
|
+
* // Call extend(ms) when the animation actually starts to re-arm the
|
|
233
|
+
* // deadline from that moment (styles flush on the next render frame,
|
|
234
|
+
* // which can lag far behind the JS call on heavy views).
|
|
227
235
|
*/
|
|
228
236
|
export function createFinishGuard (cleanup, timeoutMs) {
|
|
229
237
|
let completed = false
|
|
230
|
-
|
|
238
|
+
let safetyTimer = setTimeout(() => {
|
|
231
239
|
if (!completed) { completed = true; cleanup() }
|
|
232
240
|
}, timeoutMs)
|
|
233
241
|
|
|
@@ -238,6 +246,13 @@ export function createFinishGuard (cleanup, timeoutMs) {
|
|
|
238
246
|
clearTimeout(safetyTimer)
|
|
239
247
|
cleanup()
|
|
240
248
|
},
|
|
249
|
+
extend (ms) {
|
|
250
|
+
if (completed) return
|
|
251
|
+
clearTimeout(safetyTimer)
|
|
252
|
+
safetyTimer = setTimeout(() => {
|
|
253
|
+
if (!completed) { completed = true; cleanup() }
|
|
254
|
+
}, ms)
|
|
255
|
+
},
|
|
241
256
|
safetyTimer
|
|
242
257
|
}
|
|
243
258
|
}
|
|
@@ -263,10 +278,10 @@ export function parseMatrixString (mStr) {
|
|
|
263
278
|
)
|
|
264
279
|
if (!m) return identityMatrix()
|
|
265
280
|
return {
|
|
266
|
-
a: parseFloat(m[1])
|
|
281
|
+
a: parseFloat(m[1]),
|
|
267
282
|
b: parseFloat(m[2]) || 0,
|
|
268
283
|
c: parseFloat(m[3]) || 0,
|
|
269
|
-
d: parseFloat(m[4])
|
|
284
|
+
d: parseFloat(m[4]),
|
|
270
285
|
e: parseFloat(m[5]) || 0,
|
|
271
286
|
f: parseFloat(m[6]) || 0,
|
|
272
287
|
}
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
applyZoomOutLastState,
|
|
16
16
|
removeViewFromCanvas,
|
|
17
17
|
runLateralInstant,
|
|
18
|
-
readComputedMatrix,
|
|
19
18
|
interpolateMatrix,
|
|
20
19
|
matrixToString,
|
|
21
20
|
} from './driver-helpers.js'
|
|
21
|
+
import { readTransitionMatrix } from './transform-matrix.js'
|
|
22
22
|
|
|
23
23
|
export function runTransition (spec, onComplete) {
|
|
24
24
|
const animate = getMotionAnimate()
|
|
@@ -131,14 +131,14 @@ function computeMatrixPairs (currentView, previousView, lastView, currentStage,
|
|
|
131
131
|
if (direction === 'forward') {
|
|
132
132
|
return {
|
|
133
133
|
el,
|
|
134
|
-
from:
|
|
135
|
-
to:
|
|
134
|
+
from: readTransitionMatrix(el, backward.origin, backward.transform),
|
|
135
|
+
to: readTransitionMatrix(el, backward.origin, forward.transform),
|
|
136
136
|
}
|
|
137
137
|
} else {
|
|
138
138
|
return {
|
|
139
139
|
el,
|
|
140
|
-
from:
|
|
141
|
-
to:
|
|
140
|
+
from: readTransitionMatrix(el, forward.origin, forward.transform),
|
|
141
|
+
to: readTransitionMatrix(el, forward.origin, backward.transform),
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { identityMatrix, readComputedMatrix } from './driver-helpers.js'
|
|
2
|
+
|
|
3
|
+
// Zumly emits translate(px, px) followed by an optional uniform scale. These
|
|
4
|
+
// matrices are independent of layout and transform-origin: the browser applies
|
|
5
|
+
// the origin separately when it paints the matrix. Keep other CSS syntax on the
|
|
6
|
+
// computed-style path, including an empty inline value that can expose author CSS.
|
|
7
|
+
const number = '[-+]?(?:\\d*\\.\\d+|\\d+)(?:[eE][-+]?\\d+)?'
|
|
8
|
+
const length = `(${number})(px)?`
|
|
9
|
+
const zumlyTransform = new RegExp(`^(?:translate\\(\\s*${length}\\s*,\\s*${length}\\s*\\)\\s*)?(?:scale\\(\\s*(${number})\\s*\\))?$`)
|
|
10
|
+
|
|
11
|
+
function parseZumlyMatrix (transformStr) {
|
|
12
|
+
if (typeof transformStr !== 'string') return null
|
|
13
|
+
const value = transformStr.trim()
|
|
14
|
+
if (value === 'none') return identityMatrix()
|
|
15
|
+
if (!value) return null
|
|
16
|
+
const match = value.match(zumlyTransform)
|
|
17
|
+
if (!match) return null
|
|
18
|
+
const tx = Number(match[1] ?? 0)
|
|
19
|
+
const ty = Number(match[3] ?? 0)
|
|
20
|
+
const scale = Number(match[5] ?? 1)
|
|
21
|
+
// CSS permits unitless zero lengths, but not other unitless translations.
|
|
22
|
+
if ((tx !== 0 && !match[2]) || (ty !== 0 && !match[4])) return null
|
|
23
|
+
if (![tx, ty, scale].every(Number.isFinite)) return null
|
|
24
|
+
return { a: scale, b: 0, c: 0, d: scale, e: tx, f: ty }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Apply a driver state, resolving engine transforms without layout reads. */
|
|
28
|
+
export function readTransitionMatrix (element, origin, transformStr) {
|
|
29
|
+
const matrix = parseZumlyMatrix(transformStr)
|
|
30
|
+
if (!matrix) return readComputedMatrix(element, origin, transformStr)
|
|
31
|
+
element.style.transformOrigin = origin
|
|
32
|
+
element.style.transform = transformStr
|
|
33
|
+
return matrix
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Cleanup belongs to a resolved view instance, never to a cached template. */
|
|
2
|
+
// Drivers can be imported from a separate package entry point, so a module-local
|
|
3
|
+
// WeakMap would not see registrations made by the bundled core. A non-enumerable
|
|
4
|
+
// symbol property shares the scope through the node and is not copied by cloneNode.
|
|
5
|
+
const lifecycleKey = Symbol.for('zumly.viewLifecycles')
|
|
6
|
+
|
|
7
|
+
function runCleanup (cleanup) {
|
|
8
|
+
try {
|
|
9
|
+
const result = cleanup()
|
|
10
|
+
if (result && typeof result.then === 'function') {
|
|
11
|
+
result.catch(error => console.error('Zumly: view cleanup failed:', error)) // eslint-disable-line no-console
|
|
12
|
+
}
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('Zumly: view cleanup failed:', error) // eslint-disable-line no-console
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Internal scope: accepts registrations before or after resolution finishes. */
|
|
19
|
+
export function createViewLifecycle () {
|
|
20
|
+
let disposed = false
|
|
21
|
+
const callbacks = []
|
|
22
|
+
const lifecycle = {
|
|
23
|
+
onCleanup (cleanup) {
|
|
24
|
+
if (typeof cleanup !== 'function') throw new TypeError('Zumly: onCleanup expects a function')
|
|
25
|
+
if (disposed) runCleanup(cleanup)
|
|
26
|
+
else callbacks.push(cleanup)
|
|
27
|
+
},
|
|
28
|
+
attach (node) {
|
|
29
|
+
let entries = node[lifecycleKey]
|
|
30
|
+
if (!entries) {
|
|
31
|
+
entries = new Set()
|
|
32
|
+
Object.defineProperty(node, lifecycleKey, { value: entries, configurable: true })
|
|
33
|
+
}
|
|
34
|
+
entries.add(lifecycle)
|
|
35
|
+
},
|
|
36
|
+
dispose () {
|
|
37
|
+
if (disposed) return
|
|
38
|
+
disposed = true
|
|
39
|
+
for (const cleanup of callbacks.splice(0).reverse()) runCleanup(cleanup)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lifecycle
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dispose a view and registered descendants once. Call before permanent removal,
|
|
47
|
+
* or for an unused async result; temporary detach/keepAlive must not call this.
|
|
48
|
+
* Async cleanup is started here, with rejections handled, but is not awaited.
|
|
49
|
+
*/
|
|
50
|
+
export function disposeView (node) {
|
|
51
|
+
if (!node) return
|
|
52
|
+
// Snapshot only registered nodes before callbacks can mutate the subtree.
|
|
53
|
+
// A TreeWalker keeps the old reverse document order without allocating an
|
|
54
|
+
// array/NodeList containing every element of a large component.
|
|
55
|
+
const nodes = []
|
|
56
|
+
if (node[lifecycleKey]) nodes.push(node)
|
|
57
|
+
const document = node.ownerDocument || node
|
|
58
|
+
const walker = document.createTreeWalker(node, 1) // NodeFilter.SHOW_ELEMENT
|
|
59
|
+
let descendant
|
|
60
|
+
while ((descendant = walker.nextNode())) {
|
|
61
|
+
if (descendant[lifecycleKey]) nodes.push(descendant)
|
|
62
|
+
}
|
|
63
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
64
|
+
const child = nodes[i]
|
|
65
|
+
const entries = child[lifecycleKey]
|
|
66
|
+
if (!entries) continue
|
|
67
|
+
for (const lifecycle of entries) lifecycle.dispose()
|
|
68
|
+
delete child[lifecycleKey]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized `content-visibility` handling for view layers during zoom prep / teardown.
|
|
3
|
+
* Single source of truth — .z-view.hide only handles opacity; this module controls content-visibility.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Shared across the bundled engine and separately imported driver helpers.
|
|
7
|
+
const visibilityKey = Symbol.for('zumly.viewVisibility')
|
|
8
|
+
|
|
9
|
+
function rememberVisibility (element) {
|
|
10
|
+
if (element[visibilityKey]) return
|
|
11
|
+
Object.defineProperty(element, visibilityKey, {
|
|
12
|
+
configurable: true,
|
|
13
|
+
value: {
|
|
14
|
+
value: element.style.getPropertyValue('content-visibility'),
|
|
15
|
+
priority: element.style.getPropertyPriority('content-visibility')
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @param {HTMLElement | null | undefined} element */
|
|
21
|
+
export function hideViewContent (element) {
|
|
22
|
+
if (!element) return
|
|
23
|
+
rememberVisibility(element)
|
|
24
|
+
element.style.contentVisibility = 'hidden'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {HTMLElement | null | undefined} element
|
|
29
|
+
*/
|
|
30
|
+
export function showViewContent (element) {
|
|
31
|
+
if (!element) return
|
|
32
|
+
rememberVisibility(element)
|
|
33
|
+
element.style.contentVisibility = 'visible'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Release the temporary transition override, preserving the host's CSS policy. */
|
|
37
|
+
export function restoreViewContent (element) {
|
|
38
|
+
const original = element?.[visibilityKey]
|
|
39
|
+
if (!original) return
|
|
40
|
+
if (original.value) element.style.setProperty('content-visibility', original.value, original.priority)
|
|
41
|
+
else element.style.removeProperty('content-visibility')
|
|
42
|
+
delete element[visibilityKey]
|
|
43
|
+
}
|
package/types/zumly.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ export interface ViewContext {
|
|
|
12
12
|
/** The componentContext from Zumly constructor options. */
|
|
13
13
|
context: Map<string, unknown> | Record<string, unknown>
|
|
14
14
|
/** Data attributes from the trigger element (e.g. data-id="42" → props.id). */
|
|
15
|
-
props: Record<string,
|
|
15
|
+
props: Record<string, unknown>
|
|
16
|
+
/** Register component unmount or resource cleanup, run once when this view is discarded. */
|
|
17
|
+
onCleanup(callback: () => void | Promise<void>): void
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** A function that receives context and returns a view. */
|
|
@@ -53,7 +55,7 @@ export interface TransitionSpec {
|
|
|
53
55
|
slideDeltaX?: number
|
|
54
56
|
slideDeltaY?: number
|
|
55
57
|
/** When true, driver should not remove the outgoing view from DOM (lateral keepAlive). */
|
|
56
|
-
keepAlive?: boolean
|
|
58
|
+
keepAlive?: boolean | 'visible'
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** Custom driver function signature. */
|
|
@@ -136,7 +138,7 @@ export interface DepthNavOptions {
|
|
|
136
138
|
export interface InputsOptions {
|
|
137
139
|
/** Enable wheel zoom-out. Default: true. */
|
|
138
140
|
wheel?: boolean
|
|
139
|
-
/** Enable keyboard navigation (arrow keys). Default: true. */
|
|
141
|
+
/** Enable keyboard navigation (Enter/Space on triggers, arrow keys for back). Default: true. */
|
|
140
142
|
keyboard?: boolean
|
|
141
143
|
/** Enable click/mouseup navigation. Default: true. */
|
|
142
144
|
click?: boolean
|
|
@@ -193,7 +195,7 @@ export interface ZumlyOptions {
|
|
|
193
195
|
/** Depth navigation UI. true = default (back button, bottom-left), false = disabled. */
|
|
194
196
|
depthNav?: boolean | DepthNavOptions
|
|
195
197
|
/** Input types to enable/disable. All enabled by default. */
|
|
196
|
-
inputs?: InputsOptions
|
|
198
|
+
inputs?: boolean | InputsOptions
|
|
197
199
|
/** Enable deferred rendering (view content inserted after zoom animation). */
|
|
198
200
|
deferred?: boolean
|
|
199
201
|
}
|
|
@@ -278,27 +280,28 @@ export class Zumly {
|
|
|
278
280
|
getCurrentViewName(): string | null
|
|
279
281
|
|
|
280
282
|
/**
|
|
281
|
-
* Navigate to a view by name.
|
|
283
|
+
* Navigate to a view by name. Resolves after the transition completes.
|
|
284
|
+
* Calls made while another navigation is loading/animating are ignored.
|
|
282
285
|
*/
|
|
283
286
|
goTo(viewName: string, options?: GoToOptions): Promise<void>
|
|
284
287
|
|
|
285
288
|
/**
|
|
286
289
|
* Programmatic zoom to a named view (depth navigation).
|
|
287
|
-
* Uses a centered synthetic trigger for the transition.
|
|
290
|
+
* Uses a centered synthetic trigger for the transition. Resolves after animation.
|
|
288
291
|
*/
|
|
289
292
|
zoomTo(viewName: string, options?: ZoomToOptions): Promise<void>
|
|
290
293
|
|
|
291
294
|
/** Zoom into the view indicated by a trigger element with data-to="viewName". */
|
|
292
295
|
zoomIn(el: HTMLElement): Promise<void>
|
|
293
296
|
|
|
294
|
-
/** Zoom out one level.
|
|
295
|
-
zoomOut(): void
|
|
297
|
+
/** Zoom out one level. Resolves after animation; no-op at root. */
|
|
298
|
+
zoomOut(): Promise<void>
|
|
296
299
|
|
|
297
300
|
/**
|
|
298
301
|
* Navigate back. Pops lateral history first, then zooms out.
|
|
299
|
-
*
|
|
302
|
+
* Resolves after the navigation completes.
|
|
300
303
|
*/
|
|
301
|
-
back(): Promise<void>
|
|
304
|
+
back(): Promise<void>
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
export default Zumly
|