/*!
* Script: jQuery.dataDisplay.js
* Description: jquery.dataDisplay aids the developer in writing concise conditions against elements within a form based setting, in order to control the display of elements based on the state of a form.
* Copyright: Copyright (c) 2017 Assetinfo (a trading style of Money Marketplace LTD)
* Author: GDixon
* Email: gdixon@assetinfo.co.uk
* Licensed: MIT
* Requires: jQuery > 1.9
* Version: 0.0.2
*/
;(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module depending on jQuery
define(['jquery'], factory);
} else {
// No AMD. Register plugin with global jQuery object
factory(jQuery);
}
}(
/**
* @fileOverview $.fn.dataDisplay - A jQuery plugin that aids the developer in writing concise conditions against elements within a form based setting, in order to control the display of elements based on the state of the form.
*
* @example <caption>Basic HTML syntax and structure example:</caption>
* <div id="container">
* <input name="inputTest" type="value"/>
* <div class="dataDisplay"
* data-display="
* {inputTest} is equal to 'testing';"
* data-display-resets="
* $this.css('display', 'none');">
* <a>Test</a>
* </div>
* </div>
*
* @example <caption>Basic JS initiation example:</caption>
* $(function() {
* $('#container').dataDisplay({...});
* });
*
* @version 0.0.2
* @author Graham Dixon - gdixon@assetinfo.co.uk
* @namespace $.fn.dataDisplay
* @memberof! $.fn
*/
function ($) {
// defaults to align the dataDisplay instance with the dom situation
var defaults = {
// name to bind the events against
eventName: '.dataDisplay',
// bind DataDisplay instance to data attribute
dataAttr: 'dataDisplay',
// main attribute holding the conditions on first-load
condsAttr: 'data-display',
// attribute holding resets (defined as jquery statements against $this)
resetsAttr: 'data-display-resets',
// should the plugin fire during the setup phase?
initFire: true,
// should the plugin fire on key events?
keyEventsFire: true
};
// pre-defined helpers available in data-display's conditions
var funcs = {
'!empty': {
rgx: '\\!empty\\({([^}]+)}\\)',
exec: function (field, ctx) {
var str = '(';
$('[name*="' + field + '"]', ctx).each(function () {
var val = $(this).val();
if (str == '(') {
str = str + '\"' + val + '\"' + ' !== \"\" ';
} else {
str = str + ' && ' + '\"' + val + '\"' + ' !== \"\" ';
}
});
str = str + ')';
return str;
}
},
'empty': {
rgx: 'empty\\({([^}]+)}\\)',
exec: function (field, ctx) {
var str = '(';
$('[name*="' + field + '"]', ctx).each(function () {
var val = $(this).val();
if (str == '(') {
str = str + '\"' + val + '\"' + ' == \"\" ';
} else {
str = str + ' && ' + '\"' + val + '\"' + ' == \"\" ';
}
});
str = str + ')';
return str;
}
},
'length': {
rgx: 'length\\({([^}]+)}\\)',
exec: function (field, ctx) {
var fieldSelector = '[name*="' + field + '"]';
if (typeof $(fieldSelector + ':checked', ctx).val() !== "undefined") {
// first check for radio/checkbox - failover to inputs
var str = $(fieldSelector + ':checked', ctx).val().length;
} else {
// then check for everything else
var str = $(fieldSelector, ctx).val().length;
}
return str;
}
},
'is greater than or equal to': { // or is optional
rgx: 'is\\sgreater\\sthan\\s(?:or\\s)?equal\\sto',
exec: function (field, ctx) {
return '>=';
}
},
'is less than or equal to': { // or is optional
rgx: 'is\\sless\\sthan\\s(?:or\\s)?equal\\sto',
exec: function (field, ctx) {
return '<=';
}
},
'is greater than': {
rgx: 'is\\sgreater\\sthan',
exec: function (field, ctx) {
return '>';
}
},
'is less than': {
rgx: 'is\\sless\'sthan',
exec: function (field, ctx) {
return '<';
}
},
'is equal to': {
rgx: 'is\\sequal\\sto',
exec: function (field, ctx) {
return '==';
}
}
};
/**
* $.fn.dataDisplay - Initates a new DataDisplay instance or invokes methods against instance bound to the $el
*
* @param {object} options the settings being applied to this $el
* @param {array} options.funcs provide an array of func methods to extend the built-in helper methods
* @param {string} options.eventName name to bind the events against
* @param {string} options.dataAttr bind DataDisplay instance to data attribute
* @param {string} options.condsAttr main attribute holding the conditions on first-load
* @param {string} options.resetsAttr attribute holding resets (defined as jquery statements against $this)
* @param {string} options.initFire should the plugin fire during the setup phase?
* @param {string} options.keyEventsFire should the plugin fire on key events?
*
* @alias dataDisplay
* @memberof! $.fn.dataDisplay
*/
$.fn.dataDisplay = (function (options) {
// the options must be passed to the init func as an array - convert any strings
var optionsArray = (typeof options == "string" ? [options] : options);
// only create when the giving el exists
if ($(this).length) {
// extend defaults with options and set to settings
var settingsArray = $.extend({}, defaults, (typeof optionsArray !== "undefined" ? optionsArray : []));
// extend default funcss with funcs defined in settings
var funcsArray = $.extend({}, funcs, (typeof settingsArray.funcs !== "undefined" ? settingsArray.funcs : []));
// check that dataDisplay instance hasnt already been created
if (typeof $(this).data(settingsArray.dataAttr) == "undefined") {
// create a new instance
var instance = $(this).data(settingsArray.dataAttr, new DataDisplay(this, settingsArray, funcsArray));
} else {
// do option based functions
if (typeof options !== 'undefined' && options == "destroy") {
// fully destroys the dataDisplay instance
var instance = this.data(settingsArray.dataAttr).destroy();
} else if (typeof options !== 'undefined') {
// recreates the dataDisplay instance against the defined options
var instance = this.data(settingsArray.dataAttr).destroy().init(this, settingsArray, funcsArray);
} else {
// return the dataDisplay instance held against the el
var instance = this.data(settingsArray.dataAttr);
}
}
}
// DataDisplay instance - fully intiated or destroyed
return instance
});
/**
* @fileOverview DataDisplay - A jQuery plugin that aids the developer in writing concise conditions against elements within a form based setting, in order to control the display of elements based on the state of the form. <br/><br/>
*
* new DataDisplay($el, settings, funcs)
*
* @example <caption>Basic HTML syntax and structure example:</caption>
* <div id="container">
* <input name="inputTest" type="value"/>
* <div class="dataDisplay"
* data-display="
* {inputTest} is equal to 'testing';"
* data-display-resets="
* $this.css('display', 'none');">
* <a>Test</a>
* </div>
* </div>
*
* @example <caption>Basic JS initiation example:</caption>
* $(function() {
* $('#container').dataDisplay({...});
* });
*
* @version 0.0.2
* @author Graham Dixon - gdixon@assetinfo.co.uk
* @class DataDisplay
* @memberof $.fn.dataDisplay
*/
var DataDisplay = function ($el, settings, funcs) {
// assign internal scope to local obj
var dataDisplay = this;
/**
* Initiate dataDisplay on the given element with the given settings and funcs
*
* @function DataDisplay.init
* @param {object} $el the element that DataDisplay is being applied to
* @param {object} settings the settings being applied to this $el
* @param {array} settings.funcs provide an array of func methods to extend the built-in helper methods
* @param {string} settings.eventName name to bind the events against
* @param {string} settings.dataAttr bind DataDisplay instance to data attribute
* @param {string} settings.condsAttr main attribute holding the conditions on first-load
* @param {string} settings.resetsAttr attribute holding resets (defined as jquery statements against $this)
* @param {string} settings.initFire should the plugin fire during the setup phase?
* @param {string} settings.keyEventsFire cshould the plugin fire on key events?
* @param {object} funcs the funcs being supplied as helpers to build out conditions in a congruent manner
*
* @memberof DataDisplay
*/
dataDisplay.init = function ($el, settings, funcs) {
// associate properties the outer calling scope
var that = this;
// record the calling outer el to use as ctx internally
that.$el = $el;
that.settings = settings;
that.funcs = funcs;
// initiate the conditions defined against this collection of elements
$(that.$el).each(function () {
// keep ref to the outer context to limit the inner scope
var $ctx = $(this);
// for each discovered element of given attribute in context apply the defined condition(s)
$('[' + that.settings.condsAttr + ']', $ctx).each(function () {
// $this is always a jquery context for single elm
var $this = $(this);
// conditions are the condition we are working against for the given elm
var conditions = $this.attr(that.settings.condsAttr);
// conditions == !isNaN when dataDisplay is already loaded - stops reiniting
if (isNaN(conditions)) {
// when the conditions array is undefined create it
if (typeof that.conditions == 'undefined') {
that.conditions = [];
}
// apply the default state
var applyResets = that.debounce(function (resets, fields, $el, $ctx) {
return that.applyResets(resets, fields, $el, $ctx);
});
// apply the main debounce logic
var applyConditions = that.debounce(function (conditions, fields, $el, $ctx) {
return that.applyConditions(conditions, fields, $el, $ctx);
});
// backup the css for the given elm
var styles = (typeof $this.attr("style") !== 'undefined' ? $this.attr("style") : '');
// backup the resets for the given elm
var resets = (typeof $this.attr(that.settings.resetsAttr) !== 'undefined' ? $this.attr(that.settings.resetsAttr) : '');
// find the fields that the reset conditions might use
var resetFields = that.findFields(resets);
// find fields that these conditions will affect/use
var conditionFields = that.findFields(conditions);
// n is the current position in the conditions array
var n = that.conditions.length;
// construct a name to bind events to
var fireEvents = 'change' + that.settings.eventName + (that.settings.keyEventsFire ? ' keyup' + that.settings.eventName : '');
// for each field ensure we have a selector available and
for (i = 0; i < conditionFields.length; i++) {
// pull input field selector
var field = conditionFields[i];
var selector = that.getFieldSelector(field);
// end early on missing selector
if ($(selector, $ctx).length == 0)
return;
// using discovered selector bind the discovered events
$(selector, $ctx).on(fireEvents, {
el: $this,
context: $ctx,
resets: resets,
resetFields: resetFields,
conditions: conditions,
conditionFields: conditionFields
}, function (e) {
// apply the default conditions
applyResets(e.data.resets, e.data.resetFields, e.data.el, e.data.context);
// before applying the conditionally displayed rules
applyConditions(e.data.conditions, e.data.conditionFields, e.data.el, e.data.context);
});
}
// remove resets & conditions attr and push to arr, restore on destroy -> this to ensure the doms clean. against inspect.
$this.attr(that.settings.resetsAttr, n)
.attr(that.settings.condsAttr, n);
// apply defaults as defined in current context
that.applyResets(resets, resetFields, $this, $ctx);
// fire the conditions once onload -- useful for when we have a complete data set
if (that.settings.initFire) {
applyConditions(conditions, conditionFields, $this, $ctx);
}
// apply the context to the conditions array for restoration on destroy
that.conditions[n] = {
"this": $this,
"styles": styles,
"resets": resets,
"resetFields": resetFields,
"conditions": conditions,
"conditionFields": resetFields
};
}
});
});
// allow chainging on setup level methods
return that;
};
/**
* this.debounce()<br/><br/>Only perform the provided function once over the given threshold timeframe
*
* @function DataDisplay.debounce
* @param {function} func the provided function we would like to call
* @param {int} threshold for how long should we supress calling the function?
* @param {bool} execAsap execute the function on first load, then again after threshold
* @return {function} calling the returned function n* over a timeframe shorter than threshold will result in one invokation
* @memberof DataDisplay
*/
dataDisplay.debounce = function (func, threshold, execAsap) {
// timeout appears scope above the calling function to allow the calling function to clear itself
var timeout;
// return a function which will call the given function as a delayed timeout (delayed according to threshold)
return function () {
// save the context
var context = this;
var args = arguments;
// delay the actual function
var delayed = function () {
timeout = null;
if (!execAsap) func.apply(context, args);
};
// do we want to call the function now
var callNow = execAsap && !timeout;
// clear the function if its already been called
clearTimeout(timeout);
// and set a timeout to call the delayed
timeout = setTimeout(delayed, threshold);
// if we do want to call now, then do it
if (callNow) func.apply(context, args);
};
};
/**
* this.applyResets()<br/><br/>Apply the defaults as defined against the el
*
* @function DataDisplay.applyResets
* @param {string} resets the reset statements defined against the element
* @param {array} fields an array of all {variables} defined in the conditions string
* @param {object} $el the element we are applying defaluts for
* @param {object} $ctx the outer element we can use as context
* @memberof DataDisplay
*/
dataDisplay.applyResets = function (resets, fields, $el, $ctx) {
// default action is to hide the element
$el.hide();
// for each field present in the conditions, replace its {var} with its value
resets = this.replaceFieldValHolders(resets, fields, $ctx);
// apply resets as js functions against given el
var resolve = new Function('$this', (resets ? resets : ''));
// trigger with the given el
resolve($el);
};
/**
* this.applyConditions()<br/><br/>Apply the conditions as provided by the el
*
* @function DataDisplay.applyConditions
* @param {string} conditions the conditions defined against the element
* @param {array} fields an array of all {variables} defined in the conditions string
* @param {object} $el the element we are applying defaluts for
* @param {object} $ctx the outer element we can use as context
* @memberof DataDisplay
*/
dataDisplay.applyConditions = function (conditions, fields, $el, $ctx) {
// hold outer ctx;
var that = this;
// check for predefined functions by syntax (!empty, empty, length)
for (func in that.funcs) {
var f = that.funcs[func];
var r = new RegExp(f.rgx, 'gi');
while ((m = r.exec(conditions)) != null) {
var v = f.exec(m[1], $ctx);
conditions = conditions.replace(new RegExp(f.rgx), v);
}
}
// for each field present in the conditions, replace its {var} with its value
conditions = that.replaceFieldValHolders(conditions, fields, $ctx);
// allow for multiple side-effects in single conditions
// ... data-display="
// {val} > 0; ||
// {val} < 1 :: $(var).css('background','#bdbdbd'); ||
// {val} > 1 && {val} < 2 :: $(var).css('background','#bdbdbd');
// " ...
conditions = conditions.replace(/\s\s+/g, ' ');
// split the conditions into its collective parts
var conditionalParts = conditions.split("; ||");
// for each part of the conditions, check the condition
for (var i = 0; i < conditionalParts.length; i++) {
// check for the presents of a "::" this indicates that anything following the dash should be used instead of show/hide,
// each statement (seperated by ';') must have only one side-effect. This is enforced via the splitting of '::' and ';'
var conditionalPartsSplit = conditionalParts[i].split("::");
// allow for default condition-met-action of showing
if (conditionalPartsSplit.length == 1) {
// allow for default condition-met-action of showing
that.showOnCondition(conditionalParts[i], $el);
} else {
// carry out the single action as defined against the condition
that.funcOnCondition(conditionalPartsSplit, $el);
}
}
};
/**
* this.showOnCondition()<br/><br/>Show the el based on condition
*
* @function DataDisplay.showOnCondition
* @param {string} condition the condition being considered
* @param {object} $el the element we are applying the condition to
* @memberof DataDisplay
*/
dataDisplay.showOnCondition = function (conditions, $el) {
// strip final ';' from string before invoking
var conditions = conditions.replace(/;(\s+)?$/, '');
// check the condition and show/hide the el
var condition = "return (" + conditions + ");";
// execute the condition and if true display the element
if (new Function(condition)() === true) {
// show the given element
$el.show();
}
};
/**
* this.funcOnCondition()<br/><br/>Show the el based on condition and associated functions
*
* @function DataDisplay.funcOnCondition
* @param {array} conditionalParts a two part construct spliting the condition from the resolution funcs
* @param {object} $el the element we are applying the condition to
* @memberof DataDisplay
*/
dataDisplay.funcOnCondition = function (conditionalParts, $el) {
// carry out the single action as defined against the condition
for (var i = 0; i < conditionalParts.length; i++) {
// strip final ';' from string before invoking
var condition = "return (" + conditionalParts[0] + ");";
// when the part isnt the condition we trigger the datafunc
if (i > 0) {
var conditionalPartsSplit = conditionalParts[i].split(";");
// carry out the work defined in each conditional statement
for (var j = 0; j < conditionalPartsSplit.length; j++) {
// when the condition is satisified build and trigger the func
if (new Function(condition)() === true) {
// create the function associated with this conditionsPart
var outcomes = " return (" + (conditionalPartsSplit[j].length ? conditionalPartsSplit[j] : 'true') + ");";
// apply the dataFunc aginst the given el
var resolve = new Function('$this', outcomes);
// trigger with the given el
resolve($el);
}
}
}
}
};
/**
* this.findFields()<br/><br/>Find the fields defined across the entire condition
*
* @function DataDisplay.findFields
* @param {string} conditions the conditions defined against the element
* @return {array} an array of all {variables} defined in the confitions string
* @memberof DataDisplay
*/
dataDisplay.findFields = function (conditions) {
// find all the {fields} used in the given conditions (String)
var fields = [];
// simple regex pattern to patch anything between {}
var r = /{([^}]+)}/gi;
// find the fields defined in the conditions
while ((m = r.exec(conditions)) != null) {
fields[fields.length] = m[1];
}
return fields;
};
/**
* this.getFieldSelector()<br/><br/>Return a string selector which assumes field relates to a name, eg [name*="..."]
*
* @function DataDisplay.getFieldSelector
* @param {string} field the field we want a selector for
* @return {string} the field wrapped in a name selector
* @memberof DataDisplay
*/
dataDisplay.getFieldSelector = function (field) {
// return the provided field name wrapped in a name selctor
return '[name*="' + field + '"]';
};
/**
* this.replaceFieldValHolders()<br/><br/>Replace a {variable} field in a condition
*
* @function DataDisplay.replaceFieldValHolders
* @param {string} condition the condition being considered
* @param {string} fields the fields being replaced
* @param {object} $ctx the outer element we can use as context
* @memberof DataDisplay
*/
dataDisplay.replaceFieldValHolders = function (conditions, fields, $ctx) {
// hold outer ctx;
var that = this;
// ensure the fields are presented as an array
fields = (typeof fields == "string" ? [fields] : fields);
// for each field in the conditions string, replace its {var} with its value
for (i = 0; i < fields.length; i++) {
// grab the field selector
var field = fields[i];
// get a selector for the given field
var fieldSelector = that.getFieldSelector(field);
// replace the given field with the value of the fieldSelector in the conditions
var fieldValue = '';
var parseVal = '';
// check that the given field has a value
if (typeof $(fieldSelector + ':checked', $ctx).val() !== "undefined") {
// first check for radio/checkbox - failover to inputs
fieldValue = $(fieldSelector + ':checked', $ctx).val();
} else {
fieldValue = $(fieldSelector, $ctx).val();
}
// when value is present replace {var}
if (typeof fieldValue !== 'undefined') {
if (isNaN(fieldValue) == true) {
parseVal = '\"' + encodeURIComponent(fieldValue) + '\"';
} else {
parseVal = parseFloat(fieldValue);
}
// replace the {} var place holder with real safely encoded value
conditions = conditions.replace(new RegExp('{' + that.escapeRegExp(field) + '}', 'g'), parseVal);
}
}
// fields have been replaced throughout the entire condition list with their respective values
return conditions;
};
/**
* this.escapeRegExp()<br/><br/>Return a string which has been appropriately escaped ready for a regex expression
*
* @function DataDisplay.escapeRegExp
* @param {string} str the string we want to clean
* @return {string} the clean string
* @memberof DataDisplay
*/
dataDisplay.escapeRegExp = function (str) {
// escape the given string, allow special chars to safely appear in regex expression
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
/**
* this.destroy()<br/><br/>Destroy the conditions and restore the originial state
*
* @function DataDisplay.destroy
* @return {object} return this to allow for the base instance to be kept and removed manually
* @memberof DataDisplay
*/
dataDisplay.destroy = function (undefined) {
// hold outer ctx;
var that = this;
// destory each element (as-called) individually
$(that.$el).each(function () {
// keep ref to the outer context to limit the inner scope
var $ctx = $(this);
// find any options.condsAttr used in the call
$('[' + that.settings.condsAttr + ']', $ctx).each(function () {
// dataDisplay objects context
var $this = $(this);
// discover the location in the global obj this dataDisplay elm sits
var n = $this.attr(that.settings.condsAttr);
// pull details from global dataDisplay back to the dom
var styles = that.conditions[n]['styles'];
var resets = that.conditions[n]['resets'];
var conditions = that.conditions[n]['conditions'];
// the fields involved in any discovered conditions
var conditionFields = that.findFields(conditions);
// when there are fields we need to unbind the event watchers
if (conditionFields.length == 0) return;
// for each field unbind any dataDisplay events
for (i = 0; i < conditionFields.length; i++) {
var field = conditionFields[i];
var selector = that.getFieldSelector(field);
$(selector, $ctx).off(that.settings.eventName);
}
// restore original attributes
$this
.attr("style", styles)
.attr(that.settings.condsAttr, conditions)
.attr(that.settings.resetsAttr, resets);
});
// remove the data attr (removes all references to dataDisplay instance)
$ctx.removeData('dataDisplay');
});
// allow chainging on setup level methods
return this;
};
// call dataDisplay.init() on new dataDisplay()
return dataDisplay.init($el, settings, funcs);
};
}
));