Correct concurrent show/hide of fields from cmatch (bugfix)
commit
aea22294d1
|
@ -38,7 +38,7 @@
|
|||
"version": "3.2.0"
|
||||
},
|
||||
"easejs": {
|
||||
"version": "0.2.8"
|
||||
"version": "0.2.9"
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5"
|
||||
|
|
|
@ -23,6 +23,7 @@ const Class = require( 'easejs' ).Class;
|
|||
const EventEmitter = require( 'events' ).EventEmitter;
|
||||
const DomFieldNotFoundError = require( '../ui/field/DomFieldNotFoundError' );
|
||||
const UnknownEventError = require( './event/UnknownEventError' );
|
||||
const system = require( '../system/client' );
|
||||
|
||||
|
||||
/**
|
||||
|
@ -198,30 +199,10 @@ module.exports = Class( 'Client' )
|
|||
'private _validatorFormatter': null,
|
||||
|
||||
/**
|
||||
* Contains classification match data per field
|
||||
*
|
||||
* TODO: Move this to somewhere more appropriate
|
||||
*
|
||||
* @type {Object}
|
||||
* Classification match handler
|
||||
* @type {Cmatch}
|
||||
*/
|
||||
'private _cmatch': {},
|
||||
|
||||
/**
|
||||
* Performs classification matching on fields
|
||||
*
|
||||
* A field will have a positive match for a given index if all of its
|
||||
* classes match
|
||||
*
|
||||
* @type {FieldClassMatcher}
|
||||
*/
|
||||
'private _classMatcher': null,
|
||||
|
||||
|
||||
/**
|
||||
* Fields that were hidden (including indexes) since the last cmatch clear
|
||||
* @type {Object}
|
||||
*/
|
||||
'private _cmatchHidden': {},
|
||||
'private _cmatch': null,
|
||||
|
||||
/**
|
||||
* Automatically discards staging bucket contents
|
||||
|
@ -347,9 +328,7 @@ module.exports = Class( 'Client' )
|
|||
this, this._dataValidator, this.elementStyler, this.dataProxy, jQuery
|
||||
);
|
||||
|
||||
this._classMatcher = this._factory.createFieldClassMatcher(
|
||||
this.program.whens
|
||||
);
|
||||
this._cmatch = system.cmatch( this.program, this.__inst );
|
||||
|
||||
this._validatorFormatter = this._factory.createValidatorFormatter(
|
||||
this.program.meta.qtypes
|
||||
|
@ -428,7 +407,7 @@ module.exports = Class( 'Client' )
|
|||
{
|
||||
client.ui.displayStep( step_id, function()
|
||||
{
|
||||
client.forceCmatchAction();
|
||||
client._cmatch.forceCmatchAction();
|
||||
} );
|
||||
|
||||
client._currentStepId = step_id;
|
||||
|
@ -503,7 +482,7 @@ module.exports = Class( 'Client' )
|
|||
|
||||
client._quote.setQuickSaveData( data.content.quicksave || {} );
|
||||
|
||||
client._hookClassifier();
|
||||
client._cmatch.hookClassifier( client._dataValidator );
|
||||
|
||||
// store internal status
|
||||
client._isInternal = client.program.isInternal =
|
||||
|
@ -556,285 +535,6 @@ module.exports = Class( 'Client' )
|
|||
},
|
||||
|
||||
|
||||
/**
|
||||
* Force handling of the most recent cmatch data
|
||||
*
|
||||
* This can be used to refresh the UI to ensure that it is consistent with
|
||||
* the cmatch data.
|
||||
*
|
||||
* @return {Client} self
|
||||
*/
|
||||
'public forceCmatchAction': function()
|
||||
{
|
||||
if ( !( this._cmatch ) )
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
this._handleClassMatch( this._cmatch, true );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
'private _hookClassifier': function()
|
||||
{
|
||||
var _self = this,
|
||||
program = this.program;
|
||||
|
||||
// clear/initialize cmatches
|
||||
this._cmatch = {};
|
||||
|
||||
var cmatchprot = false;
|
||||
|
||||
// set classifier
|
||||
this._quote
|
||||
.setClassifier( program.getClassifierKnownFields(), function()
|
||||
{
|
||||
return program.classify.apply( program, arguments );
|
||||
} )
|
||||
.on( 'classify', function( classes )
|
||||
{
|
||||
if ( cmatchprot === true )
|
||||
{
|
||||
_self._handleError( Error( 'cmatch recursion' ) );
|
||||
}
|
||||
|
||||
cmatchprot = true;
|
||||
|
||||
// handle field fixes
|
||||
_self._dataValidator.validate( undefined, classes )
|
||||
.catch( e => _self.handleError( e ) );
|
||||
|
||||
_self._classMatcher.match( classes, function( cmatch )
|
||||
{
|
||||
// it's important that we do this here so that everything
|
||||
// that uses the cmatch data will consistently benefit
|
||||
_self._postProcessCmatch( cmatch );
|
||||
|
||||
// if we're not on a current step, defer
|
||||
if ( !( _self.ui.getCurrentStep() ) )
|
||||
{
|
||||
_self._cmatch = cmatch;
|
||||
cmatchprot = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_self._handleClassMatch( cmatch );
|
||||
cmatchprot = false;
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
'private _postProcessCmatch': function( cmatch )
|
||||
{
|
||||
// for any matches that are scalars (they will have no indexes), loop
|
||||
// through each field and set the index to the value of 'all'
|
||||
for ( var field in cmatch )
|
||||
{
|
||||
if ( field === '__classes' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cfield = cmatch[ field ];
|
||||
|
||||
if ( cfield.indexes.length === 0 )
|
||||
{
|
||||
var data = this.getQuote().getDataByName( field ),
|
||||
i = data.length;
|
||||
|
||||
// this will do nothing if there is no data found
|
||||
while ( i-- )
|
||||
{
|
||||
cfield.indexes[ i ] = cfield.all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmatch;
|
||||
},
|
||||
|
||||
|
||||
// from UI
|
||||
'private _cmatchVisFromUi': function( field, all )
|
||||
{
|
||||
var step = this.getUi().getCurrentStep();
|
||||
|
||||
if ( !step )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var group = step.getElementGroup( field );
|
||||
if ( !group )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var i = group.getCurrentIndexCount(),
|
||||
ret = [];
|
||||
|
||||
while ( i-- )
|
||||
{
|
||||
ret.push( all );
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
||||
'private _handleClassMatch': function( cmatch, force )
|
||||
{
|
||||
force = !!force;
|
||||
|
||||
this.ui.setCmatch( cmatch );
|
||||
|
||||
var _self = this,
|
||||
quote = this.getQuote(),
|
||||
|
||||
// oh dear god...(Demeter, specifically..)
|
||||
fields = this.getUi().getCurrentStep().getStep()
|
||||
.getExclusiveFieldNames();
|
||||
|
||||
|
||||
var visq = [];
|
||||
for ( var field in cmatch )
|
||||
{
|
||||
// ignore fields that are not on the current step
|
||||
if ( !( fields[ field ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the match is still false, then we can rest assured
|
||||
// that nothing has changed (and skip the overhead)
|
||||
if ( !force
|
||||
&& ( cmatch[ field ] === false )
|
||||
&& ( _self._cmatch[ field ] === false )
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var show = [],
|
||||
hide = [],
|
||||
|
||||
cfield = cmatch[ field ],
|
||||
|
||||
vis = cfield.indexes,
|
||||
cur = (
|
||||
( _self._cmatch[ field ] || {} ).indexes
|
||||
|| []
|
||||
);
|
||||
|
||||
// the system was previously unable to determine the length, so
|
||||
// let's attempt to get it from the UI
|
||||
if ( vis.length === 0 )
|
||||
{
|
||||
vis = this._cmatchVisFromUi( field, cfield.all );
|
||||
}
|
||||
|
||||
// consider the number of indexes in the bucket first;
|
||||
// otherwise, we might try to operate on fields that don't
|
||||
// exist (bucket/class indexes not the same). the check for
|
||||
// undefined in the first index is a workaround for the explicit
|
||||
// setting of the length property of the bucket value when
|
||||
// indexes are removed
|
||||
var curdata = quote.getDataByName( field ),
|
||||
fieldn = ( curdata.length > 0 && ( curdata[ 0 ] !== undefined ) )
|
||||
? curdata.length
|
||||
: vis.length;
|
||||
|
||||
for ( var i = 0; i < fieldn; i++ )
|
||||
{
|
||||
// do not record unchanged indexes as changed
|
||||
// (avoiding the event overhead)
|
||||
if ( !force && ( vis[ i ] === cur[ i ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
( ( vis[ i ] ) ? show : hide ).push( i );
|
||||
}
|
||||
|
||||
if ( show.length )
|
||||
{
|
||||
visq[ field ] = { event_id: 'show', name: field, indexes: show };
|
||||
this._mergeCmatchHidden( field, show, false );
|
||||
}
|
||||
|
||||
if ( hide.length )
|
||||
{
|
||||
visq[ field ] = { event_id: 'hide', name: field, indexes: hide };
|
||||
this._mergeCmatchHidden( field, hide, true );
|
||||
}
|
||||
}
|
||||
|
||||
// it's important to do this before showing/hiding fields, since
|
||||
// those might trigger events that check the current cmatches
|
||||
this._cmatch = cmatch;
|
||||
|
||||
|
||||
// allow DOM operations to complete before we trigger
|
||||
// manipulations on it (TODO: this is a workaround for group
|
||||
// show/hide issues; we need a better solution to guarantee
|
||||
// order
|
||||
setTimeout( () =>
|
||||
{
|
||||
Object.keys( visq ).forEach( field =>
|
||||
{
|
||||
const { event_id, name, indexes } = visq[ field ];
|
||||
|
||||
this.handleEvent( event_id, {
|
||||
elementName: name,
|
||||
indexes: indexes,
|
||||
} );
|
||||
|
||||
this._dapiTrigger( name );
|
||||
} );
|
||||
}, 25 );
|
||||
},
|
||||
|
||||
|
||||
'private _mergeCmatchHidden': function( name, indexes, hidden )
|
||||
{
|
||||
if ( !( this._cmatchHidden[ name ] ) )
|
||||
{
|
||||
this._cmatchHidden[ name ] = {};
|
||||
}
|
||||
|
||||
var cindexes = this._cmatchHidden[ name ];
|
||||
|
||||
for ( i in indexes )
|
||||
{
|
||||
if ( hidden )
|
||||
{
|
||||
cindexes[ indexes[ i ] ] = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete cindexes[ indexes[ i ] ];
|
||||
}
|
||||
}
|
||||
|
||||
var some = false;
|
||||
for ( var i in cindexes )
|
||||
{
|
||||
some = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !some )
|
||||
{
|
||||
// v8 devs do not recomment delete as it progressively slows down
|
||||
// property access on the object
|
||||
this._cmatchHidden[ name ] = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Hooks quote for performing validations on data change
|
||||
*
|
||||
|
@ -862,7 +562,7 @@ module.exports = Class( 'Client' )
|
|||
name,
|
||||
bucket,
|
||||
diff,
|
||||
this._cmatch,
|
||||
this._cmatch.getMatches(),
|
||||
function()
|
||||
{
|
||||
var args = arguments;
|
||||
|
@ -937,7 +637,7 @@ module.exports = Class( 'Client' )
|
|||
{
|
||||
// N.B.: We pass {} as the diff because nothing has actually changed
|
||||
_self.ui.invalidateForm(
|
||||
validate_callback( bucket, {}, _self._cmatch )
|
||||
validate_callback( bucket, {}, _self._cmatch.getMatches() )
|
||||
);
|
||||
} );
|
||||
},
|
||||
|
@ -1009,7 +709,7 @@ module.exports = Class( 'Client' )
|
|||
// force UI cmatch update, since we may have fields that have been added
|
||||
// that need to be shown/hidden based on the current set of
|
||||
// classifications
|
||||
this.forceCmatchAction();
|
||||
this._cmatch.forceCmatchAction();
|
||||
},
|
||||
|
||||
|
||||
|
@ -1049,7 +749,7 @@ module.exports = Class( 'Client' )
|
|||
catch ( e )
|
||||
{
|
||||
// todo: better suited for brokers
|
||||
this._handleError( Error(
|
||||
this.handleError( Error(
|
||||
"Error loading program data: " + e.message
|
||||
) );
|
||||
|
||||
|
@ -1058,7 +758,7 @@ module.exports = Class( 'Client' )
|
|||
|
||||
program.on( 'error', function( e )
|
||||
{
|
||||
_self._handleError( e );
|
||||
_self.handleError( e );
|
||||
} );
|
||||
|
||||
// handle field updates
|
||||
|
@ -1158,7 +858,7 @@ module.exports = Class( 'Client' )
|
|||
} )
|
||||
.on( 'error', function( e )
|
||||
{
|
||||
_self._handleError( e );
|
||||
_self.handleError( e );
|
||||
} );
|
||||
|
||||
return program;
|
||||
|
@ -1412,7 +1112,7 @@ module.exports = Class( 'Client' )
|
|||
// handle context errors
|
||||
root_context.on( 'error', function( e )
|
||||
{
|
||||
client._handleError( e );
|
||||
client.handleError( e );
|
||||
} );
|
||||
|
||||
// must init after the Ui obj is available
|
||||
|
@ -1472,7 +1172,7 @@ module.exports = Class( 'Client' )
|
|||
} )
|
||||
.on( 'error', function( e )
|
||||
{
|
||||
client._handleError( e );
|
||||
client.handleError( e );
|
||||
} );
|
||||
|
||||
return ui.saveStep( function( stepui )
|
||||
|
@ -1929,7 +1629,7 @@ module.exports = Class( 'Client' )
|
|||
var client = this;
|
||||
|
||||
// if the step contains invalid data, they must correct it
|
||||
if ( !( stepui.isValid( this._cmatch ) ) )
|
||||
if ( !( stepui.isValid( this._cmatch.getMatches() ) ) )
|
||||
{
|
||||
// well we didn't get very far
|
||||
callback( false );
|
||||
|
@ -1947,7 +1647,7 @@ module.exports = Class( 'Client' )
|
|||
|
||||
// we want to do this before save so that we don't re-mark the bucket as
|
||||
// dirty by populating it with uncommitted data
|
||||
client._clearCmatchFields();
|
||||
client._cmatch.clearCmatchFields();
|
||||
|
||||
// give devs the option to disable client-side submit events so we can
|
||||
// test server-side functionality
|
||||
|
@ -1959,7 +1659,7 @@ module.exports = Class( 'Client' )
|
|||
// shouldn't occurr, we should still throw an exception if one is
|
||||
// triggered
|
||||
var failures = this.program.submit(
|
||||
step_id, bucket, this._cmatch
|
||||
step_id, bucket, this._cmatch.getMatches()
|
||||
);
|
||||
|
||||
if ( failures !== null )
|
||||
|
@ -2072,7 +1772,7 @@ module.exports = Class( 'Client' )
|
|||
{
|
||||
event.abort();
|
||||
|
||||
_self._handleError( Error(
|
||||
_self.handleError( Error(
|
||||
'Save timeout; please try again'
|
||||
) );
|
||||
}, 15000 );
|
||||
|
@ -2371,57 +2071,6 @@ module.exports = Class( 'Client' )
|
|||
},
|
||||
|
||||
|
||||
'private _clearCmatchFields': function()
|
||||
{
|
||||
var step = this.getUi().getCurrentStep(),
|
||||
program = this.program;
|
||||
|
||||
// don't bother if we're not yet on a step
|
||||
if ( !step )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reset = {};
|
||||
for ( var name in step.getStep().getExclusiveFieldNames() )
|
||||
{
|
||||
var data = this._cmatchHidden[ name ];
|
||||
|
||||
// if there is no data or we have been asked to retain this field's
|
||||
// value, then do not clear
|
||||
if ( !data || program.cretain[ name ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// what state is the current data in?
|
||||
var cur = this.getQuote().getDataByName( name );
|
||||
|
||||
// we could have done Array.join(',').split(','), but we're trying
|
||||
// to keep performance sane here
|
||||
var indexes = [];
|
||||
for ( var i in data )
|
||||
{
|
||||
// we do *not* want to reset fields that have been removed
|
||||
if ( cur[ i ] === undefined )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
indexes.push( i );
|
||||
}
|
||||
|
||||
reset[ name ] = indexes;
|
||||
}
|
||||
|
||||
// batch reset (limit the number of times events are kicked off)
|
||||
this._resetFields( reset );
|
||||
|
||||
// we've done our deed; reset it for the next time around
|
||||
this._cmatchHidden = {};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Perform `forward' validations if needed
|
||||
*
|
||||
|
@ -2449,7 +2098,7 @@ module.exports = Class( 'Client' )
|
|||
var failures = this.program.forward(
|
||||
cur_step_id,
|
||||
bucket,
|
||||
this._cmatch,
|
||||
this._cmatch.getMatches(),
|
||||
function( trigger_event, question_id, value )
|
||||
{
|
||||
client.handleEvent( trigger_event, { stepId: +value } );
|
||||
|
@ -2561,14 +2210,12 @@ module.exports = Class( 'Client' )
|
|||
/**
|
||||
* Handles client-side events
|
||||
*
|
||||
* @param String event_name name of the event
|
||||
* @param Object data data to pass to event
|
||||
* @param Function callback function to call when event is done (if
|
||||
* not asynchronous, it'll be called immediately)
|
||||
* @param {string} event_name name of the event
|
||||
* @param {Object} data data to pass to event
|
||||
* @param {function(Object)} callback function to call when event is done
|
||||
* @param {function(Error)} error_callback function to call if event fails
|
||||
*
|
||||
* @param Function error_callback function to call if event fails
|
||||
*
|
||||
* @return Client self to allow for method chaining
|
||||
* @return {Client} self to allow for method chaining
|
||||
*/
|
||||
handleEvent: function( event_name, data, callback, error_callback )
|
||||
{
|
||||
|
@ -2577,120 +2224,26 @@ module.exports = Class( 'Client' )
|
|||
|
||||
this.emit( this.__self.$('EVENT_TRIGGER'), event_name, data );
|
||||
|
||||
try
|
||||
{
|
||||
this._eventHandler.handle(
|
||||
event_name, function( err, data )
|
||||
{
|
||||
if ( err )
|
||||
{
|
||||
error_callback( err );
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: move me
|
||||
if ( event_name === 'rate' )
|
||||
{
|
||||
_self.emit( _self.__self.$('EVENT_POST_RATE'), data );
|
||||
}
|
||||
|
||||
callback && callback( data );
|
||||
}, data
|
||||
);
|
||||
|
||||
// we had no problem handling this event; no need to continue with
|
||||
// the old event handling system
|
||||
return;
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
// segue into the old event handling system
|
||||
if ( !( Class.isA( UnknownEventError, e ) ) )
|
||||
this._eventHandler.handle(
|
||||
event_name, function( err, data )
|
||||
{
|
||||
// ruh roh
|
||||
this._handleError( e );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// perform event (XXX: replace me; see above)
|
||||
switch ( event_name )
|
||||
{
|
||||
case 'reset':
|
||||
var reset = {};
|
||||
reset[ data.elementName ] = data.indexes;
|
||||
|
||||
this._resetFields( reset );
|
||||
break;
|
||||
|
||||
default:
|
||||
this._handleError( Error(
|
||||
'Unknown client-side event: ' + event_name
|
||||
) );
|
||||
}
|
||||
|
||||
// call the callback, if one was provided
|
||||
if ( callback instanceof Function )
|
||||
{
|
||||
callback.call( this );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Trigger DataApi event for field FIELD
|
||||
*
|
||||
* @param {string} field field name
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _dapiTrigger': function( field )
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
this.getQuote().visitData( function( bucket )
|
||||
{
|
||||
_self.program.dapi(
|
||||
_self._currentStepId,
|
||||
field,
|
||||
bucket,
|
||||
{},
|
||||
_self._cmatch,
|
||||
null
|
||||
);
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
'private _resetFields': function( fields )
|
||||
{
|
||||
var update = {};
|
||||
|
||||
for ( var field in fields )
|
||||
{
|
||||
var cur = fields[ field ],
|
||||
cdata = this._quote.getDataByName( field ),
|
||||
val = this.elementStyler.getDefault( field );
|
||||
|
||||
var data = [];
|
||||
for ( var i in cur )
|
||||
{
|
||||
var index = cur[ i ];
|
||||
|
||||
if ( cdata[ index ] === val )
|
||||
if ( err )
|
||||
{
|
||||
continue;
|
||||
error_callback && error_callback( err );
|
||||
return this;
|
||||
}
|
||||
|
||||
data[ index ] = val;
|
||||
}
|
||||
// XXX: move me
|
||||
if ( event_name === 'rate' )
|
||||
{
|
||||
_self.emit( _self.__self.$('EVENT_POST_RATE'), data );
|
||||
}
|
||||
|
||||
update[ field ] = data;
|
||||
}
|
||||
callback && callback( data );
|
||||
}, data
|
||||
);
|
||||
|
||||
this._quote.setData( update );
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
|
@ -2926,7 +2479,7 @@ module.exports = Class( 'Client' )
|
|||
// proxy errors
|
||||
this._fieldMonitor.on( 'error', function( e )
|
||||
{
|
||||
_self._handleError( e );
|
||||
_self.handleError( e );
|
||||
} );
|
||||
},
|
||||
|
||||
|
@ -2960,7 +2513,7 @@ module.exports = Class( 'Client' )
|
|||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _handleError': function( e )
|
||||
'public handleError': function( e )
|
||||
{
|
||||
if ( !e )
|
||||
{
|
||||
|
@ -3024,10 +2577,4 @@ module.exports = Class( 'Client' )
|
|||
{
|
||||
return this.program.id;
|
||||
},
|
||||
|
||||
|
||||
'public getCmatchData': function()
|
||||
{
|
||||
return this._cmatch;
|
||||
}
|
||||
} );
|
||||
|
|
|
@ -82,9 +82,8 @@ var Step = require( '../step/Step' ),
|
|||
NavStyler = require( '../ui/nav/NavStyler' ),
|
||||
Sidebar = require( '../ui/sidebar/Sidebar' ),
|
||||
|
||||
FieldClassMatcher = require( '../field/FieldClassMatcher' ),
|
||||
DataApiFactory = require( '../dapi/DataApiFactory' ),
|
||||
DataApiManager = require( '../dapi/DataApiManager' ),
|
||||
DataApiFactory = require( '../dapi/DataApiFactory' ),
|
||||
DataApiManager = require( '../dapi/DataApiManager' ),
|
||||
|
||||
RootDomContext = require( '../ui/context/RootDomContext' ),
|
||||
DomFieldFactory = require( '../ui/field/DomFieldFactory' ),
|
||||
|
@ -345,8 +344,6 @@ module.exports = Class( 'ClientDependencyFactory',
|
|||
|
||||
createNotifyBar: UiNotifyBar,
|
||||
|
||||
createFieldClassMatcher: FieldClassMatcher,
|
||||
|
||||
|
||||
createClientEventHandler: function(
|
||||
client, data_validator, styler, data_proxy, jquery
|
||||
|
@ -373,5 +370,5 @@ module.exports = Class( 'ClientDependencyFactory',
|
|||
|
||||
'action$cvv2Dialog': requireh( 'Cvv2DialogEventHandler' )( jquery )
|
||||
} );
|
||||
}
|
||||
});
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,515 @@
|
|||
/**
|
||||
* Liza classification match (cmatch) handling
|
||||
*
|
||||
* Copyright (C) 2018 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
* TODO: This is code directly extracted from Client, modified to maintain
|
||||
* references to necessary objects. It is coupled with far too many things,
|
||||
* and the code is a mess. Getting this clean and well-tested is important,
|
||||
* as this is one of the core systems and is both complicated and complex.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Class } = require( 'easejs' );
|
||||
|
||||
|
||||
module.exports = Class( 'Cmatch',
|
||||
{
|
||||
/**
|
||||
* Contains classification match data per field
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
'private _cmatch': {},
|
||||
|
||||
/**
|
||||
* Fields that were hidden (including indexes) since the last cmatch
|
||||
* clear
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
'private _cmatchHidden': {},
|
||||
|
||||
/**
|
||||
* Performs classification matching on fields
|
||||
*
|
||||
* A field will have a positive match for a given index if all of its
|
||||
* classes match
|
||||
*
|
||||
* @type {FieldClassMatcher}
|
||||
*/
|
||||
'private _classMatcher': null,
|
||||
|
||||
/**
|
||||
* Program client
|
||||
* @type {Client}
|
||||
*/
|
||||
'private _client': null,
|
||||
|
||||
|
||||
/**
|
||||
* Initialize match handler
|
||||
*
|
||||
* This relies on too many objects; see header.
|
||||
*
|
||||
* @param {FieldClassMatcher} class_matcher class/field matcher
|
||||
* @param {Program} program active program
|
||||
* @param {Client} client active client
|
||||
*/
|
||||
constructor( class_matcher, program, client )
|
||||
{
|
||||
this._classMatcher = class_matcher;
|
||||
this._program = program;
|
||||
this._client = client;
|
||||
},
|
||||
|
||||
|
||||
'private _cmatchVisFromUi': function( field, all )
|
||||
{
|
||||
var step = this._client.getUi().getCurrentStep();
|
||||
|
||||
if ( !step )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var group = step.getElementGroup( field );
|
||||
if ( !group )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var i = group.getCurrentIndexCount(),
|
||||
ret = [];
|
||||
|
||||
while ( i-- )
|
||||
{
|
||||
ret.push( all );
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
||||
'public hookClassifier': function( data_validator )
|
||||
{
|
||||
var _self = this,
|
||||
program = this._program;
|
||||
|
||||
// clear/initialize cmatches
|
||||
this._cmatch = {};
|
||||
|
||||
var cmatchprot = false;
|
||||
|
||||
// set classifier
|
||||
this._client.getQuote()
|
||||
.setClassifier( program.getClassifierKnownFields(), function()
|
||||
{
|
||||
return program.classify.apply( program, arguments );
|
||||
} )
|
||||
.on( 'classify', function( classes )
|
||||
{
|
||||
if ( cmatchprot === true )
|
||||
{
|
||||
_self._client.handleError( Error( 'cmatch recursion' ) );
|
||||
}
|
||||
|
||||
cmatchprot = true;
|
||||
|
||||
// handle field fixes
|
||||
data_validator.validate( undefined, classes )
|
||||
.catch( e => _self.client._handleError( e ) );
|
||||
|
||||
_self._classMatcher.match( classes, function( cmatch )
|
||||
{
|
||||
// it's important that we do this here so that everything
|
||||
// that uses the cmatch data will consistently benefit
|
||||
_self._postProcessCmatch( cmatch );
|
||||
|
||||
// if we're not on a current step, defer
|
||||
if ( !( _self._client.getUi().getCurrentStep() ) )
|
||||
{
|
||||
_self._cmatch = cmatch;
|
||||
cmatchprot = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_self._handleClassMatch( cmatch );
|
||||
cmatchprot = false;
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
'private _postProcessCmatch': function( cmatch )
|
||||
{
|
||||
// for any matches that are scalars (they will have no indexes), loop
|
||||
// through each field and set the index to the value of 'all'
|
||||
for ( var field in cmatch )
|
||||
{
|
||||
if ( field === '__classes' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cfield = cmatch[ field ];
|
||||
|
||||
if ( cfield.indexes.length === 0 )
|
||||
{
|
||||
var data = this._client.getQuote().getDataByName( field ),
|
||||
i = data.length;
|
||||
|
||||
// this will do nothing if there is no data found
|
||||
while ( i-- )
|
||||
{
|
||||
cfield.indexes[ i ] = cfield.all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmatch;
|
||||
},
|
||||
|
||||
|
||||
'private _mergeCmatchHidden': function( name, indexes, hidden )
|
||||
{
|
||||
if ( !( this._cmatchHidden[ name ] ) )
|
||||
{
|
||||
this._cmatchHidden[ name ] = {};
|
||||
}
|
||||
|
||||
var cindexes = this._cmatchHidden[ name ];
|
||||
|
||||
for ( i in indexes )
|
||||
{
|
||||
if ( hidden )
|
||||
{
|
||||
cindexes[ indexes[ i ] ] = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete cindexes[ indexes[ i ] ];
|
||||
}
|
||||
}
|
||||
|
||||
var some = false;
|
||||
for ( var i in cindexes )
|
||||
{
|
||||
some = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !some )
|
||||
{
|
||||
// v8 devs do not recomment delete as it progressively slows down
|
||||
// property access on the object
|
||||
this._cmatchHidden[ name ] = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
'private _handleClassMatch': function( cmatch, force )
|
||||
{
|
||||
force = !!force;
|
||||
|
||||
this._client.getUi().setCmatch( cmatch );
|
||||
|
||||
var _self = this,
|
||||
quote = this._client.getQuote(),
|
||||
|
||||
// oh dear god...(Demeter, specifically..)
|
||||
fields = this._client.getUi().getCurrentStep().getStep()
|
||||
.getExclusiveFieldNames();
|
||||
|
||||
|
||||
var visq = {};
|
||||
for ( var field in cmatch )
|
||||
{
|
||||
// ignore fields that are not on the current step
|
||||
if ( !( fields[ field ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the match is still false, then we can rest assured
|
||||
// that nothing has changed (and skip the overhead)
|
||||
if ( !force
|
||||
&& ( cmatch[ field ] === false )
|
||||
&& ( _self._cmatch[ field ] === false )
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var show = [],
|
||||
hide = [],
|
||||
|
||||
cfield = cmatch[ field ],
|
||||
|
||||
vis = cfield.indexes,
|
||||
cur = (
|
||||
( _self._cmatch[ field ] || {} ).indexes
|
||||
|| []
|
||||
);
|
||||
|
||||
// TODO: Figure out something better here. This is currently
|
||||
// needed for hiding statics---they are registered as exclusive
|
||||
// fields (`fields', above), but aren't actually fields (they're
|
||||
// not in the bucket). But we must show/hide them appropriately.
|
||||
if ( vis.length === 0 )
|
||||
{
|
||||
vis = this._cmatchVisFromUi( field, cfield.all );
|
||||
}
|
||||
|
||||
// consider the number of indexes in the bucket first;
|
||||
// otherwise, we might try to operate on fields that don't
|
||||
// exist (bucket/class indexes not the same). the check for
|
||||
// undefined in the first index is a workaround for the explicit
|
||||
// setting of the length property of the bucket value when
|
||||
// indexes are removed
|
||||
var curdata = quote.getDataByName( field ),
|
||||
fieldn = ( curdata.length > 0 && ( curdata[ 0 ] !== undefined ) )
|
||||
? curdata.length
|
||||
: vis.length;
|
||||
|
||||
for ( var i = 0; i < fieldn; i++ )
|
||||
{
|
||||
// do not record unchanged indexes as changed
|
||||
// (avoiding the event overhead)
|
||||
if ( !force && ( vis[ i ] === cur[ i ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
( ( vis[ i ] ) ? show : hide ).push( i );
|
||||
}
|
||||
|
||||
this.markShowHide( field, visq, show, hide );
|
||||
}
|
||||
|
||||
// it's important to do this before showing/hiding fields, since
|
||||
// those might trigger events that check the current cmatches
|
||||
this._cmatch = cmatch;
|
||||
|
||||
|
||||
// allow DOM operations to complete before we trigger
|
||||
// manipulations on it (TODO: this is a workaround for group
|
||||
// show/hide issues; we need a better solution to guarantee
|
||||
// order
|
||||
setTimeout( () =>
|
||||
{
|
||||
Object.keys( visq ).forEach( field =>
|
||||
{
|
||||
const field_vis = visq[ field ];
|
||||
|
||||
Object.keys( field_vis ).forEach( event_id =>
|
||||
{
|
||||
const indexes = field_vis[ event_id ];
|
||||
|
||||
this._client.handleEvent( event_id, {
|
||||
elementName: field,
|
||||
indexes: indexes,
|
||||
} );
|
||||
} );
|
||||
|
||||
this._dapiTrigger( name );
|
||||
} );
|
||||
}, 25 );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Mark fields to be shown/hidden
|
||||
*
|
||||
* This also updates the cached visibility of field FIELD.
|
||||
*
|
||||
* @param {string} field field name
|
||||
* @param {Array} show indexes to show
|
||||
* @param {Array} hide indexes to hide
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'virtual protected markShowHide'( field, visq, show, hide )
|
||||
{
|
||||
if ( !( show.length || hide.length ) )
|
||||
{
|
||||
return visq;
|
||||
}
|
||||
|
||||
const { [field]: result = {} } = visq;
|
||||
|
||||
if ( show.length )
|
||||
{
|
||||
this._mergeCmatchHidden( field, show, false );
|
||||
result.show = show;
|
||||
}
|
||||
|
||||
if ( hide.length )
|
||||
{
|
||||
this._mergeCmatchHidden( field, hide, true );
|
||||
result.hide = hide;
|
||||
}
|
||||
|
||||
visq[ field ] = result;
|
||||
|
||||
return visq;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Trigger DataApi event for field FIELD
|
||||
*
|
||||
* @param {string} field field name
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _dapiTrigger': function( field )
|
||||
{
|
||||
const current_step_id = this._client.nav.getCurrentStepId();
|
||||
|
||||
this._client.getQuote().visitData( bucket =>
|
||||
{
|
||||
this._program.dapi(
|
||||
current_step_id,
|
||||
field,
|
||||
bucket,
|
||||
{},
|
||||
this._cmatch,
|
||||
null
|
||||
);
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
'public clearCmatchFields': function()
|
||||
{
|
||||
var step = this._client.getUi().getCurrentStep(),
|
||||
program = this._program;
|
||||
|
||||
// don't bother if we're not yet on a step
|
||||
if ( !step )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reset = {};
|
||||
for ( var name in step.getStep().getExclusiveFieldNames() )
|
||||
{
|
||||
var data = this._cmatchHidden[ name ];
|
||||
|
||||
// if there is no data or we have been asked to retain this field's
|
||||
// value, then do not clear
|
||||
if ( !data || program.cretain[ name ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// what state is the current data in?
|
||||
var cur = this._client.getQuote().getDataByName( name );
|
||||
|
||||
// we could have done Array.join(',').split(','), but we're trying
|
||||
// to keep performance sane here
|
||||
var indexes = [];
|
||||
for ( var i in data )
|
||||
{
|
||||
// we do *not* want to reset fields that have been removed
|
||||
if ( cur[ i ] === undefined )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
indexes.push( i );
|
||||
}
|
||||
|
||||
reset[ name ] = indexes;
|
||||
}
|
||||
|
||||
// batch reset (limit the number of times events are kicked off)
|
||||
this._resetFields( reset );
|
||||
|
||||
// we've done our deed; reset it for the next time around
|
||||
this._cmatchHidden = {};
|
||||
},
|
||||
|
||||
|
||||
'private _resetFields': function( fields )
|
||||
{
|
||||
const quote = this._client.getQuote();
|
||||
const update = {};
|
||||
|
||||
for ( var field in fields )
|
||||
{
|
||||
var cur = fields[ field ],
|
||||
cdata = quote.getDataByName( field ),
|
||||
val = this._client.elementStyler.getDefault( field );
|
||||
|
||||
var data = [];
|
||||
for ( var i in cur )
|
||||
{
|
||||
var index = cur[ i ];
|
||||
|
||||
if ( cdata[ index ] === val )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
data[ index ] = val;
|
||||
}
|
||||
|
||||
update[ field ] = data;
|
||||
}
|
||||
|
||||
quote.setData( update );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Force handling of the most recent cmatch data
|
||||
*
|
||||
* This can be used to refresh the UI to ensure that it is consistent with
|
||||
* the cmatch data.
|
||||
*
|
||||
* @return {Client} self
|
||||
*/
|
||||
'public forceCmatchAction': function()
|
||||
{
|
||||
if ( !( this._cmatch ) )
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
this._handleClassMatch( this._cmatch, true );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get matches from last classifier application
|
||||
*
|
||||
* TODO: Remove me; breaks encapsulation. Intended for transition from
|
||||
* mammoth Client.
|
||||
*
|
||||
* @return {Object} classification matches
|
||||
*/
|
||||
'public getMatches'()
|
||||
{
|
||||
return this._cmatch;
|
||||
},
|
||||
} );
|
|
@ -21,7 +21,9 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const store = require( '../store' );
|
||||
const Cmatch = require( '../client/Cmatch' );
|
||||
const field = require( '../field' );
|
||||
const store = require( '../store' );
|
||||
|
||||
|
||||
/**
|
||||
|
@ -33,6 +35,12 @@ const store = require( '../store' );
|
|||
* This is incomplete; it will be added to as code is ported to liza.
|
||||
*/
|
||||
module.exports = {
|
||||
cmatch: ( program, client ) => Cmatch(
|
||||
field.FieldClassMatcher( program.whens ),
|
||||
program,
|
||||
client
|
||||
),
|
||||
|
||||
data: {
|
||||
/**
|
||||
* Create a store suitable for comparing diffs
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Test case for Cmatch
|
||||
*
|
||||
* Copyright (C) 2018 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const { event } = require( '../../' ).client;
|
||||
const { expect } = require( 'chai' );
|
||||
|
||||
const Sut = require( '../../src/client/Cmatch' )
|
||||
.extend(
|
||||
{
|
||||
'override constructor'( _, __, ___ ) {},
|
||||
|
||||
// make public
|
||||
'override public markShowHide'( field, visq, show, hide )
|
||||
{
|
||||
return this.__super( field, visq, show, hide );
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
// these tests aren't terribly effective right now
|
||||
describe( "Cmatch", () =>
|
||||
{
|
||||
it( "marks hidden fields on class change to show", () =>
|
||||
{
|
||||
expect(
|
||||
Sut().markShowHide( 'foo', {}, [ 1, 2 ], [] )
|
||||
).to.deep.equal( { foo: { show: [ 1, 2 ] } } );
|
||||
} );
|
||||
|
||||
|
||||
it( "marks shown fields on class change to hide", () =>
|
||||
{
|
||||
expect(
|
||||
Sut().markShowHide( 'foo', {}, [], [ 3, 4, 5 ] )
|
||||
).to.deep.equal( { foo: { hide: [ 3, 4, 5 ] } } );
|
||||
} );
|
||||
|
||||
|
||||
it( "marks combination show/hide on class change", () =>
|
||||
{
|
||||
expect(
|
||||
Sut().markShowHide( 'foo', {}, [ 2, 3 ], [ 4, 5, 6 ] )
|
||||
).to.deep.equal( {
|
||||
foo: {
|
||||
show: [ 2, 3 ],
|
||||
hide: [ 4, 5, 6 ],
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
it( "marks no fields with no show or hide", () =>
|
||||
{
|
||||
expect(
|
||||
Sut().markShowHide( 'foo', {}, [], [] )
|
||||
).to.deep.equal( {} );
|
||||
} );
|
||||
|
||||
|
||||
it( "does not affect marking of other fields", () =>
|
||||
{
|
||||
const barval = {};
|
||||
const visq = { bar: barval };
|
||||
|
||||
Sut().markShowHide( 'foo', {}, [ 1 ], [ 0 ] );
|
||||
|
||||
expect( visq.bar ).to.equal( barval );
|
||||
} );
|
||||
} );
|
Loading…
Reference in New Issue