/** * @private */Ext.define('Ext.fx.runner.CssTransition', { extend: 'Ext.fx.runner.Css', requires: ['Ext.AnimationQueue'], alternateClassName: 'Ext.Animator', singleton: true, listenersAttached: false, constructor: function() { this.runningAnimationsData = {}; // if multiple animations run at the same time then we need to queue them // to apply them in the same animation frame (see requestAnimationFrame in "run") this.transitionQueue = { toData: {}, transitionData: {} }; return this.callParent(arguments); }, attachListeners: function() { // NOTE: Ext.getWin() has been used for many years but it doesn't appear to work // in the test runner iframe. var target = (top === window) ? Ext.getWin() : Ext.getBody(); this.listenersAttached = true; target.on('transitionend', 'onTransitionEnd', this); }, onTransitionEnd: function(e) { var target = e.target, id = target.id; if (id && this.runningAnimationsData.hasOwnProperty(id)) { this.refreshRunningAnimationsData(Ext.get(target), [e.browserEvent.propertyName]); } }, getElementId: function(element) { // usually when the element is destroyed the getId function is nullified return element.getId ? element.getId() : element.id; }, onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) { var id = this.getElementId(element), runningData = this.runningAnimationsData[id], endRules = {}, endData = {}, runningNameMap, toPropertyNames, i, ln, name; animation.un('stop', 'onAnimationStop', this); if (runningData) { runningNameMap = runningData.nameMap; } endRules[id] = endData; if (data.onBeforeEnd) { data.onBeforeEnd.call(data.scope || this, element, isInterrupted); } animation.fireEvent('animationbeforeend', animation, element, isInterrupted); this.fireEvent('animationbeforeend', this, animation, element, isInterrupted); if (isReplaced || (!isInterrupted && !data.preserveEndState)) { toPropertyNames = data.toPropertyNames; for (i = 0, ln = toPropertyNames.length; i < ln; i++) { name = toPropertyNames[i]; if (runningNameMap && !runningNameMap.hasOwnProperty(name)) { endData[name] = null; } } } if (data.after) { Ext.merge(endData, data.after); } this.applyStyles(endRules); if (data.onEnd) { data.onEnd.call(data.scope || this, element, isInterrupted); } animation.fireEvent('animationend', animation, element, isInterrupted); this.fireEvent('animationend', this, animation, element, isInterrupted); Ext.AnimationQueue.stop(Ext.emptyFn, animation); }, onAllAnimationsEnd: function(element) { var id = this.getElementId(element), transitionQueue = this.transitionQueue, endRules = {}; delete this.runningAnimationsData[id]; endRules[id] = { 'transition-property': null, 'transition-duration': null, 'transition-timing-function': null, 'transition-delay': null }; delete transitionQueue.toData[id]; delete transitionQueue.transitionData[id]; this.applyStyles(endRules); this.fireEvent('animationallend', this, element); }, hasRunningAnimations: function(element) { var id = this.getElementId(element), runningAnimationsData = this.runningAnimationsData; return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0; }, refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) { var id = this.getElementId(element), runningAnimationsData = this.runningAnimationsData, runningData = runningAnimationsData[id], hasCompletedSession = false, nameMap, nameList, sessions, name, session, map, list, i, ln, j, subLn; if (!runningData) { return; } nameMap = runningData.nameMap; nameList = runningData.nameList; sessions = runningData.sessions; interrupt = Boolean(interrupt); replace = Boolean(replace); if (!sessions) { return this; } ln = sessions.length; if (ln === 0) { return this; } if (replace) { runningData.nameMap = {}; nameList.length = 0; for (i = 0; i < ln; i++) { session = sessions[i]; this.onAnimationEnd(element, session.data, session.animation, interrupt, replace); } sessions.length = 0; } else { for (i = 0; i < ln; i++) { session = sessions[i]; map = session.map; list = session.list; for (j = 0, subLn = propertyNames.length; j < subLn; j++) { name = propertyNames[j]; if (map[name]) { delete map[name]; Ext.Array.remove(list, name); session.length--; if (--nameMap[name] === 0) { delete nameMap[name]; Ext.Array.remove(nameList, name); } } } if (session.length === 0) { sessions.splice(i, 1); i--; ln--; hasCompletedSession = true; this.onAnimationEnd(element, session.data, session.animation, interrupt); } } } if (!replace && !interrupt && sessions.length === 0 && hasCompletedSession) { this.onAllAnimationsEnd(element); } }, getRunningData: function(id) { var runningAnimationsData = this.runningAnimationsData; if (!runningAnimationsData.hasOwnProperty(id)) { runningAnimationsData[id] = { nameMap: {}, nameList: [], sessions: [] }; } return runningAnimationsData[id]; }, getTestElement: function() { var me = this, testElement = me.testElement, iframe = me.iframe, iframeDocument, iframeStyle; if (testElement) { // https://sencha.jira.com/browse/EXTJS-21131 // Forward navigation in Chrome 50 navigates iframes, and orphans // the testElement in a detached document. Reconnect it if this has happened. if (testElement.ownerDocument.defaultView !== iframe.contentWindow) { iframeDocument = iframe.contentDocument; iframeDocument.body.appendChild(testElement); // eslint-disable-next-line max-len me.testElementComputedStyle = iframeDocument.defaultView.getComputedStyle(testElement); } } else { iframe = me.iframe = document.createElement('iframe'); //<debug> // Set an attribute that tells the test runner to ignore this node when checking // for dom cleanup iframe.setAttribute('data-sticky', true); //</debug> iframe.setAttribute('tabIndex', -1); iframeStyle = iframe.style; iframeStyle.setProperty('visibility', 'hidden', 'important'); iframeStyle.setProperty('width', '0px', 'important'); iframeStyle.setProperty('height', '0px', 'important'); iframeStyle.setProperty('position', 'absolute', 'important'); iframeStyle.setProperty('border', '0px', 'important'); iframeStyle.setProperty('zIndex', '-1000', 'important'); document.body.appendChild(iframe); iframeDocument = iframe.contentDocument; iframeDocument.open(); iframeDocument.writeln('</body>'); iframeDocument.close(); me.testElement = testElement = iframeDocument.createElement('div'); testElement.style.setProperty('position', 'absolute', 'important'); iframeDocument.body.appendChild(testElement); me.testElementComputedStyle = iframeDocument.defaultView.getComputedStyle(testElement); } return testElement; }, getCssStyleValue: function(name, value) { var testElement = this.getTestElement(), computedStyle = this.testElementComputedStyle, style = testElement.style; style.setProperty(name, value); if (Ext.browser.is.Firefox) { // We force a repaint of the element in Firefox to make sure the computedStyle // to be updated // eslint-disable-next-line no-unused-expressions testElement.offsetHeight; } value = computedStyle.getPropertyValue(name); style.removeProperty(name); return value; }, run: function(animations) { var me = this, ret = [], isLengthPropertyMap = me.lengthProperties, fromData = {}, toData = me.transitionQueue.toData, data = {}, transitionData = me.transitionQueue.transitionData, element, elementId, from, to, before, fromPropertyNames, toPropertyNames, doApplyTo, message, runningData, elementData, i, j, ln, animation, propertiesLength, sessionNameMap, computedStyle, formattedName, name, toFormattedValue, computedValue, fromFormattedValue, isLengthProperty, runningNameMap, runningNameList, runningSessions, runningSession, messageTimerFn, onBeforeStart; if (!me.listenersAttached) { me.attachListeners(); } animations = Ext.Array.from(animations); for (i = 0, ln = animations.length; i < ln; i++) { animation = animations[i]; animation = Ext.factory(animation, Ext.fx.Animation); ret.push(animation); me.activeElement = element = animation.getElement(); // Empty function to prevent idleTasks from running while we animate. Ext.AnimationQueue.start(Ext.emptyFn, animation); computedStyle = window.getComputedStyle(element.dom); elementId = me.getElementId(element); data[elementId] = data = Ext.merge({}, animation.getData()); onBeforeStart = animation.getOnBeforeStart(); if (onBeforeStart) { onBeforeStart.call(animation.scope || me, element); } // Allow listeners to mutate animation data animation.fireEvent('animationstart', animation, data); me.fireEvent('animationstart', me, animation, data); before = data.before; from = data.from; to = data.to; data.fromPropertyNames = fromPropertyNames = []; data.toPropertyNames = toPropertyNames = []; for (name in to) { if (to.hasOwnProperty(name)) { to[name] = toFormattedValue = me.formatValue(to[name], name); formattedName = me.formatName(name); isLengthProperty = isLengthPropertyMap.hasOwnProperty(name); if (!isLengthProperty) { toFormattedValue = me.getCssStyleValue(formattedName, toFormattedValue); } if (from.hasOwnProperty(name)) { from[name] = fromFormattedValue = me.formatValue(from[name], name); if (!isLengthProperty) { fromFormattedValue = me.getCssStyleValue(formattedName, fromFormattedValue); } if (toFormattedValue !== fromFormattedValue) { fromPropertyNames.push(formattedName); toPropertyNames.push(formattedName); } } else { computedValue = computedStyle.getPropertyValue(formattedName); if (toFormattedValue !== computedValue) { toPropertyNames.push(formattedName); } } } } propertiesLength = toPropertyNames.length; if (propertiesLength === 0) { me.onAnimationEnd(element, data, animation); continue; } runningData = me.getRunningData(elementId); runningSessions = runningData.sessions; if (runningSessions.length > 0) { me.refreshRunningAnimationsData( element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious ); } runningNameMap = runningData.nameMap; runningNameList = runningData.nameList; sessionNameMap = {}; for (j = 0; j < propertiesLength; j++) { name = toPropertyNames[j]; sessionNameMap[name] = true; if (!runningNameMap.hasOwnProperty(name)) { runningNameMap[name] = 1; runningNameList.push(name); } else { runningNameMap[name]++; } } runningSession = { element: element, map: sessionNameMap, list: toPropertyNames.slice(), length: propertiesLength, data: data, animation: animation }; runningSessions.push(runningSession); animation.on('stop', 'onAnimationStop', me); elementData = Ext.apply({}, before); Ext.apply(elementData, from); if (runningNameList.length > 0) { fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames); toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames); elementData['transition-property'] = fromPropertyNames; } fromData[elementId] = elementData; toData[elementId] = Ext.apply({}, to); transitionData[elementId] = { 'transition-property': toPropertyNames, 'transition-duration': data.duration, 'transition-timing-function': data.easing, 'transition-delay': data.delay }; animation.startTime = Date.now(); } me.activeElement = null; message = me.$className; me.applyStyles(fromData); doApplyTo = function(e) { if (e.data === message && e.source === window) { window.removeEventListener('message', doApplyTo, false); me.applyStyles(me.transitionQueue.toData); } }; if (!me.messageTimerId) { messageTimerFn = function() { var messageFollowupFn; me.messageTimerId = null; if (Ext.isIE) { // https://sencha.jira.com/browse/EXTJS-22362 // In some cases IE will fail to animate if the "to" and "transition" styles // are added simultaneously. That is the reason for the multi-delay below. // The first one defines the transition parameters ('transition-property', // 'transition-delay' etc) and the second delay sets the values of the // animating properties, or, the "to" properties. The second delay // is what actually starts the animation. me.applyStyles(me.transitionQueue.transitionData); if (!me.messageFollowupId) { messageFollowupFn = function() { me.messageFollowupId = null; window.addEventListener('message', doApplyTo, false); window.postMessage(message, '*'); }; //<debug> messageFollowupFn.$skipTimerCheck = true; //</debug> me.messageFollowupId = Ext.raf(messageFollowupFn); } } else { // In non-IE browsers the above approach can cause a flicker, // so in these browsers we apply all the styles at the same time. Ext.merge(me.transitionQueue.toData, me.transitionQueue.transitionData); window.addEventListener('message', doApplyTo, false); window.postMessage(message, '*'); } }; //<debug> messageTimerFn.$skipTimerCheck = true; //</debug> me.messageTimerId = Ext.raf(messageTimerFn); } // TODO: This method needs to attach something to the element it is animating // we then need to monitor for destruction of that element // and clean up any animations that remain. return ret; }, onAnimationStop: function(animation) { var me = this, runningAnimationsData = me.runningAnimationsData, activeAnimations = 0, stoppedAnimations = 0, id, runningData, sessions, i, ln, session; for (id in runningAnimationsData) { if (runningAnimationsData.hasOwnProperty(id)) { runningData = runningAnimationsData[id]; sessions = runningData.sessions; activeAnimations++; for (i = 0, ln = sessions.length; i < ln; i++) { session = sessions[i]; if (session.animation === animation) { me.refreshRunningAnimationsData(session.element, session.list.slice(), false); if (animation.destroying) { stoppedAnimations++; } } } } } if (activeAnimations === stoppedAnimations) { if (me.messageFollowupId) { Ext.unraf(me.messageFollowupId); me.messageFollowupId = null; } if (me.messageTimerId) { Ext.unraf(me.messageTimerId); me.messageTimerId = null; } Ext.apply(me.transitionQueue, { toData: {}, transitionData: {} }); } }});