quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
995 lines (864 loc) • 35.3 kB
JavaScript
import { isObject } from '../is/is.js'
let id = 0
let offsetBase = void 0
function getAbsolutePosition (el, resize) {
if (offsetBase === void 0) {
offsetBase = document.createElement('div')
offsetBase.style.cssText = 'position: absolute; left: 0; top: 0'
document.body.appendChild(offsetBase)
}
const boundingRect = el.getBoundingClientRect()
const baseRect = offsetBase.getBoundingClientRect()
const { marginLeft, marginRight, marginTop, marginBottom } = window.getComputedStyle(el)
const marginH = parseInt(marginLeft, 10) + parseInt(marginRight, 10)
const marginV = parseInt(marginTop, 10) + parseInt(marginBottom, 10)
return {
left: boundingRect.left - baseRect.left,
top: boundingRect.top - baseRect.top,
width: boundingRect.right - boundingRect.left,
height: boundingRect.bottom - boundingRect.top,
widthM: boundingRect.right - boundingRect.left + (resize === true ? 0 : marginH),
heightM: boundingRect.bottom - boundingRect.top + (resize === true ? 0 : marginV),
marginH: resize === true ? marginH : 0,
marginV: resize === true ? marginV : 0
}
}
function getAbsoluteSize (el) {
return {
width: el.scrollWidth,
height: el.scrollHeight
}
}
// firefox rulez
const styleEdges = [ 'Top', 'Right', 'Bottom', 'Left' ]
const styleBorderRadiuses = [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ]
const reStyleSkipKey = /-block|-inline|block-|inline-/
const reStyleSkipRule = /(-block|-inline|block-|inline-).*:/
function getComputedStyle (el, props) {
const style = window.getComputedStyle(el)
const fixed = {}
for (let i = 0; i < props.length; i++) {
const prop = props[ i ]
if (style[ prop ] === '') {
if (prop === 'cssText') {
const styleLen = style.length
let val = ''
for (let i = 0; i < styleLen; i++) {
if (reStyleSkipKey.test(style[ i ]) !== true) {
val += style[ i ] + ': ' + style[ style[ i ] ] + '; '
}
}
fixed[ prop ] = val
}
else if ([ 'borderWidth', 'borderStyle', 'borderColor' ].indexOf(prop) !== -1) {
const suffix = prop.replace('border', '')
let val = ''
for (let j = 0; j < styleEdges.length; j++) {
const subProp = 'border' + styleEdges[ j ] + suffix
val += style[ subProp ] + ' '
}
fixed[ prop ] = val
}
else if (prop === 'borderRadius') {
let val1 = ''
let val2 = ''
for (let j = 0; j < styleBorderRadiuses.length; j++) {
const val = style[ styleBorderRadiuses[ j ] ].split(' ')
val1 += val[ 0 ] + ' '
val2 += (val[ 1 ] === void 0 ? val[ 0 ] : val[ 1 ]) + ' '
}
fixed[ prop ] = val1 + '/ ' + val2
}
else {
fixed[ prop ] = style[ prop ]
}
}
else {
if (prop === 'cssText') {
fixed[ prop ] = style[ prop ]
.split(';')
.filter(val => reStyleSkipRule.test(val) !== true)
.join(';')
}
else {
fixed[ prop ] = style[ prop ]
}
}
}
return fixed
}
const zIndexPositions = [ 'absolute', 'fixed', 'relative', 'sticky' ]
function getMaxZIndex (elStart) {
let el = elStart
let maxIndex = 0
while (el !== null && el !== document) {
const { position, zIndex } = window.getComputedStyle(el)
const zIndexNum = Number(zIndex)
if (
zIndexNum > maxIndex
&& (el === elStart || zIndexPositions.includes(position) === true)
) {
maxIndex = zIndexNum
}
el = el.parentNode
}
return maxIndex
}
function normalizeElements (opts) {
return {
from: opts.from,
to: opts.to !== void 0
? opts.to
: opts.from
}
}
function normalizeOptions (options) {
if (typeof options === 'number') {
options = {
duration: options
}
}
else if (typeof options === 'function') {
options = {
onEnd: options
}
}
return {
...options,
waitFor: options.waitFor === void 0 ? 0 : options.waitFor,
duration: isNaN(options.duration) === true ? 300 : parseInt(options.duration, 10),
easing: typeof options.easing === 'string' && options.easing.length !== 0 ? options.easing : 'ease-in-out',
delay: isNaN(options.delay) === true ? 0 : parseInt(options.delay, 10),
fill: typeof options.fill === 'string' && options.fill.length !== 0 ? options.fill : 'none',
resize: options.resize === true,
// account for UMD too where modifiers will be lowercased to work
useCSS: options.useCSS === true || options.usecss === true,
// account for UMD too where modifiers will be lowercased to work
hideFromClone: options.hideFromClone === true || options.hidefromclone === true,
// account for UMD too where modifiers will be lowercased to work
keepToClone: options.keepToClone === true || options.keeptoclone === true,
tween: options.tween === true,
tweenFromOpacity: isNaN(options.tweenFromOpacity) === true ? 0.6 : parseFloat(options.tweenFromOpacity),
tweenToOpacity: isNaN(options.tweenToOpacity) === true ? 0.5 : parseFloat(options.tweenToOpacity)
}
}
function getElement (element) {
const type = typeof element
return type === 'function'
? element()
: (
type === 'string'
? document.querySelector(element)
: element
)
}
function isValidElement (element) {
return element
&& element.ownerDocument === document
&& element.parentNode !== null
}
export default function morph (_options) {
let cancel = () => false
let cancelStatus = false
let endElementTo = true
const elements = normalizeElements(_options)
const options = normalizeOptions(_options)
const elFrom = getElement(elements.from)
if (isValidElement(elFrom) !== true) {
// we return a cancel function that return false, meaning the cancel function failed
return cancel
}
// we clean other morphs running on this element
typeof elFrom.qMorphCancel === 'function' && elFrom.qMorphCancel()
let animationFromClone = void 0
let animationFromTween = void 0
let animationToClone = void 0
let animationTo = void 0
const elFromParent = elFrom.parentNode
const elFromNext = elFrom.nextElementSibling
// we get the dimensions and characteristics
// of the parent of the initial element before changes
const elFromPosition = getAbsolutePosition(elFrom, options.resize)
const {
width: elFromParentWidthBefore,
height: elFromParentHeightBefore
} = getAbsoluteSize(elFromParent)
const {
borderWidth: elFromBorderWidth,
borderStyle: elFromBorderStyle,
borderColor: elFromBorderColor,
borderRadius: elFromBorderRadius,
backgroundColor: elFromBackground,
transform: elFromTransform,
position: elFromPositioningType,
cssText: elFromCssText
} = getComputedStyle(elFrom, [ 'borderWidth', 'borderStyle', 'borderColor', 'borderRadius', 'backgroundColor', 'transform', 'position', 'cssText' ])
const elFromClassSaved = elFrom.classList.toString()
const elFromStyleSaved = elFrom.style.cssText
// we make a clone of the initial element and
// use it to display until the final element is ready
// and to change the occupied space during animation
const elFromClone = elFrom.cloneNode(true)
const elFromTween = options.tween === true ? elFrom.cloneNode(true) : void 0
if (elFromTween !== void 0) {
elFromTween.className = elFromTween.classList.toString().split(' ').filter(c => /^bg-/.test(c) === false).join(' ')
}
// if the initial element is not going to be removed do not show the placeholder
options.hideFromClone === true && elFromClone.classList.add('q-morph--internal')
// prevent interaction with placeholder
elFromClone.setAttribute('aria-hidden', 'true')
elFromClone.style.transition = 'none'
elFromClone.style.animation = 'none'
elFromClone.style.pointerEvents = 'none'
elFromParent.insertBefore(elFromClone, elFromNext)
// we mark the element with its cleanup function
elFrom.qMorphCancel = () => {
cancelStatus = true
// we clean the clone of the initial element
elFromClone.remove()
elFromTween !== void 0 && elFromTween.remove()
options.hideFromClone === true && elFromClone.classList.remove('q-morph--internal')
// we remove the cleanup function from the element
elFrom.qMorphCancel = void 0
}
// will be called after Vue catches up with the changes done by _options.onToggle() function
const calculateFinalState = () => {
const elTo = getElement(elements.to)
if (cancelStatus === true || isValidElement(elTo) !== true) {
typeof elFrom.qMorphCancel === 'function' && elFrom.qMorphCancel()
return
}
// we clean other morphs running on this element
elFrom !== elTo && typeof elTo.qMorphCancel === 'function' && elTo.qMorphCancel()
// we hide the final element and the clone of the initial element
// we don't hide the final element if we want both it and the animated one visible
options.keepToClone !== true && elTo.classList.add('q-morph--internal')
elFromClone.classList.add('q-morph--internal')
// we get the dimensions of the parent of the initial element after changes
// the difference is how much we should animate the clone
const {
width: elFromParentWidthAfter,
height: elFromParentHeightAfter
} = getAbsoluteSize(elFromParent)
// we get the dimensions of the parent of the final element before changes
const {
width: elToParentWidthBefore,
height: elToParentHeightBefore
} = getAbsoluteSize(elTo.parentNode)
// then we show the clone of the initial element if we don't want it hidden
options.hideFromClone !== true && elFromClone.classList.remove('q-morph--internal')
// we mark the element with its cleanup function
elTo.qMorphCancel = () => {
cancelStatus = true
// we clean the clone of the initial element
elFromClone.remove()
elFromTween !== void 0 && elFromTween.remove()
options.hideFromClone === true && elFromClone.classList.remove('q-morph--internal')
// we show the final element
options.keepToClone !== true && elTo.classList.remove('q-morph--internal')
// we remove the cleanup function from the elements
elFrom.qMorphCancel = void 0
elTo.qMorphCancel = void 0
}
// will be called after waitFor (give time to render the final element)
const animate = () => {
if (cancelStatus === true) {
typeof elTo.qMorphCancel === 'function' && elTo.qMorphCancel()
return
}
// now the animation starts, so we only need the clone
// of the initial element as a spacer
// we also hide it to calculate the dimensions of the
// parent of the final element after the changes
if (options.hideFromClone !== true) {
elFromClone.classList.add('q-morph--internal')
elFromClone.innerHTML = ''
elFromClone.style.left = 0
elFromClone.style.right = 'unset'
elFromClone.style.top = 0
elFromClone.style.bottom = 'unset'
elFromClone.style.transform = 'none'
}
// we show the final element
if (options.keepToClone !== true) {
elTo.classList.remove('q-morph--internal')
}
// we get the dimensions of the parent of the final element after changes
// the difference is how much we should animate the clone
const elToParent = elTo.parentNode
const {
width: elToParentWidthAfter,
height: elToParentHeightAfter
} = getAbsoluteSize(elToParent)
const elToClone = elTo.cloneNode(options.keepToClone)
elToClone.setAttribute('aria-hidden', 'true')
if (options.keepToClone !== true) {
elToClone.style.left = 0
elToClone.style.right = 'unset'
elToClone.style.top = 0
elToClone.style.bottom = 'unset'
elToClone.style.transform = 'none'
elToClone.style.pointerEvents = 'none'
}
elToClone.classList.add('q-morph--internal')
// if elFrom is the same as elTo the next element is elFromClone
const elToNext = elTo === elFrom && elFromParent === elToParent ? elFromClone : elTo.nextElementSibling
elToParent.insertBefore(elToClone, elToNext)
const {
borderWidth: elToBorderWidth,
borderStyle: elToBorderStyle,
borderColor: elToBorderColor,
borderRadius: elToBorderRadius,
backgroundColor: elToBackground,
transform: elToTransform,
position: elToPositioningType,
cssText: elToCssText
} = getComputedStyle(elTo, [ 'borderWidth', 'borderStyle', 'borderColor', 'borderRadius', 'backgroundColor', 'transform', 'position', 'cssText' ])
const elToClassSaved = elTo.classList.toString()
const elToStyleSaved = elTo.style.cssText
// we set the computed styles on the element (to be able to remove classes)
elTo.style.cssText = elToCssText
elTo.style.transform = 'none'
elTo.style.animation = 'none'
elTo.style.transition = 'none'
// we strip the background classes (background color can no longer be animated if !important is used)
elTo.className = elToClassSaved.split(' ').filter(c => /^bg-/.test(c) === false).join(' ')
const elToPosition = getAbsolutePosition(elTo, options.resize)
const deltaX = elFromPosition.left - elToPosition.left
const deltaY = elFromPosition.top - elToPosition.top
const scaleX = elFromPosition.width / (elToPosition.width > 0 ? elToPosition.width : 10)
const scaleY = elFromPosition.height / (elToPosition.height > 0 ? elToPosition.height : 100)
const elFromParentWidthDiff = elFromParentWidthBefore - elFromParentWidthAfter
const elFromParentHeightDiff = elFromParentHeightBefore - elFromParentHeightAfter
const elToParentWidthDiff = elToParentWidthAfter - elToParentWidthBefore
const elToParentHeightDiff = elToParentHeightAfter - elToParentHeightBefore
const elFromCloneWidth = Math.max(elFromPosition.widthM, elFromParentWidthDiff)
const elFromCloneHeight = Math.max(elFromPosition.heightM, elFromParentHeightDiff)
const elToCloneWidth = Math.max(elToPosition.widthM, elToParentWidthDiff)
const elToCloneHeight = Math.max(elToPosition.heightM, elToParentHeightDiff)
const elSharedSize = elFrom === elTo
&& [ 'absolute', 'fixed' ].includes(elToPositioningType) === false
&& [ 'absolute', 'fixed' ].includes(elFromPositioningType) === false
// if the final element has fixed position or if a parent
// has fixed position we need to animate it as fixed
let elToNeedsFixedPosition = elToPositioningType === 'fixed'
let parent = elToParent
while (elToNeedsFixedPosition !== true && parent !== document) {
elToNeedsFixedPosition = window.getComputedStyle(parent).position === 'fixed'
parent = parent.parentNode
}
// we show the spacer for the initial element
if (options.hideFromClone !== true) {
elFromClone.style.display = 'block'
elFromClone.style.flex = '0 0 auto'
elFromClone.style.opacity = 0
elFromClone.style.minWidth = 'unset'
elFromClone.style.maxWidth = 'unset'
elFromClone.style.minHeight = 'unset'
elFromClone.style.maxHeight = 'unset'
elFromClone.classList.remove('q-morph--internal')
}
// we show the spacer for the final element
if (options.keepToClone !== true) {
elToClone.style.display = 'block'
elToClone.style.flex = '0 0 auto'
elToClone.style.opacity = 0
elToClone.style.minWidth = 'unset'
elToClone.style.maxWidth = 'unset'
elToClone.style.minHeight = 'unset'
elToClone.style.maxHeight = 'unset'
}
elToClone.classList.remove('q-morph--internal')
// we apply classes specified by user
if (typeof options.classes === 'string') {
elTo.className += ' ' + options.classes
}
// we apply styles specified by user
if (typeof options.style === 'string') {
elTo.style.cssText += ' ' + options.style
}
else if (isObject(options.style) === true) {
for (const prop in options.style) {
elTo.style[ prop ] = options.style[ prop ]
}
}
const elFromZIndex = getMaxZIndex(elFromClone)
const elToZIndex = getMaxZIndex(elTo)
// we position the morphing element
// if we use fixed position for the final element we need to adjust for scroll
const documentScroll = elToNeedsFixedPosition === true
? document.documentElement
: { scrollLeft: 0, scrollTop: 0 }
elTo.style.position = elToNeedsFixedPosition === true ? 'fixed' : 'absolute'
elTo.style.left = `${ elToPosition.left - documentScroll.scrollLeft }px`
elTo.style.right = 'unset'
elTo.style.top = `${ elToPosition.top - documentScroll.scrollTop }px`
elTo.style.margin = 0
if (options.resize === true) {
elTo.style.minWidth = 'unset'
elTo.style.maxWidth = 'unset'
elTo.style.minHeight = 'unset'
elTo.style.maxHeight = 'unset'
elTo.style.overflow = 'hidden'
elTo.style.overflowX = 'hidden'
elTo.style.overflowY = 'hidden'
}
document.body.appendChild(elTo)
if (elFromTween !== void 0) {
elFromTween.style.cssText = elFromCssText
elFromTween.style.transform = 'none'
elFromTween.style.animation = 'none'
elFromTween.style.transition = 'none'
elFromTween.style.position = elTo.style.position
elFromTween.style.left = `${ elFromPosition.left - documentScroll.scrollLeft }px`
elFromTween.style.right = 'unset'
elFromTween.style.top = `${ elFromPosition.top - documentScroll.scrollTop }px`
elFromTween.style.margin = 0
elFromTween.style.pointerEvents = 'none'
if (options.resize === true) {
elFromTween.style.minWidth = 'unset'
elFromTween.style.maxWidth = 'unset'
elFromTween.style.minHeight = 'unset'
elFromTween.style.maxHeight = 'unset'
elFromTween.style.overflow = 'hidden'
elFromTween.style.overflowX = 'hidden'
elFromTween.style.overflowY = 'hidden'
}
document.body.appendChild(elFromTween)
}
const commonCleanup = aborted => {
// we put the element back in it's place
// and restore the styles and classes
if (elFrom === elTo && endElementTo !== true) {
elTo.style.cssText = elFromStyleSaved
elTo.className = elFromClassSaved
}
else {
elTo.style.cssText = elToStyleSaved
elTo.className = elToClassSaved
}
elToClone.parentNode === elToParent && elToParent.insertBefore(elTo, elToClone)
// we clean the spacers
elFromClone.remove()
elToClone.remove()
elFromTween !== void 0 && elFromTween.remove()
// cancel will be no longer available
cancel = () => false
elFrom.qMorphCancel = void 0
elTo.qMorphCancel = void 0
// we are ready
if (typeof options.onEnd === 'function') {
options.onEnd(endElementTo === true ? 'to' : 'from', aborted === true)
}
}
if (options.useCSS !== true && typeof elTo.animate === 'function') {
const resizeFrom = options.resize === true
? {
transform: `translate(${ deltaX }px, ${ deltaY }px)`,
width: `${ elFromCloneWidth }px`,
height: `${ elFromCloneHeight }px`
}
: {
transform: `translate(${ deltaX }px, ${ deltaY }px) scale(${ scaleX }, ${ scaleY })`
}
const resizeTo = options.resize === true
? {
width: `${ elToCloneWidth }px`,
height: `${ elToCloneHeight }px`
}
: {}
const resizeFromTween = options.resize === true
? {
width: `${ elFromCloneWidth }px`,
height: `${ elFromCloneHeight }px`
}
: {}
const resizeToTween = options.resize === true
? {
transform: `translate(${ -1 * deltaX }px, ${ -1 * deltaY }px)`,
width: `${ elToCloneWidth }px`,
height: `${ elToCloneHeight }px`
}
: {
transform: `translate(${ -1 * deltaX }px, ${ -1 * deltaY }px) scale(${ 1 / scaleX }, ${ 1 / scaleY })`
}
const tweenFrom = elFromTween !== void 0
? { opacity: options.tweenToOpacity }
: { backgroundColor: elFromBackground }
const tweenTo = elFromTween !== void 0
? { opacity: 1 }
: { backgroundColor: elToBackground }
animationTo = elTo.animate([
{
margin: 0,
borderWidth: elFromBorderWidth,
borderStyle: elFromBorderStyle,
borderColor: elFromBorderColor,
borderRadius: elFromBorderRadius,
zIndex: elFromZIndex,
transformOrigin: '0 0',
...resizeFrom,
...tweenFrom
},
{
margin: 0,
borderWidth: elToBorderWidth,
borderStyle: elToBorderStyle,
borderColor: elToBorderColor,
borderRadius: elToBorderRadius,
zIndex: elToZIndex,
transformOrigin: '0 0',
transform: elToTransform,
...resizeTo,
...tweenTo
}
], {
duration: options.duration,
easing: options.easing,
fill: options.fill,
delay: options.delay
})
animationFromTween = elFromTween === void 0 ? void 0 : elFromTween.animate([
{
opacity: options.tweenFromOpacity,
margin: 0,
borderWidth: elFromBorderWidth,
borderStyle: elFromBorderStyle,
borderColor: elFromBorderColor,
borderRadius: elFromBorderRadius,
zIndex: elFromZIndex,
transformOrigin: '0 0',
transform: elFromTransform,
...resizeFromTween
},
{
opacity: 0,
margin: 0,
borderWidth: elToBorderWidth,
borderStyle: elToBorderStyle,
borderColor: elToBorderColor,
borderRadius: elToBorderRadius,
zIndex: elToZIndex,
transformOrigin: '0 0',
...resizeToTween
}
], {
duration: options.duration,
easing: options.easing,
fill: options.fill,
delay: options.delay
})
animationFromClone = options.hideFromClone === true || elSharedSize === true ? void 0 : elFromClone.animate([
{
margin: `${ elFromParentHeightDiff < 0 ? elFromParentHeightDiff / 2 : 0 }px ${ elFromParentWidthDiff < 0 ? elFromParentWidthDiff / 2 : 0 }px`,
width: `${ elFromCloneWidth + elFromPosition.marginH }px`,
height: `${ elFromCloneHeight + elFromPosition.marginV }px`
},
{
margin: 0,
width: 0,
height: 0
}
], {
duration: options.duration,
easing: options.easing,
fill: options.fill,
delay: options.delay
})
animationToClone = options.keepToClone === true ? void 0 : elToClone.animate([
elSharedSize === true
? {
margin: `${ elFromParentHeightDiff < 0 ? elFromParentHeightDiff / 2 : 0 }px ${ elFromParentWidthDiff < 0 ? elFromParentWidthDiff / 2 : 0 }px`,
width: `${ elFromCloneWidth + elFromPosition.marginH }px`,
height: `${ elFromCloneHeight + elFromPosition.marginV }px`
}
: {
margin: 0,
width: 0,
height: 0
},
{
margin: `${ elToParentHeightDiff < 0 ? elToParentHeightDiff / 2 : 0 }px ${ elToParentWidthDiff < 0 ? elToParentWidthDiff / 2 : 0 }px`,
width: `${ elToCloneWidth + elToPosition.marginH }px`,
height: `${ elToCloneHeight + elToPosition.marginV }px`
}
], {
duration: options.duration,
easing: options.easing,
fill: options.fill,
delay: options.delay
})
const cleanup = abort => {
animationFromClone !== void 0 && animationFromClone.cancel()
animationFromTween !== void 0 && animationFromTween.cancel()
animationToClone !== void 0 && animationToClone.cancel()
animationTo.cancel()
animationTo.removeEventListener('finish', cleanup)
animationTo.removeEventListener('cancel', cleanup)
commonCleanup(abort)
// we clean the animations
animationFromClone = void 0
animationFromTween = void 0
animationToClone = void 0
animationTo = void 0
}
elFrom.qMorphCancel = () => {
elFrom.qMorphCancel = void 0
cancelStatus = true
cleanup()
}
elTo.qMorphCancel = () => {
elTo.qMorphCancel = void 0
cancelStatus = true
cleanup()
}
animationTo.addEventListener('finish', cleanup)
animationTo.addEventListener('cancel', cleanup)
cancel = abort => {
// we are not in a morph that we can cancel
if (cancelStatus === true || animationTo === void 0) {
return false
}
if (abort === true) {
cleanup(true)
return true
}
endElementTo = endElementTo !== true
animationFromClone !== void 0 && animationFromClone.reverse()
animationFromTween !== void 0 && animationFromTween.reverse()
animationToClone !== void 0 && animationToClone.reverse()
animationTo.reverse()
return true
}
}
else {
const qAnimId = `q-morph-anim-${ ++id }`
const style = document.createElement('style')
const resizeFrom = options.resize === true
? `
transform: translate(${ deltaX }px, ${ deltaY }px);
width: ${ elFromCloneWidth }px;
height: ${ elFromCloneHeight }px;
`
: `transform: translate(${ deltaX }px, ${ deltaY }px) scale(${ scaleX }, ${ scaleY });`
const resizeTo = options.resize === true
? `
width: ${ elToCloneWidth }px;
height: ${ elToCloneHeight }px;
`
: ''
const resizeFromTween = options.resize === true
? `
width: ${ elFromCloneWidth }px;
height: ${ elFromCloneHeight }px;
`
: ''
const resizeToTween = options.resize === true
? `
transform: translate(${ -1 * deltaX }px, ${ -1 * deltaY }px);
width: ${ elToCloneWidth }px;
height: ${ elToCloneHeight }px;
`
: `transform: translate(${ -1 * deltaX }px, ${ -1 * deltaY }px) scale(${ 1 / scaleX }, ${ 1 / scaleY });`
const tweenFrom = elFromTween !== void 0
? `opacity: ${ options.tweenToOpacity };`
: `background-color: ${ elFromBackground };`
const tweenTo = elFromTween !== void 0
? 'opacity: 1;'
: `background-color: ${ elToBackground };`
const keyframesFromTween = elFromTween === void 0
? ''
: `
@keyframes ${ qAnimId }-from-tween {
0% {
opacity: ${ options.tweenFromOpacity };
margin: 0;
border-width: ${ elFromBorderWidth };
border-style: ${ elFromBorderStyle };
border-color: ${ elFromBorderColor };
border-radius: ${ elFromBorderRadius };
z-index: ${ elFromZIndex };
transform-origin: 0 0;
transform: ${ elFromTransform };
${ resizeFromTween }
}
100% {
opacity: 0;
margin: 0;
border-width: ${ elToBorderWidth };
border-style: ${ elToBorderStyle };
border-color: ${ elToBorderColor };
border-radius: ${ elToBorderRadius };
z-index: ${ elToZIndex };
transform-origin: 0 0;
${ resizeToTween }
}
}
`
const keyframesFrom = options.hideFromClone === true || elSharedSize === true
? ''
: `
@keyframes ${ qAnimId }-from {
0% {
margin: ${ elFromParentHeightDiff < 0 ? elFromParentHeightDiff / 2 : 0 }px ${ elFromParentWidthDiff < 0 ? elFromParentWidthDiff / 2 : 0 }px;
width: ${ elFromCloneWidth + elFromPosition.marginH }px;
height: ${ elFromCloneHeight + elFromPosition.marginV }px;
}
100% {
margin: 0;
width: 0;
height: 0;
}
}
`
const keyframeToStart = elSharedSize === true
? `
margin: ${ elFromParentHeightDiff < 0 ? elFromParentHeightDiff / 2 : 0 }px ${ elFromParentWidthDiff < 0 ? elFromParentWidthDiff / 2 : 0 }px;
width: ${ elFromCloneWidth + elFromPosition.marginH }px;
height: ${ elFromCloneHeight + elFromPosition.marginV }px;
`
: `
margin: 0;
width: 0;
height: 0;
`
const keyframesTo = options.keepToClone === true
? ''
: `
@keyframes ${ qAnimId }-to {
0% {
${ keyframeToStart }
}
100% {
margin: ${ elToParentHeightDiff < 0 ? elToParentHeightDiff / 2 : 0 }px ${ elToParentWidthDiff < 0 ? elToParentWidthDiff / 2 : 0 }px;
width: ${ elToCloneWidth + elToPosition.marginH }px;
height: ${ elToCloneHeight + elToPosition.marginV }px;
}
}
`
style.innerHTML = `
@keyframes ${ qAnimId } {
0% {
margin: 0;
border-width: ${ elFromBorderWidth };
border-style: ${ elFromBorderStyle };
border-color: ${ elFromBorderColor };
border-radius: ${ elFromBorderRadius };
background-color: ${ elFromBackground };
z-index: ${ elFromZIndex };
transform-origin: 0 0;
${ resizeFrom }
${ tweenFrom }
}
100% {
margin: 0;
border-width: ${ elToBorderWidth };
border-style: ${ elToBorderStyle };
border-color: ${ elToBorderColor };
border-radius: ${ elToBorderRadius };
background-color: ${ elToBackground };
z-index: ${ elToZIndex };
transform-origin: 0 0;
transform: ${ elToTransform };
${ resizeTo }
${ tweenTo }
}
}
${ keyframesFrom }
${ keyframesFromTween }
${ keyframesTo }
`
document.head.appendChild(style)
let animationDirection = 'normal'
elFromClone.style.animation = `${ options.duration }ms ${ options.easing } ${ options.delay }ms ${ animationDirection } ${ options.fill } ${ qAnimId }-from`
if (elFromTween !== void 0) {
elFromTween.style.animation = `${ options.duration }ms ${ options.easing } ${ options.delay }ms ${ animationDirection } ${ options.fill } ${ qAnimId }-from-tween`
}
elToClone.style.animation = `${ options.duration }ms ${ options.easing } ${ options.delay }ms ${ animationDirection } ${ options.fill } ${ qAnimId }-to`
elTo.style.animation = `${ options.duration }ms ${ options.easing } ${ options.delay }ms ${ animationDirection } ${ options.fill } ${ qAnimId }`
const cleanup = evt => {
if (evt === Object(evt) && evt.animationName !== qAnimId) {
return
}
elTo.removeEventListener('animationend', cleanup)
elTo.removeEventListener('animationcancel', cleanup)
commonCleanup()
// we clean the animations
style.remove()
}
elFrom.qMorphCancel = () => {
elFrom.qMorphCancel = void 0
cancelStatus = true
cleanup()
}
elTo.qMorphCancel = () => {
elTo.qMorphCancel = void 0
cancelStatus = true
cleanup()
}
elTo.addEventListener('animationend', cleanup)
elTo.addEventListener('animationcancel', cleanup)
cancel = abort => {
// we are not in a morph that we can cancel
if (cancelStatus === true || !elTo || !elFromClone || !elToClone) {
return false
}
if (abort === true) {
cleanup()
return true
}
endElementTo = endElementTo !== true
animationDirection = animationDirection === 'normal' ? 'reverse' : 'normal'
elFromClone.style.animationDirection = animationDirection
elFromTween.style.animationDirection = animationDirection
elToClone.style.animationDirection = animationDirection
elTo.style.animationDirection = animationDirection
return true
}
}
}
if (
options.waitFor > 0
|| options.waitFor === 'transitionend'
|| (options.waitFor === Object(options.waitFor) && typeof options.waitFor.then === 'function')
) {
const delayPromise = options.waitFor > 0
? new Promise(resolve => setTimeout(resolve, options.waitFor))
: (
options.waitFor === 'transitionend'
? new Promise(resolve => {
const endFn = () => {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
if (elTo) {
elTo.removeEventListener('transitionend', endFn)
elTo.removeEventListener('transitioncancel', endFn)
}
resolve()
}
let timer = setTimeout(endFn, 400)
elTo.addEventListener('transitionend', endFn)
elTo.addEventListener('transitioncancel', endFn)
})
: options.waitFor
)
delayPromise
.then(animate)
.catch(() => {
typeof elTo.qMorphCancel === 'function' && elTo.qMorphCancel()
})
}
else {
animate()
}
}
typeof _options.onToggle === 'function' && _options.onToggle()
requestAnimationFrame(calculateFinalState)
// we return the cancel function
// returns:
// false if the cancel cannot be performed (the morph ended already or has not started)
// true else
return abort => cancel(abort)
}