diff --git a/src/ui/context/Context.js b/src/ui/context/Context.js new file mode 100644 index 0000000..527a349 --- /dev/null +++ b/src/ui/context/Context.js @@ -0,0 +1,34 @@ +/** + * Field group context + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Interface = require( 'easejs' ).Interface; + + +/** + * A subset of a larger collection of fields that can be used to restrict + * operations for both convenience and (moreso) performance + */ +module.exports = Interface( 'Context', +{ + 'public getFieldByName': [ 'name', 'index', 'filter' ], + + 'public split': [ 'on' ] +} ); diff --git a/src/ui/context/DomContext.js b/src/ui/context/DomContext.js new file mode 100644 index 0000000..7bee154 --- /dev/null +++ b/src/ui/context/DomContext.js @@ -0,0 +1,347 @@ +/** + * DOM subset context + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + Context = require( './Context' ), + + EventEmitter = require( 'events' ).EventEmitter; + + +/** + * A subset of the DOM that can be used to restrict operations for both + * convenience and (moreso) performance + */ +module.exports = Class( 'DomContext' ) + .implement( Context ) + .extend( EventEmitter, +{ + /** + * Parent context, if any + * @type {DomContext} + */ + 'private _pcontext': null, + + /** + * DOM content for this particular context + * @type {HTMLElement} + */ + 'private _content': null, + + /** + * Parent to re-attach to + * @type {HtmlElement} + */ + 'private _contentParent': null, + + /** + * Factory used to produce DomFields + * @type {DomFieldFactory} + */ + 'private _fieldFactory': null, + + /** + * Cache of fields that have been looked up previously + * @type {Object} + */ + 'private _fieldCache': {}, + + /** + * Continuations to be invoked once attached to the DOM + * @type {Array.} + */ + 'private _attachq': [], + + /** + * Continuations to be invoked once detached from the DOM + * @type {Array.} + */ + 'private _detachq': [], + + + __construct: function( content, field_factory, pcontext, cache ) + { + // older browsers do not support HTMLElement, but we still want the type + // check for newer ones + if ( window.HTMLElement && !( content instanceof HTMLElement ) ) + { + throw TypeError( "Context content must be a valid HTMLElement" ); + } + else if ( !( this.verifyParentContext( pcontext ) ) ) + { + throw TypeError( "Invalid parent DomContext" ); + } + + this._content = content; + this._fieldFactory = field_factory; + this._pcontext = pcontext || null; + this._fieldCache = cache || {}; + }, + + + 'virtual protected verifyParentContext': function( context ) + { + return Class.isA( module.exports, context ); + }, + + + 'public split': function( on_id, c ) + { + var _self = this, + inst = _self.__inst; + + this._getElementById( on_id, function( element ) + { + // if the element could not be found, just return self + c( ( element ) + ? module.exports( + element, + _self._fieldFactory, + inst, + _self._fieldCache + ).on( 'error', function( e ) + { + // "bubble up" errors + _self.emit( 'error', e ); + } ) + + : inst + ); + } ); + + return this; + }, + + + 'public getFieldByName': function( name, index, filter ) + { + var result = this._fromCache( name, index ); + + if ( filter ) + { + throw Error( "TODO: filter" ); + } + + return result; + }, + + + 'private _getElementById': function( id, c ) + { + id = ''+id; + + if ( !id ) + { + c( null ); + return; + } + + // we cannot perform the highly performant getElementById() unless we + // are attached to the DOM + this.whenAttached( function() + { + c( document.getElementById( id ) ); + } ); + }, + + + 'private _fromCache': function( name, index, lookup ) + { + var data = ( + this._fieldCache[ name ] = this._fieldCache[ name ] || [] + ); + + // if already present within the cache, simply return it + if ( data[ index ] ) + { + return data[ index ]; + } + + // add to cache and return + var _self = this; + return data[ index ] = this._fieldFactory.create( + name, index, + + // this is intended to defer request of the root element until this + // context is attached to the DOM; this ensures that the requester + // can take advantage of features of the attached DOM such as + // getElementById() and defers initial DOM operations until the + // element is actually available on the DOM + function( c ) + { + // invoke the continuation as soon as we're attached to the DOM + _self.whenAttached( c ); + } + ).on( 'error', function( e ) + { + // forward errors + _self.emit( 'error', e ); + } ); + }, + + + /** + * Determines whether this context is currently attached to the DOM + * + * @return {boolean} true if attached to the DOM, otherwise false + */ + 'virtual public isAttached': function() + { + // we are attached if (a) our content node has a parent and (b) if our + // parent context is also attached + return !!this._content.parentElement && this._pcontext.isAttached(); + }, + + + /** + * Schedules a continunation to be invoked once the context becomes attached + * to the DOM + * + * If already attached, the continuation will be executed immediately + * (synchronously). + * + * @param {function()} c continuation to be invoked + * + * @return {DomContext} self + */ + 'public whenAttached': function( c ) + { + // invoke immediately if we're already attached + if ( this.isAttached() ) + { + c(); + return this; + } + + // queue continuation + var _self = this; + this._attachq.push( function() + { + // ensure that we're still attached to the DOM by the time this + // continuation is actually invoked + if ( !( _self.isAttached() ) ) + { + // tough luck; try again later + _self.whenAttached( c ); + return; + } + + c(); + } ); + + return this; + }, + + + 'public whenDetached': function( c ) + { + // invoke immediately if we're not attached + if ( this.isAttached() === false ) + { + c(); + return this; + } + + // queue the continuation + var _self = this; + this._detachc.push( function() + { + // ensure that we're still detached from the DOM + if ( _self.isAttached() ) + { + // tough luck; try again later + _self.whenDetached( c ); + return; + } + + c(); + } ); + + return this; + }, + + + 'virtual public attach': function( to ) + { + var _self = this; + + // if we are already attached to the DOM, then do nothing (note that we + // check the parent element of our content node because something could + // have detached the node from the DOM without us knowing) + if ( this._content.parentElement ) + { + return this; + } + + // default to the stored parent if they did not provide anything + to = ( to || this._contentParent ); + if ( !( Class.isA( HTMLElement, to ) ) ) + { + throw TypeError( "Cannot attach context to " + to.toString() ); + } + + // re-attach ourselves to our parent and dequeue the continuations only + // once our parent is attached (will execute immediately if we are + // already attached) + to.appendChild( this._content ); + this._pcontext.whenAttached( function() + { + _self._dequeue( _self._attachq ); + } ); + + return this; + }, + + + 'virtual public detach': function() + { + // do nothing if we are not attached to the DOM (note that we check the + // parent element of the content because something else could have + // re-attached our content node to the DOM without us knowing) + if ( !( this._content.parentElement ) ) + { + return this; + } + + // store the parent so that we know where to re-attach ourselves + this._contentParent = this._content.parentElement; + + // detach from the DOM and dequeue the conintinuations (we don't care if + // our parent is detached since we're still detached regardless) + this._contentParent.removeChild( this._content ); + this._dequeue( this._detachq ); + + return this; + }, + + + 'private _dequeue': function( q ) + { + // transfer continuation queue onto the JS timeout stack + var c, _self = this; + while ( c = q.shift() ) + { + // ensures that the continuations will be executed without locking + // up the browser; this is important, since these are DOM + // manipulations and therefore may be intensive! + setTimeout( c, 25 ); + } + } +} ); diff --git a/src/ui/context/DynamicContext.js b/src/ui/context/DynamicContext.js new file mode 100644 index 0000000..8a517fb --- /dev/null +++ b/src/ui/context/DynamicContext.js @@ -0,0 +1,64 @@ +/** + * Dynamic field context + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + Context = require( './Context' ); + + +/** + * Mutable Context + * + * This exists primarily to ease refactoring of old parts of the framework; + * it should not be preferred going forward. + */ +module.exports = Class( 'DynamicContext' ) + .implement( Context ) + .extend( +{ + /** + * Current context + * @type {Context} + */ + 'private _context': null, + + + __construct: function( initial ) + { + this.assign( initial ); + }, + + + 'public assign': function( context ) + { + if ( !( Class.isA( Context, context ) ) ) + { + throw TypeError( "Invalid context" ); + } + + this._context = context; + return this; + }, + + + 'public proxy getFieldByName': '_context', + + 'public proxy split': '_context' +} ); diff --git a/src/ui/context/RootDomContext.js b/src/ui/context/RootDomContext.js new file mode 100644 index 0000000..15574da --- /dev/null +++ b/src/ui/context/RootDomContext.js @@ -0,0 +1,63 @@ +/** + * DOM context representing document root + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + DomContext = require( './DomContext' ); + + +/** + * Intended to serve as the topmost context in a context tree + * + * Since all other DomContexts besides this one must have a parent, it may + * be useful to create other DomContext objects by split()'ing an instance + * of this class. + * + * The root context cannot be detached from the DOM. + */ +module.exports = Class( 'RootDomContext' ) + .extend( DomContext, +{ + 'override protected verifyParentContext': function( context ) + { + // we have no parent... :( + // (this class has Mommy/Daddy issues) + return true; + }, + + + 'override public isAttached': function() + { + // of course we are. + return true; + }, + + + 'override public attach': function( to ) + { + throw Error( "Cannot attach DOM root" ); + }, + + + 'override public detach': function( to ) + { + throw Error( "Cannot detach DOM root" ); + } +} );