diff --git a/src/core/Context.js b/src/core/Context.js index 7b2ff4cd..f5160dce 100644 --- a/src/core/Context.js +++ b/src/core/Context.js @@ -146,6 +146,8 @@ define(function (require, exports, module) { if (contextParameters.origin) this._nodeContext.origin = contextParameters.origin; if (contextParameters.align) this._nodeContext.align = contextParameters.align; if (contextParameters.size) this._nodeContext.size = contextParameters.size; + if (contextParameters.size) this._nodeContext.size = contextParameters.size; + this._nodeContext.hide = contextParameters.hide; if (contextParameters.allocator) { this._nodeContext.allocator = contextParameters.allocator; } else { diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js new file mode 100644 index 00000000..af106a2b --- /dev/null +++ b/src/core/DOMBuffer.js @@ -0,0 +1,103 @@ +/** + * Created by lundfall on 02/06/2017. + */ + + +define(function (require, exports, module) { + + /** + * Singleton class optimized for high performance in DOM updates. All DOM updates that are done through this class will + * be cached and can be flushed at the same order the instructions came in. + * + * + * @type {{}} + */ + var DOMBuffer = {}; + var enqueuedOperations = []; + + + DOMBuffer.assignProperty = function (object, property, value) { + enqueuedOperations.push({ data: [object, property, value], operation: 'assignProperty' }); + }; + + DOMBuffer.setAttribute = function (element, attribute, value) { + enqueuedOperations.push({ data: [element, attribute, value], operation: 'setAttribute' }); + }; + + DOMBuffer.addToObject = function (object, value) { + enqueuedOperations.push({ data: [object, value], operation: 'addToObject' }); + }; + + DOMBuffer.setAttributeOnDescendants = function (element, attribute, attributeValue) { + enqueuedOperations.push({ data: [element, attribute, attributeValue], operation: 'setAttributeOnDescendants' }); + }; + + DOMBuffer.removeFromObject = function (object, attribute) { + enqueuedOperations.push({ data: [object, attribute], operation: 'removeFromObject' }); + }; + + DOMBuffer.removeAttribute = function (element, attribute) { + enqueuedOperations.push({ data: [element, attribute], operation: 'removeAttribute' }); + }; + + DOMBuffer.removeChild = function (parent, childToRemove) { + enqueuedOperations.push({ data: [parent, childToRemove], operation: 'removeChild' }); + }; + + DOMBuffer.appendChild = function (parent, childToAppend) { + enqueuedOperations.push({ data: [parent, childToAppend], operation: 'appendChild' }); + }; + + DOMBuffer.insertBefore = function (parent, childBefore, childToInsert) { + enqueuedOperations.push({ data: [parent, childBefore, childToInsert], operation: 'insertBefore' }); + }; + + DOMBuffer.flushUpdates = function () { + for (var index = 0; index < enqueuedOperations.length; index++) { + var enqueuedOperation = enqueuedOperations[index]; + var operationName = enqueuedOperation.operation; + var data = enqueuedOperation.data; + switch (operationName) { + case 'appendChild': + data[0].appendChild(data[1]); + break; + case 'insertBefore': + data[0].insertBefore(data[1], data[2]); + break; + case 'setAttribute': + data[0].setAttribute(data[1], data[2]); + break; + case 'removeChild': + if (data[0].childNodes.length && data[0].contains(data[1])) { + data[0].removeChild(data[1]); + } + break; + case 'removeAttribute': + data[0].removeAttribute(data[1]); + break; + case 'addToObject': + data[0].add(data[1]); + break; + case 'removeFromObject': + data[0].remove(data[1]); + break; + case 'assignProperty': + data[0][data[1]] = data[2]; + break; + case 'setAttributeOnDescendants': + /* Gets all the descendants for element + * https://stackoverflow.com/questions/26325278/how-can-i-get-all-descendant-elements-for-parent-container + * */ + var descendants = data[0].querySelectorAll("*"); + for (var i = 0; i < descendants.length; i++) { + descendants[i].setAttribute(data[1], data[2]); + } + break; + } + } + enqueuedOperations = []; + }; + + module.exports = DOMBuffer; +}); + diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js new file mode 100644 index 00000000..7e46e680 --- /dev/null +++ b/src/core/DOMEventHandler.js @@ -0,0 +1,69 @@ + +/** + * Created by lundfall on 01/06/2017. + */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Owner: mark@famo.us + * @license MPL 2.0 + * @copyright Famous Industries, Inc. 2015 + */ + +define(function (require, exports, module) { + + var DOMEventHandler = {}; + var EventEmitter = require('./EventEmitter.js'); + var DOMBuffer = require('./DOMBuffer'); + + //TODO Add more to complete list + var singleElementEvents = [ + 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll' + ]; + + var initializedListeners = {}; + + DOMEventHandler.isNativeEvent = function(eventName) { + return typeof document.body["on" + eventName] !== "undefined" + || + /* Needed because otherwise not able to use mobile emulation in browser! */ + ['touchmove', 'touchstart', 'touchend'].includes(eventName); + }; + + DOMEventHandler.addEventListener = function(id, element, type, callback){ + if(!DOMEventHandler.isNativeEvent(type)){ + return; + } + + if(singleElementEvents.includes(type)){ + return element.addEventListener(type, callback); + } + DOMBuffer.setAttribute(element, 'data-arvaid', id); + var eventEmitter = initializedListeners[type]; + if(!eventEmitter){ + eventEmitter = initializedListeners[type] = new EventEmitter(); + window.addEventListener(type, function (event) { + var target = event.relatedTarget || event.target; + var recievedID = target && target.getAttribute && target.getAttribute('data-arvaid'); + if(recievedID){ + eventEmitter.emit(recievedID, event); + } + }); + } + eventEmitter.on(id, callback); + + }; + + DOMEventHandler.removeEventListener = function(element, id, type, callback) { + if(singleElementEvents.includes(type)){ + return element.addEventListener(type, callback); + } + if(initializedListeners[type]){ + initializedListeners[type].removeListener(id, callback); + } + }; + + module.exports = DOMEventHandler; +}); diff --git a/src/core/ElementAllocator.js b/src/core/ElementAllocator.js index 9a6b6091..8dd373ba 100644 --- a/src/core/ElementAllocator.js +++ b/src/core/ElementAllocator.js @@ -9,6 +9,7 @@ define(function (require, exports, module) { var Context = require('./Context.js'); + var DOMBuffer = require('./DOMBuffer'); /** * Internal helper object to Context that handles the process of @@ -42,11 +43,13 @@ define(function (require, exports, module) { if (container === oldContainer) return; if (oldContainer instanceof DocumentFragment) { - container.appendChild(oldContainer); + DOMBuffer.appendChild(container, oldContainer); } else { - while (oldContainer.hasChildNodes()) { - container.appendChild(oldContainer.firstChild); + var children = oldContainer.childNodes || []; + //TODO Confirm that this works + for(var i = 0;i< children.length; i++){ + DOMBuffer.appendChild(container, children[i]); } } @@ -59,6 +62,7 @@ define(function (require, exports, module) { * @private * @method allocate * + * @param {Object} options * @param {String} options.type type of element, e.g. 'div' * @param {Boolean} options.insertFirst Whether it should be allocated from the top instead of the bottom * or at the end. Defaults to false (at the bottom). @@ -106,9 +110,9 @@ define(function (require, exports, module) { ElementAllocator.prototype._allocateNewHtmlOutput = function _allocateNewElementOutput(type, insertFirst) { var result = document.createElement(type); if (insertFirst) { - this.container.insertBefore(result, this.container.firstChild); + DOMBuffer.insertBefore(this.container, result, this.container.firstChild); } else { - this.container.appendChild(result); + DOMBuffer.appendChild(this.container, result); } return result; }; diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index cdea3e0b..2a837305 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -11,6 +11,8 @@ define(function(require, exports, module) { var Entity = require('./Entity'); var EventHandler = require('./EventHandler'); var Transform = require('./Transform'); + var DOMEventHandler = require('./DOMEventHandler'); + var DOMBuffer = require('./DOMBuffer'); var usePrefix = !('transform' in document.documentElement.style); var devicePixelRatio = window.devicePixelRatio || 1; @@ -60,12 +62,12 @@ define(function(require, exports, module) { * @return {EventHandler} this */ ElementOutput.prototype.on = function on(type, fn) { - if (this._element) this._element.addEventListener(type, this.eventForwarder); + if (this._element) DOMEventHandler.addEventListener(this.id, this._element, type, this.eventForwarder); this._eventOutput.on(type, fn); }; ElementOutput.prototype.once = function on(type, fn) { - if (this._element) this._element.addEventListener(type, this.eventForwarder); + if (this._element) DOMEventHandler.addEventListener(this.id, this._element, type, this.eventForwarder); this._eventOutput.once(type, fn); }; @@ -142,7 +144,7 @@ define(function(require, exports, module) { // Calling this enables methods like #on and #pipe. function _addEventListeners(target) { for (var i in this._eventOutput.listeners) { - target.addEventListener(i, this.eventForwarder); + DOMEventHandler.addEventListener(this.id, target, i, this.eventForwarder); } } @@ -150,7 +152,7 @@ define(function(require, exports, module) { // document element. This occurs just before detach from the document. function _removeEventListeners(target) { for (var i in this._eventOutput.listeners) { - target.removeEventListener(i, this.eventForwarder); + DOMEventHandler.removeEventListener(target, this.id, i, this.eventForwarder) } } @@ -175,7 +177,6 @@ define(function(require, exports, module) { result += m[15] + ')'; return result; } - /** * Directly apply given FamousMatrix to the document element as the * appropriate webkit CSS style. @@ -191,12 +192,12 @@ define(function(require, exports, module) { var _setMatrix; if (usePrefix) { _setMatrix = function(element, matrix) { - element.style.webkitTransform = _formatCSSTransform(matrix); + DOMBuffer.assignProperty(element.style, 'webkitTransform', _formatCSSTransform(matrix)); }; } else { _setMatrix = function(element, matrix) { - element.style.transform = _formatCSSTransform(matrix); + DOMBuffer.assignProperty(element.style, 'transform', _formatCSSTransform(matrix)); }; } @@ -208,18 +209,18 @@ define(function(require, exports, module) { // Directly apply given origin coordinates to the document element as the // appropriate webkit CSS style. var _setOrigin = usePrefix ? function(element, origin) { - element.style.webkitTransformOrigin = _formatCSSOrigin(origin); + DOMBuffer.assignProperty(element.style, 'webkitTransform', _formatCSSOrigin(origin)); } : function(element, origin) { - element.style.transformOrigin = _formatCSSOrigin(origin); + DOMBuffer.assignProperty(element.style, 'transformOrigin', _formatCSSOrigin(origin)); }; // Shrink given document element until it is effectively invisible. var _setInvisible = usePrefix ? function(element) { - element.style.webkitTransform = 'scale3d(0.0001,0.0001,0.0001)'; - element.style.opacity = 0; + DOMBuffer.assignProperty(element.style, 'webkitTransform', 'scale3d(0.0001,0.0001,0.0001)'); + DOMBuffer.assignProperty(element.style, 'opacity', '0'); } : function(element) { - element.style.transform = 'scale3d(0.0001,0.0001,0.0001)'; - element.style.opacity = 0; + DOMBuffer.assignProperty(element.style, 'transform', 'scale3d(0.0001,0.0001,0.0001)'); + DOMBuffer.assignProperty(element.style, 'opacity', '0'); }; function _xyNotEquals(a, b) { @@ -256,12 +257,12 @@ define(function(require, exports, module) { if (this._invisible) { this._invisible = false; - this._element.style.display = ''; + DOMBuffer.assignProperty(this._element.style, 'display', ''); } if (this._opacity !== opacity) { this._opacity = opacity; - target.style.opacity = (opacity >= 1) ? '0.999999' : opacity; + DOMBuffer.assignProperty(target.style, 'opacity', (opacity >= 1) ? '0.999999' : opacity); } if (this._transformDirty || this._originDirty || this._sizeDirty) { @@ -282,9 +283,9 @@ define(function(require, exports, module) { this._matrix = matrix; var aaMatrix = this._size ? Transform.thenMove(matrix, [-this._size[0]*origin[0], -this._size[1]*origin[1], 0]) : matrix; _setMatrix(target, aaMatrix); - /* Since a lot of browsers are buggy, they need the z-index to be set as well in order to successfully place things - * on top of each other*/ - target.style.zIndex = aaMatrix[14]; + /* Since a lot of browsers are buggy, they need the z-index to be set as well besides the 3d transformation + * matrix to successfully place things on top of each other*/ + DOMBuffer.assignProperty(target.style, 'zIndex', Math.round(aaMatrix[14])); this._transformDirty = false; } }; @@ -292,7 +293,7 @@ define(function(require, exports, module) { ElementOutput.prototype.cleanup = function cleanup() { if (this._element) { this._invisible = true; - this._element.style.display = 'none'; + DOMBuffer.assignProperty(this._element.style, 'display', 'none'); } }; @@ -305,7 +306,7 @@ define(function(require, exports, module) { */ ElementOutput.prototype.attach = function attach(target) { this._element = target; - _addEventListeners.call(this, target); + _addEventListeners.call(this, target); }; /** @@ -321,7 +322,7 @@ define(function(require, exports, module) { _removeEventListeners.call(this, target); if (this._invisible) { this._invisible = false; - this._element.style.display = ''; + DOMBuffer.assignProperty(this._element.style, 'display', ''); } } this._element = null; diff --git a/src/core/Engine.js b/src/core/Engine.js index 4d3940e7..16d2f5b8 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -28,6 +28,17 @@ define(function (require, exports, module) { var ElementAllocator = require('./ElementAllocator'); var EventHandler = require('./EventHandler'); var OptionsManager = require('./OptionsManager'); + var DOMBuffer = require('./DOMBuffer'); + + /* Precise function for comparing time stamps*/ + var getTime = (typeof window !== 'undefined' && window.performance && window.performance.now) ? + function() { + return window.performance.now(); + } + : function() { + return Date.now(); + }; + var Engine = {}; @@ -40,7 +51,9 @@ define(function (require, exports, module) { var deferQueue = []; - var lastTime = Date.now(); + /* The last timestamp of the previous frame */ + var lastTime = getTime(); + var frameTime; var frameTimeLimit; var loopEnabled = true; @@ -59,6 +72,13 @@ define(function (require, exports, module) { /** @const */ var MAX_DEFER_FRAME_TIME = 10; + + Engine.PriorityLevels = { + critical: Infinity, + normal: 130, + generous: 0 + }; + /** * Inside requestAnimationFrame loop, step() is called, which: * calculates current FPS (throttling loop if it is over limit set in setFPSCap), @@ -75,13 +95,21 @@ define(function (require, exports, module) { currentFrame++; nextTickFrame = currentFrame; - var currentTime = Date.now(); + var currentTime = getTime(); this._lastFrameTimeDelta = currentTime - lastTime; // skip frame if we're over our framerate cap if (frameTimeLimit && this._lastFrameTimeDelta < frameTimeLimit) return; - var i = 0; + this._priorityLevel = Infinity; + var priorityLevels = Object.keys(Engine.PriorityLevels); + for (var i = 0; i < priorityLevels.length; i++) { + var priority = priorityLevels[i]; + var priorityLevelCriteria = Engine.PriorityLevels[priority]; + if (this._lastFrameTimeDelta < priorityLevelCriteria && priorityLevelCriteria <= this._priorityLevel){ + this._priorityLevel = priorityLevelCriteria; + } + } frameTime = currentTime - lastTime; lastTime = currentTime; @@ -93,15 +121,47 @@ define(function (require, exports, module) { while (numFunctions--) (nextTickQueue.shift())(currentFrame); // limit total execution time for deferrable functions - while (deferQueue.length && (Date.now() - currentTime) < MAX_DEFER_FRAME_TIME) { + while (deferQueue.length && (getTime() - currentTime) < MAX_DEFER_FRAME_TIME) { deferQueue.shift().call(this); } - for (i = 0; i < contexts.length; i++) contexts[i].update(); + for (var i = 0; i < contexts.length; i++) contexts[i].update(); + + DOMBuffer.flushUpdates(); eventHandler.emit('postrender'); + + }; + /** + * @example + * + * Engine.restrictAnimations({ + * size: Engine.PriorityLevel.critical, + * opacity: Engine.PriorityLevel.critical + * }) + * + * Instructs the engine to disable the animations for the different properties passed. + * + * @param options + */ + Engine.restrictAnimations = function disableAnimationsWhen(options) { + this._disableAnimationSpec = options; + }; + + Engine.shouldPropertyAnimate = function shouldPropertyAnimate(propertyName){ + if(!this._disableAnimationSpec){ + return true; + } + var priorityLevel = this._disableAnimationSpec[propertyName]; + if(priorityLevel === undefined){ + return true; + } + return this._priorityLevel < priorityLevel; + }; + + Engine.getFrameTimeDelta = function getFrameTimeDelta() { return this._lastFrameTimeDelta; }; @@ -141,13 +201,23 @@ define(function (require, exports, module) { handleResize(); } - Engine.touchMoveEnabled = false; + Engine.touchMoveEnabled = true; - Engine.enableTouchMove = function enableTouchMove() { - if (!this.touchMoveEnabled) { - console.log("Warning: Touch move enabled. Outcomes might be unwated"); + Engine.getPriorityLevel = function () { + return this._priorityLevel; + }; + Engine.disableTouchMove = function disableTouchMove() { + if (this.touchMoveEnabled) { + // prevent scrolling via browser + window.addEventListener('touchmove', function (event) { + if (event.target.tagName === 'TEXTAREA' || this.touchMoveEnabled) { + return true; + } else { + event.preventDefault(); + } + }.bind(this), { capture: true, passive: false }); + this.touchMoveEnabled = false; } - this.touchMoveEnabled = true; }; @@ -159,6 +229,7 @@ define(function (require, exports, module) { * @method initialize */ function initialize() { + // prevent scrolling via browser window.addEventListener('touchmove', function (event) { if (event.target.tagName === 'TEXTAREA' || this.touchMoveEnabled) { @@ -183,6 +254,15 @@ define(function (require, exports, module) { document.documentElement.classList.add('famous-root'); } + var canvas; + Engine.getCachedCanvas = function() { + if(!canvas){ + canvas = document.createElement('canvas'); + document.createDocumentFragment().appendChild(canvas); + } + return canvas; + }; + /** * Add event handler object to set of downstream handlers. * @@ -333,6 +413,9 @@ define(function (require, exports, module) { * @return {Context} new Context within el */ Engine.createContext = function createContext(el) { + + this._priorityLevel = Engine.PriorityLevels.critical; + if (!initialized && options.appMode) Engine.nextTick(initialize.bind(this)); var needMountContainer = false; @@ -413,6 +496,8 @@ define(function (require, exports, module) { nextTickQueue.push(fn); }; + Engine.now = getTime; + /** * Queue a function to be executed sometime soon, at a time that is * unlikely to affect frame rate. diff --git a/src/core/EventEmitter.js b/src/core/EventEmitter.js index 828fdd43..ed665961 100644 --- a/src/core/EventEmitter.js +++ b/src/core/EventEmitter.js @@ -41,7 +41,6 @@ define(function(require, exports, module) { } var handlers = this.listeners[type]; if (handlers) { - handlers = Array.from(handlers); for (var i = 0; i < handlers.length; i++) { handlers[i].apply(this._owner, args); } diff --git a/src/core/Group.js b/src/core/Group.js index 2b37b2d5..4a765780 100644 --- a/src/core/Group.js +++ b/src/core/Group.js @@ -147,6 +147,7 @@ define(function (require, exports, module) { allocator: this._allocator, transform: Transform.translate(-origin[0] * size[0], -origin[1] * size[1], 0), origin: origin, + hide: context.opacity === 0 || context.hide, size: size }); return result; diff --git a/src/core/SpecParser.js b/src/core/SpecParser.js index 2aa281ff..7ecd61b5 100644 --- a/src/core/SpecParser.js +++ b/src/core/SpecParser.js @@ -99,13 +99,15 @@ define(function(require, exports, module) { if (typeof spec === 'number') { id = spec; - transform = parentContext.transform; + var hide = parentContext.hide || parentContext.opacity === 0; + transform = hide ? Transform.scale(0, 0, 0) : parentContext.transform; align = parentContext.align || _zeroZero; if (parentContext.size && align && (align[0] || align[1])) { var alignAdjust = [align[0] * parentContext.size[0], align[1] * parentContext.size[1], 0]; transform = Transform.thenMove(transform, _vecInContext(alignAdjust, sizeContext)); } this.result[id] = { + hide: hide, transform: transform, opacity: parentContext.opacity, origin: parentContext.origin || _zeroZero, @@ -128,10 +130,14 @@ define(function(require, exports, module) { origin = parentContext.origin; align = parentContext.align; size = parentContext.size; + /* If parent is hidden, also this element should be hidden */ + var hide = spec.hide || parentContext.hide || opacity === 0; var nextSizeContext = sizeContext; if (spec.opacity !== undefined) opacity = parentContext.opacity * spec.opacity; - if (spec.transform) transform = Transform.multiply(parentContext.transform, spec.transform); + if (hide) transform = Transform.scale(0, 0, 0); + else if (spec.transform) transform = Transform.multiply(parentContext.transform, spec.transform); + if (spec.origin) { origin = spec.origin; nextSizeContext = parentContext.transform; @@ -167,7 +173,8 @@ define(function(require, exports, module) { opacity: opacity, origin: origin, align: align, - size: size + size: size, + hide: hide }, nextSizeContext); } }; diff --git a/src/core/Surface.js b/src/core/Surface.js index 235ae846..3ed7dbf6 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -7,539 +7,551 @@ * @copyright Famous Industries, Inc. 2015 */ -define(function(require, exports, module) { - var ElementOutput = require('./ElementOutput'); - - /** - * A base class for viewable content and event - * targets inside a Famo.us application, containing a renderable document - * fragment. Like an HTML div, it can accept internal markup, - * properties, classes, and handle events. - * - * @class Surface - * @constructor - * - * @param {Object} [options] default option overrides - * @param {Array.Number} [options.size] [width, height] in pixels - * @param {Array.string} [options.classes] CSS classes to set on target div - * @param {Array} [options.properties] string dictionary of CSS properties to set on target div - * @param {Array} [options.attributes] string dictionary of HTML attributes to set on target div - * @param {string} [options.content] inner (HTML) content of surface - */ - function Surface(options) { - ElementOutput.call(this); - - this.options = {}; - - this.properties = {}; - this.attributes = {}; - this.content = ''; - this.classList = []; - this.size = null; - - this._classesDirty = true; - this._stylesDirty = true; - this._attributesDirty = true; - this._sizeDirty = true; - this._contentDirty = true; - this._trueSizeCheck = true; - - this._dirtyClasses = []; - this._dirtyAttributes = []; - - if (options) this.setOptions(options); - - this._currentTarget = null; +define(function (require, exports, module) { + var ElementOutput = require('./ElementOutput'); + var DOMBuffer = require('./DOMBuffer'); + + /** + * A base class for viewable content and event + * targets inside a Famo.us application, containing a renderable document + * fragment. Like an HTML div, it can accept internal markup, + * properties, classes, and handle events. + * + * @class Surface + * @constructor + * + * @param {Object} [options] default option overrides + * @param {Array.Number} [options.size] [width, height] in pixels + * @param {Array.string} [options.classes] CSS classes to set on target div + * @param {Array} [options.properties] string dictionary of CSS properties to set on target div + * @param {Array} [options.attributes] string dictionary of HTML attributes to set on target div + * @param {string} [options.content] inner (HTML) content of surface + */ + function Surface(options) { + ElementOutput.call(this); + + this.options = {}; + + this.properties = {}; + this.attributes = {}; + this.content = ''; + this.classList = []; + this.size = null; + + this._classesDirty = true; + this._stylesDirty = true; + this._attributesDirty = true; + this._sizeDirty = true; + this._contentDirty = true; + this._trueSizeCheck = true; + + this._dirtyClasses = []; + this._dirtyAttributes = []; + + if (options) this.setOptions(options); + + this._currentTarget = null; + } + + Surface.prototype = Object.create(ElementOutput.prototype); + Surface.prototype.constructor = Surface; + Surface.prototype.elementType = 'div'; + Surface.prototype.elementClass = 'famous-surface'; + + /** + * Set HTML attributes on this Surface. Note that this will cause + * dirtying and thus re-rendering, even if values do not change. + * + * @method setAttributes + * @param {Object} attributes property dictionary of "key" => "value" + */ + Surface.prototype.setAttributes = function setAttributes(attributes) { + for (var n in attributes) { + if (n === 'style') throw new Error('Cannot set styles via "setAttributes" as it will break Famo.us. Use "setProperties" instead.'); + this.attributes[n] = attributes[n]; + /* Remove the attribute that is about to be removed, if applicable */ + var attributeToBeRemovedIndex = this._dirtyAttributes.indexOf(n); + if (attributeToBeRemovedIndex !== -1) { + this._dirtyAttributes.splice(attributeToBeRemovedIndex, 1); + } } - Surface.prototype = Object.create(ElementOutput.prototype); - Surface.prototype.constructor = Surface; - Surface.prototype.elementType = 'div'; - Surface.prototype.elementClass = 'famous-surface'; - - /** - * Set HTML attributes on this Surface. Note that this will cause - * dirtying and thus re-rendering, even if values do not change. - * - * @method setAttributes - * @param {Object} attributes property dictionary of "key" => "value" - */ - Surface.prototype.setAttributes = function setAttributes(attributes) { - for (var n in attributes) { - if (n === 'style') throw new Error('Cannot set styles via "setAttributes" as it will break Famo.us. Use "setProperties" instead.'); - this.attributes[n] = attributes[n]; - /* Remove the attribute that is about to be removed, if applicable */ - var attributeToBeRemovedIndex = this._dirtyAttributes.indexOf(n); - if(attributeToBeRemovedIndex !== -1){ - this._dirtyAttributes.splice(attributeToBeRemovedIndex, 1); - } - } - this._attributesDirty = true; - }; - - /** - * Get HTML attributes on this Surface. - * - * @method getAttributes - * - * @return {Object} Dictionary of this Surface's attributes. - */ - Surface.prototype.getAttributes = function getAttributes() { - return this.attributes; - }; - - /** - * Removes existing attributes from this Surface (e.g. needed for 'disabled'). - * @method removeAttributes - * @param {Array} attributes List of attribute names to remove - */ - Surface.prototype.removeAttributes = function removeAttributes(attributes) { - for(var index in attributes) { - var name = attributes[index]; - delete this.attributes[name]; - this._dirtyAttributes.push(name); - } - this._attributesDirty = true; - }; - - /** - * Set CSS-style properties on this Surface. Note that this will cause - * dirtying and thus re-rendering, even if values do not change. - * - * @method setProperties - * @chainable - * @param {Object} properties property dictionary of "key" => "value" - */ - Surface.prototype.setProperties = function setProperties(properties) { - for (var n in properties) { - this.properties[n] = properties[n]; - } - this._stylesDirty = true; - return this; - }; - - /** - * Get CSS-style properties on this Surface. - * - * @method getProperties - * - * @return {Object} Dictionary of this Surface's properties. - */ - Surface.prototype.getProperties = function getProperties() { - return this.properties; - }; - - /** - * Add CSS-style class to the list of classes on this Surface. Note - * this will map directly to the HTML property of the actual - * corresponding rendered
. - * - * @method addClass - * @chainable - * @param {string} className name of class to add - */ - Surface.prototype.addClass = function addClass(className) { - if (this.classList.indexOf(className) < 0) { - this.classList.push(className); - this._classesDirty = true; - } - return this; - }; - - /** - * Remove CSS-style class from the list of classes on this Surface. - * Note this will map directly to the HTML property of the actual - * corresponding rendered
. - * - * @method removeClass - * @chainable - * @param {string} className name of class to remove - */ - Surface.prototype.removeClass = function removeClass(className) { - var i = this.classList.indexOf(className); - if (i >= 0) { - this._dirtyClasses.push(this.classList.splice(i, 1)[0]); - this._classesDirty = true; - } - return this; - }; - - /** - * Toggle CSS-style class from the list of classes on this Surface. - * Note this will map directly to the HTML property of the actual - * corresponding rendered
. - * - * @method toggleClass - * @param {string} className name of class to toggle - */ - Surface.prototype.toggleClass = function toggleClass(className) { - var i = this.classList.indexOf(className); - if (i >= 0) { - this.removeClass(className); - } else { - this.addClass(className); - } - return this; - }; - - /** - * Reset class list to provided dictionary. - * @method setClasses - * @chainable - * @param {Array.string} classList - */ - Surface.prototype.setClasses = function setClasses(classList) { - var i = 0; - var removal = []; - for (i = 0; i < this.classList.length; i++) { - if (classList.indexOf(this.classList[i]) < 0) removal.push(this.classList[i]); - } - for (i = 0; i < removal.length; i++) this.removeClass(removal[i]); - // duplicates are already checked by addClass() - for (i = 0; i < classList.length; i++) this.addClass(classList[i]); - return this; - }; - - /** - * Get array of CSS-style classes attached to this div. - * - * @method getClasslist - * @return {Array.string} array of class names - */ - Surface.prototype.getClassList = function getClassList() { - return this.classList; - }; - - /** - * Set or overwrite inner (HTML) content of this surface. Note that this - * causes a re-rendering if the content has changed. - * - * @method setContent - * @chainable - * @param {string|Document Fragment} content HTML content - */ - Surface.prototype.setContent = function setContent(content) { - if (this.content !== content) { - this.content = content; - this._contentDirty = true; - } - return this; - }; - - /** - * Return inner (HTML) content of this surface. - * - * @method getContent - * - * @return {string} inner (HTML) content - */ - Surface.prototype.getContent = function getContent() { - return this.content; - }; - - /** - * Set options for this surface - * - * @method setOptions - * @chainable - * @param {Object} [options] overrides for default options. See constructor. - */ - Surface.prototype.setOptions = function setOptions(options) { - if (options.size) this.setSize(options.size); - if (options.classes) this.setClasses(options.classes); - if (options.properties) this.setProperties(options.properties); - if (options.attributes) this.setAttributes(options.attributes); - if (options.content) this.setContent(options.content); - return this; - }; - - // Apply to document all changes from removeClass() since last setup(). - function _cleanupClasses(target) { - for (var i = 0; i < this._dirtyClasses.length; i++) target.classList.remove(this._dirtyClasses[i]); - this._dirtyClasses = []; + this._attributesDirty = true; + }; + + /** + * Get HTML attributes on this Surface. + * + * @method getAttributes + * + * @return {Object} Dictionary of this Surface's attributes. + */ + Surface.prototype.getAttributes = function getAttributes() { + return this.attributes; + }; + + /** + * Removes existing attributes from this Surface (e.g. needed for 'disabled'). + * @method removeAttributes + * @param {Array} attributes List of attribute names to remove + */ + Surface.prototype.removeAttributes = function removeAttributes(attributes) { + for (var index in attributes) { + var name = attributes[index]; + delete this.attributes[name]; + this._dirtyAttributes.push(name); } + this._attributesDirty = true; + }; - // Apply values of all Famous-managed styles to the document element. - // These will be deployed to the document on call to #setup(). - function _applyStyles(target) { - for (var n in this.properties) { - target.style[n] = this.properties[n]; - } + /** + * Set CSS-style properties on this Surface. Note that this will cause + * dirtying and thus re-rendering, even if values do not change. + * + * @method setProperties + * @chainable + * @param {Object} properties property dictionary of "key" => "value" + */ + Surface.prototype.setProperties = function setProperties(properties) { + for (var n in properties) { + this.properties[n] = properties[n]; } + this._stylesDirty = true; + return this; + }; - // Clear all Famous-managed styles from the document element. - // These will be deployed to the document on call to #setup(). - function _cleanupStyles(target) { - for (var n in this.properties) { - target.style[n] = ''; - } + /** + * Get CSS-style properties on this Surface. + * + * @method getProperties + * + * @return {Object} Dictionary of this Surface's properties. + */ + Surface.prototype.getProperties = function getProperties() { + return this.properties; + }; + + /** + * Add CSS-style class to the list of classes on this Surface. Note + * this will map directly to the HTML property of the actual + * corresponding rendered
. + * + * @method addClass + * @chainable + * @param {string} className name of class to add + */ + Surface.prototype.addClass = function addClass(className) { + if (this.classList.indexOf(className) < 0) { + this.classList.push(className); + this._classesDirty = true; } + return this; + }; - // Apply values of all Famous-managed attributes to the document element. - // These will be deployed to the document on call to #setup(). - function _applyAttributes(target) { - for (var n in this.attributes) { - target.setAttribute(n, this.attributes[n]); - } - for (var index in this._dirtyAttributes) { - var name = this._dirtyAttributes[index]; - target.removeAttribute(name); - this._dirtyAttributes.shift(); - } + /** + * Remove CSS-style class from the list of classes on this Surface. + * Note this will map directly to the HTML property of the actual + * corresponding rendered
. + * + * @method removeClass + * @chainable + * @param {string} className name of class to remove + */ + Surface.prototype.removeClass = function removeClass(className) { + var i = this.classList.indexOf(className); + if (i >= 0) { + this._dirtyClasses.push(this.classList.splice(i, 1)[0]); + this._classesDirty = true; } + return this; + }; - // Clear all Famous-managed attributes from the document element. - // These will be deployed to the document on call to #setup(). - function _cleanupAttributes(target) { - for (var n in this.attributes) { - target.removeAttribute(n); - } + /** + * Toggle CSS-style class from the list of classes on this Surface. + * Note this will map directly to the HTML property of the actual + * corresponding rendered
. + * + * @method toggleClass + * @param {string} className name of class to toggle + */ + Surface.prototype.toggleClass = function toggleClass(className) { + var i = this.classList.indexOf(className); + if (i >= 0) { + this.removeClass(className); + } else { + this.addClass(className); } + return this; + }; - function _xyNotEquals(a, b) { - return (a && b) ? (a[0] !== b[0] || a[1] !== b[1]) : a !== b; + /** + * Reset class list to provided dictionary. + * @method setClasses + * @chainable + * @param {Array.string} classList + */ + Surface.prototype.setClasses = function setClasses(classList) { + var i = 0; + var removal = []; + for (i = 0; i < this.classList.length; i++) { + if (classList.indexOf(this.classList[i]) < 0) removal.push(this.classList[i]); } + for (i = 0; i < removal.length; i++) this.removeClass(removal[i]); + // duplicates are already checked by addClass() + for (i = 0; i < classList.length; i++) this.addClass(classList[i]); + return this; + }; - /** - * One-time setup for an element to be ready for commits to document. - * - * @private - * @method setup - * - * @param {ElementAllocator} allocator document element pool for this context - */ - Surface.prototype.setup = function setup(allocator) { - var target = this.allocate(allocator); - if (this.elementClass) { - if (this.elementClass instanceof Array) { - for (var i = 0; i < this.elementClass.length; i++) { - target.classList.add(this.elementClass[i]); - } - } - else { - target.classList.add(this.elementClass); - } + /** + * Get array of CSS-style classes attached to this div. + * + * @method getClasslist + * @return {Array.string} array of class names + */ + Surface.prototype.getClassList = function getClassList() { + return this.classList; + }; + + /** + * Set or overwrite inner (HTML) content of this surface. Note that this + * causes a re-rendering if the content has changed. + * + * @method setContent + * @chainable + * @param {string|Document Fragment} content HTML content + */ + Surface.prototype.setContent = function setContent(content) { + if (this.content !== content) { + this.content = content; + this._contentDirty = true; + } + return this; + }; + + /** + * Return inner (HTML) content of this surface. + * + * @method getContent + * + * @return {string} inner (HTML) content + */ + Surface.prototype.getContent = function getContent() { + return this.content; + }; + + /** + * Set options for this surface + * + * @method setOptions + * @chainable + * @param {Object} [options] overrides for default options. See constructor. + */ + Surface.prototype.setOptions = function setOptions(options) { + if (options.size) this.setSize(options.size); + if (options.classes) this.setClasses(options.classes); + if (options.properties) this.setProperties(options.properties); + if (options.attributes) this.setAttributes(options.attributes); + if (options.content) this.setContent(options.content); + return this; + }; + + // Apply to document all changes from removeClass() since last setup(). + function _cleanupClasses(target) { + for (var i = 0; i < this._dirtyClasses.length; i++) DOMBuffer.removeFromObject(target.classList, this._dirtyClasses[i]); + this._dirtyClasses = []; + } + + // Apply values of all Famous-managed styles to the document element. + // These will be deployed to the document on call to #setup(). + function _applyStyles(target) { + for (var n in this.properties) { + DOMBuffer.assignProperty(target.style, n, this.properties[n]); + } + } + + // Clear all Famous-managed styles from the document element. + // These will be deployed to the document on call to #setup(). + function _cleanupStyles(target) { + for (var n in this.properties) { + DOMBuffer.assignProperty(target.style, n, ''); + } + } + + // Apply values of all Famous-managed attributes to the document element. + // These will be deployed to the document on call to #setup(). + function _applyAttributes(target) { + for (var n in this.attributes) { + DOMBuffer.setAttribute(target, n, this.attributes[n]); + } + for (var index in this._dirtyAttributes) { + var name = this._dirtyAttributes[index]; + DOMBuffer.removeAttribute(target, name); + this._dirtyAttributes.shift(); + } + } + + // Clear all Famous-managed attributes from the document element. + // These will be deployed to the document on call to #setup(). + function _cleanupAttributes(target) { + for (var n in this.attributes) { + DOMBuffer.removeAttribute(target, n); + } + } + + function _xyNotEquals(a, b) { + return (a && b) ? (a[0] !== b[0] || a[1] !== b[1]) : a !== b; + } + + /** + * One-time setup for an element to be ready for commits to document. + * + * @private + * @method setup + * + * @param {ElementAllocator} allocator document element pool for this context + */ + Surface.prototype.setup = function setup(allocator) { + var target = this.allocate(allocator); + if (this.elementClass) { + if (this.elementClass instanceof Array) { + for (var i = 0; i < this.elementClass.length; i++) { + DOMBuffer.addToObject(target.classList, this.elementClass[i]); } - target.style.display = ''; - this.attach(target); - this._opacity = null; - this._currentTarget = target; - this._stylesDirty = true; - this._classesDirty = true; - this._attributesDirty = true; - this._sizeDirty = true; - this._contentDirty = true; - this._originDirty = true; - this._transformDirty = true; - }; - - Surface.prototype.deallocate = function deallocate(allocator, target){ + } + else { + DOMBuffer.addToObject(target.classList, this.elementClass); + } + } + DOMBuffer.assignProperty(target.style, 'display', ''); + this.attach(target); + this._opacity = null; + this._currentTarget = target; + this._stylesDirty = true; + this._classesDirty = true; + this._attributesDirty = true; + this._sizeDirty = true; + this._contentDirty = true; + this._originDirty = true; + this._transformDirty = true; + }; + + Surface.prototype.deallocate = function deallocate(allocator, target) { return allocator.deallocate(target); }; - Surface.prototype.allocate = function allocate(allocator){ - return allocator.allocate({type: this.elementType}); + Surface.prototype.allocate = function allocate(allocator) { + return allocator.allocate({ type: this.elementType }); }; - /** - * Apply changes from this component to the corresponding document element. - * This includes changes to classes, styles, size, content, opacity, origin, - * and matrix transforms. - * - * @private - * @method commit - * @param {Context} context commit context - */ - Surface.prototype.commit = function commit(context) { - if (!this._currentTarget) this.setup(context.allocator); - var target = this._currentTarget; - var size = context.size; - - if (this._classesDirty) { - _cleanupClasses.call(this, target); - var classList = this.getClassList(); - for (var i = 0; i < classList.length; i++) target.classList.add(classList[i]); - this._classesDirty = false; - this._trueSizeCheck = true; - } + /** + * Apply changes from this component to the corresponding document element. + * This includes changes to classes, styles, size, content, opacity, origin, + * and matrix transforms. + * + * @private + * @method commit + * @param {Context} context commit context + */ + Surface.prototype.commit = function commit(context) { + if (!this._currentTarget) this.setup(context.allocator); + var target = this._currentTarget; + var size = context.size; + + if (this._classesDirty) { + _cleanupClasses.call(this, target); + var classList = this.getClassList(); + for (var i = 0; i < classList.length; i++) DOMBuffer.addToObject(target.classList, classList[i]); + this._classesDirty = false; + this._trueSizeCheck = true; + } - if (this._stylesDirty) { - _applyStyles.call(this, target); - this._stylesDirty = false; - this._trueSizeCheck = true; - } + if (this._stylesDirty) { + _applyStyles.call(this, target); + this._stylesDirty = false; + this._trueSizeCheck = true; + } - if (this._attributesDirty) { - _applyAttributes.call(this, target); - this._attributesDirty = false; - this._trueSizeCheck = true; - } + if (this._attributesDirty) { + _applyAttributes.call(this, target); + this._attributesDirty = false; + this._trueSizeCheck = true; + } - if (this.size) { - var origSize = context.size; - size = [this.size[0], this.size[1]]; - if (size[0] === undefined) size[0] = origSize[0]; - if (size[1] === undefined) size[1] = origSize[1]; - if (size[0] === true || size[1] === true) { - if (size[0] === true){ - if (this._trueSizeCheck || (this._size[0] === 0)) { - var width = target.offsetWidth; - if (this._size && this._size[0] !== width) { - this._size[0] = width; - this._sizeDirty = true; - } - size[0] = width; - } else { - if (this._size) size[0] = this._size[0]; - } - } - if (size[1] === true){ - if (this._trueSizeCheck || (this._size[1] === 0)) { - var height = target.offsetHeight; - if (this._size && this._size[1] !== height) { - this._size[1] = height; - this._sizeDirty = true; - } - size[1] = height; - } else { - if (this._size) size[1] = this._size[1]; - } - } - this._trueSizeCheck = false; + if (this.size) { + var origSize = context.size; + size = [this.size[0], this.size[1]]; + if (size[0] === undefined) size[0] = origSize[0]; + if (size[1] === undefined) size[1] = origSize[1]; + if (size[0] === true || size[1] === true) { + if (size[0] === true) { + if (this._trueSizeCheck) { + var width = target.offsetWidth; + if (this._size && this._size[0] !== width) { + this._size[0] = width; + this._sizeDirty = true; + } + size[0] = width; + } else { + if (this._size) size[0] = this._size[0]; + } + } + if (size[1] === true) { + if (this._trueSizeCheck) { + var height = target.offsetHeight; + if (this._size && this._size[1] !== height) { + this._size[1] = height; + this._sizeDirty = true; } + size[1] = height; + } else { + if (this._size) size[1] = this._size[1]; + } } + this._trueSizeCheck = false; + } + } - if (_xyNotEquals(this._size, size)) { - if (!this._size) this._size = [0, 0]; - this._size[0] = size[0]; - this._size[1] = size[1]; + if (_xyNotEquals(this._size, size)) { + if (!this._size) this._size = [0, 0]; + this._size[0] = size[0]; + this._size[1] = size[1]; - this._sizeDirty = true; - } + this._sizeDirty = true; + } - if (this._sizeDirty) { - if (this._size) { - target.style.width = this.size && this.size[0] === true || this._size[0] === true ? '' : this._size[0] + 'px'; - target.style.height = this.size && this.size[1] === true || this._size[1] === true ? '' : this._size[1] + 'px'; - } + if (this._sizeDirty) { + if (this._size) { + var resolvedWidth = this.size && this.size[0] === true || this._size[0] === true ? '' : this._size[0] + 'px'; + var resolvedHeight = this.size && this.size[1] === true || this._size[1] === true ? '' : this._size[1] + 'px'; + DOMBuffer.assignProperty(target.style, 'width', resolvedWidth); + DOMBuffer.assignProperty(target.style, 'height', resolvedHeight); + } - this._eventOutput.emit('resize'); - } + this._eventOutput.emit('resize'); + } - if (this._contentDirty) { - this.deploy(target); - this._eventOutput.emit('deploy'); - this._contentDirty = false; - this._trueSizeCheck = true; - } + if (this._contentDirty) { + this.deploy(target); + this._eventOutput.emit('deploy'); + this._contentDirty = false; + this._trueSizeCheck = true; + } - ElementOutput.prototype.commit.call(this, context); - }; - - /** - * Remove all Famous-relevant attributes from a document element. - * This is called by SurfaceManager's detach(). - * This is in some sense the reverse of .deploy(). - * - * @private - * @method cleanup - * @param {ElementAllocator} allocator - */ - Surface.prototype.cleanup = function cleanup(allocator) { - /* If clean-up done twice, return. This happens when a surface is cleaned up from - * one context (e.g. group) and needs to be removed from another context that used to - * display this surface. */ - if(!this._currentTarget){ - return; - } - var i = 0; - var target = this._currentTarget; - this._eventOutput.emit('recall'); - this.recall(target); - target.style.display = 'none'; - target.style.opacity = ''; - target.style.width = ''; - target.style.height = ''; - _cleanupStyles.call(this, target); - _cleanupAttributes.call(this, target); - var classList = this.getClassList(); - _cleanupClasses.call(this, target); - for (i = 0; i < classList.length; i++) target.classList.remove(classList[i]); - if (this.elementClass) { - if (this.elementClass instanceof Array) { - for (i = 0; i < this.elementClass.length; i++) { - target.classList.remove(this.elementClass[i]); - } - } - else { - target.classList.remove(this.elementClass); - } + ElementOutput.prototype.commit.call(this, context); + }; + + /** + * Remove all Famous-relevant attributes from a document element. + * This is called by SurfaceManager's detach(). + * This is in some sense the reverse of .deploy(). + * + * @private + * @method cleanup + * @param {ElementAllocator} allocator + */ + Surface.prototype.cleanup = function cleanup(allocator) { + /* If clean-up done twice, return. This happens when a surface is cleaned up from + * one context (e.g. group) and needs to be removed from another context that used to + * display this surface. */ + if (!this._currentTarget) { + return; + } + var i = 0; + var target = this._currentTarget; + this._eventOutput.emit('recall'); + this.recall(target); + DOMBuffer.assignProperty(target.style, 'display', 'none'); + DOMBuffer.assignProperty(target.style, 'opacity', ''); + DOMBuffer.assignProperty(target.style, 'width', ''); + DOMBuffer.assignProperty(target.style, 'height', ''); + _cleanupStyles.call(this, target); + _cleanupAttributes.call(this, target); + var classList = this.getClassList(); + _cleanupClasses.call(this, target); + for (i = 0; i < classList.length; i++) target.classList.remove(classList[i]); + if (this.elementClass) { + if (this.elementClass instanceof Array) { + for (i = 0; i < this.elementClass.length; i++) { + DOMBuffer.removeFromObject(target.classList, this.elementClass[i]); } - this.detach(target); - this._currentTarget = null; - this.deallocate(allocator, target); - }; - - /** - * Place the document element that this component manages into the document. - * - * @private - * @method deploy - * @param {Node} target document parent of this container - */ - Surface.prototype.deploy = function deploy(target) { - var content = this.getContent(); - if (content instanceof Node) { - while (target.hasChildNodes()) - target.removeChild(target.firstChild); - target.appendChild(content); + } + else { + DOMBuffer.removeFromObject(target.classList, this.elementClass); + } + } + this.detach(target); + this._currentTarget = null; + this.deallocate(allocator, target); + }; + + /** + * Place the document element that this component manages into the document. + * + * @private + * @method deploy + * @param {Node} target document parent of this container + */ + Surface.prototype.deploy = function deploy(target) { + var content = this.getContent(); + + if (content instanceof Node) { + var children = target.childNodes || []; + //TODO Confirm that this works + for (var i = 0; i < children.length; i++) { + DOMBuffer.removeChild(target, children[i]); + } + DOMBuffer.appendChild(target, content); + } else { + /* textContent proved to be faster than innerHTML: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ + if (content && content.includes && content.includes('<')) { + DOMBuffer.assignProperty(target, 'innerHTML', content); + DOMBuffer.setAttributeOnDescendants(target, 'data-arvaid', this.id); } else { - target.innerHTML = content; + DOMBuffer.assignProperty(target, 'textContent', content); } - this.content = target.innerHTML; - }; - - /** - * FIX for famous-bug: https://github.com/Famous/famous/issues/673 - * - * There is a bug in recall which causes the latest setContent() - * call to be ignored, if the element is removed from the DOM in - * the next render-cycle. - */ - Surface.prototype.recall = function recall(target) { - if (!this._contentDirty) { - var df = document.createDocumentFragment(); - while (target.hasChildNodes()) { - df.appendChild(target.firstChild); - } - this.setContent(df); - } - else { - this._contentDirty = true; - } - }; - - /** - * Get the x and y dimensions of the surface. - * - * @method getSize - * @return {Array.Number} [x,y] size of surface - */ - Surface.prototype.getSize = function getSize() { - return this._size ? this._size : this.size; - }; - - /** - * Set x and y dimensions of the surface. - * - * @method setSize - * @chainable - * @param {Array.Number} size as [width, height] - */ - Surface.prototype.setSize = function setSize(size) { - this.size = size ? [size[0], size[1]] : null; - this._sizeDirty = true; - return this; - }; - - module.exports = Surface; + } + + }; + + /** + * FIX for famous-bug: https://github.com/Famous/famous/issues/673 + * + * There is a bug in recall which causes the latest setContent() + * call to be ignored, if the element is removed from the DOM in + * the next render-cycle. + */ + Surface.prototype.recall = function recall(target) { + var df = document.createDocumentFragment(); + var children = target.childNodes || []; + //TODO Confirm that this works + for (var i = 0; i < children.length; i++) { + DOMBuffer.appendChild(df, children[i]); + } + this._contentDirty = true; + + }; + + /** + * Get the x and y dimensions of the surface. + * + * @method getSize + * @return {Array.Number} [x,y] size of surface + */ + Surface.prototype.getSize = function getSize() { + return this._size ? this._size : this.size; + }; + + /** + * Set x and y dimensions of the surface. + * + * @method setSize + * @chainable + * @param {Array.Number} size as [width, height] + */ + Surface.prototype.setSize = function setSize(size) { + this.size = size ? [size[0], size[1]] : null; + this._sizeDirty = true; + return this; + }; + + module.exports = Surface; }); diff --git a/src/surfaces/ContainerSurface.js b/src/surfaces/ContainerSurface.js index c238c36a..85420b02 100644 --- a/src/surfaces/ContainerSurface.js +++ b/src/surfaces/ContainerSurface.js @@ -95,20 +95,13 @@ define(function (require, exports, module) { * @private * @method commit * @param {Context} context commit context - * @param {Transform} transform unused TODO - * @param {Number} opacity unused TODO - * @param {Array.Number} origin unused TODO - * @param {Array.Number} size unused TODO * @return {undefined} TODO returns an undefined value */ - ContainerSurface.prototype.commit = function commit(context, transform, opacity, origin, size) { + ContainerSurface.prototype.commit = function commit(context) { var previousSize = this._size ? [this._size[0], this._size[1]] : null; var result = Surface.prototype.commit.apply(this, arguments); - if (this._shouldRecalculateSize || (previousSize && (this._size[0] !== previousSize[0] || this._size[1] !== previousSize[1]))) { - this.context.setSize(); - this._shouldRecalculateSize = false; - } - this.context.update(); + this.context.setSize(context.size); + this.context.update({hide: context.opacity === 0 || context.hide}); return result; }; diff --git a/src/surfaces/InputSurface.js b/src/surfaces/InputSurface.js index 262c248b..60924662 100644 --- a/src/surfaces/InputSurface.js +++ b/src/surfaces/InputSurface.js @@ -9,6 +9,7 @@ define(function(require, exports, module) { var Surface = require('../core/Surface'); + var DOMBuffer = require('../core/DOMBuffer'); /** * A Famo.us surface in the form of an HTML input element. @@ -151,10 +152,11 @@ define(function(require, exports, module) { * @param {Node} target document parent of this container */ InputSurface.prototype.deploy = function deploy(target) { - if (this._placeholder !== '') target.placeholder = this._placeholder; - target.value = this._value; - target.type = this._type; - target.name = this._name; + if (this._placeholder !== '') + DOMBuffer.assignProperty(target, 'placeholder', this._placeholder); + DOMBuffer.assignProperty(target, 'value', this._value); + DOMBuffer.assignProperty(target, 'type', this._type); + DOMBuffer.assignProperty(target, 'name', this._name); }; module.exports = InputSurface; diff --git a/src/utilities/Timer.js b/src/utilities/Timer.js index 7ea87f6e..17669e5f 100644 --- a/src/utilities/Timer.js +++ b/src/utilities/Timer.js @@ -23,13 +23,7 @@ define(function(require, exports, module) { var _event = 'prerender'; - var getTime = (typeof window !== 'undefined' && window.performance && window.performance.now) ? - function() { - return window.performance.now(); - } - : function() { - return Date.now(); - }; + /** * Add a function to be run on every prerender @@ -58,9 +52,9 @@ define(function(require, exports, module) { * @return {function} function passed in as parameter */ function setTimeout(fn, duration) { - var t = getTime(); + var t = FamousEngine.now(); var callback = function() { - var t2 = getTime(); + var t2 = FamousEngine.now(); if (t2 - t >= duration) { fn.apply(this, arguments); FamousEngine.removeListener(_event, callback); @@ -82,12 +76,12 @@ define(function(require, exports, module) { * @return {function} function passed in as parameter */ function setInterval(fn, duration) { - var t = getTime(); + var t = FamousEngine.now(); var callback = function() { - var t2 = getTime(); + var t2 = FamousEngine.now(); if (t2 - t >= duration) { fn.apply(this, arguments); - t = getTime(); + t = FamousEngine.now(); } }; return addTimerFunction(callback); @@ -171,10 +165,10 @@ define(function(require, exports, module) { return function() { ctx = this; args = arguments; - timestamp = getTime(); + timestamp = FamousEngine.now(); var fn = function() { - var last = getTime - timestamp; + var last = FamousEngine.now() - timestamp; if (last < wait) { timeout = setTimeout(fn, wait - last);