UNPKG

18.9 kBJavaScriptView Raw
1import Modifier from 'ember-modifier';
2import { registerDestructor } from '@ember/destroyable';
3import { macroCondition, getOwnConfig, dependencySatisfies } from '@embroider/macros';
4import { buildWaiter } from '@ember/test-waiters';
5import { later } from '@ember/runloop';
6
7function _defineProperty(obj, key, value) {
8 key = _toPropertyKey(key);
9 if (key in obj) {
10 Object.defineProperty(obj, key, {
11 value: value,
12 enumerable: true,
13 configurable: true,
14 writable: true
15 });
16 } else {
17 obj[key] = value;
18 }
19 return obj;
20}
21function _toPrimitive(input, hint) {
22 if (typeof input !== "object" || input === null) return input;
23 var prim = input[Symbol.toPrimitive];
24 if (prim !== undefined) {
25 var res = prim.call(input, hint || "default");
26 if (typeof res !== "object") return res;
27 throw new TypeError("@@toPrimitive must return a primitive value.");
28 }
29 return (hint === "string" ? String : Number)(input);
30}
31function _toPropertyKey(arg) {
32 var key = _toPrimitive(arg, "string");
33 return typeof key === "symbol" ? key : String(key);
34}
35
36/**
37 * Function that returns a promise that resolves after DOM changes
38 * have been flushed and after a browser repaint.
39 *
40 * @function nextTick
41 * @export nextTick
42 * @return {Promise} the promise
43 */
44function nextTick() {
45 return new Promise(resolve => {
46 window.requestAnimationFrame(() => resolve());
47 });
48}
49
50/**
51 * Function that returns a promise that resolves after `ms` milliseconds.
52 *
53 * @function sleep
54 * @export sleep
55 * @param {number} ms number of milliseconds after which the promise will resolve
56 * @return {Promise} the promise that will resolve after `ms` milliseconds
57 */
58function sleep(ms) {
59 return new Promise(resolve => {
60 later(() => resolve(), ms);
61 });
62}
63
64/**
65 * Computes the time a css animation will take.
66 * Uses `getComputedStyle` to get durations and delays.
67 *
68 * @function computeTimeout
69 * @export computeTimeout
70 * @param {Element} element element used calculate the animation duration based on `getComputedStyle`
71 * @return {number} the calculated animation duration + delay
72 */
73function computeTimeout(element) {
74 let {
75 transitionDuration,
76 transitionDelay,
77 animationDuration,
78 animationDelay,
79 animationIterationCount
80 } = window.getComputedStyle(element);
81
82 // `getComputedStyle` returns durations and delays in the Xs format.
83 // Conveniently if `parseFloat` encounters a character other than a sign (+ or -),
84 // numeral (0-9), a decimal point, or an exponent, it returns the value up to that point
85 // and ignores that character and all succeeding characters.
86
87 let maxDelay = Math.max(parseFloat(animationDelay), parseFloat(transitionDelay));
88 let maxDuration = Math.max(parseFloat(animationDuration) * parseFloat(animationIterationCount), parseFloat(transitionDuration));
89 return (maxDelay + maxDuration) * 1000;
90}
91
92let waiter;
93if (macroCondition(getOwnConfig()?.useTestWaiters)) {
94 waiter = buildWaiter('ember-css-transitions');
95} else {
96 waiter = {
97 beginAsync() {
98 /* fake */
99 },
100 endAsync() {
101 /* fake */
102 }
103 };
104}
105let modifier;
106if (macroCondition(dependencySatisfies('ember-modifier', '>=3.2.0 || 4.x'))) {
107 /**
108 Modifier that applies classes. Usage:
109 ```hbs
110 <div {{css-transition name="example"}}>
111 <p>Hello world</p>
112 </div>
113 ```
114 @class CssTransitionModifier
115 @argument {Function} [didTransitionIn]
116 @argument {Function} [didTransitionOut]
117 @public
118 */
119 class CssTransitionModifier extends Modifier {
120 /**
121 * @property el
122 * @type {(HTMLElement|undefined)}
123 * @private
124 * @readonly
125 */
126 get el() {
127 return this.clone || this.element;
128 }
129
130 /**
131 * @property didTransitionIn
132 * @type {(Function|undefined)}
133 * @private
134 */
135
136 constructor(owner, args) {
137 super(owner, args);
138 _defineProperty(this, "element", null);
139 _defineProperty(this, "clone", null);
140 _defineProperty(this, "parentElement", null);
141 _defineProperty(this, "nextElementSibling", null);
142 _defineProperty(this, "installed", false);
143 _defineProperty(this, "finishedTransitionIn", false);
144 _defineProperty(this, "isEnabled", true);
145 _defineProperty(this, "parentSelector", void 0);
146 _defineProperty(this, "didTransitionIn", void 0);
147 /**
148 * @property didTransitionOut
149 * @type {(Function|undefined)}
150 * @private
151 */
152 _defineProperty(this, "didTransitionOut", void 0);
153 /**
154 * @property transitionName
155 * @type {(String|undefined)}
156 * @private
157 */
158 _defineProperty(this, "transitionName", void 0);
159 /**
160 * @property enterClass
161 * @type {(String|undefined)}
162 * @private
163 */
164 _defineProperty(this, "enterClass", void 0);
165 /**
166 * @property enterActiveClass
167 * @type {(String|undefined)}
168 * @private
169 */
170 _defineProperty(this, "enterActiveClass", void 0);
171 /**
172 * @property enterToClass
173 * @type {(String|undefined)}
174 * @private
175 */
176 _defineProperty(this, "enterToClass", void 0);
177 /**
178 * @property leaveClass
179 * @type {(String|undefined)}
180 * @private
181 */
182 _defineProperty(this, "leaveClass", void 0);
183 /**
184 * @property leaveActiveClass
185 * @type {(String|undefined)}
186 * @private
187 */
188 _defineProperty(this, "leaveActiveClass", void 0);
189 /**
190 * @property leaveToClass
191 * @type {(String|undefined)}
192 * @private
193 */
194 _defineProperty(this, "leaveToClass", void 0);
195 registerDestructor(this, () => {
196 if (this.isEnabled === false || !this.finishedTransitionIn) {
197 return;
198 }
199 this.guardedRun(this.transitionOut);
200 });
201 }
202 modify(element, positional, named) {
203 this.element = element;
204 this.setupProperties(positional, named);
205 if (named.isEnabled === false || this.installed) {
206 return;
207 }
208 this.installed = true;
209 let el = this.getElementToClone();
210 this.parentElement = el.parentElement;
211 this.nextElementSibling = el.nextElementSibling;
212 this.guardedRun(this.transitionIn);
213 }
214 setupProperties(positional, named) {
215 this.isEnabled = named.isEnabled !== false;
216 this.transitionName = positional[0] || named.name;
217 this.didTransitionIn = named.didTransitionIn;
218 this.didTransitionOut = named.didTransitionOut;
219 this.parentSelector = named.parentSelector;
220 this.enterClass = named.enterClass || this.transitionName && `${this.transitionName}-enter`;
221 this.enterActiveClass = named.enterActiveClass || this.transitionName && `${this.transitionName}-enter-active`;
222 this.enterToClass = named.enterToClass || this.transitionName && `${this.transitionName}-enter-to`;
223 this.leaveClass = named.leaveClass || this.transitionName && `${this.transitionName}-leave`;
224 this.leaveActiveClass = named.leaveActiveClass || this.transitionName && `${this.transitionName}-leave-active`;
225 this.leaveToClass = named.leaveToClass || this.transitionName && `${this.transitionName}-leave-to`;
226 }
227
228 /**
229 * Adds a clone to the parentElement, so it can be transitioned out.
230 *
231 * @private
232 * @method addClone
233 */
234 addClone() {
235 let original = this.getElementToClone();
236 let parentElement = original.parentElement || this.parentElement;
237 let nextElementSibling = original.nextElementSibling || this.nextElementSibling;
238 if (nextElementSibling && nextElementSibling.parentElement !== parentElement) {
239 nextElementSibling = null;
240 }
241 let clone = original.cloneNode(true);
242 clone.setAttribute('id', `${original.id}_clone`);
243 parentElement.insertBefore(clone, nextElementSibling);
244 this.clone = clone;
245 }
246
247 /**
248 * Finds the correct element to clone. If `parentSelector` is present, we will
249 * use the closest parent element that matches that selector. Otherwise, we use
250 * the element's immediate parentElement directly.
251 *
252 * @private
253 * @method getElementToClone
254 */
255 getElementToClone() {
256 if (this.parentSelector) {
257 return this.element.closest(this.parentSelector);
258 } else {
259 return this.element;
260 }
261 }
262
263 /**
264 * Removes the clone from the parentElement
265 *
266 * @private
267 * @method removeClone
268 */
269 removeClone() {
270 if (this.clone.isConnected && this.clone.parentNode !== null) {
271 this.clone.parentNode.removeChild(this.clone);
272 }
273 }
274 *transitionIn() {
275 if (this.enterClass) {
276 yield* this.transition({
277 className: this.enterClass,
278 activeClassName: this.enterActiveClass,
279 toClassName: this.enterToClass
280 });
281 if (this.didTransitionIn) {
282 this.didTransitionIn();
283 }
284 }
285 this.finishedTransitionIn = true;
286 }
287 *transitionOut() {
288 if (this.leaveClass) {
289 // We can't stop ember from removing the element
290 // so we clone the element to animate it out
291 this.addClone();
292 yield nextTick();
293 yield* this.transition({
294 className: this.leaveClass,
295 activeClassName: this.leaveActiveClass,
296 toClassName: this.leaveToClass
297 });
298 this.removeClone();
299 if (this.didTransitionOut) {
300 this.didTransitionOut();
301 }
302 this.clone = null;
303 }
304 }
305
306 /**
307 * Transitions the element.
308 *
309 * @private
310 * @method transition
311 * @param {Object} args
312 * @param {String} args.className the class representing the starting state
313 * @param {String} args.activeClassName the class applied during the entire transition. This class can be used to define the duration, delay and easing curve.
314 * @param {String} args.toClassName the class representing the finished state
315 * @return {Generator}
316 */
317 *transition({
318 className,
319 activeClassName,
320 toClassName
321 }) {
322 let element = this.el;
323
324 // add first class right away
325 this.addClass(className);
326 this.addClass(activeClassName);
327 yield nextTick();
328
329 // This is for to force a repaint,
330 // which is necessary in order to transition styles when adding a class name.
331 element.scrollTop;
332
333 // after repaint
334 this.addClass(toClassName);
335 this.removeClass(className);
336
337 // wait for ember to apply classes
338 // set timeout for animation end
339 yield sleep(computeTimeout(element) || 0);
340 this.removeClass(toClassName);
341 this.removeClass(activeClassName);
342 }
343
344 /**
345 * Add classNames to el.
346 *
347 * @private
348 * @method addClass
349 * @param {String} className
350 */
351 addClass(className) {
352 this.el.classList.add(...className.trim().split(/\s+/));
353 }
354
355 /**
356 * Remove classNames from el.
357 *
358 * @private
359 * @method removeClass
360 * @param {String} className
361 */
362 removeClass(className) {
363 this.el.classList.remove(...className.trim().split(/\s+/));
364 }
365 async guardedRun(f, ...args) {
366 const token = waiter.beginAsync();
367 let gen = f.call(this, ...args);
368 let isDone = false;
369
370 // stop if the function doesn't have anything else to yield
371 // or if the element is no longer present
372 while (!isDone && this.el) {
373 let {
374 value,
375 done
376 } = gen.next();
377 isDone = done;
378 await value;
379 }
380 waiter.endAsync(token);
381 }
382 }
383 modifier = CssTransitionModifier;
384} else {
385 modifier = class modifier extends Modifier {
386 constructor(...args) {
387 super(...args);
388 _defineProperty(this, "clone", null);
389 _defineProperty(this, "parentElement", null);
390 _defineProperty(this, "nextElementSibling", null);
391 _defineProperty(this, "installed", false);
392 }
393 /**
394 * @property el
395 * @type {(HTMLElement|undefined)}
396 * @private
397 * @readonly
398 */
399 get el() {
400 return this.clone || this.element;
401 }
402
403 /**
404 * @property transitionName
405 * @type {(String|undefined)}
406 * @private
407 * @readonly
408 */
409 get transitionName() {
410 return this.args.positional[0] || this.args.named.name;
411 }
412
413 /**
414 * @property enterClass
415 * @type {(String|undefined)}
416 * @private
417 * @readonly
418 */
419 get enterClass() {
420 return this.args.named.enterClass || this.transitionName && `${this.transitionName}-enter`;
421 }
422
423 /**
424 * @property enterActiveClass
425 * @type {(String|undefined)}
426 * @private
427 * @readonly
428 */
429 get enterActiveClass() {
430 return this.args.named.enterActiveClass || this.transitionName && `${this.transitionName}-enter-active`;
431 }
432
433 /**
434 * @property enterToClass
435 * @type {(String|undefined)}
436 * @private
437 * @readonly
438 */
439 get enterToClass() {
440 return this.args.named.enterToClass || this.transitionName && `${this.transitionName}-enter-to`;
441 }
442
443 /**
444 * @property leaveClass
445 * @type {(String|undefined)}
446 * @private
447 * @readonly
448 */
449 get leaveClass() {
450 return this.args.named.leaveClass || this.transitionName && `${this.transitionName}-leave`;
451 }
452
453 /**
454 * @property leaveActiveClass
455 * @type {(String|undefined)}
456 * @private
457 * @readonly
458 */
459 get leaveActiveClass() {
460 return this.args.named.leaveActiveClass || this.transitionName && `${this.transitionName}-leave-active`;
461 }
462
463 /**
464 * @property leaveToClass
465 * @type {(String|undefined)}
466 * @private
467 * @readonly
468 */
469 get leaveToClass() {
470 return this.args.named.leaveToClass || this.transitionName && `${this.transitionName}-leave-to`;
471 }
472 didInstall() {
473 if (this.args.named.isEnabled === false) {
474 return;
475 }
476 let el = this.getElementToClone();
477 this.parentElement = el.parentElement;
478 this.nextElementSibling = el.nextElementSibling;
479 this.guardedRun(this.transitionIn);
480 }
481 willRemove() {
482 if (this.args.named.isEnabled === false || !this.installed) {
483 return;
484 }
485 this.guardedRun(this.transitionOut);
486 }
487
488 /**
489 * Adds a clone to the parentElement so it can be transitioned out
490 *
491 * @private
492 * @method addClone
493 */
494 addClone() {
495 let original = this.getElementToClone();
496 let parentElement = original.parentElement || this.parentElement;
497 let nextElementSibling = original.nextElementSibling || this.nextElementSibling;
498 if (nextElementSibling && nextElementSibling.parentElement !== parentElement) {
499 nextElementSibling = null;
500 }
501 let clone = original.cloneNode(true);
502 clone.setAttribute('id', `${original.id}_clone`);
503 parentElement.insertBefore(clone, nextElementSibling);
504 this.clone = clone;
505 }
506
507 /**
508 * Finds the correct element to clone. If `parentSelector` is present, we will
509 * use the closest parent element that matches that selector. Otherwise we use
510 * the element's immediate parentElement directly.
511 *
512 * @private
513 * @method getElementToClone
514 */
515 getElementToClone() {
516 if (this.args.named.parentSelector) {
517 return this.element.closest(this.args.named.parentSelector);
518 } else {
519 return this.element;
520 }
521 }
522
523 /**
524 * Removes the clone from the parentElement
525 *
526 * @private
527 * @method removeClone
528 */
529 removeClone() {
530 if (this.clone.isConnected && this.clone.parentNode !== null) {
531 this.clone.parentNode.removeChild(this.clone);
532 }
533 }
534 *transitionIn() {
535 if (this.enterClass) {
536 yield* this.transition({
537 className: this.enterClass,
538 activeClassName: this.enterActiveClass,
539 toClassName: this.enterToClass
540 });
541 if (this.args.named.didTransitionIn) {
542 this.args.named.didTransitionIn();
543 }
544 }
545 this.installed = true;
546 }
547 *transitionOut() {
548 if (this.leaveClass) {
549 // We can't stop ember from removing the element
550 // so we clone the element to animate it out
551 this.addClone();
552 yield nextTick();
553 yield* this.transition({
554 className: this.leaveClass,
555 activeClassName: this.leaveActiveClass,
556 toClassName: this.leaveToClass
557 });
558 this.removeClone();
559 if (this.args.named.didTransitionOut) {
560 this.args.named.didTransitionOut();
561 }
562 this.clone = null;
563 }
564 }
565
566 /**
567 * Transitions the element.
568 *
569 * @private
570 * @method transition
571 * @param {Object} args
572 * @param {String} args.className the class representing the starting state
573 * @param {String} args.activeClassName the class applied during the entire transition. This class can be used to define the duration, delay and easing curve.
574 * @param {String} args.toClassName the class representing the finished state
575 * @return {Generator}
576 */
577 *transition({
578 className,
579 activeClassName,
580 toClassName
581 }) {
582 let element = this.el;
583
584 // add first class right away
585 this.addClass(className);
586 this.addClass(activeClassName);
587 yield nextTick();
588
589 // This is for to force a repaint,
590 // which is necessary in order to transition styles when adding a class name.
591 element.scrollTop;
592
593 // after repaint
594 this.addClass(toClassName);
595 this.removeClass(className);
596
597 // wait for ember to apply classes
598 // set timeout for animation end
599 yield sleep(computeTimeout(element) || 0);
600 this.removeClass(toClassName);
601 this.removeClass(activeClassName);
602 }
603
604 /**
605 * Add classNames to el.
606 *
607 * @private
608 * @method addClass
609 * @param {String} className
610 */
611 addClass(className) {
612 this.el.classList.add(...className.trim().split(/\s+/));
613 }
614
615 /**
616 * Remove classNames from el.
617 *
618 * @private
619 * @method removeClass
620 * @param {String} className
621 */
622 removeClass(className) {
623 this.el.classList.remove(...className.trim().split(/\s+/));
624 }
625 async guardedRun(f, ...args) {
626 const token = waiter.beginAsync();
627 let gen = f.call(this, ...args);
628 let isDone = false;
629
630 // stop if the function doesn't have anything else to yield
631 // or if the element is no longer present
632 while (!isDone && this.el) {
633 let {
634 value,
635 done
636 } = gen.next();
637 isDone = done;
638 await value;
639 }
640 waiter.endAsync(token);
641 }
642 };
643}
644var modifier$1 = modifier;
645
646export { modifier$1 as default };
647//# sourceMappingURL=css-transition.js.map