Comparing version 0.2.4 to 0.2.5
@@ -1,126 +0,2 @@ | ||
/** | ||
* `dom-flip` | ||
* | ||
* FLIP move animations for web components. | ||
* | ||
* Wrap this around your child elements and they will slide smoothly over the screen | ||
* if their order or position changes. | ||
* | ||
* @example | ||
* ```html | ||
* <!-- The divs' positions will be animated if they change. --> | ||
* <dom-flip> | ||
* <div>Item 1</div | ||
* <div>Item 2</div | ||
* <div>Item 3</div | ||
* <div>Item 4</div | ||
* </dom-flip> | ||
* ``` | ||
* | ||
* @demo demo/index.html | ||
* @see https://aerotwist.com/blog/flip-your-animations/ | ||
*/ | ||
export default class DomFlip extends HTMLElement { | ||
static readonly observedAttributes: string[]; | ||
/** | ||
* Indicates whether the element will listen for changes and apply | ||
* animations. | ||
* | ||
* Disable this if you want to temporarily control the DOM-animations yourself. | ||
* Defaults to `true`. | ||
* | ||
* @type Boolean | ||
*/ | ||
active: boolean; | ||
/** | ||
* The name of the attribute to use as model ID. | ||
* | ||
* Defaults to `data-flip-id`. | ||
* | ||
* @type String | ||
*/ | ||
attrName: string; | ||
/** | ||
* The CSS animation delay in milliseconds. | ||
* | ||
* Defaults to 0ms. | ||
* | ||
* @type Number | ||
*/ | ||
delayMs: number; | ||
/** | ||
* The CSS animation duration in milliseconds. | ||
* | ||
* Defaults to 200ms. | ||
* | ||
* @type Number | ||
*/ | ||
durationMs: number; | ||
/** | ||
* The CSS animation easing mode to use. | ||
* | ||
* Defaults to `ease-in-out`. | ||
* | ||
* @type String | ||
*/ | ||
easing: string; | ||
/** | ||
* The class name to apply when the elements are moving. This | ||
* only need be changed in case of conflicts. | ||
* | ||
* Defaults to `dom-flip-transitioning`. | ||
* | ||
* @type String | ||
*/ | ||
transitionClassName: string; | ||
/** | ||
* Whether a dom change event handler is enqueued for the current animation frame. | ||
*/ | ||
private _animationEnqueued; | ||
/** | ||
* The last known client rects of the children keyed by their ID. | ||
*/ | ||
private _childData; | ||
/** | ||
* The mutation observer listening for changes to the attributes of the child elements. | ||
*/ | ||
private _childObserver; | ||
/** | ||
* The shadow slot containing the children. | ||
*/ | ||
private _slot; | ||
/** | ||
* The bound event handler for mutation observer updating. | ||
*/ | ||
private _mutationObserverUpdateHandler; | ||
constructor(); | ||
connectedCallback(): void; | ||
attributeChangedCallback(name: string, oldValue: string, newValue: string): void; | ||
/** | ||
* Animates the transition of the elements that have moved. | ||
*/ | ||
private _animateChangedElements(); | ||
/** | ||
* Goes through the node's children and collects styling metadata in a map. | ||
* | ||
* @returns A map with styling data by child ID. | ||
*/ | ||
private _collectChildData(); | ||
/** | ||
* Enqueues the animation of moved elements at animation frame timing. | ||
*/ | ||
private _enqueueAnimateChangedElements(); | ||
/** | ||
* Updates the registered event handlers, mutation observers and triggers animation.. | ||
*/ | ||
private _updateListeners(); | ||
/** | ||
* Updates the mutation observer configuration and collects child position data, | ||
* if necessary. | ||
*/ | ||
private _updateMutationObserver(); | ||
/** | ||
* Render the shadow dom contents. | ||
*/ | ||
private _render(); | ||
} | ||
import DomFlip from './element'; | ||
export default DomFlip; |
@@ -1,284 +0,4 @@ | ||
/** | ||
* Generates a CSS `translate`-rule compatible string that does a 2D transform. | ||
* | ||
* @param {number} dx the X delta | ||
* @param {number} dy the Y delta | ||
* @param {number} sx the X scale | ||
* @param {number} sy the Y scale | ||
* @return {string} the CSS rule | ||
*/ | ||
const generateTransformString = (dx, dy, sx, sy) => `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`; | ||
/** | ||
* Determines whether the actual number lies within a close margin to the | ||
* target number. | ||
* | ||
* @param {number} actual The number to check. | ||
* @param {number} target The target number. | ||
* @param {number} epsilon The allowed margin of error. Defaults to 1e-5. | ||
*/ | ||
const isCloseTo = (actual, target, epsilon = 1e-5) => Math.abs(actual - target) <= epsilon; | ||
/** | ||
* The regex used to parse the transform matrix string. | ||
*/ | ||
const transformRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*(-?\d*\.?\d+),\s*0,\s*0\)/; | ||
/** | ||
* `dom-flip` | ||
* | ||
* FLIP move animations for web components. | ||
* | ||
* Wrap this around your child elements and they will slide smoothly over the screen | ||
* if their order or position changes. | ||
* | ||
* @example | ||
* ```html | ||
* <!-- The divs' positions will be animated if they change. --> | ||
* <dom-flip> | ||
* <div>Item 1</div | ||
* <div>Item 2</div | ||
* <div>Item 3</div | ||
* <div>Item 4</div | ||
* </dom-flip> | ||
* ``` | ||
* | ||
* @demo demo/index.html | ||
* @see https://aerotwist.com/blog/flip-your-animations/ | ||
*/ | ||
export default class DomFlip extends HTMLElement { | ||
constructor() { | ||
super(); | ||
/** | ||
* Indicates whether the element will listen for changes and apply | ||
* animations. | ||
* | ||
* Disable this if you want to temporarily control the DOM-animations yourself. | ||
* Defaults to `true`. | ||
* | ||
* @type Boolean | ||
*/ | ||
this.active = true; | ||
/** | ||
* The name of the attribute to use as model ID. | ||
* | ||
* Defaults to `data-flip-id`. | ||
* | ||
* @type String | ||
*/ | ||
this.attrName = 'data-flip-id'; | ||
/** | ||
* The CSS animation delay in milliseconds. | ||
* | ||
* Defaults to 0ms. | ||
* | ||
* @type Number | ||
*/ | ||
this.delayMs = 0; | ||
/** | ||
* The CSS animation duration in milliseconds. | ||
* | ||
* Defaults to 200ms. | ||
* | ||
* @type Number | ||
*/ | ||
this.durationMs = 200; | ||
/** | ||
* The CSS animation easing mode to use. | ||
* | ||
* Defaults to `ease-in-out`. | ||
* | ||
* @type String | ||
*/ | ||
this.easing = 'ease-in-out'; | ||
/** | ||
* The class name to apply when the elements are moving. This | ||
* only need be changed in case of conflicts. | ||
* | ||
* Defaults to `dom-flip-transitioning`. | ||
* | ||
* @type String | ||
*/ | ||
this.transitionClassName = 'dom-flip-transitioning'; | ||
/** | ||
* Whether a dom change event handler is enqueued for the current animation frame. | ||
*/ | ||
this._animationEnqueued = false; | ||
/** | ||
* The last known client rects of the children keyed by their ID. | ||
*/ | ||
this._childData = new Map(); | ||
this._childObserver = new MutationObserver(() => this._enqueueAnimateChangedElements()); | ||
this._mutationObserverUpdateHandler = () => this._updateMutationObserver(); | ||
this.attachShadow({ mode: 'open' }); | ||
} | ||
static get observedAttributes() { | ||
return [ | ||
"active" /* Active */, | ||
"attr-name" /* AttrName */, | ||
"delay-ms" /* DelayMs */, | ||
"duration-ms" /* DurationMs */, | ||
"easing" /* Easing */, | ||
"transition-class-name" /* TransitionClassName */, | ||
]; | ||
} | ||
connectedCallback() { | ||
this._render(); | ||
this._slot = this.shadowRoot.querySelector('slot'); | ||
this._updateListeners(); | ||
} | ||
attributeChangedCallback(name, oldValue, newValue) { | ||
switch (name) { | ||
case "active" /* Active */: | ||
this.active = !!newValue; | ||
this._updateListeners(); | ||
break; | ||
case "attr-name" /* AttrName */: | ||
this.attrName = newValue; | ||
this._updateListeners(); | ||
break; | ||
case "delay-ms" /* DelayMs */: | ||
this.delayMs = Number(newValue); | ||
this._render(); | ||
break; | ||
case "duration-ms" /* DurationMs */: | ||
this.durationMs = Number(newValue); | ||
this._render(); | ||
break; | ||
case "easing" /* Easing */: | ||
this.easing = newValue; | ||
this._render(); | ||
break; | ||
case "transition-class-name" /* TransitionClassName */: | ||
this.transitionClassName = newValue; | ||
this._render(); | ||
break; | ||
} | ||
} | ||
/** | ||
* Animates the transition of the elements that have moved. | ||
*/ | ||
_animateChangedElements() { | ||
const newChildData = this._collectChildData(); | ||
for (const [id, [el, n]] of newChildData.entries()) { | ||
const oldChildData = this._childData.get(id); | ||
if (!oldChildData) { | ||
continue; | ||
} | ||
const [_, old] = oldChildData; | ||
const dT = old.top - n.top; | ||
const dL = old.left - n.left; | ||
// Animate only if there have been changes | ||
if (isCloseTo(dT, 0) && | ||
isCloseTo(dL, 0) && | ||
isCloseTo(old.scaleX / n.scaleX, 1) && | ||
isCloseTo(old.scaleY / n.scaleY, 1)) { | ||
continue; | ||
} | ||
el.classList.remove(this.transitionClassName); | ||
// Revert new layout into old positions | ||
el.style.opacity = String(old.opacity); | ||
el.style.transform = generateTransformString(dL, dT, old.scaleX, old.scaleY); | ||
requestAnimationFrame(() => { | ||
el.classList.add(this.transitionClassName); | ||
// Remove our reverts and let animation play | ||
el.style.opacity = String(n.opacity); | ||
el.style.transform = generateTransformString(0, 0, n.scaleX, n.scaleY); | ||
setTimeout(() => el.classList.remove(this.transitionClassName), this.durationMs); | ||
}); | ||
} | ||
this._childData = newChildData; | ||
} | ||
/** | ||
* Goes through the node's children and collects styling metadata in a map. | ||
* | ||
* @returns A map with styling data by child ID. | ||
*/ | ||
_collectChildData() { | ||
const bbox = this.getBoundingClientRect(); | ||
const map = new Map(); | ||
for (const el of this._slot.assignedNodes()) { | ||
if (!(el instanceof HTMLElement)) { | ||
continue; | ||
} | ||
const id = el.getAttribute(this.attrName); | ||
if (!id) { | ||
continue; | ||
} | ||
const elemBox = el.getBoundingClientRect(); | ||
const styles = window.getComputedStyle(el); | ||
let scaleX = '1.0'; | ||
let scaleY = '1.0'; | ||
const matches = styles.transform.match(transformRegex); | ||
if (matches) { | ||
[, scaleX, scaleY] = matches; | ||
} | ||
const data = { | ||
top: elemBox.top - bbox.top, | ||
left: elemBox.left - bbox.left, | ||
opacity: Number.parseFloat(styles.opacity || '1'), | ||
scaleX: Number.parseFloat(scaleX), | ||
scaleY: Number.parseFloat(scaleY), | ||
}; | ||
map.set(id, [el, data]); | ||
} | ||
return map; | ||
} | ||
/** | ||
* Enqueues the animation of moved elements at animation frame timing. | ||
*/ | ||
_enqueueAnimateChangedElements() { | ||
if (this._animationEnqueued) { | ||
return; | ||
} | ||
this._animationEnqueued = true; | ||
// Render at microtask timing to prevent Safari flickers | ||
Promise.resolve().then(() => { | ||
this._animationEnqueued = false; | ||
this._animateChangedElements(); | ||
}); | ||
} | ||
/** | ||
* Updates the registered event handlers, mutation observers and triggers animation.. | ||
*/ | ||
_updateListeners() { | ||
this.removeEventListener('dom-change', this._mutationObserverUpdateHandler); | ||
this._slot.removeEventListener('slotchange', this._mutationObserverUpdateHandler); | ||
if (this.active) { | ||
this.addEventListener('dom-change', this._mutationObserverUpdateHandler); | ||
this._slot.addEventListener('slotchange', this._mutationObserverUpdateHandler); | ||
} | ||
this._updateMutationObserver(); | ||
} | ||
/** | ||
* Updates the mutation observer configuration and collects child position data, | ||
* if necessary. | ||
*/ | ||
_updateMutationObserver() { | ||
this._childObserver.disconnect(); | ||
if (!this.active) { | ||
return; | ||
} | ||
this._slot.assignedNodes() | ||
.filter(el => el instanceof HTMLElement) | ||
.forEach(child => this._childObserver.observe(child, { | ||
attributes: true, | ||
attributeFilter: [this.attrName], | ||
})); | ||
this._enqueueAnimateChangedElements(); | ||
} | ||
/** | ||
* Render the shadow dom contents. | ||
*/ | ||
_render() { | ||
this.shadowRoot.innerHTML = ` | ||
<style> | ||
::slotted(.${this.transitionClassName}) { | ||
transition: transform ${this.durationMs}ms ${this.easing} ${this.delayMs}ms, | ||
opacity ${this.durationMs}ms ${this.easing} ${this.delayMs}ms; | ||
} | ||
</style> | ||
<slot></slot> | ||
`; | ||
} | ||
} | ||
import DomFlip from './element'; | ||
customElements.define('dom-flip', DomFlip); | ||
export default DomFlip; | ||
//# sourceMappingURL=dom-flip.js.map |
{ | ||
"name": "dom-flip", | ||
"version": "0.2.4", | ||
"version": "0.2.5", | ||
"description": "Smooth position animation for web components", | ||
@@ -5,0 +5,0 @@ "main": "dist/dom-flip.js", |
# \<dom-flip\> | ||
[![Build Status](https://travis-ci.org/Festify/dom-flip.svg?branch=master)](https://travis-ci.org/Festify/dom-flip) | ||
[![Build Status](https://travis-ci.org/NeoLegends/dom-flip.svg?branch=master)](https://travis-ci.org/NeoLegends/dom-flip) | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/Festify/dom-flip.svg)](https://greenkeeper.io/) | ||
@@ -15,3 +15,3 @@ [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/Festify/dom-flip) | ||
## Usage | ||
You can use this element together with any templatizing element that modifies the DOM. The animated elements must be direct children of the `dom-flip` element. | ||
You can use this element together with any element that modifies the DOM. The animated elements must be direct children of the `dom-flip` element. | ||
@@ -51,3 +51,3 @@ To be able to correlate changes in the model to changes to the DOM, this element requires that you give every element a unique ID. This must be an attribute on the element itself and cannot be a property (because properties cannot be observed via MutationObserver). | ||
// ... | ||
// ... next animation frame | ||
@@ -68,3 +68,9 @@ // Change their order | ||
### Automatic registration | ||
You can import the custom element class from `dom-flip/element` if you don't want it to automatically be registered within the custom elements registry. | ||
## Performance | ||
The element is designed to avoid layout thrashing as much as possible by batching work into microtasks and to animation frame timing. | ||
## License | ||
MIT |
@@ -20,4 +20,4 @@ { | ||
"files": [ | ||
"dom-flip.ts" | ||
"src/dom-flip.ts" | ||
] | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47565
16
834
73
1