var raf = ( typeof window !== 'undefined' && window . requestAnimationFrame ) || setTimeout ;
var nextFrame = function ( fn ) { raf ( function ( ) { raf ( fn ) ; } ) ; } ;
function setNextFrame ( obj , prop , val ) {
nextFrame ( function ( ) { obj [ prop ] = val ; } ) ;
}
function getTextNodeRect ( textNode ) {
var rect ;
if ( document . createRange ) {
var range = document . createRange ( ) ;
range . selectNodeContents ( textNode ) ;
if ( range . getBoundingClientRect ) {
rect = range . getBoundingClientRect ( ) ;
}
}
return rect ;
}
function calcTransformOrigin ( isTextNode , textRect , boundingRect ) {
if ( isTextNode ) {
if ( textRect ) {
//calculate pixels to center of text from left edge of bounding box
var relativeCenterX = textRect . left + textRect . width / 2 - boundingRect . left ;
var relativeCenterY = textRect . top + textRect . height / 2 - boundingRect . top ;
return relativeCenterX + 'px ' + relativeCenterY + 'px' ;
}
}
return '0 0' ; //top left
}
function getTextDx ( oldTextRect , newTextRect ) {
if ( oldTextRect && newTextRect ) {
return ( ( oldTextRect . left + oldTextRect . width / 2 ) - ( newTextRect . left + newTextRect . width / 2 ) ) ;
}
return 0 ;
}
function getTextDy ( oldTextRect , newTextRect ) {
if ( oldTextRect && newTextRect ) {
return ( ( oldTextRect . top + oldTextRect . height / 2 ) - ( newTextRect . top + newTextRect . height / 2 ) ) ;
}
return 0 ;
}
function isTextElement ( elm ) {
return elm . childNodes . length === 1 && elm . childNodes [ 0 ] . nodeType === 3 ;
}
var removed , created ;
function pre ( oldVnode , vnode ) {
removed = { } ;
created = [ ] ;
}
function create ( oldVnode , vnode ) {
var hero = vnode . data . hero ;
if ( hero && hero . id ) {
created . push ( hero . id ) ;
created . push ( vnode ) ;
}
}
function destroy ( vnode ) {
var hero = vnode . data . hero ;
if ( hero && hero . id ) {
var elm = vnode . elm ;
vnode . isTextNode = isTextElement ( elm ) ; //is this a text node?
vnode . boundingRect = elm . getBoundingClientRect ( ) ; //save the bounding rectangle to a new property on the vnode
vnode . textRect = vnode . isTextNode ? getTextNodeRect ( elm . childNodes [ 0 ] ) : null ; //save bounding rect of inner text node
var computedStyle = window . getComputedStyle ( elm , null ) ; //get current styles (includes inherited properties)
vnode . savedStyle = JSON . parse ( JSON . stringify ( computedStyle ) ) ; //save a copy of computed style values
removed [ hero . id ] = vnode ;
}
}
function post ( ) {
var i , id , newElm , oldVnode , oldElm , hRatio , wRatio ,
oldRect , newRect , dx , dy , origTransform , origTransition ,
newStyle , oldStyle , newComputedStyle , isTextNode ,
newTextRect , oldTextRect ;
for ( i = 0 ; i < created . length ; i += 2 ) {
id = created [ i ] ;
newElm = created [ i + 1 ] . elm ;
oldVnode = removed [ id ] ;
if ( oldVnode ) {
isTextNode = oldVnode . isTextNode && isTextElement ( newElm ) ; //Are old & new both text?
newStyle = newElm . style ;
newComputedStyle = window . getComputedStyle ( newElm , null ) ; //get full computed style for new element
oldElm = oldVnode . elm ;
oldStyle = oldElm . style ;
//Overall element bounding boxes
newRect = newElm . getBoundingClientRect ( ) ;
oldRect = oldVnode . boundingRect ; //previously saved bounding rect
//Text node bounding boxes & distances
if ( isTextNode ) {
newTextRect = getTextNodeRect ( newElm . childNodes [ 0 ] ) ;
oldTextRect = oldVnode . textRect ;
dx = getTextDx ( oldTextRect , newTextRect ) ;
dy = getTextDy ( oldTextRect , newTextRect ) ;
} else {
//Calculate distances between old & new positions
dx = oldRect . left - newRect . left ;
dy = oldRect . top - newRect . top ;
}
hRatio = newRect . height / ( Math . max ( oldRect . height , 1 ) ) ;
wRatio = isTextNode ? hRatio : newRect . width / ( Math . max ( oldRect . width , 1 ) ) ; //text scales based on hRatio
// Animate new element
origTransform = newStyle . transform ;
origTransition = newStyle . transition ;
if ( newComputedStyle . display === 'inline' ) //inline elements cannot be transformed
newStyle . display = 'inline-block' ; //this does not appear to have any negative side effects
newStyle . transition = origTransition + 'transform 0s' ;
newStyle . transformOrigin = calcTransformOrigin ( isTextNode , newTextRect , newRect ) ;
newStyle . opacity = '0' ;
newStyle . transform = origTransform + 'translate(' + dx + 'px, ' + dy + 'px) ' +
'scale(' + 1 / wRatio + ', ' + 1 / hRatio + ')' ;
setNextFrame ( newStyle , 'transition' , origTransition ) ;
setNextFrame ( newStyle , 'transform' , origTransform ) ;
setNextFrame ( newStyle , 'opacity' , '1' ) ;
// Animate old element
for ( var key in oldVnode . savedStyle ) { //re-apply saved inherited properties
if ( parseInt ( key ) != key ) {
var ms = key . substring ( 0 , 2 ) === 'ms' ;
var moz = key . substring ( 0 , 3 ) === 'moz' ;
var webkit = key . substring ( 0 , 6 ) === 'webkit' ;
if ( ! ms && ! moz && ! webkit ) //ignore prefixed style properties
oldStyle [ key ] = oldVnode . savedStyle [ key ] ;
}
}
oldStyle . position = 'absolute' ;
oldStyle . top = oldRect . top + 'px' ; //start at existing position
oldStyle . left = oldRect . left + 'px' ;
oldStyle . width = oldRect . width + 'px' ; //Needed for elements who were sized relative to their parents
oldStyle . height = oldRect . height + 'px' ; //Needed for elements who were sized relative to their parents
oldStyle . margin = 0 ; //Margin on hero element leads to incorrect positioning
oldStyle . transformOrigin = calcTransformOrigin ( isTextNode , oldTextRect , oldRect ) ;
oldStyle . transform = '' ;
oldStyle . opacity = '1' ;
document . body . appendChild ( oldElm ) ;
setNextFrame ( oldStyle , 'transform' , 'translate(' + - dx + 'px, ' + - dy + 'px) scale(' + wRatio + ', ' + hRatio + ')' ) ; //scale must be on far right for translate to be correct
setNextFrame ( oldStyle , 'opacity' , '0' ) ;
oldElm . addEventListener ( 'transitionend' , function ( ev ) {
if ( ev . propertyName === 'transform' )
document . body . removeChild ( ev . target ) ;
} ) ;
}
}
removed = created = undefined ;
}
module . exports = { pre : pre , create : create , destroy : destroy , post : post } ;