Skip to content

Instantly share code, notes, and snippets.

@bingomanatee
Created May 5, 2014 22:02
Show Gist options
  • Save bingomanatee/11548653 to your computer and use it in GitHub Desktop.
Save bingomanatee/11548653 to your computer and use it in GitHub Desktop.
because surfaces want to be human
/* 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