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
commit
ea23f08b4f
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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': []
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -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
|
@ -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' ]
|
||||||
|
} );
|
|
@ -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 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
Loading…
Reference in New Issue