ember-nav-stack 6.1.2 → 7.1.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.
Files changed (79) hide show
  1. package/README.md +165 -21
  2. package/addon-main.cjs +4 -0
  3. package/dist/_app_/components/nav-stack-inner-wrapper.js +1 -0
  4. package/dist/_app_/components/nav-stack.js +1 -0
  5. package/dist/_app_/components/to-nav-stack.js +1 -0
  6. package/dist/_app_/helpers/nav-layer-indices.js +1 -0
  7. package/dist/_app_/modifiers/back-swipe.js +1 -0
  8. package/dist/_app_/services/gesture.js +1 -0
  9. package/dist/_app_/services/nav-stacks.js +1 -0
  10. package/dist/_app_/templates/stackable.js +1 -0
  11. package/dist/back-swipe-gesture.js +261 -0
  12. package/dist/back-swipe-gesture.js.map +1 -0
  13. package/dist/components/nav-stack-inner-wrapper.js +10 -0
  14. package/dist/components/nav-stack-inner-wrapper.js.map +1 -0
  15. package/dist/components/nav-stack.js +700 -0
  16. package/dist/components/nav-stack.js.map +1 -0
  17. package/dist/components/to-nav-stack.js +22 -0
  18. package/dist/components/to-nav-stack.js.map +1 -0
  19. package/dist/helpers/nav-layer-indices.js +21 -0
  20. package/dist/helpers/nav-layer-indices.js.map +1 -0
  21. package/dist/index.js +7 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/modifiers/back-swipe.js +40 -0
  24. package/dist/modifiers/back-swipe.js.map +1 -0
  25. package/dist/routes/stackable-route.js +99 -0
  26. package/dist/routes/stackable-route.js.map +1 -0
  27. package/{addon → dist}/services/gesture.js +7 -9
  28. package/dist/services/gesture.js.map +1 -0
  29. package/dist/services/nav-stacks.js +137 -0
  30. package/dist/services/nav-stacks.js.map +1 -0
  31. package/dist/styles/nav-stack.css +399 -0
  32. package/dist/templates/stackable.js +8 -0
  33. package/dist/templates/stackable.js.map +1 -0
  34. package/{addon-test-support → dist/test-support}/in-viewport.js +7 -10
  35. package/dist/test-support/in-viewport.js.map +1 -0
  36. package/dist/test-support/index.js +2 -0
  37. package/dist/test-support/index.js.map +1 -0
  38. package/{addon → dist}/utils/animation.js +17 -40
  39. package/dist/utils/animation.js.map +1 -0
  40. package/{addon → dist}/utils/back-swipe-recognizer.js +29 -49
  41. package/dist/utils/back-swipe-recognizer.js.map +1 -0
  42. package/dist/utils/clone-store.js +88 -0
  43. package/dist/utils/clone-store.js.map +1 -0
  44. package/dist/utils/component.js +121 -0
  45. package/dist/utils/component.js.map +1 -0
  46. package/dist/utils/header-style.js +46 -0
  47. package/dist/utils/header-style.js.map +1 -0
  48. package/dist/utils/transition-decision.js +71 -0
  49. package/dist/utils/transition-decision.js.map +1 -0
  50. package/dist/utils/waiter-state.js +130 -0
  51. package/dist/utils/waiter-state.js.map +1 -0
  52. package/package.json +79 -91
  53. package/.vscode/settings.json +0 -2
  54. package/CHANGELOG.md +0 -208
  55. package/MODULE_REPORT.md +0 -27
  56. package/RELEASE.md +0 -54
  57. package/addon/components/nav-stack/component.js +0 -690
  58. package/addon/components/nav-stack/template.hbs +0 -37
  59. package/addon/components/to-nav-stack.js +0 -32
  60. package/addon/helpers/nav-layer-indices.js +0 -29
  61. package/addon/routes/stackable-route.js +0 -61
  62. package/addon/services/nav-stacks.js +0 -157
  63. package/addon/utils/component.js +0 -40
  64. package/app/components/nav-stack/component.js +0 -1
  65. package/app/components/nav-stack/template.js +0 -1
  66. package/app/components/to-nav-stack.js +0 -1
  67. package/app/helpers/nav-layer-indices.js +0 -1
  68. package/app/services/gesture.js +0 -1
  69. package/app/services/nav-stacks.js +0 -1
  70. package/app/styles/nav-stack.scss +0 -117
  71. package/app/templates/stackable.hbs +0 -8
  72. package/app/utils/animation.js +0 -1
  73. package/config/deploy.js +0 -29
  74. package/config/environment.js +0 -5
  75. package/config/release.js +0 -21
  76. package/docs/ember-nav-stack-waiters-plan.md +0 -125
  77. package/index.js +0 -15
  78. package/tsconfig.json +0 -6
  79. package/vendor/wobble-shim.js +0 -3
@@ -0,0 +1,700 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { scheduleOnce, run, next } from '@ember/runloop';
4
+ import { setTransform } from '../utils/animation.js';
5
+ import { Spring } from 'wobble';
6
+ import { macroCondition, isTesting } from '@embroider/macros';
7
+ import { getOwner } from '@ember/application';
8
+ import { service } from '@ember/service';
9
+ import { reads, mapBy, bool } from 'macro-decorators';
10
+ import { guidFor } from '@ember/object/internals';
11
+ import { extractComponentKey, extractComponentName } from '../utils/component.js';
12
+ import { decideTransition } from '../utils/transition-decision.js';
13
+ import { styleHeaderElements, currentTransitionPercentage, HEADER_PARALLAX_OFFSET } from '../utils/header-style.js';
14
+ import CloneStore from '../utils/clone-store.js';
15
+ import { precompileTemplate } from '@ember/template-compilation';
16
+ import { g, i, n } from 'decorator-transforms/runtime';
17
+ import { setComponentTemplate } from '@ember/component';
18
+
19
+ var TEMPLATE = precompileTemplate("<div\n id={{this.elementId}}\n class={{concat\n \"NavStack \"\n (if this.birdsEyeDebugging \"is-birdsEyeDebugging \")\n this.layerIndexCssClass\n \" \"\n (if this.hasFooter \"NavStack--withFooter \")\n (if @rootClass (concat \" \" @rootClass))\n }}\n {{back-swipe\n onReady=this.handleBackSwipeReady\n getCanNavigateBack=this.getCanNavigateBack\n onBack=this.handleBack\n onBackSwipeOverlay=this.handleBackSwipeOverlay\n }}\n {{did-update this.handleStackFingerprintChange this.stackItemsFingerprint}}\n ...attributes\n>\n <NavStackInnerWrapper @class={{@innerWrapperClass}}>\n {{#if this.headerIsTop}}\n <div\n class={{concat\n \"NavStack-header\"\n (if @headerContainerClass (concat \" \" @headerContainerClass))\n }}\n >\n {{#if this.parentItemHeaderComponent}}\n <div\n class=\"NavStack-headerContainer NavStack-parentItemHeaderContainer\"\n inert={{true}}\n >\n {{#if (has-block \"header\")}}\n {{yield\n (component\n this.parentItemHeaderComponent\n class=\"NavStack-headerComponent\"\n back=@back\n )\n to=\"header\"\n }}\n {{else}}\n {{component\n this.parentItemHeaderComponent\n class=\"NavStack-headerComponent\"\n back=@back\n }}\n {{/if}}\n </div>\n {{/if}}\n <div class=\"NavStack-headerContainer NavStack-currentHeaderContainer\">\n {{#if (has-block \"header\")}}\n {{yield\n (component\n this.headerComponent class=\"NavStack-headerComponent\" back=@back\n )\n to=\"header\"\n }}\n {{else}}\n {{component\n this.headerComponent\n class=\"NavStack-headerComponent\"\n back=@back\n }}\n {{/if}}\n </div>\n </div>\n {{/if}}\n <div\n class={{concat\n \"NavStack-itemContainer\"\n (if @itemContainerClass (concat \" \" @itemContainerClass))\n }}\n >\n {{#each this.renderableEntries as |entry|}}\n <div\n class={{concat \"NavStack-item NavStack-item-\" entry.index}}\n inert={{entry.isInert}}\n >\n {{#if entry.shouldRender}}\n {{component entry.component}}\n {{/if}}\n </div>\n {{/each}}\n </div>\n {{#unless this.headerIsTop}}\n <div\n class={{concat\n \"NavStack-header\"\n (if @headerContainerClass (concat \" \" @headerContainerClass))\n }}\n >\n {{#if this.parentItemHeaderComponent}}\n <div\n class=\"NavStack-headerContainer NavStack-parentItemHeaderContainer\"\n inert={{true}}\n >\n {{#if (has-block \"header\")}}\n {{yield\n (component\n this.parentItemHeaderComponent\n class=\"NavStack-headerComponent\"\n back=@back\n )\n to=\"header\"\n }}\n {{else}}\n {{component\n this.parentItemHeaderComponent\n class=\"NavStack-headerComponent\"\n back=@back\n }}\n {{/if}}\n </div>\n {{/if}}\n <div class=\"NavStack-headerContainer NavStack-currentHeaderContainer\">\n {{#if (has-block \"header\")}}\n {{yield\n (component\n this.headerComponent class=\"NavStack-headerComponent\" back=@back\n )\n to=\"header\"\n }}\n {{else}}\n {{component\n this.headerComponent\n class=\"NavStack-headerComponent\"\n back=@back\n }}\n {{/if}}\n </div>\n </div>\n {{/unless}}\n {{yield to=\"bannerAd\"}}\n {{#if this.hasFooter}}\n <div\n class={{concat\n \"NavStack-footer\"\n (if @footerClass (concat \" \" @footerClass))\n }}\n >\n {{component @footer}}\n </div>\n {{/if}}\n </NavStackInnerWrapper>\n</div>");
20
+
21
+ class NavStack extends Component {
22
+ get birdsEyeDebugging() {
23
+ return this.args.birdsEyeDebugging || false;
24
+ }
25
+ element;
26
+ get elementId() {
27
+ return guidFor(this);
28
+ }
29
+ get layer() {
30
+ return this.args.layer;
31
+ }
32
+ static {
33
+ g(this.prototype, "navStacksService", [service('nav-stacks')]);
34
+ }
35
+ #navStacksService = (i(this, "navStacksService"), void 0);
36
+ static {
37
+ g(this.prototype, "gesture", [service]);
38
+ }
39
+ #gesture = (i(this, "gesture"), void 0);
40
+ constructor() {
41
+ super(...arguments);
42
+ this.navStacksService.register(this);
43
+ let config = getOwner(this).resolveRegistration('config:environment');
44
+ this.#suppressAnimation = !!config?.['ember-nav-stack']?.suppressAnimation;
45
+ }
46
+ willDestroy() {
47
+ super.willDestroy(...arguments);
48
+ this._resizeObserver?.disconnect();
49
+ this._resizeObserver = null;
50
+ this.navStacksService.unregister(this);
51
+ // Defensive: sweep every tracked clone in case some transition was
52
+ // abandoned before its normal cleanup ran (e.g. a back-swipe in flight
53
+ // when the consumer tore down the route). The `parentNode?` guard in
54
+ // CloneStore handles clones whose parents are already detached. The
55
+ // detached slide-down clone is not tracked here, so it survives this
56
+ // sweep and keeps animating after we're gone.
57
+ this._clones.clearAll();
58
+ this._pendingGestureBackOverlay = null;
59
+ }
60
+
61
+ // Called by the service's `_process` immediately before it publishes
62
+ // `this.stacks = newStacks` for the case where our layer's stack just
63
+ // went from non-empty to empty. This is the only moment when our DOM is
64
+ // still attached and we have advance notice that we're about to be torn
65
+ // down by the host's `{{#each (nav-layer-indices)}}`. willDestroy is too
66
+ // late (Glimmer has already detached `this.element` by then, leaving us
67
+ // with no parent to attach the clone to). `_runDetachedSlideDown`
68
+ // self-guards against double-fire from `_applyTransition('slideDown')`,
69
+ // which is the parallel path for setups that don't use nav-layer-indices.
70
+ layerDidEmpty() {
71
+ if (this.args.layer === 0) {
72
+ return;
73
+ }
74
+ this._runDetachedSlideDown();
75
+ }
76
+ get layerIndexCssClass() {
77
+ return `NavStack--layer${this.args.layer}`;
78
+ }
79
+ get headerComponent() {
80
+ if (this.stackItems.length === 0) {
81
+ return null;
82
+ }
83
+ return this.stackItems[this.stackItems.length - 1].headerComponent;
84
+ }
85
+ get parentItemHeaderComponent() {
86
+ if (this.stackItems.length < 2) {
87
+ return null;
88
+ }
89
+ return this.stackItems[this.stackItems.length - 2].headerComponent;
90
+ }
91
+ get stackItems() {
92
+ return this.navStacksService.stacks[`layer${this.args.layer}`] || [];
93
+ }
94
+ static {
95
+ g(this.prototype, "stackDepth", [reads('stackItems.length')]);
96
+ }
97
+ #stackDepth = (i(this, "stackDepth"), void 0);
98
+ static {
99
+ g(this.prototype, "components", [mapBy('stackItems', 'component')]);
100
+ }
101
+ #components = (i(this, "components"), void 0);
102
+ // One entry per stack item. The template renders an empty `.NavStack-item-N`
103
+ // div for each (so the layout's left-offset CSS classes are correct), but
104
+ // only mounts the actual curried component when `shouldRender` is true —
105
+ // i.e. only the last two items in the stack are alive at any time. Items
106
+ // further back are positioned off-screen by the cut animation and their
107
+ // contents aren't worth keeping mounted. `isInert` is true on every item
108
+ // except the topmost (active) one so non-active items can't be reached by
109
+ // keyboard or assistive tech — they're still in the DOM (briefly visible
110
+ // during a horizontal slide) but should not be in the focus order.
111
+ get renderableEntries() {
112
+ let components = this.components;
113
+ let renderThreshold = components.length - 2;
114
+ let topIndex = components.length - 1;
115
+ return components.map((component, index) => ({
116
+ index,
117
+ component,
118
+ shouldRender: index >= renderThreshold,
119
+ isInert: index !== topIndex
120
+ }));
121
+ }
122
+ static {
123
+ g(this.prototype, "hasFooter", [bool('args.footer')]);
124
+ }
125
+ #hasFooter = (i(this, "hasFooter"), void 0);
126
+ // `@headerPosition` controls whether the header markup is rendered above
127
+ // or below the item container in the DOM. Defaults to `'bottom'` to
128
+ // preserve the historical layout (header DOM-after-items). `'top'` is for
129
+ // consumers whose chrome needs the header to come first in the focus order
130
+ // / scroll flow. The addon's animation logic is selector-based, so neither
131
+ // position affects transitions.
132
+ get headerIsTop() {
133
+ return this.args.headerPosition === 'top';
134
+ }
135
+
136
+ // Resolved once in the constructor from
137
+ // `config.environment['ember-nav-stack'].suppressAnimation`.
138
+ #suppressAnimation = false;
139
+
140
+ // Tracks groups of cloned DOM nodes for unified cleanup. Groups used:
141
+ // - 'stackItems' cloned by _slideBack (the leaving last item)
142
+ // - 'headers' cloned for slide animations (leaving header)
143
+ // - 'gestureBackOverlays' target-header snapshot during a back-swipe;
144
+ // cleanup is queued via `_pendingGestureBackOverlay` and performed by
145
+ // `_finishGestureBack` — scheduled from `_handleStackDepthChange` once
146
+ // the stack pop actually lands, NOT on a fixed delay from finalize.
147
+ // Tying cleanup to the observed pop matters because the default `back`
148
+ // action calls `router.transitionTo`, which resolves asynchronously
149
+ // through route hooks; cleaning up before the pop lands would restore
150
+ // `.NavStack-currentHeaderContainer` to full opacity while it still
151
+ // holds the leaving page's header, flashing it. willDestroy sweeps
152
+ // any clones left from abandoned transitions via _clones.clearAll().
153
+ _clones = new CloneStore();
154
+
155
+ // The {{back-swipe}} modifier (template-level) calls this once its
156
+ // BackSwipeGesture is instantiated against the rendered element. We
157
+ // hold the gesture reference so _transitionDidBegin / _transitionDidEnd
158
+ // can pause / re-arm the pan recognizer during programmatic
159
+ // transitions, and the element reference because
160
+ // _horizontalTransition / _verticalTransition / _cut / _slide* all read
161
+ // from `this.element`.
162
+ handleBackSwipeReady(gesture, element) {
163
+ this._backSwipeGesture = gesture;
164
+ this.element = element;
165
+ this._observeResize(element);
166
+ let isInitialRender = this.navStacksService.isInitialRender;
167
+ scheduleOnce('afterRender', this, this._handleStackDepthChange, isInitialRender);
168
+ }
169
+
170
+ // Recompute the item-container's translateX when the NavStack's outer
171
+ // width changes — e.g. window resize, orientation change, parent grid
172
+ // reflow, browser dev-tools opening. Without this, `_computeXPosition`'s
173
+ // `(stackDepth - 1) * itemWidth * -1` snapshot is stale and the visible
174
+ // item shifts off-center on the next viewport change. We deliberately
175
+ // skip the very first ResizeObserver callback (the synthetic "initial
176
+ // size" report fired right after `observe`) because we haven't run any
177
+ // transition yet — there's no translateX to reposition and forcing one
178
+ // would cancel any in-flight slide.
179
+ static {
180
+ n(this.prototype, "handleBackSwipeReady", [action]);
181
+ }
182
+ _observeResize(element) {
183
+ if (typeof ResizeObserver === 'undefined') {
184
+ return;
185
+ }
186
+ let first = true;
187
+ this._resizeObserver = new ResizeObserver(() => {
188
+ if (first) {
189
+ first = false;
190
+ return;
191
+ }
192
+ this._handleViewportResize();
193
+ });
194
+ this._resizeObserver.observe(element);
195
+ }
196
+ _handleViewportResize() {
197
+ if (this.isDestroying || this.isDestroyed || !this.element) {
198
+ return;
199
+ }
200
+ // A horizontal slide is in flight: the spring's `onStop` will commit
201
+ // a translateX based on the pre-resize geometry, clobbering anything
202
+ // we set here. Defer until the spring rests — `_horizontalTransition`
203
+ // checks `_resizePending` in its finish path and re-runs us with the
204
+ // fresh dimensions.
205
+ if (this._horizontalSpring) {
206
+ this._resizePending = true;
207
+ return;
208
+ }
209
+ let itemContainerElement = this.element.querySelector('.NavStack-itemContainer');
210
+ if (!itemContainerElement) {
211
+ return;
212
+ }
213
+ setTransform(itemContainerElement, `translateX(${this._computeXPosition()}px)`);
214
+ // The back-swipe gesture caches `startingX`, `backX`, and `thresholdX`
215
+ // off the (now-stale) element measurements. Re-seed those so the next
216
+ // pan uses the post-resize geometry.
217
+ this._backSwipeGesture?.setupContext();
218
+ }
219
+ getCanNavigateBack() {
220
+ return !!this.args.back && this.stackDepth > 1;
221
+ }
222
+ static {
223
+ n(this.prototype, "getCanNavigateBack", [action]);
224
+ }
225
+ handleBack() {
226
+ run(this.args, this.args.back);
227
+ }
228
+ static {
229
+ n(this.prototype, "handleBack", [action]);
230
+ }
231
+ handleBackSwipeOverlay(clone) {
232
+ if (!clone) return;
233
+ this._clones.track('gestureBackOverlays', clone);
234
+ this._pendingGestureBackOverlay = clone;
235
+ }
236
+ static {
237
+ n(this.prototype, "handleBackSwipeOverlay", [action]);
238
+ }
239
+ stackItemsDidChange() {
240
+ this._handleStackDepthChange(false);
241
+ }
242
+
243
+ // A fingerprint over stackItems that reflects BOTH structural changes
244
+ // (push/pop) and per-item arg changes (e.g. a curried component whose
245
+ // bound model.id has changed). Reading each item's component via
246
+ // `extractComponentKey` consumes the underlying Glimmer reference tags,
247
+ // so this getter invalidates when any of those references change. The
248
+ // `{{did-update}}` modifier in the template observes this fingerprint
249
+ // and re-runs the transition logic when it changes — which is how we
250
+ // pick up arg-only changes (e.g. a parent route re-currying with a new
251
+ // model on a dynamic-segment transition) that don't produce a
252
+ // pushItem/removeItem on the service.
253
+ static {
254
+ n(this.prototype, "stackItemsDidChange", [action]);
255
+ }
256
+ get stackItemsFingerprint() {
257
+ let items = this.stackItems;
258
+ let extract = this.args.extractComponentKey || extractComponentKey;
259
+ return items.map(item => {
260
+ if (!item || !item.component) return '_';
261
+ return extract(item.component);
262
+ }).join('|');
263
+ }
264
+ handleStackFingerprintChange() {
265
+ this._handleStackDepthChange(false);
266
+ }
267
+ static {
268
+ n(this.prototype, "handleStackFingerprintChange", [action]);
269
+ }
270
+ _clearRootJustChanged() {
271
+ this._rootJustChanged = false;
272
+ }
273
+ _handleStackDepthChange(isInitialRender) {
274
+ if (this.isDestroying || this.isDestroyed) {
275
+ return;
276
+ }
277
+ let stackItems = this.stackItems || [];
278
+ let stackDepth = stackItems.length;
279
+ let extract = this.args.extractComponentKey || extractComponentKey;
280
+ let rootComponentRef = stackItems[0]?.component;
281
+ let rootComponentKey = extract(rootComponentRef);
282
+ let pendingGestureBackOverlay = this._pendingGestureBackOverlay;
283
+ this._pendingGestureBackOverlay = null;
284
+ let intent = decideTransition({
285
+ isInitialRender,
286
+ previousDepth: this._stackDepth,
287
+ newDepth: stackDepth,
288
+ previousRootKey: this._rootComponentKey,
289
+ newRootKey: rootComponentKey,
290
+ layer: this.args.layer,
291
+ rootJustChanged: this._rootJustChanged
292
+ });
293
+ if (intent.startsRootChange) {
294
+ this._rootJustChanged = true;
295
+ next(this, this._clearRootJustChanged);
296
+ }
297
+ this._applyTransition(intent);
298
+
299
+ // Gesture-back overlay cleanup is orthogonal to which transition runs —
300
+ // it just needs the stack pop to have landed (newDepth < previousDepth).
301
+ // The overlay snapshot was made by `finalize` after the back-swipe
302
+ // completed; we restore the live header containers once the re-render
303
+ // following `args.back` has swapped their content. `next()` defers the
304
+ // cleanup past the current flush so the swap is visible by the time we
305
+ // restore opacity.
306
+ if (pendingGestureBackOverlay) {
307
+ if (stackDepth < this._stackDepth) {
308
+ next(this, this._finishGestureBack, pendingGestureBackOverlay);
309
+ } else {
310
+ this._clones.remove('gestureBackOverlays', pendingGestureBackOverlay);
311
+ }
312
+ }
313
+
314
+ // Fire the active-item-change callback when the top stack item's
315
+ // component identity changes (push, pop, or root swap). Compared via
316
+ // component name only — `extractComponentName` skips the `:model.id`
317
+ // suffix that `extractComponentKey` adds, because the model ref's
318
+ // `lastValue` populates lazily and would otherwise produce two fires
319
+ // per push (`track` → `track:1` across consecutive render ticks).
320
+ // Consumers needing finer per-model granularity should watch their
321
+ // own model state via `did-update` or similar. Initial render fires
322
+ // once when the stack starts non-empty.
323
+ let topComponentRef = stackItems[stackDepth - 1]?.component;
324
+ let currentTopKey = stackDepth === 0 ? undefined : extractComponentName(topComponentRef);
325
+ let previousTopKey = this._activeItemKey;
326
+ if (currentTopKey !== previousTopKey) {
327
+ this.args.onActiveItemChange?.({
328
+ isInitialRender,
329
+ previousDepth: this._stackDepth ?? 0,
330
+ currentDepth: stackDepth,
331
+ previousTopKey,
332
+ currentTopKey
333
+ });
334
+ this._activeItemKey = currentTopKey;
335
+ }
336
+ this._stackDepth = stackDepth;
337
+ this._rootComponentKey = rootComponentKey;
338
+ }
339
+ _applyTransition(intent) {
340
+ switch (intent.kind) {
341
+ case 'cut':
342
+ this._schedule(this._cut);
343
+ break;
344
+ case 'slideUp':
345
+ this._schedule(this._slideUp);
346
+ break;
347
+ case 'slideDown':
348
+ this._runDetachedSlideDown();
349
+ break;
350
+ case 'slideBack':
351
+ this._cloneLastStackItem();
352
+ this._cloneHeader();
353
+ this._schedule(this._slideBack);
354
+ break;
355
+ case 'slideForward':
356
+ this._cloneHeader();
357
+ this._schedule(this._slideForward);
358
+ break;
359
+ }
360
+ }
361
+ _schedule(method) {
362
+ scheduleOnce('afterRender', this, method);
363
+ }
364
+ _computeXPosition() {
365
+ let stackDepth = this.stackDepth;
366
+ if (stackDepth === 0) {
367
+ return 0;
368
+ }
369
+ let currentStackItemElement = this.element.querySelector('.NavStack-item:last-child');
370
+ if (!currentStackItemElement) {
371
+ return 0;
372
+ }
373
+ let itemWidth = currentStackItemElement.getBoundingClientRect().width;
374
+ let layerX = (stackDepth - 1) * itemWidth * -1;
375
+ return layerX;
376
+ }
377
+ repositionX() {
378
+ let itemContainerElement = this.element.querySelector('.NavStack-itemContainer');
379
+ let newX = this._computeXPosition();
380
+ setTransform(itemContainerElement, `translateX(${newX}px)`);
381
+ }
382
+ static {
383
+ n(this.prototype, "repositionX", [action]);
384
+ }
385
+ _cut() {
386
+ if (this.isDestroying || this.isDestroyed) {
387
+ return;
388
+ }
389
+ this._horizontalTransition({
390
+ toValue: this._computeXPosition(),
391
+ animate: false
392
+ });
393
+ if (this.args.layer > 0 && this.stackDepth > 0) {
394
+ this._verticalTransition({
395
+ element: this.element,
396
+ toValue: 0,
397
+ animate: false
398
+ });
399
+ }
400
+ }
401
+ _slideForward() {
402
+ if (this.isDestroying || this.isDestroyed) {
403
+ return;
404
+ }
405
+ this._horizontalTransition({
406
+ toValue: this._computeXPosition(),
407
+ finishCallback: () => {
408
+ this._clones.clear('headers');
409
+ }
410
+ });
411
+ }
412
+ _slideBack() {
413
+ if (this.isDestroying || this.isDestroyed) {
414
+ return;
415
+ }
416
+ this._horizontalTransition({
417
+ toValue: this._computeXPosition(),
418
+ finishCallback: () => {
419
+ this._clones.clear('stackItems');
420
+ this._clones.clear('headers');
421
+ }
422
+ });
423
+ }
424
+ _slideUp() {
425
+ if (this.isDestroying || this.isDestroyed) {
426
+ return;
427
+ }
428
+ let debug = this.birdsEyeDebugging;
429
+ this._verticalTransition({
430
+ element: this.element,
431
+ toValue: 0,
432
+ fromValue: debug ? 480 : this.element.getBoundingClientRect().height
433
+ });
434
+ }
435
+
436
+ // Runs the slide-down dismissal entirely outside our own lifecycle.
437
+ // Called from two sites:
438
+ // 1. `layerDidEmpty()` — invoked by the service's `_process` immediately
439
+ // BEFORE it publishes `this.stacks = newStacks`, when our layer just
440
+ // transitioned non-empty → empty. This is the common case under
441
+ // `nav-layer-indices`, where publishing the new stacks causes the
442
+ // host's `{{#each}}` to tear us down. willDestroy would be too
443
+ // late (Glimmer detaches `this.element` before willDestroy fires,
444
+ // leaving us with no parent to attach the clone to), which is why
445
+ // we hook in pre-publish while our DOM is still mounted.
446
+ // 2. `_applyTransition('slideDown')`, for setups that keep the NavStack
447
+ // alive while its items go to zero (manual layer management without
448
+ // `nav-layer-indices`). In that case `layerDidEmpty` still fires —
449
+ // both paths can land on the same NavStack in degenerate flows, so
450
+ // we self-guard against a double fire.
451
+ // Clone+parent uses our DOM neighbor (which survives our teardown), the
452
+ // live element is hidden, and the spring runs with closure-captured
453
+ // refs — completion and DOM cleanup happen regardless of NavStack
454
+ // lifecycle. We bypass CloneStore so willDestroy's clearAll sweep can't
455
+ // tear our animation out, and we talk to the service directly for waiter
456
+ // tracking (the `_notifyTransition*` indirections would no-op once we're
457
+ // destroyed).
458
+ _runDetachedSlideDown() {
459
+ if (this._slideDownFired) {
460
+ return;
461
+ }
462
+ this._slideDownFired = true;
463
+ let element = this.element;
464
+ let parent = element?.parentNode;
465
+ if (!element || !parent) {
466
+ return;
467
+ }
468
+ let clone = element.cloneNode(true);
469
+ clone.setAttribute('id', `${this.elementId}_clone`);
470
+ parent.appendChild(clone);
471
+ element.style.display = 'none';
472
+ // Force layout so the upcoming transform animates from rest, not from
473
+ // wherever the browser would interpolate from with no committed style.
474
+ void clone.offsetHeight;
475
+ let toValue = this.birdsEyeDebugging ? 480 : clone.getBoundingClientRect().height;
476
+ let service = this.navStacksService;
477
+ let removeClone = () => {
478
+ clone.parentNode?.removeChild(clone);
479
+ };
480
+ if (this.#suppressAnimation || toValue === 0) {
481
+ // No-op short-circuit: when there's nothing to animate (animations
482
+ // suppressed, or the element rendered with no height — which happens
483
+ // when the host's container is torn down before us, e.g. on app
484
+ // teardown during a test), snap to the final state and bail. Don't
485
+ // open a transition waiter for a spring that has no work — wobble's
486
+ // onStop doesn't reliably fire for a degenerate 0→0 spring, which
487
+ // would leave the waiter perpetually open and hang `settled()`.
488
+ setTransform(clone, `translateY(${toValue}px)`);
489
+ removeClone();
490
+ return;
491
+ }
492
+ service.notifyTransitionStart();
493
+ new Spring({
494
+ initialVelocity: 0,
495
+ fromValue: 0,
496
+ toValue,
497
+ stiffness: 1000,
498
+ damping: 500,
499
+ mass: 3
500
+ }).onUpdate(s => {
501
+ setTransform(clone, `translateY(${s.currentValue}px)`);
502
+ }).onStop(() => {
503
+ removeClone();
504
+ service.notifyTransitionEnd();
505
+ }).start();
506
+ }
507
+ _horizontalTransition({
508
+ toValue,
509
+ fromValue,
510
+ animate = !this.#suppressAnimation,
511
+ finishCallback
512
+ }) {
513
+ let itemContainerElement = this.element.querySelector('.NavStack-itemContainer');
514
+ let currentHeaderElement = this.element.querySelector('.NavStack-currentHeaderContainer');
515
+ let clonedHeaderElement = this.element.querySelector('.NavStack-clonedHeaderContainer');
516
+
517
+ // If a prior horizontal transition is still animating (e.g. an intermediate
518
+ // slideBack scheduled during a staged route change like a tab switch),
519
+ // stop it so this transition can take over from the current position.
520
+ // The prior spring's onStop runs its finish (cleaning up its clones and
521
+ // decrementing the transition counter) before we begin the new one.
522
+ if (this._horizontalSpring) {
523
+ let prior = this._horizontalSpring;
524
+ this._horizontalSpring = null;
525
+ prior.stop();
526
+ }
527
+ this._transitionDidBegin();
528
+ this._notifyTransitionStart();
529
+ let isNoOp = false;
530
+ let finish = () => {
531
+ setTransform(itemContainerElement, `translateX(${toValue}px)`);
532
+ // The no-op case occurs during slideBack after a completed back-swipe:
533
+ // the header styles were already set as part of the swipe, so we don't
534
+ // want to change them further here.
535
+ if (!isNoOp) {
536
+ styleHeaderElements(currentTransitionPercentage(fromValue, toValue, toValue), fromValue === undefined || fromValue > toValue, currentHeaderElement, clonedHeaderElement);
537
+ }
538
+ this._notifyTransitionEnd();
539
+ this._transitionDidEnd();
540
+ if (finishCallback) {
541
+ finishCallback();
542
+ }
543
+ };
544
+ if (animate) {
545
+ fromValue = fromValue || this._getX(itemContainerElement);
546
+ if (fromValue === toValue) {
547
+ isNoOp = true;
548
+ run(finish);
549
+ return;
550
+ }
551
+ let spring = this._createSpring({
552
+ fromValue,
553
+ toValue
554
+ });
555
+ this._horizontalSpring = spring;
556
+ spring.onUpdate(s => {
557
+ setTransform(itemContainerElement, `translateX(${s.currentValue}px)`);
558
+ styleHeaderElements(currentTransitionPercentage(fromValue, toValue, s.currentValue), fromValue > toValue, currentHeaderElement, clonedHeaderElement);
559
+ }).onStop(() => {
560
+ if (this._horizontalSpring === spring) {
561
+ this._horizontalSpring = null;
562
+ }
563
+ run(finish);
564
+ // If a viewport resize landed during this slide, we deferred it
565
+ // (the spring's geometry was based on pre-resize measurements).
566
+ // Now that the spring has rested, re-run the reposition against
567
+ // the current width.
568
+ if (this._resizePending && !this.isDestroying && !this.isDestroyed) {
569
+ this._resizePending = false;
570
+ next(this, this._handleViewportResize);
571
+ }
572
+ }).start();
573
+ return;
574
+ }
575
+ run(finish);
576
+ }
577
+ _verticalTransition({
578
+ element,
579
+ toValue,
580
+ fromValue,
581
+ animate = !this.#suppressAnimation,
582
+ finishCallback
583
+ }) {
584
+ this._transitionDidBegin();
585
+ this._notifyTransitionStart();
586
+ let finish = () => {
587
+ setTransform(element, `translateY(${toValue}px)`);
588
+ this._notifyTransitionEnd();
589
+ this._transitionDidEnd();
590
+ if (finishCallback) {
591
+ finishCallback();
592
+ }
593
+ };
594
+ if (animate) {
595
+ fromValue = fromValue || element.getBoundingClientRect().top;
596
+ if (fromValue === toValue) {
597
+ run(finish);
598
+ return;
599
+ }
600
+ let spring = this._createSpring({
601
+ fromValue,
602
+ toValue
603
+ });
604
+ spring.onUpdate(s => {
605
+ setTransform(element, `translateY(${s.currentValue}px)`);
606
+ }).onStop(() => {
607
+ run(finish);
608
+ }).start();
609
+ return;
610
+ }
611
+ run(finish);
612
+ }
613
+ _createSpring({
614
+ initialVelocity = 0,
615
+ fromValue,
616
+ toValue
617
+ }) {
618
+ return new Spring({
619
+ initialVelocity,
620
+ fromValue,
621
+ toValue,
622
+ stiffness: 1000,
623
+ damping: 500,
624
+ mass: 3
625
+ });
626
+ }
627
+ _transitionDidBegin() {
628
+ this._backSwipeGesture?.disablePan();
629
+ }
630
+ _transitionDidEnd() {
631
+ if (!this.element || this.stackDepth <= 1) {
632
+ return;
633
+ }
634
+ this._backSwipeGesture?.setupContext();
635
+ }
636
+ _notifyTransitionStart() {
637
+ this.navStacksService.notifyTransitionStart();
638
+ }
639
+ _notifyTransitionEnd() {
640
+ this.navStacksService.notifyTransitionEnd();
641
+ }
642
+ _finishGestureBack(parentClone) {
643
+ if (this.isDestroying || this.isDestroyed) {
644
+ this._clones.remove('gestureBackOverlays', parentClone);
645
+ return;
646
+ }
647
+ let currentHeaderElement = this.element?.querySelector('.NavStack-currentHeaderContainer');
648
+ let parentHeaderElement = this.element?.querySelector('.NavStack-parentItemHeaderContainer');
649
+ if (currentHeaderElement) {
650
+ currentHeaderElement.style.opacity = 1;
651
+ setTransform(currentHeaderElement, 'translateX(0px)');
652
+ }
653
+ if (parentHeaderElement) {
654
+ parentHeaderElement.style.opacity = 0;
655
+ setTransform(parentHeaderElement, `translateX(${-HEADER_PARALLAX_OFFSET}px)`);
656
+ }
657
+ this._clones.remove('gestureBackOverlays', parentClone);
658
+ }
659
+ _cloneLastStackItem() {
660
+ let clone = this.element.querySelector('.NavStack-item:last-child').cloneNode(true);
661
+ clone.setAttribute('id', `${this.elementId}_clonedStackItem`);
662
+ this._clones.track('stackItems', clone);
663
+ this.element.querySelector('.NavStack-itemContainer').appendChild(clone);
664
+ }
665
+ _cloneHeader() {
666
+ this._clones.clear('headers');
667
+ let liveHeader = this.element.querySelector('.NavStack-currentHeaderContainer');
668
+ if (!liveHeader) {
669
+ return;
670
+ }
671
+ let clone = liveHeader.cloneNode(true);
672
+ clone.classList.remove('NavStack-currentHeaderContainer');
673
+ clone.classList.add('NavStack-clonedHeaderContainer');
674
+ this._clones.track('headers', clone);
675
+ let headerWrapper = this.element.querySelector('.NavStack-header');
676
+ headerWrapper.insertBefore(clone, headerWrapper.firstChild);
677
+ }
678
+ _getTestContainerEl() {
679
+ if (this._testContainerEl === undefined) {
680
+ this._testContainerEl = document.querySelector('#ember-testing') || false;
681
+ }
682
+ return this._testContainerEl;
683
+ }
684
+ _getX(element) {
685
+ return this._adjustX(element.getBoundingClientRect().left);
686
+ }
687
+ _adjustX(x) {
688
+ if (macroCondition(isTesting())) {
689
+ let testContainerEl = this._getTestContainerEl();
690
+ if (testContainerEl) {
691
+ return x - testContainerEl.getBoundingClientRect().left;
692
+ }
693
+ }
694
+ return x;
695
+ }
696
+ }
697
+ setComponentTemplate(TEMPLATE, NavStack);
698
+
699
+ export { NavStack as default };
700
+ //# sourceMappingURL=nav-stack.js.map