diff --git a/src/field/BucketField.js b/src/field/BucketField.js new file mode 100644 index 0000000..4a6e8bf --- /dev/null +++ b/src/field/BucketField.js @@ -0,0 +1,60 @@ +/** + * Field representing bucket value + * + * Copyright (C) 2015 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, + Field = require( './Field' ); + + +module.exports = Class( 'BucketField' ) + .implement( Field ) + .extend( +{ + /** + * Field name + * @type {string} + */ + 'private _name': '', + + /** + * Field index + * @type {string}' + */ + 'private _index': 0, + + + __construct: function( name, index ) + { + this._name = ''+name; + this._index = +index; + }, + + + 'public getName': function() + { + return this._name; + }, + + + 'public getIndex': function() + { + return this._index; + } +} ); diff --git a/src/field/Field.js b/src/field/Field.js new file mode 100644 index 0000000..83128ec --- /dev/null +++ b/src/field/Field.js @@ -0,0 +1,31 @@ +/** + * Field representation + * + * Copyright (C) 2015 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; + + +module.exports = Interface( 'Field', +{ + 'public getName': [], + + 'public getIndex': [] +} ); diff --git a/src/ui/field/DomField.js b/src/ui/field/DomField.js new file mode 100644 index 0000000..d252785 --- /dev/null +++ b/src/ui/field/DomField.js @@ -0,0 +1,300 @@ +/** + * Field represented by DOM element + * + * Copyright (C) 2015 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, + Field = require( '../../field/Field' ), + + EventEmitter = require( 'events' ).EventEmitter; + + +module.exports = Class( 'DomField' ) + .implement( Field ) + .extend( EventEmitter, +{ + /** + * Wrapped field + * @type {Field} + */ + 'private _field': null, + + 'private _element': null, + + 'private _idPrefix': 'q_', + + /** + * Currently active styles + * @type {Object} + */ + 'private _styles': {}, + + + __construct: function( field, element ) + { + if ( !( Class.isA( Field, field ) ) ) + { + throw TypeError( "Invalid field provided" ); + } + + this._field = field; + this._element = element; + }, + + + 'public proxy getName': '_field', + 'public proxy getIndex': '_field', + + + 'private _getElement': function( callback ) + { + // if the provided root is a function, then it should be lazily laoded + if ( this._element === null ) + { + // if the element is null, then we have some serious problems; do + // not even invoke the callback + return; + } + else if ( typeof this._element === 'function' ) + { + var _self = this, + f = this._element; + + // any further requests for this element should be queued rather + // than resulting in a thundering herd toward the DOM (imporant: do + // this *before* invoking the function, since it may be synchronous) + var queue = []; + this._element = function( c ) + { + queue.push( c ); + }; + + // attempt to retrieve our element from the DOM + f( function( element ) + { + if ( !element ) + { + _self._element = null; + _self.emit( 'error', Error( + "Cannot locate DOM element for field " + + _self.getName() + "[" + _self.getIndex() + "]" + ) ); + + // do not even finish; this shit is for real. + return; + } + + _self._element = element; + callback( element ); + + // if we have any queued requests, process them when we're not + // busy + var c; + while ( c = queue.shift() ) + { + setTimeout( function() + { + // return the element to the queued callback + c( element ); + }, 25 ); + } + } ); + + return; + } + + // we already have the element; immediately return it + callback( this._element ); + }, + + + 'private _hasStyle': function( style ) + { + return !!this._styles[ style.getId() ]; + }, + + + 'private _flagStyle': function( style, flag ) + { + this._styles[ style.getId() ] = !!flag; + }, + + + 'public applyStyle': function( style ) + { + var _self = this; + + // if we already have this style applied, then ignore this request + if ( this._hasStyle( style ) ) + { + return this; + } + + // all remaining arguments should be passed to the style + var sargs = Array.prototype.slice.call( arguments, 1 ); + + // flag style immediately to ensure we do not queue multiple application + // requests + this._flagStyle( style, true ); + + // wait for our element to become available on the DOM and perform the + // styling + this._getElement( function( root ) + { + style.applyStyle.apply( + style, + [ _self.__inst, root, _self.getContainingRow() ].concat( sargs ) + ); + } ); + + return this; + }, + + + 'public revokeStyle': function( style ) + { + var _self = this; + + // if this style is not applied, then do nothing + if ( !( this._hasStyle( style ) ) ) + { + return this; + } + + // immediately flag style to ensure that we do not queue multiple + // revocation requests + this._flagStyle( style, false ); + + this._getElement( function( root ) + { + style.revokeStyle( _self.__inst, root, _self.getContainingRow() ); + } ); + + return this; + }, + + + /** + * Resolves a field into an id that may be used to query the DOM + * + * @return {string} expected id of element on the DOM + */ + 'protected resolveId': function() + { + return this.doResolveId( + this._field.getName(), + this._field.getIndex() + ); + }, + + + /** + * Resolves a field into an id that may be used to query the DOM + * + * This may be overridden by a subtype to alter the resolution logic. The + * name and index are passed to the method to ensure that the field itself + * remains encapsulated. + * + * @param {string} name field name + * @param {number} index field index + * + * @return {string} expected id of element on the DOM + */ + 'virtual protected doResolveId': function( name, index ) + { + return ( this._idPrefix + name + '_' + index ); + }, + + + // TODO: move me + 'protected getContainingRow': function() + { + var dd = this.getParent( this._element, 'dd' ), + dt = ( dd ) ? this.getPrecedingSibling( dd, 'dt' ) : null; + + return ( dt ) + ? [ dd, dt ] + : [ this.getParent( this._element ) ]; + }, + + + 'protected getParent': function( element, type ) + { + var parent = element.parentElement; + + if ( parent === null ) + { + return null; + } + else if ( !type ) + { + return parent; + } + + // nodeName is in caps + if ( type.toUpperCase() === parent.nodeName ) + { + return parent; + } + + // otherwise, keep looking + return this.getParent( parent, type ); + }, + + + 'protected getPrecedingSibling': function( element, type ) + { + return this.getSibling( element, type, -1 ); + }, + + + 'protected getFollowingSibling': function( element, type ) + { + return this.getSibling( element, type, 1 ); + }, + + + 'protected getSibling': function( element, type, direction ) + { + // if no direction was provided, then search in both + if ( !direction ) + { + return ( this.getSibling( element, type, -1 ) + || this.getSibling( element, type, 1 ) + ); + } + + // get the next node relative to the direction + var next = element[ + ( direction === -1 ) ? 'previousSibling' : 'nextSibling' + ]; + if ( next === null ) + { + return null; + } + + // if we found our sibling, return it + if ( type.toUpperCase() === next.nodeName ) + { + return next; + } + + return this.getSibling( next, type, direction ); + } +} ); diff --git a/src/ui/field/DomFieldFactory.js b/src/ui/field/DomFieldFactory.js new file mode 100644 index 0000000..14718d5 --- /dev/null +++ b/src/ui/field/DomFieldFactory.js @@ -0,0 +1,95 @@ +/** + * Creates DomField + * + * Copyright (C) 2015 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 . + * + * @needsLove + * - Dependencies need to be liberated: + * - ElementStyler. + * @end needsLove + */ + +var Class = require( 'easejs' ).Class, + + BucketField = require( '../../field/BucketField' ), + DomField = require( './DomField' ); + + +module.exports = Class( 'DomFieldFactory', +{ + 'private _elementStyler': null, + + + __construct: function( element_styler ) + { + this._elementStyler = element_styler; + }, + + + /** + * Create a DomField from the given field description + * + * The provided DomField will wait to access the DOM until an operation + * requires it. + * + * @param {string} name field name + * @param {number} index field index + * + * @param {function(HtmlElement)|HtmlElement} root root element containing + * the field (optionally + * lazy) + * + * @return {DomField} generated field + */ + 'public create': function( name, index, root ) + { + var _self = this; + + return DomField( + BucketField( name, index ), + + // lazy load on first access + function( callback ) + { + // are we lazy? + if ( typeof root === 'function' ) + { + // wait to fulfill this request until after the element + // becomes available + root( function( result ) + { + root = result; + c(); + } ); + + return; + } + + // not lazy; continue immediately + c(); + + function c() + { + callback( _self._elementStyler.getElementByName( + name, index, null, root + )[0] ); + } + } + ); + } +} );