/* Copyright © 2015-2016 David Valdman */
// TODO: Enable CSS properties on Context
define(function(require, exports, module) {
var DOMAllocator = require('./_DOMAllocator');
var Engine = require('../core/Engine');
var RootNode = require('../core/nodes/RootNode');
var Transitionable = require('../core/Transitionable');
var OptionsManager = require('../core/_OptionsManager');
var SimpleStream = require('../streams/SimpleStream');
var EventHandler = require('../events/EventHandler');
/**
* A Context defines a top-level DOM element inside which other nodes (like Surfaces) are rendered.
*
* The CSS class `samsara-context` is applied, which provides the minimal CSS necessary
* to create a performant 3D context (specifically `preserve-3d`).
*
* The Context must be mounted to a DOM node via the `mount` method. If no node is specified
* it is mounted to `document.body`.
*
* @example
*
* var context = Context();
*
* var surface = new Surface({
* size : [100,100],
* properties : {background : 'red'}
* });
*
* context.add(surface);
* context.mount(document.body)
*
* @class Context
* @constructor
* @namespace DOM
* @uses Core.RootNode
*
* @param [options] {Object} Options
* @param [options.enableScroll=false] {Boolean} Allow scrolling on mobile devices
*/
function Context(options) {
this.options = OptionsManager.setOptions(this, options, Context.DEFAULT_OPTIONS);
this._node = new RootNode();
this._size = new SimpleStream();
this._layout = new SimpleStream();
this._cachedSize = [];
this.size = this._size.map(function(type){
// If `end` event, simply return cache. Otherwise cache busting fails
// as the `end` size is the same as the `start` size for immediate sets
if (type === 'end'){
return this._cachedSize;
}
var width = this.container.clientWidth;
var height = this.container.clientHeight;
if (width !== this._cachedSize[0] || height !== this._cachedSize[1]){
this._cachedSize[0] = width;
this._cachedSize[1] = height;
this.emit('resize', this._cachedSize);
// TODO: shouldn't need to create new array - DOMOutput bug
return [width, height];
}
else return false;
}.bind(this));
this._perspective = new Transitionable();
this._perspectiveOrigin = new Transitionable();
this.perspectiveFrom(this._perspective);
this.perspectiveOriginFrom(this._perspectiveOrigin);
this._eventOutput = new EventHandler();
this._eventForwarder = function _eventForwarder(event) {
this._eventOutput.emit(event.type, event);
}.bind(this);
// Prevents dragging of entire page
if (this.options.enableScroll === false){
this.on('deploy', function(target) {
target.addEventListener('touchmove', function(event) {
event.preventDefault();
}, false);
});
}
}
Context.prototype.elementClass = 'samsara-context';
Context.DEFAULT_OPTIONS = {
enableScroll : false
};
/**
* Extends the render tree beginning with the Context's RootNode with a new node.
* Delegates to RootNode's `add` method.
*
* @method add
*
* @param {Object} Renderable
* @return {RenderTreeNode} Wrapped node
*/
Context.prototype.add = function add() {
return RootNode.prototype.add.apply(this._node, arguments);
};
Context.prototype.remove = function remove(){
if (this.elementClass instanceof Array){
for (var i = 0; i < this.elementClass.length; i++)
this.container.classList.remove(this.elementClass[i])
}
else this.container.classList.remove(this.elementClass);
this._node.remove();
while (this.container.hasChildNodes())
this.container.removeChild(this.container.firstChild);
Engine.deregisterContext(this);
};
/**
* Pull the perspective value from a transitionable.
*
* @method perspectiveFrom
* @param perspective {Transitionable} Perspective transitionable
*/
Context.prototype.perspectiveFrom = function perspectiveFrom(perspective){
this._perspective = perspective;
this._perspective.on('update', function(perspective){
setPerspective(this.container, perspective);
}.bind(this));
this._perspective.on('end', function(perspective){
setPerspective(this.container, perspective);
}.bind(this));
};
/**
* Pull the perspective-origin value from a transitionable.
*
* @method perspectiveOriginFrom
* @param perspectiveOrigin {Transitionable} Perspective-origin transitionable
*/
Context.prototype.perspectiveOriginFrom = function perspectiveOriginFrom(perspectiveOrigin){
this._perspectiveOrigin = perspectiveOrigin;
this._perspectiveOrigin.on('update', function(origin){
setPerspectiveOrigin(this.container, origin);
}.bind(this));
this._perspectiveOrigin.on('end', function(origin){
setPerspectiveOrigin(this.container, origin);
}.bind(this));
};
/**
* Get current perspective of this Context in pixels.
*
* @method getPerspective
* @return {Number} Perspective in pixels
*/
Context.prototype.getPerspective = function getPerspective() {
return this._perspective.get();
};
/**
* Set current perspective of the `context` in pixels.
*
* @method setPerspective
* @param perspective {Number} Perspective in pixels
* @param [transition] {Object} Transition definition
* @param [callback] {Function} Callback executed on completion of transition
*/
Context.prototype.setPerspective = function setPerspective(perspective, transition, callback) {
if (this.container)
this._perspective.set(perspective, transition, callback);
else {
this.on('deploy', function(){
this._perspective.set(perspective, transition, callback);
}.bind(this));
}
};
/**
* Set current perspective of the `context` in pixels.
*
* @method setPerspective
* @param perspective {Number} Perspective in pixels
* @param [transition] {Object} Transition definition
* @param [callback] {Function} Callback executed on completion of transition
*/
Context.prototype.setPerspectiveOrigin = function setPerspectiveOrigin(origin, transition, callback) {
if (this.container)
this._perspectiveOrigin.set(origin, transition, callback);
else {
this.on('deploy', function() {
this._perspectiveOrigin.set(origin, transition, callback);
}.bind(this));
}
};
/**
* Allocate contents of the `context` to a DOM node.
*
* @method mount
* @param node {Node} DOM element
*/
Context.prototype.mount = function mount(node){
node = node || window.document.body;
this.container = node;
if (this.elementClass instanceof Array) {
for (var i = 0; i < this.elementClass.length; i++)
this.container.classList.add(this.elementClass[i])
}
else this.container.classList.add(this.elementClass);
var allocator = new DOMAllocator(this.container);
this._node.setAllocator(allocator);
this._node._size.subscribe(this.size);
this._node._layout.subscribe(this._layout);
this.emit('deploy', this.container);
Engine.registerContext(this);
};
/**
* Adds a handler to the `type` channel which will be executed on `emit`.
* These events should be DOM events that occur on the DOM node the
* context has been mounted to.
*
* @method on
* @param type {String} Channel name
* @param handler {Function} Callback
*/
Context.prototype.on = function on(type, handler){
if (this.container)
this.container.addEventListener(type, this._eventForwarder);
else {
this._eventOutput.on('deploy', function(target){
target.addEventListener(type, this._eventForwarder);
}.bind(this));
}
EventHandler.prototype.on.apply(this._eventOutput, arguments);
};
/**
* Removes the `handler` from the `type`.
* Undoes the work of `on`.
*
* @method off
* @param type {String} Channel name
* @param handler {Function} Callback
*/
Context.prototype.off = function off(type, handler) {
EventHandler.prototype.off.apply(this._eventOutput, arguments);
};
/**
* Used internally when context is subscribed to.
*
* @method emit
* @private
* @param type {String} Channel name
* @param data {Object} Payload
*/
Context.prototype.emit = function emit(type, payload) {
EventHandler.prototype.emit.apply(this._eventOutput, arguments);
};
var usePrefix = !('perspective' in window.document.documentElement.style);
var setPerspective = usePrefix
? function setPerspective(element, perspective) {
element.style.webkitPerspective = perspective ? (perspective | 0) + 'px' : '0px';
}
: function setPerspective(element, perspective) {
element.style.perspective = perspective ? (perspective | 0) + 'px' : '0px';
};
function _formatCSSOrigin(origin) {
return (100 * origin[0]) + '% ' + (100 * origin[1]) + '%';
}
var setPerspectiveOrigin = usePrefix
? function setPerspectiveOrigin(element, origin) {
element.style.webkitPerspectiveOrigin = origin ? _formatCSSOrigin(origin) : '50% 50%';
}
: function setPerspectiveOrigin(element, origin) {
element.style.perspectiveOrigin = origin ? _formatCSSOrigin(origin) : '50% 50%';
};
module.exports = Context;
});