1
0
Fork 0

Liberate step, group, and misc.

These are largely unchanged---any changes from the originals, aside from
maybe some whitespace fixes, are in separate commits after their
introduction into liza.

If I hold all the code to the standards that I wished to hold it to before
release, then they'll never be released.  So here we are.

Much more to come.
master
Mike Gerwitz 2015-12-03 00:37:15 -05:00
commit ea23f08b4f
15 changed files with 5006 additions and 0 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
} );

31
src/field/Field.js 100644
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
var Interface = require( 'easejs' ).Interface;
module.exports = Interface( 'Field',
{
'public getName': [],
'public getIndex': []
} );

247
src/group/Group.js 100644
View File

@ -0,0 +1,247 @@
/**
* Group of fields
*
* 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 <http://www.gnu.org/licenses/>.
*/
var Class = require( 'easejs' ).Class;
/**
* Group of fields
*/
module.exports = Class( 'Group',
{
/**
* Maximum number of rows permitted
*
* Must be 0 by default (not 1) to ensure we are unbounded by default.
*
* @type {number}
*/
'private _maxRows': 0,
/**
* Minimum number of rows permitted
* @type {number}
*/
'private _minRows': 1,
/**
* Whether the group is preventing from adding/removing rows
* @type {boolean}
*/
'private _locked': false,
/**
* Stores names of fields available in the group (includes linked)
* @type {Array.<string>}
*/
'private _fields': [],
/**
* Stores names of fields available exclusively in the group (no linked)
* @type {Array.<string>}
*/
'private _exclusiveFields': [],
/**
* Hashed exclusive fields for quick lookup
* @type {Object}
*/
'private _exclusiveHash': {},
/**
* Names of fields that are visible to the user
*
* For example: excludes external fields, but includes display.
*
* @type {Array.<string>}
*/
'private _userFields': [],
/**
* The id of the field that will determine the number of indexes in the
* group
*
* @type {string}
*/
'private _indexFieldName': '',
/**
* Gets or sets the maximum numbers of rows that may appear in the group
*
* @param Integer max maximum number of rows
*
* @return Group|Boolean self if settings, otherwise min rows value
*/
maxRows: function( max )
{
if ( max !== undefined )
{
this._maxRows = +max;
return this;
}
return this._maxRows;
},
/**
* Gets or sets the minimum numbers of rows that may appear in the group
*
* @param Integer min minimum number of rows
*
* @return Group|Boolean self if setting, otherwise the min row value
*/
minRows: function( min )
{
if ( min !== undefined )
{
this._minRows = +min;
return this;
}
return this._minRows;
},
/**
* Gets or sets the locked status of a group
*
* When a group is locked, rows/groups cannot be removed
*
* @param Boolean locked whether the group should be locked
*
* @return Group|Boolean self if setting, otherwise locked status
*/
locked: function( locked )
{
if ( locked !== undefined )
{
this._locked = !!locked;
return this;
}
return this._locked;
},
/**
* Set names of fields available in the group
*
* @param {Array.<string>} fields field names
*
* @return {undefined}
*/
'public setFieldNames': function( fields )
{
// store copy of the fields to ensure that modifying the array that was
// passed in does not modify our values
this._fields = fields.slice( 0 );
return this;
},
/**
* Returns the group field names
*
* @return {Array.<string>}
*/
'public getFieldNames': function()
{
return this._fields;
},
/**
* Set names of fields available in the group (no linked)
*
* @param {Array.<string>} fields field names
*
* @return {undefined}
*/
'public setExclusiveFieldNames': function( fields )
{
// store copy of the fields to ensure that modifying the array that was
// passed in does not modify our values
this._exclusiveFields = fields.slice( 0 );
// hash 'em for quick lookup
var i = fields.length;
while ( i-- )
{
this._exclusiveHash[ fields[ i ] ] = true;
}
return this;
},
/**
* Returns the group field names (no linked)
*
* @return {Array.<string>}
*/
'public getExclusiveFieldNames': function()
{
return this._exclusiveFields;
},
'public setUserFieldNames': function( fields )
{
this._userFields = fields.slice( 0 );
return this;
},
'public getUserFieldNames': function()
{
return this._userFields;
},
/**
* Returns whether the group contains the given field
*
* @param {string} field name of field
*
* @return {boolean} true if exclusively contains field, otherwise false
*/
'public hasExclusiveField': function( field )
{
return !!this._exclusiveHash[ field ];
},
'public setIndexFieldName': function( name )
{
this._indexFieldName = ''+name;
return this;
},
'public getIndexFieldName': function()
{
return this._indexFieldName;
}
} );

View File

@ -0,0 +1,136 @@
/**
* Sorting with multiple criteria
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - References to "quote" should be replaced with generic terminology
* representing a document.
* - Dependencies need to be liberated:
* - ElementStyler;
* - BucketDataValidator.
* - Global references (e.g. jQuery) must be removed.
* - Checkbox-specific logic must be extracted.
* - This class is doing too much.
* @end needsLove
*/
var Class = require( 'easejs' ).Class;
/**
* A simple recursive sorter with support for multiple criteria
*
* For simplicity's sake, this simply uses JavaScript's built-in sort()
* method using the supplied predicates. It then iterates through the sorted
* result and, using the supplied predicates, determines how the results
* should be grouped for sub-sorting. Because of this extra iteration, this
* isn't a very efficient algorithm, but it doesn't need to be for our
* purposes.
*
* Sorting is then performed recursively using the determined groups and the
* next provided predicate.
*/
module.exports = Class( 'MultiSort',
{
/**
* Recursively sorts the given data using the provided predicates
*
* The predicate used depends on the depth of the sort. Results will be
* grouped according to similarity and recursively sorted until either no
* predicates remain or until the results are so dissimilar that they cannot
* be further sorted.
*
* @param {Array} data data to be sorted
* @param {Array.<function(*,*)>} preds predicates for arbitrary depth
*
* @return {Array} sorted data
*/
'public sort': function( data, preds )
{
// nothing can be done if we (a) don't have a length (non-array?), (b)
// the array is empty or (c) if we have no more preds
if ( ( preds.length === 0 ) || ( data.length < 2 ) )
{
return data;
}
var sorted = Array.prototype.slice.call( data ),
pred = preds[ 0 ],
next_preds = Array.prototype.slice.call( preds, 1 );
// sort according to the current predicate
sorted.sort( pred );
// if we cannot do any more sub-sorting, then simply return this sorted
// result
if ( preds.length === 1 )
{
return sorted;
}
return this._subsort( sorted, pred, next_preds );
},
/**
* Recursively sorts sorted results by grouping similar elements
*/
'private _subsort': function( sorted, pred, next_preds )
{
var i = 0,
len = sorted.length;
var result = [],
cur = [ sorted[ 0 ] ];
// note that this increment is intentional---at the bottom of this loop,
// we push the current element into the current group. Therefore, this
// extra step (past the end of the sorted array) ensures that the last
// element will be properly processed as part of the last group. The
// fact that we push undefined onto cur before returning is of no
// consequence.
while ( i++ < len )
{
// if we are at the last element in the array OR if the current
// element is to be sorted differently than the previous, process
// the current group of elements before continuing
if ( ( i === len )
|| ( pred( sorted[ i - 1 ], sorted[ i ] ) !== 0 )
)
{
// the element is different; sub-sort
var sub = ( cur.length > 1 )
? this.sort( cur, next_preds )
: cur;
for ( var j in sub )
{
result.push( sub[ j ] );
}
cur = [];
}
cur.push( sorted[ i ] );
}
return result;
}
} );

378
src/step/Step.js 100644
View File

@ -0,0 +1,378 @@
/**
* Step abstraction
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - References to "quote" should be replaced with generic terminology
* representing a document.
* - Sorting logic must be extracted, and MultiSort decoupled.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
EventEmitter = require( 'events' ).EventEmitter,
// XXX: tightly coupled
MultiSort = require( '../sort/MultiSort' );
/**
* Represents a single step to be displayed in the UI
*/
module.exports = Class( 'Step' )
.extend( EventEmitter,
{
/**
* Called when quote is changed
* @type {string}
*/
'const EVENT_QUOTE_UPDATE': 'updateQuote',
/**
* Step identifier
* @type {number}
*/
'private _id': 0,
/**
* Data bucket to store the raw data for submission
* @type {StepDataBucket}
*/
'private _bucket': null,
/**
* Fields contained exclusively on the step (no linked)
* @type {Object}
*/
'private _exclusiveFields': {},
/**
* Fields that must contain a value
* @type {Object}
*/
'private _requiredFields': {},
/**
* Whether all fields on the step contain valid data
* @type {boolean}
*/
'private _valid': true,
/**
* Explanation of what made the step valid/invalid, if applicable
*
* This is useful for error messages
*
* @type {string}
*/
'private _validCause': '',
/**
* Sorted group sets
* @type {Object}
*/
'private _sortedGroups': {},
/**
* Initializes step
*
* @param {number} id step identifier
* @param {QuoteClient} quote quote to contain step data
*
* @return {undefined}
*/
'public __construct': function( id, quote )
{
var _self = this;
this._id = +id;
// TODO: this is temporary; do not pass bucket, pass quote
quote.visitData( function( bucket )
{
_self._bucket = bucket;
} );
},
/**
* Returns the numeric step identifier
*
* @return Integer step identifier
*/
'public getId': function()
{
return this._id;
},
/**
* Return the bucket associated with this step
*
* XXX: Remove me; breaks encapsulation.
*
* @return {Bucket} bucket associated with step
*/
'public getBucket': function()
{
return this._bucket;
},
/**
* Set whether or not the data on the step is valid
*
* @param {boolean} valid whether the step contains only valid data
*
* @return {Step} self
*/
'public setValid': function( valid, cause )
{
this._valid = !!valid;
this._validCause = cause;
return this;
},
/**
* Returns whether all the elements in the step contain valid data
*
* @return Boolean true if all elements are valid, otherwise false
*/
'public isValid': function( cmatch )
{
if ( !cmatch )
{
throw Error( 'Missing cmatch data' );
}
return this._valid && ( this.getNextRequired( cmatch ) === null );
},
'public getValidCause': function()
{
return this._validCause;
},
/**
* Retrieve the next required value that is empty
*
* Aborts on first missing required field with its name and index.
*
* @param {Object} cmatch cmatch data
*
* @return {!Array.<string, number>} first missing required field
*/
'public getNextRequired': function( cmatch )
{
cmatch = cmatch || {};
// check to ensure that each required field has a value in the bucket
for ( var name in this._requiredFields )
{
var data = this._bucket.getDataByName( name ),
cdata = cmatch[ name ];
// a non-empty string indicates that the data is missing (absense of
// an index has no significance)
for ( var i in data )
{
// any falsy value will be considered empty (note that !"0" ===
// false, so this will work)
if ( !data[ i ] && ( data[ i ] !== 0 ) )
{
if ( !cdata || ( cdata && cdata.indexes[ i ] ) )
{
return [ name, i ];
}
}
}
}
// all required fields have values
return null;
},
/**
* Sets a new bucket to be used for data storage and retrieval
*
* @param {QuoteDataBucket} bucket new bucket
*
* @return {Step} self
*/
'public updateQuote': function( quote )
{
// todo: Temporary
var _self = this,
bucket = null;
quote.visitData( function( quote_bucket )
{
bucket = quote_bucket;
} );
_self._bucket = bucket;
_self.emit( this.__self.$('EVENT_QUOTE_UPDATE') );
return this;
},
/**
* Adds field names exclusively contained on this step (no linked)
*
* @param {Array.<string>} fields field names
*
* @return {StepUi} self
*/
'public addExclusiveFieldNames': function( fields )
{
var i = fields.length;
while ( i-- )
{
this._exclusiveFields[ fields[ i ] ] = true;
}
return this;
},
/**
* Retrieve list of field names (no linked)
*
* @return {Object.<string>} field names
*/
'public getExclusiveFieldNames': function()
{
return this._exclusiveFields;
},
/**
* Set names of fields that must contain a value
*
* @param {Object} required required field names
*
* @return {StepUi} self
*/
'public setRequiredFieldNames': function( required )
{
this._requiredFields = required;
return this;
},
'public setSortedGroupSets': function( sets )
{
this._sortedGroups = sets;
return this;
},
'public eachSortedGroupSet': function( c )
{
var sets = {};
var data = [];
for ( var id in this._sortedGroups )
{
// call continuation with each sorted set containing the group ids
c( this._processSortedGroup( this._sortedGroups[ id ] ) );
}
},
'private _processSortedGroup': function( group_data )
{
var data = [];
for ( var i in group_data )
{
var cur = group_data[ i ],
name = cur[ 0 ],
fields = cur[ 1 ];
// get data for each of the fields
var fdata = [];
for ( var i in fields )
{
fdata.push( this._bucket.getDataByName( fields[ i ] ) );
}
data.push( [ name, fdata ] );
}
var toint = [ 0, 0, 1 ];
function pred( i, a, b )
{
var vala = a[ 1 ][ i ][ 0 ],
valb = b[ 1 ][ i ][ 0 ];
// convert to numeric if it makes sense to do so (otherwise, we may
// be comparing them as strings, which does not quite give us the
// ordering we desire)
if ( toint[ i ] )
{
vala = +vala;
valb = +valb;
}
if ( vala > valb )
{
return 1;
}
else if ( vala < valb )
{
return -1;
}
return 0;
}
// generate predicates
var preds = [];
for ( var i in group_data[ 0 ][ 1 ] )
{
( function( i )
{
preds.push( function( a, b )
{
return pred( i, a, b );
} );
} )( i );
}
// sort the data
var sorted = MultiSort().sort( data, preds );
// return the group names
var ret = [];
for ( var i in sorted )
{
// add name
ret.push( sorted[ i ][ 0 ] );
}
return ret;
}
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 );
}
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* @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] );
}
}
);
}
} );

View File

@ -0,0 +1,433 @@
/**
* Group collapsable table UI
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Remove reliance on jQuery.
* - Dependencies need to be liberated: Styler; Group.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
GroupUi = require( './GroupUi' );
module.exports = Class( 'CollapseTableGroupUi' )
.extend( GroupUi,
{
/**
* Percentage width of the left column
* @type {number}
*/
'private const WIDTH_COL_LEFT_PERCENT': 30,
/**
* Base rows for each unit
* @type {jQuery}
*/
'private _$baseRows': null,
/**
* Number of rows in the unit
* @type {number}
*/
'private _rowCount': 0,
/**
* Indexes to use for styled elements
* @type {number}
*/
'private _elementIndex': 1,
/**
* Flags that, when true in the bucket, will replace each individual row
* with a single cell (used for ineligibility, for example
*
* @type {Array.<string>}
*/
'private _blockFlags': [],
/**
* Contains true/false values of each of the block flags
* @var {Object}
*/
'private _blockFlagValues': {},
/**
* Summary to display on unit row if block flag is set
* @var {string}
*/
'private _blockFlagSummary': '',
'private _blockDisplays': null,
'override protected processContent': function()
{
this._processTableRows();
// determine if we should lock this group down
if ( this.$content.find( '.groupTable' ).hasClass( 'locked' ) )
{
this.group.locked( true );
}
// if the group is locked, there will be no adding of rows
if ( this.group.locked() )
{
this.$content.find( '.addrow' ).remove();
}
var $tbody = this.$content.find( 'tbody' );
// block flags are comma-separated (derived from the XML, which has
// comma-separated values for consistency with the other properties)
this._blockFlags = $tbody.attr( 'blockFlags' ).split( ',' );
this._blockFlagSummary = $tbody.attr( 'blockFlagSummary' ) || '';
this._blockDisplays = this._getBlockDisplays();
},
'private _processTableRows': function()
{
this._$baseRows = this.$content
.find( 'tbody > tr:not(.footer)' )
.detach();
this._calcColumnWidths();
this._rowCount = this._$baseRows.length;
},
/**
* Retrieve and detach block-mode displays for each column
*
* @return {jQuery} block-mode display elements
*/
'private _getBlockDisplays': function()
{
return this.$content
.find( 'div.block-display' )
.detach();
},
/**
* Calculates column widths
*
* Ensures that the left column takes up a consistent amount of space and
* that each of the remaining columns are distributed evenly across the
* remaining width of the table.
*
* As a consequence of this operation, any table with N columns will be
* perfectly aligned with any other table of N columns.
*
* See FS#7916 and FS#7917.
*
* @return {undefined}
*/
'private _calcColumnWidths': function()
{
// the left column will take up a consistent amount of width and the
// remainder of the width (in percent) will be allocated to the
// remaining columns
var left = this.__self.$( 'WIDTH_COL_LEFT_PERCENT' ),
remain = 100 - left,
// we will calculate and apply the width to the parent columns (this
// allows subcols to vary, which we may not want, but ensures that
// tables of N columns will always be aligned even if they have no
// subcolumns)
$cols = this.$content.find( 'tr:nth(0) > th:not(:first)' ),
count = $cols.length,
width = Math.floor( remain / count );
// set the widths of the left column and each of the remaining columns
this.$content.find( 'tr:first > th:first' )
.attr( 'width', ( left + '%' ) );
$cols.attr( 'width', ( width + '%' ) );
},
/**
* Collapses all units
*
* @param {jQuery} $unit unit to collapse
*
* @return {undefined}
*/
'private _collapse': function( $unit )
{
$unit.filter( ':not(.unit)' ).hide();
$unit.filter( '.unit' )
.addClass( 'collapsed' )
.find( 'td:first' )
.addClass( 'first' )
.addClass( 'collapsed' );
},
/**
* Initializes unit toggle on click
*
* @param {jQuery} $unit unit to initialize toggle on
*
* @return {undefined}
*/
'private _initToggle': function( $unit )
{
$unit.filter( 'tr.unit' )
// we set the CSS here because IE doesn't like :first-child
.css( 'cursor', 'pointer' )
.click( function()
{
var $node = $( this );
$node.filter( '.unit' )
.toggleClass( 'collapsed' )
.find( 'td:first' )
.toggleClass( 'collapsed' );
$node.nextUntil( '.unit, .footer' ).toggle();
} )
.find( 'td:first' )
.addClass( 'first' );
},
'private _getTableBody': function()
{
return this.$content.find( 'tbody' );
},
/**
* Determines if the block flag is set for any column and converts it to a
* block as necessary
*
* This looks more complicated than it really is. Here's what we're doing:
* - For the unit row:
* - Remove all cells except the first and span first across area
* - For all other rows:
* - Remove all but the first cell
* - Expand first cell to fit the area occupied by all of the removed
* cells
* - Replace content with text content of the flag
* - Adjust width slightly so it doesn't take up too much room
*
* @param {jQuery} $unit generated unit nodes
*
* @return {undefined}
*/
'private _initBlocks': function( $unit )
{
for ( var i = 0, len = this._blockFlags.length; i < len; i++ )
{
var flag = this._blockFlags[ i ];
// ignore if the flag is not set
if ( this._blockFlagValues[ flag ] === false )
{
continue;
}
var $rows = $unit.filter( 'tr:not(.unit)' ),
$cols = $rows.find( 'td[columnIndex="' + i + '"]' ),
col_len = $rows
.first()
.find( 'td[columnIndex="' + i + '"]' )
.length
;
// combine cells in unit row and remove content
$unit.filter( '.unit' )
.find( 'td[columnIndex="' + i + '"]' )
.filter( ':not(:first)' )
.remove()
.end()
.attr( 'colspan', col_len )
.addClass( 'blockSummary' )
// TODO: this doesn't really belong here; dynamic block flag
// summaries would be better
.text( ( /Please wait.../.test( '' ) )
? 'Please wait...'
: this._blockFlagSummary
);
// remove all cells associated with this unit except for the first,
// which we will expand to fill the area previously occupied by all
// the cells and replace with the content of the block flag (so it's
// not really a flag; I know)
$cols
.filter( ':not(:first)' )
.remove()
.end()
.addClass( 'block' )
.attr( {
colspan: col_len,
rowspan: $rows.length
} )
.html( '' )
.append( this._blockDisplays[ i ] );
}
},
/**
* Returns all rows associated with a unit index
*
* The provided index is expected to be 1-based.
*
* @param {number} index 1-based index of unit
*
* @return {jQuery} unit rows
*/
'private _getUnitByIndex': function( index )
{
return this._getTableBody()
.find( 'tr.unit:not(.footer):nth(' + index + ')' )
.nextUntil( '.unit, .footer' )
.andSelf();
},
'public addRow': function()
{
var $unit = this._$baseRows.clone( true ),
index = this.getCurrentIndex();
// properly name the elements to prevent id conflicts
this.setElementIdIndexes( $unit.find( '*' ), index );
// add the index to the row title
$unit.find( 'span.rowindex' ).text( ' ' + ( index + 1 ) );
// add to the table (before the footer, if one has been provided)
var footer = this._getTableBody().find( 'tr.footer' );
if ( footer.length > 0 )
{
footer.before( $unit );
}
else
{
this._getTableBody().append( $unit );
}
// finally, style our new elements
this.styler.apply( $unit );
// the unit should be hidden by default and must be toggle-able (fun
// word)
this._initBlocks( $unit );
this._initToggle( $unit );
// only collapse if we have multiple units
if ( index > 0 )
{
this._collapse( $unit );
// if this is the 2nd unit, we need to collapse the first
if ( index === 1 )
{
this._collapse( this._getUnitByIndex( index - 1 ) );
}
}
// this will handle post-add processing, such as answer hooking
this.postAddRow( $unit, index );
},
'public removeRow': function()
{
var $rows = this._getUnit( this.getCurrentIndex() );
// remove rows
this.styler.remove( $rows );
$rows.remove();
return this;
},
'private _getUnit': function( index )
{
var start = this._rowCount * index;
return this._getTableBody()
.find( 'tr:nth(' + start + '):not( .footer )' )
.nextUntil( '.unit, .footer' )
.andSelf();
},
'override public preEmptyBucket': function( bucket, updated )
{
// retrieve values for each of the block flags
for ( var i = 0, len = this._blockFlags.length; i < len; i++ )
{
var flag = this._blockFlags[ i ];
this._blockFlagValues[ flag ] =
bucket.getDataByName( flag )[ 0 ] || false;
}
var _super = this.__super;
// remove and then re-add each index (the super method will re-add)
// TODO: this is until we can properly get ourselves out of block mode
while ( this.getCurrentIndexCount() )
{
this.removeIndex();
}
_super.call( this, bucket );
return this;
},
'override protected addIndex': function( index )
{
// increment id before doing our own stuff
this.__super( index );
this.addRow();
return this;
},
'override public removeIndex': function( index )
{
// remove our stuff before decrementing our id
this.removeRow();
this.__super( index );
return this;
}
} );

View File

@ -0,0 +1,281 @@
/**
* Group side-formatted table UI
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Remove reliance on jQuery.
* - Dependencies need to be liberated: Styler; Group.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
GroupUi = require( './GroupUi' );
/**
* Represents a side-formatted table group
*
* This class extends from the generic Group class. It contains logic to
* support tabbed groups, allowing for the adding and removal of tabs.
*/
module.exports = Class( 'SideTableGroupUi' )
.extend( GroupUi,
{
/**
* Percentage width of the left column
* @type {number}
*/
'private const WIDTH_COL_LEFT_PERCENT': 30,
/**
* Stores the base title for each new tab
* @type {string}
*/
$baseHeadColumn: null,
/**
* Stores the base tab content to be duplicated for tabbed groups
* @type {jQuery}
*/
$baseBodyColumn: null,
/**
* Table element
* @type {jQuery}
*/
$table: null,
/**
* Number of subcolumns within each column
* @type {number}
*/
subcolCount: 1,
/**
* Template method used to process the group content to prepare it for
* display
*
* @return void
*/
'override protected processContent': function()
{
this.__super();
// determine if we should lock this group down
if ( this.$content.find( '.groupTable' ).hasClass( 'locked' ) )
{
this.group.locked( true );
}
this._processTable();
},
_processTable: function()
{
this.$table = this._getTable();
// important: do this before we begin detaching things
this._calcColumnWidths();
// Any content that is not the side column is to be considered the first
// data column. detach() is used to ensure events and data remain.
this.$baseHeadColumn = this.$table.find( 'thead' )
.find( 'th:not( .groupTableSide )' ).detach();
this.$baseBodyColumn = this.$table.find( 'tbody' )
.find( 'td:not( .groupTableSide )' ).detach();
this.subcolCount = +( $( this.$baseHeadColumn[0] ).attr( 'colspan' ) );
// if the group is locked, there will be no adding of rows
if ( this.group.locked() )
{
this.$content.find( '.addrow' ).remove();
}
},
/**
* Calculates column widths
*
* Ensures that the left column takes up a consistent amount of space and
* that each of the remaining columns are distributed evenly across the
* remaining width of the table.
*
* As a consequence of this operation, any table with N columns will be
* perfectly aligned with any other table of N columns.
*
* See FS#7916 and FS#7917.
*
* @return {undefined}
*/
'private _calcColumnWidths': function()
{
// the left column will take up a consistent amount of width and the
// remainder of the width (in percent) will be allocated to the
// remaining columns
var left = this.__self.$( 'WIDTH_COL_LEFT_PERCENT' ),
remain = 100 - left,
$cols = this.$content.find( 'tr:nth(1) > th' ),
count = $cols.length,
width = Math.floor( remain / count );
// set the widths of the left column and each of the remaining columns
this.$content.find( 'tr:first > th:first' )
.attr( 'width', ( left + '%' ) );
$cols.attr( 'width', ( width + '%' ) );
},
_getTable: function()
{
return this.$content.find( 'table.groupTable' );
},
addColumn: function()
{
var $col_head = this.$baseHeadColumn.clone( true ),
$col_body = this.$baseBodyColumn.clone( true ),
$thead = this.$table.find( 'thead' ),
$tbody = this.$table.find( 'tbody' ),
index = this.getCurrentIndex();
// properly name the elements to prevent id conflicts
this.setElementIdIndexes( $col_head.find( '*' ), index );
this.setElementIdIndexes( $col_body.find( '*' ), index );
// add the column headings
$col_head.each( function( i, th )
{
var $th = $( th );
// the first cell goes in the first header row and all others go in
// the following row
var $parent = null;
if ( i === 0 )
{
$parent = $thead.find( 'tr:nth(0)' );
// add the index to the column title
$th.find( 'span.colindex' ).text( ' ' + ( index + 1 ) );
}
else
{
$parent = $thead.find( 'tr:nth(1)' );
}
$parent.append( $th );
});
// add the column body cells
var subcol_count = this.subcolCount;
$col_body.each( function( i, $td )
{
$tbody.find( 'tr:nth(' + ( Math.floor( i / subcol_count ) ) + ')' )
.append( $td );
});
// finally, style our new elements
this.styler
.apply( $col_head )
.apply( $col_body );
// raise event
this.postAddRow( $col_head, index )
.postAddRow( $col_body, index );
return this;
},
/**
* Removes a column from the table
*
* @return {SideTableGroupUi} self
*/
removeColumn: function()
{
// remove the last column
var index = this.getCurrentIndex();
// the column index needs to take into account that the first column is
// actually the side column (which shouldn't be considered by the user)
var col_index = ( index + 1 ),
$subcols = this._getSubColumns( index );
// remove the top header for this column
this.$table.find( 'thead > tr:first > th:nth(' + col_index + ')' )
.remove();
// remove sub-columns
this.styler.remove( $subcols );
$subcols.remove();
return this;
},
_getSubColumns: function( index )
{
// calculate the positions of the sub-columns
var start = ( ( index * this.subcolCount ) + 1 ),
end = start + this.subcolCount;
var selector = '';
for ( var i = start; i < end; i++ )
{
if ( selector )
{
selector += ',';
}
// add this sub-column to the selector
selector += 'thead > tr:nth(1) > th:nth(' + ( i - 1 ) + '), ' +
'tbody > tr > td:nth-child(' + ( i + 1 ) + ')';
}
return this.$table.find( selector );
},
'override protected addIndex': function( index )
{
// increment id before doing our own stuff
this.__super( index );
this.addColumn();
return this;
},
'override public removeIndex': function( index )
{
// remove our stuff before decrementing our id
this.removeColumn();
this.__super( index );
return this;
}
} );

View File

@ -0,0 +1,495 @@
/**
* Group tabbed block UI
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Remove reliance on jQuery.
* - Dependencies need to be liberated: Styler; Group.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
GroupUi = require( './GroupUi' );
/**
* Represents a tabbed block group
*
* Does not currently support removing individual tabs (it will only clear
* out all tabs)
*/
module.exports = Class( 'TabbedGroupUi' ).extend( GroupUi,
{
/**
* The parent element boxy thingy that contains all other elements
* @type {jQuery}
*/
'private _$box': null,
/**
* The list containing all clickable tabs
* @type {jQuery}
*/
'private _$tabList': null,
/**
* Element representing a tab itself
* @type {jQuery}
*/
'private _$tabItem': null,
/**
* Base tab content element
* @type {jQuery}
*/
'private _$contentItem': null,
/**
* Index of the currently selected tab
* @type {number}
*/
'private _curIndex': 0,
/**
* Disable flags
* @type {string}
*/
'private _disableFlags': [],
/**
* Bucket prefix for "tab extraction" source data
* @type {string}
*/
'private _tabExtractSrc': '',
/**
* Bucket prefix for "tab extraction" result data
* @type {string}
*/
'private _tabExtractDest': '',
'private _bucket': null,
/**
* Field to check for length (number of tabs); will default to first field
* @type {string}
*/
'private _lengthField': '',
'override protected processContent': function( quote )
{
this.__super();
// determine if we should lock this group down
if ( this.$content.find( '.groupTabbedBlock' ).hasClass( 'locked' ) )
{
this.group.locked( true );
}
this._processNonInternalHides( quote );
this._processTabExtract();
this._processElements();
this._processAddButton();
this._processLengthField();
},
'private _processNonInternalHides': function( quote )
{
var _self = this;
// hide flags
this._disableFlags = this._getBox()
.attr( 'data-disable-flags' )
.split( ';' );
quote.visitData( function( bucket )
{
_self._bucket = bucket;
} );
},
'private _processTabExtract': function()
{
var $box = this._getBox();
this._tabExtractSrc = $box.attr( 'data-tabextract-src' );
this._tabExtractDest = $box.attr( 'data-tabextract-dest' );
},
'private _processElements': function()
{
this._$box = this.$content.find( '.groupTabbedBlock' );
this._$tabList = this._$box.find( 'ul.tabs' );
this._$tabItem = this._$box.find( 'li' ).detach();
this._$contentItem = this._$box.find( '.tab-content' ).detach();
},
'private _processAddButton': function()
{
var _self = this,
$btn = this._getAddButton();
if ( this.group.locked() )
{
$btn.hide();
return;
}
$btn.click( function()
{
_self.initTab();
} );
},
'private _processLengthField': function()
{
this._lengthField = this._getBox().attr( 'data-length-field' ) || '';
},
'private _processHideFlags': function( data )
{
var n = 0;
var disables = [];
for ( var i in this._disableFlags )
{
var flag = this._disableFlags[ i ];
for ( var tabi in ( data[ flag ] || {} ) )
{
var val = data[ flag ][ tabi ],
hide = !( ( val === '' ) || ( +val === 0 ) );
// hides should be preserved for multiple criteria
disables[ tabi ] = ( disables[ tabi ] || false ) || hide;
}
}
// perform the show/hide
for ( var tabi in disables )
{
var hide = disables[ tabi ];
this._disableTab( tabi, hide );
// count the number of hidden
n += +hide;
}
this._getBox().toggleClass(
'disabled',
( n >= this._getTabCount() )
);
},
'private _disableTab': function( i, disable )
{
this._getTab( i ).toggleClass( 'disabled', disable );
//this._getTabContent( i ).addClass( 'hidden', disable );
},
'private _removeTab': function( index )
{
this._getTab( index ).remove();
this._getTabContent( index ).remove();
},
'override public getFirstElementName': function( _ )
{
return this._lengthField || this.__super();
},
'override protected postPreEmptyBucketFirst': function()
{
// select the first tab
this._selectTab( 0 );
return this;
},
'override protected addIndex': function( index )
{
this.addTab();
this.__super( index );
return this;
},
'override public removeIndex': function( index )
{
this._removeTab( this.getCurrentIndexCount() - 1 );
this.__super( index );
return this;
},
'private _getAddButton': function()
{
return this.$content.find( '.addTab:first' );
},
'private _showAddButton': function()
{
// only show if we're not locked
if ( this.group.locked() )
{
return;
}
this._getAddButton().show();
},
'private _hideAddButton': function()
{
this._getAddButton().hide();
},
'private _checkAddButton': function()
{
// max rows reached
if ( this.group.maxRows()
&& ( this.getCurrentIndexCount() === this.group.maxRows() )
)
{
this._hideAddButton();
}
else
{
this._showAddButton();
}
},
'private _getNextIndex': function()
{
var index = this.getCurrentIndexCount();
if ( this.group.maxRows()
&& ( index === this.group.maxRows() )
)
{
throw Error( 'Max rows reached' );
}
return index;
},
'private _getTabCount': function()
{
return this.getCurrentIndexCount();
},
'public addTab': function()
{
try
{
var index = this._getNextIndex();
}
catch ( e )
{
this._checkAddButton();
return false;
}
// append the tab itself
this._$tabList.append( this._createTab( index ) );
// append the tab content
this._$box
.find( '.tabClear:last' )
.before( this._createTabContent( index ) );
// hide the add button if needed
this._checkAddButton();
this._hideTab( index );
return true;
},
'private _createTab': function( index )
{
var _self = this;
return this._finalizeContent( index,
this._$tabItem.clone( true )
.click( function()
{
_self._selectTab( index );
} )
.find( 'a' )
// prevent anchor clicks from updating the URL
.click( function( event )
{
event.preventDefault();
} )
.end()
);
},
'private _createTabContent': function( index )
{
return this._finalizeContent( index,
this._$contentItem.clone( true )
);
},
'private _finalizeContent': function( index, $content )
{
// apply styling and id safeguards
this.setElementIdIndexes( $content.find( '*' ), index );
this.styler.apply( $content );
// allow hooks to perform their magic on our content
this.postAddRow( $content, index );
return $content;
},
'private _selectTab': function( index )
{
this._hideTab( this._curIndex );
this._showTab( this._curIndex = +index );
this._tabExtract( index );
},
'private _tabExtract': function( index )
{
var _self = this;
function pred( name )
{
// determine if the name matches the expected prefix (previously,
// this was a regex, but profiling showed that performance was very
// negatively impacted, so this is the faster solution)
return ( name.substr( 0, _self._tabExtractSrc.length ) ===
_self._tabExtractSrc
);
}
// wait for a repaint so that we don't slow down the tab selection
setTimeout( function()
{
var cur = {};
_self._bucket.filter( pred, function( data, name )
{
var curdata = data[ index ];
// ignore bogus data
if ( ( curdata === undefined ) || ( curdata === null ) )
{
return;
}
// guess if this is an array (if not, then it needs to be, since
// we'll be storing it in the bucket)
if ( ( typeof curdata === 'string' )
|| ( curdata.length === undefined )
)
{
curdata = [ curdata ];
}
cur[ _self._tabExtractDest + name ] = curdata;
} );
_self._bucket.setValues( cur );
}, 25 );
},
'private _getBox': function()
{
// avoiding use of jQuery selector because it caches DOM elements, which
// causes problems in other parts of the framework
return $( this.$content[ 0 ].getElementsByTagName( 'div' )[ 0 ] );
},
'private _getTabContent': function( index )
{
return this._$box.find( '.tab-content:nth(' + index + ')' );
},
'private _getTab': function( index )
{
return this._$tabList.find( 'li:nth(' + index + ')' );
},
'private _showTab': function( index )
{
this._getTab( index ).removeClass( 'inactive' );
this._getTabContent( index ).removeClass( 'inactive' );
},
'private _hideTab': function( index )
{
this._getTab( index ).addClass( 'inactive' );
this._getTabContent( index ).addClass( 'inactive' );
},
'private _getLastEligibleTab': function()
{
var tab_index = this._$tabList.find( 'li' ).not( '.disabled' ).last().index();
return ( tab_index === -1 )
? 0
: tab_index;
},
'override public visit': function()
{
// let supertype do its own thing
this.__super();
// we will have already rated once by the time this is called
this._processHideFlags( this._bucket.getData() );
// select first tab that is eligible and
// perform tab extraction (to reflect first eligible tab)
this._selectTab( this._getLastEligibleTab() );
return this;
}
} );

View File

@ -0,0 +1,448 @@
/**
* Group tabbed UI
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Remove reliance on jQuery.
* - Dependencies need to be liberated: Styler; Group.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
GroupUi = require( './GroupUi' );
/**
* Represents a tabbed group
*
* This class extends from the generic Group class. It contains logic to
* support tabbed groups, allowing for the adding and removal of tabs.
*/
module.exports = Class( 'TabbedGroupUi' )
.extend( GroupUi,
{
/**
* Stores the base title for each new tab
* @type {string}
*/
$baseTabTitle: '',
/**
* Stores the base tab content to be duplicated for tabbed groups
* @type {jQuery}
*/
$baseTabContent: null,
/**
* Index of the currently selected tab
* @type {number}
*/
'private _selectedIndex': 0,
/**
* Template method used to process the group content to prepare it for
* display
*
* @return void
*/
'override protected processContent': function( quote )
{
this.__super();
// determine if we should lock this group down
if ( this.$content.find( 'div.groupTabs' ).hasClass( 'locked' ) )
{
this.group.locked( true );
}
this._processTabs();
this._attachAddTabHandlers();
this.watchFirstElement( this.$baseTabContent, quote );
},
/**
* Initializes the tabs
*
* This method will locate the area of HTML that should be tabbed and
* initialize it. The content of the first tab will be removed and stored in
* memory for duplication.
*
* @return void
*/
_processTabs: function()
{
var group = this;
var $container = this._getTabContainer();
if ( $container.length == 0 )
{
return;
}
// grab the title to be used for all the tabs
this.$baseTabTitle = $container.find( 'li:first' ).remove()
.find( 'a' ).text();
// the base content to be used for each of the tabs (detach() not
// remove() to ensure the data remains)
this.$baseTabContent = $container.find( 'div:first' ).detach();
// transform into tabbed div
$container.tabs( {
tabTemplate:
'<li><a href="#{href}">#{label}</a>' +
( ( this.group.locked() === false )
? '<span class="ui-icon ui-icon-close">Remove Tab</span>'
: ''
) + '</li>',
select: function( _, event )
{
group._selectedIndex = event.index;
},
add: function()
{
var $this = $( this );
// if this is our max, hide the button
if ( $this.tabs( 'length' ) == group.group.maxRows() )
{
group._getAddButton().hide();
}
// select the new tab
$this.tabs( 'select', $this.tabs( 'length' ) - 1 );
// remove tabs when the remove button is clicked (for whatever
// reason, live() stopped working, so here we are...)
$container.find( 'span.ui-icon-close:last' ).click( function()
{
var index = $container.find( 'li' )
.index( $( this ).parent() );
group.destroyIndex( index );
});
},
remove: function()
{
// should we re-show the add button?
if ( $( this ).tabs( 'length' ) ==
( group.group.maxRows() - 1 )
)
{
group._getAddButton().show();
}
}
} );
},
/**
* Attaches click event handlers to add tab elements
*
* @return void
*/
_attachAddTabHandlers: function()
{
// reference to ourself for use in the closure
var group = this;
// if we're locked, we won't allow additions
if ( this.group.locked() )
{
this._getAddButton().remove();
return;
}
// any time an .addrow element is clicked, we want to add a row to the
// group
this._getAddButton().click( function()
{
group.initIndex();
});
},
/**
* Returns the element containing the tabs
*
* @return jQuery element containing the tabs
*/
_getTabContainer: function()
{
return this.$content.find( '.groupTabs' );
},
_getAddButton: function()
{
return this.$content.find( '.addTab:first' );
},
'private _getTabTitleIndex': function()
{
return this.getCurrentIndexCount();
},
/**
* Adds a tab
*
* @return TabbedGroup self to allow for method chaining
*/
addTab: function()
{
var $container = this._getTabContainer();
var $content = this.$baseTabContent.clone( true );
var id = $content.attr( 'id' );
var index = this.getCurrentIndex();
// generate a new id
id = ( id + '_' + index );
$content.attr( 'id', id );
// properly name the elements to prevent id conflicts
this.setElementIdIndexes( $content.find( '*' ), index );
// append the content
$container.append( $content );
// create the new tab
var title = ( this.$baseTabTitle + ' ' + this._getTabTitleIndex() );
$container.tabs( 'add', ( '#' + id ), title );
// finally, style our new elements
this.styler.apply( $content );
// raise event
this.postAddRow( $content, index );
return this;
},
/**
* Removes a tab
*
* @return TabbedGroup self to allow for method chaining
*/
removeTab: function()
{
// we can simply remove the last tab since the bucket will re-order
// itself and update each of the previous tabs
var index = this.getCurrentIndex();
var $container = this._getTabContainer(),
$panel = this._getTabContent( index );
// remove the tab
this.styler.remove( $panel );
$container.tabs( 'remove', index );
return this;
},
'private _getTabContent': function( index )
{
return this._getTabContainer().find(
'div.ui-tabs-panel:nth(' + index + ')'
);
},
'override protected postPreEmptyBucketFirst': function()
{
// select the first tab
this._getTabContainer().tabs( 'select', 0 );
return this;
},
'override protected addIndex': function( index )
{
// increment id before doing our own stuff
this.__super( index );
this.addTab();
return this;
},
'override public removeIndex': function( index )
{
// decrement after we do our own stuff
this.removeTab();
this.__super( index );
return this;
},
/**
* Display the requested field
*
* The field is not given focus; it is simply brought to the foreground.
*
* @param {string} field_name name of field to display
* @param {number} i index of field
*
* @return {TabbedGroupUi} self
*/
'override public displayField': function( field, i )
{
var $element = this.styler.getWidgetByName( field, i );
// if we were unable to locate it, then don't worry about it
if ( $element.length == 0 )
{
return;
}
// get the index of the tab that this element is on
var id = $element.parents( 'div.ui-tabs-panel' ).attr( 'id' );
var index = id.substring( id.lastIndexOf( '_' ) );
// select that tab
this._getTabContainer().tabs( 'select', index );
return this;
},
/**
* Shows/hides add/remove row buttons
*
* @param {boolean} value whether to hide (default: true)
*
* @return {TabbedGroupUi} self
*/
hideAddRemove: function( value )
{
if ( value === true )
{
this._getTabContainer().find( '.ui-icon-close' ).hide();
this._getAddButton().hide();
}
else
{
this._getTabContainer().find( '.ui-icon-close' ).show();
this._getAddButton().show();
}
},
isOnVisibleTab: function( field, index )
{
// fast check
return ( +index === this._selectedIndex );
},
'override protected doHideField': function( field, index, force )
{
var _self = this;
// if we're not on the active tab, then we can defer this request until
// we're not busy
if ( !force && !this.isOnVisibleTab( field, index ) )
{
setTimeout( function()
{
_self.doHideField( field, index, true );
}, 25 );
}
var $element = this.getElementByName( field, index );
var $elements = ( $element.parents( 'dd' ).length )
? $element.parents( 'dd' ).prev( 'dt' ).andSelf()
: $element;
$elements.stop( true, true );
if ( this.isOnVisibleTab( field, index ) )
{
$elements.slideUp( 500, function()
{
$( this ).addClass( 'hidden' );
} );
}
else
{
$elements.hide().addClass( 'hidden' );
}
},
'override protected doShowField': function( field, index, force )
{
var _self = this;
// if we're not on the active tab, then we can defer this request until
// we're not busy
if ( !force && !this.isOnVisibleTab( field, index ) )
{
setTimeout( function()
{
_self.doShowField( field, index, true );
}, 25 );
}
var $element = this.getElementByName( field, index );
var $elements = ( $element.parents( 'dd' ).length )
? $element.parents( 'dd' ).prev( 'dt' ).andSelf()
: $element;
// it's important to stop animations *before* removing the hidden class,
// since forcing its completion may add it
$elements
.stop( true, true )
.find( '.hidden' )
.andSelf()
.removeClass( 'hidden' );
if ( this.isOnVisibleTab( field, index ) )
{
$elements.slideDown( 500 );
}
else
{
$elements.show();
}
},
'override public getContentByIndex': function( name, index )
{
// get the tab that this index should be on and set a property to notify
// the caller that no index check should be performed (since there is
// only one)
var $content = this._getTabContent( index );
$content.singleIndex = true;
return $content;
}
} );

View File

@ -0,0 +1,428 @@
/**
* Group table UI
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Remove reliance on jQuery.
* - Dependencies need to be liberated: Styler; Group.
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
GroupUi = require( './GroupUi' );
/**
* Represents a table group
*
* This class extends from the generic Group class. It contains logic to
* support table groups, allowing for the adding and removal of rows.
*/
module.exports = Class( 'TableGroupUi' )
.extend( GroupUi,
{
/**
* Stores the base row to be duplicated for table groups
* @type {jQuery}
*/
$baseRow: null,
/**
* Template method used to process the group content to prepare it for
* display and retrieve common data
*
* @return void
*/
'override protected processContent': function( quote )
{
this.__super();
// determine if we should lock this group down
if ( this.$content.find( 'table' ).hasClass( 'locked' ) )
{
this.group.locked( true );
}
this._processTables();
this._attachAddRowHandlers();
this.watchFirstElement( this.$baseRow, quote );
},
/**
* Attaches the add row event handlers so new rows are added on click
*
* @return void
*/
_attachAddRowHandlers: function()
{
// reference to ourself for use in the closure
var _self = this;
// if we're locked, then there'll be no row adding
if ( this.group.locked() )
{
this._getAddRowButton().remove();
return;
}
// any time an .addrow element is clicked, we want to add a row to the
// group
this._getAddRowButton().click( function()
{
// initialize a new index
_self.initIndex();
});
},
/**
* Processes tables, preparing them for row duplication
*
* The first row is used as the model for duplication. It is removed from
* the DOM and stored in memory, which will be later cloned. It is stored
* unstyled to make manipulation easier and limit problems with restyling.
*
* This was chosen over simply duplicating and clearing out the first row
* because we (a) have a clean slate and (b) Dojo does not work well if you
* duplicate dijit HTML.
*
* @return void
*/
_processTables: function()
{
// reference to ourself for use in the closure
var groupui = this;
// if we're locked down, we won't be removing any rows
if ( this.group.locked() )
{
this.$content.find( '.delrow' ).remove();
}
// remove the first row of the group tables
this.$content.find( '.groupTable > tbody > tr:first' )
.each( function( i )
{
// remove the row and store it in memory as the base row, which
// will be used for duplication (adding new rows)
//
// NOTE: detach() must be used rather than remove(), because
// remove() also removes any data attached to the element
groupui.$baseRow = $( this ).detach();
}
);
},
/**
* Returns the table associated with the given group id
*
* @return jQuery group table
*/
_getTable: function()
{
return this.$content.find( 'table.groupTable' );
},
/**
* Returns the row of the group table for the specified group and row id
*
* @param Integer row_id id of the row to retrieve
*
* @return jQuery group table row
*/
_getTableRow: function( row_id )
{
row_id = +row_id;
return this._getTable().find(
'tbody > tr[id=' + this._genTableRowId( row_id ) + ']'
);
},
'private _getLastTableRow': function()
{
return this._getTableRow( this.getCurrentIndex() );
},
/**
* Generates the id to be used for the group table row
*
* This id lets us find the row for styling and removal.
*
* @param Integer row_id id of the row
*
* @return String row id for the table row
*/
_genTableRowId: function( row_id )
{
row_id = +row_id;
return ( this.getGroupId() + '_row_' + row_id );
},
/**
* Returns the element used to add rows to the table
*
* @return jQuery add row element
*/
_getAddRowButton: function()
{
return this.$content.find( '.addrow:first' );
},
/**
* Adds a row to a group that supports rows
*
* @return Step self to allow for method chaining
*/
addRow: function()
{
var $group_table = this._getTable();
var row_count = $group_table.find( 'tbody > tr' ).length;
var max = this.group.maxRows();
var _self = this;
// hide the add row button if we've reached the max
if ( max && ( row_count == ( max - 1 ) ) )
{
this._getAddRowButton().hide();
}
// html of first group_row
var $row_base = this.$baseRow;
if ( $row_base === undefined )
{
throw "NoBaseRow " + this.getGroupId();
}
// duplicate the base row
$row_new = $row_base.clone( true );
// increment row ids
var new_index = this._incRow( $row_new );
// attach remove event
var $del = $row_new.find( 'td.delrow' );
$del.click( function()
{
_self.destroyIndex( new_index );
} );
// append it to the group
$group_table.find( 'tbody' ).append( $row_new );
// aplying styling
this._applyStyle( new_index );
// raise event
this.postAddRow( $row_new, $row_new.index() );
return this;
},
/**
* Increments the index of the elements in the row
*
* This generates both a new name and a new id. The formats expected are:
* - name: foo[i]
* - id: foo_i
*
* @param jQuery $row row to increment
*
* @return Integer the new index
*/
_incRow: function( $row )
{
var new_index = this.getCurrentIndex();
// update the row id
$row.attr( 'id', this._genTableRowId( new_index ) );
// properly name the elements to prevent id conflicts
this.setElementIdIndexes( $row.find( '*' ), new_index );
return new_index;
},
/**
* Applies UI transformations to a row
*
* @param Integer row_id id of the row to be styled
*
* @return Step self to allow for method chaining
*/
'private _applyStyle': function( row_id )
{
// style only the specified row
this.styler.apply( this._getTableRow( row_id ), true );
return this;
},
/**
* Removes the specified row from a group
*
* @return Step self to allow for method chaining
*/
removeRow: function()
{
// get parent table and row count
var $group_table = this._getTable(),
$row = this._getLastTableRow(),
row_index = $row.index(),
row_count = $group_table.find( 'tbody > tr' ).length,
group = this;
// cleared so they can be restyled later)
this.styler.remove( $row );
$row.remove();
// re-add the add row button
this._getAddRowButton().show();
return this;
},
'override protected addIndex': function( index )
{
// increment id before doing our own stuff
this.__super( index );
this.addRow();
return this;
},
'override public removeIndex': function( index )
{
// remove our stuff before decrementing our id
this.removeRow();
this.__super( index );
return this;
},
/**
* Returns all elements that are a part of the column at the given index
*
* @param Integer index column position (0-based)
*
* @return jQuery collection of matched elements
*/
_getColumnElements: function( index )
{
index = +index;
return this._getTable().find(
'thead th:nth(' + index + '), ' +
'tr > td:nth-child(' + ( index + 1 ) + ')'
);
},
'override protected doHideField': function( field, index )
{
var $element = this.getElementByName( field, index ),
$parent = $element.parents( 'td' ),
cindex = $parent.index();
$parent.append( $( '<div>' )
.addClass( 'na' )
.text( 'N/A' )
);
$element.hide();
this._checkColumnVis( field, cindex );
},
'override protected doShowField': function( field, index )
{
var $element = this.getElementByName( field, index ),
$parent = $element.parents( 'td' ),
cindex = $parent.index();
$parent.find( '.na' ).remove();
$element.show();
this._checkColumnVis( field, cindex );
},
'private _checkColumnVis': function( field, cindex )
{
var $e = this._getColumnElements( cindex );
if ( this.isFieldVisible( field ) )
{
$e.stop( true, true ).slideDown( 500 );
}
else
{
$e.stop( true, true ).slideUp( 500 );
}
},
/**
* Shows/hides add/remove row buttons
*
* @param {boolean} value whether to hide (default: true)
*
* @return {TableGroupUi} self
*/
hideAddRemove: function( value )
{
if ( value === true )
{
this._getAddRowButton().hide();
this.$content.find( '.delrow' ).hide();
}
else
{
this._getAddRowButton().show();
this.$content.find( '.delrow' ).show();
}
return this;
},
/**
* Returns the number of rows currently in the table
*
* @return {number}
*/
'public getRowCount': function()
{
return this.getCurrentIndexCount();
}
} );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,218 @@
/**
* Step user interface
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - API is doing too much; see GeneralStepUi.
* @end needsLove
*/
var Interface = require( 'easejs' ).Interface;
/**
* Interactive interface for steps
*/
module.exports = Interface( 'StepUi',
{
/**
* Initializes step
*
* @return {undefined}
*/
'public init': [],
'public initGroupFieldData': [],
/**
* Sets content to be displayed
*
* @param {HTMLElement} content content to display
*
* @return {StepUi} self
*/
'public setContent': [ 'content' ],
/**
* Returns the step that this object is styling
*
* @return {Step}
*/
'public getStep': [],
/**
* Returns the generated step content as a jQuery object
*
* @return {HTMLElement} generated step content
*/
'public getContent': [],
/**
* Will mark the step as dirty when the content is changed and update
* the staging bucket
*
* @return undefined
*/
'public setDirtyTrigger': [],
/**
* Called after the step is appended to the DOM
*
* This method will simply loop through all the groups that are a part of
* this step and call their postAppend() methods. If the group does not have
* an element id, it will not function properly.
*
* @return {StepUi} self to allow for method chaining
*/
'public postAppend': [],
/**
* Empties the bucket into the step (filling the fields with its values)
*
* @param {Function} callback function to call when bucket has been emptied
*
* @return {StepUi} self to allow for method chaining
*/
'public emptyBucket': [ 'callback', 'delay' ],
/**
* Resets a step to its previous state or hooks the event
*
* @param {Function} callback function to call when reset is complete
*
* @return {StepUi} self to allow for method chaining
*/
'public reset': [ 'callback' ],
/**
* Returns whether all the elements in the step contain valid data
*
* @return Boolean true if all elements are valid, otherwise false
*/
'public isValid': [ 'cmatch' ],
/**
* Returns the id of the first failed field if isValid() failed
*
* Note that the returned element may not be visible. Visible elements will
* take precidence --- that is, invisible elements will be returned only if
* there are no more invalid visible elements, except in the case of
* required fields.
*
* @param {Object} cmatch cmatch data
*
* @return String id of element, or empty string
*/
'public getFirstInvalidField': [ 'cmatch' ],
/**
* Scrolls to the element identified by the given id
*
* @param {string} field name of field to scroll to
* @param {number} i index of field to scroll to
* @param {boolean} show_message whether to show the tooltip
* @param {string} message tooltip message to display
*
* @return {StepUi} self to allow for method chaining
*/
'public scrollTo': [ 'field', 'i', 'show_message', 'message' ],
/**
* Invalidates the step, stating that it should be reset next time it is
* displayed
*
* Resetting the step will clear the invalidation flag.
*
* @return StepUi self to allow for method chaining
*/
'public invalidate': [],
/**
* Returns whether the step has been invalidated
*
* @return Boolean true if step has been invalidated, otherwise false
*/
'public isInvalid': [],
/**
* Returns the GroupUi object associated with the given element name, if
* known
*
* @param {string} name element name
*
* @return {GroupUi} group if known, otherwise null
*/
getElementGroup: [ 'name' ],
/**
* Forwards add/remove hiding requests to groups
*
* @param {boolean} value whether to hide (default: true)
*
* @return {StepUi} self
*/
'public hideAddRemove': [ 'value' ],
'public preRender': [],
'public visit': [ 'callback' ],
/**
* Marks a step as active (or inactive)
*
* A step should be marked as active when it is the step that is currently
* accessible to the user.
*
* @param {boolean} active whether step is active
*
* @return {StepUi} self
*/
'public setActive': [ 'active' ],
/**
* Lock/unlock a step (preventing modifications)
*
* If the lock status has changed, the elements on the step will be
* disabled/enabled respectively.
*
* @param {boolean} lock whether step should be locked
*
* @return {StepUi} self
*/
'public lock': [ 'lock' ]
} );

View File

@ -0,0 +1,271 @@
/**
* Builds UI from template
*
* 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 <http://www.gnu.org/licenses/>.
*
* @needsLove
* - Global references to jQuery must be removed.
* - Dependencies need to be liberated:
* - ElementStyler;
* - UI.
* - This may not be needed, may be able to be handled differently, and
* really should load from data rather than a pre-generated template (?)
* @end needsLove
*/
var Class = require( 'easejs' ).Class,
EventEmitter = require( 'events' ).EventEmitter;
module.exports = Class( 'StepUiBuilder' )
.extend( EventEmitter,
{
/**
* Used to style elements
* @type {ElementStyler}
*/
'private _elementStyler': null,
/**
* Used for building groups
* @type {function()}
*/
'private _groupBuilder': null,
/**
* Retrieves step data
* @type {function( step_id: number )}
*/
'private _dataGet': null,
/**
* Step that the StepUi is being modeled after
* @type {Step}
*/
'private _step': null,
/**
* Format bucket data for display
* @type {BucketDataValidator}
*/
'private _formatter': null,
'public __construct': function(
element_styler,
formatter,
groupBuilder,
dataGet
)
{
this._elementStyler = element_styler;
this._formatter = formatter;
this._groupBuilder = groupBuilder;
this._dataGet = dataGet;
},
/**
* Sets the underlying step
*
* @param {Step} step
*
* @return {StepUiBuilder} self
*/
'public setStep': function( step )
{
this._step = step;
return this;
},
'public build': function( StepUi, callback )
{
var _self = this;
if ( !( this._step ) )
{
throw Error( 'No step provided' );
}
// create a new StepUi
var ui = StepUi(
this._step,
this._elementStyler,
this._formatter
);
// retrieve and process the step data (this kick-starts the entire
// process)
this._getData( function( data )
{
_self._processData( data, ui );
// build is complete
callback.call( null, ui );
});
return this;
},
/**
* Retrieves step data using the previously provided function
*
* This process may be asynchronous.
*
* @param {function( data: Object )} callback function to call with data
*
* @return {undefined}
*/
'private _getData': function( callback )
{
this._dataGet.call( this, this._step.getId(), function( data )
{
callback( data );
});
},
/**
* Processes the step data after it has been retrieved
*
* @param Object data step data (source should return as JSON)
*
* @return void
*/
'private _processData': function( data, ui )
{
// sanity check
if ( !( data.content.html ) )
{
// todo: show more information and give user option to retry
data.content.html = '<h1>Error</h1><p>A problem was encountered ' +
'while attempting to view this step.</p>';
}
// enclose it in a div so that we have a single element we can query,
// making our lives much easier (TODO: this is transitional code
// moving from jQuery to vanilla DOM)
ui.setContent(
$( '<div class="step-groups" />')
.append( $( data.content.html ) )[ 0 ]
);
// free the content from memory, as it's no longer needed (we don't need
// both the DOM representation and the string representation in memory
// for the life of the script - it's a waste)
delete data.content;
// create the group objects
this._createGroups( ui );
// track changes so we know when to validate and post
ui.setDirtyTrigger();
// let others do any final processing before we consider ourselves
// ready
ui.emit( ui.__self.$( 'EVENT_POST_PROCESS' ) );
},
/**
* Instantiates Group objects for each group in the step content, then
* styles them
*
* TODO: refactor into own builder
*
* @param {StepUi} ui new ui instance
*
* @return {undefined}
*/
'private _createGroups': function( ui )
{
// reference to self for use in closure
var _self = this,
groups = {},
group = null,
group_id = 0,
step = ui.getStep();
var $content = $( ui.getContent() );
// instantiate a group object for each of the groups within this step
var $groups = $content.find( '.stepGroup' ).each( function()
{
group = _self._groupBuilder( $( this ), _self._elementStyler );
group_id = group.getGroupId();
groups[ group_id ] = group;
// let the step know what fields it contains
step.addExclusiveFieldNames(
group.getGroup().getExclusiveFieldNames()
);
_self._hookGroup( group, ui );
} );
// XXX: remove public property assignment
ui.groups = groups;
ui.initGroupFieldData();
// we can style all the groups, since the elements that cannot be styled
// (e.g. table groups) have been removed already
_self._elementStyler.apply( $groups, false );
},
/**
* Hook various group events for processing
*
* @param {GroupUi} group group to hook
* @param {StepUi} ui new ui instance
*
* @return {undefined}
*/
'private _hookGroup': function( group, ui )
{
group
.invalidate( function()
{
ui.invalidate();
} )
.on( 'indexAdd', function( index )
{
ui.emit( ui.__self.$( 'EVENT_INDEX_ADD' ), index, this );
} )
.on( 'indexRemove', function( index )
{
ui.emit( ui.__self.$( 'EVENT_INDEX_REMOVE' ), index, this );
} ).on( 'indexReset', function( index )
{
ui.emit( ui.__self.$( 'EVENT_INDEX_RESET' ), index, this );
} )
.on( 'action', function( type, ref, index )
{
// simply forward
ui.emit( ui.__self.$( 'EVENT_ACTION' ), type, ref, index );
} )
.on( 'postAddRow', function( index )
{
ui.emit( 'postAddRow', index );
} );
}
} );