Created
May 5, 2014 22:02
-
-
Save bingomanatee/11548653 to your computer and use it in GitHub Desktop.
because surfaces want to be human
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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: [email protected] | |
* @license MPL 2.0 | |
* @copyright Famous Industries, Inc. 2014 | |
*/ | |
define(function(require, exports, module) { | |
var Entity = require('famous/core/Entity'); | |
var EventHandler = require('famous/core/EventHandler'); | |
var Transform = require('famous/core/Transform'); | |
var usePrefix = document.body.style.webkitTransform !== undefined; | |
var devicePixelRatio = window.devicePixelRatio || 1; | |
/** | |
* DataSurface is an extension of the Surface Class that enables data attributes | |
* | |
* @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 inner content | |
* @param {Array} [options.properties] string dictionary of HTML attributes to set on target div | |
* @param {string} [options.content] inner (HTML) content of surface | |
*/ | |
function DataSurface(options) { | |
this.options = {}; | |
this.properties = {}; | |
this.data = {}; | |
this.content = ''; | |
this.classList = []; | |
this.size = null; | |
this._classesDirty = true; | |
this._stylesDirty = true; | |
this._sizeDirty = true; | |
this._contentDirty = true; | |
this._dataDirty = true; | |
this._dirtyClasses = []; | |
this._matrix = null; | |
this._opacity = 1; | |
this._origin = null; | |
this._size = null; | |
/** @ignore */ | |
this.eventForwarder = function eventForwarder(event) { | |
this.emit(event.type, event); | |
}.bind(this); | |
this.eventHandler = new EventHandler(); | |
this.eventHandler.bindThis(this); | |
this.id = Entity.register(this); | |
if (options) this.setOptions(options); | |
this._currTarget = null; | |
} | |
DataSurface.prototype.elementType = 'div'; | |
DataSurface.prototype.elementClass = 'famous-surface'; | |
/** | |
* Bind a callback function to an event type handled by this object. | |
* | |
* @method "on" | |
* | |
* @param {string} type event type key (for example, 'click') | |
* @param {function(string, Object)} fn handler callback | |
* @return {EventHandler} this | |
*/ | |
DataSurface.prototype.on = function on(type, fn) { | |
if (this._currTarget) this._currTarget.addEventListener(type, this.eventForwarder); | |
this.eventHandler.on(type, fn); | |
}; | |
/** | |
* Unbind an event by type and handler. | |
* This undoes the work of "on" | |
* | |
* @method removeListener | |
* @param {string} type event type key (for example, 'click') | |
* @param {function(string, Object)} fn handler | |
*/ | |
DataSurface.prototype.removeListener = function removeListener(type, fn) { | |
this.eventHandler.removeListener(type, fn); | |
}; | |
/** | |
* Trigger an event, sending to all downstream handlers | |
* listening for provided 'type' key. | |
* | |
* @method emit | |
* | |
* @param {string} type event type key (for example, 'click') | |
* @param {Object} [event] event data | |
* @return {EventHandler} this | |
*/ | |
DataSurface.prototype.emit = function emit(type, event) { | |
if (event && !event.origin) event.origin = this; | |
var handled = this.eventHandler.emit(type, event); | |
if (handled && event && event.stopPropagation) event.stopPropagation(); | |
return handled; | |
}; | |
/** | |
* Add event handler object to set of downstream handlers. | |
* | |
* @method pipe | |
* | |
* @param {EventHandler} target event handler target object | |
* @return {EventHandler} passed event handler | |
*/ | |
DataSurface.prototype.pipe = function pipe(target) { | |
return this.eventHandler.pipe(target); | |
}; | |
/** | |
* Remove handler object from set of downstream handlers. | |
* Undoes work of "pipe" | |
* | |
* @method unpipe | |
* | |
* @param {EventHandler} target target handler object | |
* @return {EventHandler} provided target | |
*/ | |
DataSurface.prototype.unpipe = function unpipe(target) { | |
return this.eventHandler.unpipe(target); | |
}; | |
/** | |
* Return spec for this surface. Note that for a base surface, this is | |
* simply an id. | |
* | |
* @method render | |
* @private | |
* @return {Object} render spec for this surface (spec id) | |
*/ | |
DataSurface.prototype.render = function render() { | |
return this.id; | |
}; | |
/** | |
* Set CSS-style properties on this DataSurface. Note that this will cause | |
* dirtying and thus re-rendering, even if values do not change. | |
* | |
* @method setProperties | |
* @param {Object} properties property dictionary of "key" => "value" | |
*/ | |
DataSurface.prototype.setProperties = function setProperties(properties) { | |
for (var n in properties) { | |
this.properties[n] = properties[n]; | |
} | |
this._stylesDirty = true; | |
}; | |
/** | |
* Set CSS-style properties on this DataSurface. Note that this will cause | |
* dirtying and thus re-rendering, even if values do not change. | |
* | |
* @method setProperties | |
* @param {Object} properties property dictionary of "key" => "value" | |
*/ | |
DataSurface.prototype.setData = function setData(properties) { | |
for (var n in properties) { | |
this.data[n] = properties[n]; | |
} | |
this._dataDirty = true; | |
}; | |
DataSurface.prototype.getData = function getData(){ | |
return this.data; | |
}; | |
function _applyData(target) { | |
for (var n in this.data) { | |
var attr = 'data-' + n; | |
target.setAttribute(attr, this.data[n]); | |
} | |
} | |
/** | |
* Get CSS-style properties on this DataSurface. | |
* | |
* @method getProperties | |
* | |
* @return {Object} Dictionary of this DataSurface's properties. | |
*/ | |
DataSurface.prototype.getProperties = function getProperties() { | |
return this.properties; | |
}; | |
/** | |
* Add CSS-style class to the list of classes on this DataSurface. Note | |
* this will map directly to the HTML property of the actual | |
* corresponding rendered <div>. | |
* | |
* @method addClass | |
* @param {string} className name of class to add | |
*/ | |
DataSurface.prototype.addClass = function addClass(className) { | |
if (this.classList.indexOf(className) < 0) { | |
this.classList.push(className); | |
this._classesDirty = true; | |
} | |
}; | |
/** | |
* Remove CSS-style class from the list of classes on this DataSurface. | |
* Note this will map directly to the HTML property of the actual | |
* corresponding rendered <div>. | |
* | |
* @method removeClass | |
* @param {string} className name of class to remove | |
*/ | |
DataSurface.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; | |
} | |
}; | |
/** | |
* Reset class list to provided dictionary. | |
* @method setClasses | |
* @param {Array.string} classList | |
*/ | |
DataSurface.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]); | |
}; | |
/** | |
* Get array of CSS-style classes attached to this div. | |
* | |
* @method getClasslist | |
* @return {Array.string} array of class names | |
*/ | |
DataSurface.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 | |
* @param {string} content HTML content | |
*/ | |
DataSurface.prototype.setContent = function setContent(content) { | |
if (this.content !== content) { | |
this.content = content; | |
this._contentDirty = true; | |
} | |
}; | |
/** | |
* Return inner (HTML) content of this surface. | |
* | |
* @method getContent | |
* | |
* @return {string} inner (HTML) content | |
*/ | |
DataSurface.prototype.getContent = function getContent() { | |
return this.content; | |
}; | |
/** | |
* Set options for this surface | |
* | |
* @method setOptions | |
* @param {Object} [options] overrides for default options. See constructor. | |
*/ | |
DataSurface.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.content) this.setContent(options.content); | |
if (options.data) this.setData(options.data); | |
}; | |
// Attach Famous event handling to document events emanating from target | |
// document element. This occurs just after deployment to the document. | |
// Calling this enables methods like #on and #pipe. | |
function _addEventListeners(target) { | |
for (var i in this.eventHandler.listeners) { | |
target.addEventListener(i, this.eventForwarder); | |
} | |
} | |
// Detach Famous event handling from document events emanating from target | |
// document element. This occurs just before recall from the document. | |
function _removeEventListeners(target) { | |
for (var i in this.eventHandler.listeners) { | |
target.removeEventListener(i, this.eventForwarder); | |
} | |
} | |
// 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 = []; | |
} | |
// 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]; | |
} | |
} | |
// 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] = ''; | |
} | |
} | |
/** | |
* Return a Matrix's webkit css representation to be used with the | |
* CSS3 -webkit-transform style. | |
* Example: -webkit-transform: matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,716,243,0,1) | |
* | |
* @method _formatCSSTransform | |
* @private | |
* @param {FamousMatrix} m matrix | |
* @return {string} matrix3d CSS style representation of the transform | |
*/ | |
function _formatCSSTransform(m) { | |
m[12] = Math.round(m[12] * devicePixelRatio) / devicePixelRatio; | |
m[13] = Math.round(m[13] * devicePixelRatio) / devicePixelRatio; | |
var result = 'matrix3d('; | |
for (var i = 0; i < 15; i++) { | |
result += (m[i] < 0.000001 && m[i] > -0.000001) ? '0,' : m[i] + ','; | |
} | |
result += m[15] + ')'; | |
return result; | |
} | |
/** | |
* Directly apply given FamousMatrix to the document element as the | |
* appropriate webkit CSS style. | |
* | |
* @method setMatrix | |
* | |
* @static | |
* @private | |
* @param {Element} element document element | |
* @param {FamousMatrix} matrix | |
*/ | |
var _setMatrix; | |
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { | |
_setMatrix = function(element, matrix) { | |
element.style.zIndex = (matrix[14] * 1000000) | 0; // fix for Firefox z-buffer issues | |
element.style.transform = _formatCSSTransform(matrix); | |
}; | |
} | |
else if (usePrefix) { | |
_setMatrix = function(element, matrix) { | |
element.style.webkitTransform = _formatCSSTransform(matrix); | |
}; | |
} | |
else { | |
_setMatrix = function(element, matrix) { | |
element.style.transform = _formatCSSTransform(matrix); | |
}; | |
} | |
// format origin as CSS percentage string | |
function _formatCSSOrigin(origin) { | |
return (100 * origin[0]).toFixed(6) + '% ' + (100 * origin[1]).toFixed(6) + '%'; | |
} | |
// 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); | |
} : function(element, origin) { | |
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,1)'; | |
element.style.opacity = 0; | |
} : function(element) { | |
element.style.transform = 'scale3d(0.0001,0.0001,1)'; | |
element.style.opacity = 0; | |
}; | |
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 | |
*/ | |
DataSurface.prototype.setup = function setup(allocator) { | |
var target = allocator.allocate(this.elementType); | |
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); | |
} | |
} | |
target.style.display = ''; | |
_addEventListeners.call(this, target); | |
_setOrigin(target, [0, 0]); // handled internally | |
this._currTarget = target; | |
this._stylesDirty = true; | |
this._classesDirty = true; | |
this._sizeDirty = true; | |
this._contentDirty = true; | |
this._matrix = null; | |
this._opacity = undefined; | |
this._origin = null; | |
this._size = null; | |
}; | |
/** | |
* 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 | |
*/ | |
DataSurface.prototype.commit = function commit(context) { | |
if (!this._currTarget) this.setup(context.allocator); | |
var target = this._currTarget; | |
var matrix = context.transform; | |
var opacity = context.opacity; | |
var origin = context.origin; | |
var size = context.size; | |
if (this.size) { | |
var origSize = size; | |
size = [this.size[0], this.size[1]]; | |
if (size[0] === undefined && origSize[0]) size[0] = origSize[0]; | |
if (size[1] === undefined && origSize[1]) size[1] = origSize[1]; | |
} | |
if (_xyNotEquals(this._size, size)) { | |
this._size = [size[0], size[1]]; | |
this._sizeDirty = true; | |
} | |
if (!matrix && this._matrix) { | |
this._matrix = null; | |
this._opacity = 0; | |
_setInvisible(target); | |
return; | |
} | |
if (this._opacity !== opacity) { | |
this._opacity = opacity; | |
target.style.opacity = (opacity >= 1) ? '0.999999' : opacity; | |
} | |
if (_xyNotEquals(this._origin, origin) || Transform.notEquals(this._matrix, matrix)) { | |
if (!matrix) matrix = Transform.identity; | |
this._matrix = matrix; | |
var aaMatrix = matrix; | |
if (origin) { | |
if (!this._origin) this._origin = [0, 0]; | |
this._origin[0] = origin[0]; | |
this._origin[1] = origin[1]; | |
aaMatrix = Transform.moveThen([-this._size[0] * origin[0], -this._size[1] * origin[1], 0], matrix); | |
} | |
_setMatrix(target, aaMatrix); | |
} | |
if (!(this._classesDirty || this._dataDirty || this._stylesDirty || this._sizeDirty || this._contentDirty)) return; | |
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; | |
} | |
if (this._stylesDirty) { | |
//@TODO: remove data attributes not present in surface.data | |
_applyStyles.call(this, target); | |
this._stylesDirty = false; | |
} | |
if (this._sizeDirty) { | |
if (this._size) { | |
target.style.width = (this._size[0] !== true) ? this._size[0] + 'px' : ''; | |
target.style.height = (this._size[1] !== true) ? this._size[1] + 'px' : ''; | |
} | |
this._sizeDirty = false; | |
} | |
if (this._dataDirty){ | |
_applyData.call(this, target); | |
this._dataDirty = false; | |
} | |
if (this._contentDirty) { | |
this.deploy(target); | |
this.eventHandler.emit('deploy'); | |
this._contentDirty = false; | |
} | |
}; | |
/** | |
* 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 | |
*/ | |
DataSurface.prototype.cleanup = function cleanup(allocator) { | |
var i = 0; | |
var target = this._currTarget; | |
this.eventHandler.emit('recall'); | |
this.recall(target); | |
target.style.display = 'none'; | |
target.style.width = ''; | |
target.style.height = ''; | |
this._size = null; | |
_cleanupStyles.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); | |
} | |
} | |
_removeEventListeners.call(this, target); | |
this._currTarget = null; | |
allocator.deallocate(target); | |
_setInvisible(target); | |
}; | |
/** | |
* Place the document element that this component manages into the document. | |
* | |
* @private | |
* @method deploy | |
* @param {Node} target document parent of this container | |
*/ | |
DataSurface.prototype.deploy = function deploy(target) { | |
var content = this.getContent(); | |
if (content instanceof Node) { | |
while (target.hasChildNodes()) target.removeChild(target.firstChild); | |
target.appendChild(content); | |
} | |
else target.innerHTML = content; | |
}; | |
/** | |
* Remove any contained document content associated with this surface | |
* from the actual document. | |
* | |
* @private | |
* @method recall | |
*/ | |
DataSurface.prototype.recall = function recall(target) { | |
var df = document.createDocumentFragment(); | |
while (target.hasChildNodes()) df.appendChild(target.firstChild); | |
this.setContent(df); | |
}; | |
/** | |
* Get the x and y dimensions of the surface. | |
* | |
* @method getSize | |
* @param {boolean} actual return computed size rather than provided | |
* @return {Array.Number} [x,y] size of surface | |
*/ | |
DataSurface.prototype.getSize = function getSize(actual) { | |
return actual ? this._size : (this.size || this._size); | |
}; | |
/** | |
* Set x and y dimensions of the surface. | |
* | |
* @method setSize | |
* @param {Array.Number} size as [width, height] | |
*/ | |
DataSurface.prototype.setSize = function setSize(size) { | |
this.size = size ? [size[0], size[1]] : null; | |
this._sizeDirty = true; | |
}; | |
module.exports = DataSurface; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment