/*
* We need to intercept sessionStorage calls and point to the window object if the
* sessionStorage is unavailable which is usually the case on iframes (3rd party
* cookie being blocked) and incognito/private mode on android and iOS devices
*
*/
var use_session_storage = false;
/**
* check whether we can use session storage
* @return Boolean
*/
function isSessionStorageAvailable(){
var test = 'test';
try {
sessionStorage.setItem(test, test);
sessionStorage.removeItem(test);
return true;
} catch(e) {
return false;
}
}
/**
* Description
* @key String key value of item in store
* @value String
*/
function setStoreItem(key, value) {
if (use_session_storage) {
sessionStorage.setItem( key, value );
} else {
// to be sure localStorageMimic has been set up yet
if (window['localStorageMimic'] === undefined) {
window['localStorageMimic'] = {};
}
window['localStorageMimic'][key] = value;
}
}
/**
* Description
* @key String key value of item in store
* @return String
*/
function getStoreItem(key) {
var return_val;
if (use_session_storage) {
return_val = sessionStorage.getItem( key );
} else {
// in case localStorageMimic hasn't been set up yet
if (window['localStorageMimic'] === undefined) {
return null;
}
return_val = window['localStorageMimic'][key];
}
// mimic what we expect a missing session store item will be
if (return_val === undefined) {
return_val = null;
}
return return_val;
}
/**
* Remove item in store
* @key String key value of item in store
*/
function removeStoreItem(key) {
if (use_session_storage) {
sessionStorage.removeItem( key );
} else {
delete window['localStorageMimic'];
}
}
/**
* Clear store for this domain
* @key String key value of item in store
*/
function clearStore() {
if (use_session_storage) {
sessionStorage.clear();
} else {
window['localStorageMimic'] = {};
}
}
if(isSessionStorageAvailable()){
use_session_storage = true;
} else {
// set a holding area for the session storage, session storage would store
// under a specific domain but as the new window is recreated from page to
// page this is unnecessary
window['localStorageMimic'] = {};
}
/**
Complex Tables initialisation function
*/
function complexTableToolInit(){
// get data from the page
var skv_data = $( '#first_load' ).data() || {};
var arr_required_props = [
'config',
'agreements',
'is_multimake',
'arr_required_filters',
'from_cash_or_car'
]
// check the skv_data object has all the
// required properties for complex tables
var has_all_complex_table_props = arr_required_props.reduce(
function( accumulator, currentValue ){
// both must be true to return true, therefore all values must be
// true for final reduction to be true
return accumulator && skv_data.hasOwnProperty( currentValue );
},
// initial accumulator value
true
);
// if we have all the data we need, set up the complex table
if( has_all_complex_table_props ){
complexTableSetup( skv_data );
}
}
/**
Complex tables setup functions, which are only called if the necessary data
is present
*/
function complexTableSetup( skv_data ){
var table_id = skv_data.table.table_id;
// if(
// window.hasOwnProperty( 'css_framework' ) &&
// window.css_framework === 'bones' &&
// !uiFramework.isSessionStorageAvailable()
// ){
// $('.title-text').text('Tool not loaded.');
// uiFramework.showSessionStorageWarning();
// return;
// }
// get the agreement type from the URL if there is one
var agreement_type = getAgreementType( skv_data );
// clear variables from the session if requested by a url param
clearSession( table_id );
// add any filters from the URL
addURLFilters( skv_data, table_id );
// if this tool / channel requires one of a array of filters, handle that
handleRequiredFilters( skv_data, table_id );
// if this tool has been linked to from cash or car, handle that
cashOrCarSetup( skv_data );
var skv_request = {
"action" : "first_load",
"payload" : "",
"skv_callbacks" : {}
};
// ajax call to load the table component
requestTable( skv_request );
}
/**
Clear the session storage if a url param is provided
*/
function clearSession( table_id ){
// array of keys which may potentially be in the session storage
// which should be removed
var arr_storage_keys = [
'arr_filters_',
'skv_applied_filters_',
'current_filter_tab_',
'skv_table_'
];
// only proceed if there are any URL params
if( !window.location.search ){
return false;
}
// get struct of URL params
var skv_url = getURLParams();
// if we passed in a URL param asking us to clear the session
if(
skv_url.hasOwnProperty( 'clear_session' ) &&
skv_url.clear_session
){
// loop over the keys in the session storage
arr_storage_keys.forEach( function( key ){
// and remove them
removeStoreItem( key + table_id );
});
}
// we might need things to be in the URL if the page reloads or we go "back"
// // If there are URL params, which we will have already used,
// // change the URL but don't reload the page
// var new_url = window.location.href.replace( /\?.*$/, '' )
// window.history.replaceState( {}, "", new_url );
}
/**
Get agreement type from data attributes
*/
function getAgreementType( skv_data ){
// default to blank
var agreement_type = '';
// try to get agreement type from the url
var path_first = window.location.pathname
.replace( /^\//, '' )
.replace( /\/$/, '' )
.split( '/' )[0];
var skv_agreements = skv_data.agreements;
// if the agreement type we got from the url is a key in the agreements obj
if( skv_agreements[ path_first ] ){
// use that value as the agreement type
agreement_type = path_first;
}
return agreement_type;
}
/**
URL filters are put in to the page by coldfusion if they are in a list of
acceptable fields. Here, we retrieve filters from the session, add filters
from the URL and update the session with the new filters
*/
function addURLFilters( skv_data, table_id ){
// resolvable values are taken from coldfusion and need to be ascii encoded
if( skv_data.table_config.hasOwnProperty( 'skv_resolvables' ) ){
for( var key in skv_data.table_config.skv_resolvables ){
var value = skv_data.table_config.skv_resolvables[key];
var encoded = asciiEncode( value );
skv_data.table_config.skv_resolvables[key] = encoded;
}
}
// get filters from the session
var arr_filters = JSON.parse( getStoreItem( 'arr_filters_' + table_id ) ) || [];
var skv_filters = JSON.parse( getStoreItem( 'skv_applied_filters_' + table_id ) ) || {};
var is_previous_URL_filters = false;
// create filters from URL params (they get put in the page by the CF)
for(var key in skv_data.config ){
var skv_item = skv_data.config[ key ];
// only try and do things if we have the right properties
if(
!(
skv_item.hasOwnProperty( 'type' ) &&
skv_item.hasOwnProperty( 'value' )
)
){
// ignore this if we don't have what we need
continue;
}
var str_match_type = skv_item['type'];
var str_value = escapeHtml( skv_item['value'] );
// "Yes" and "No" are stored as "|Yes|" and "|No|" to avoid a bug in coldfusion's
// serializeJSON function which would otherwise convert them to boolean true or false values.
// Note on Regex: three capture groups:
// 1. Find opening pipe - start of string, literal pipe "|", escaped by "\" as "|" generally means or
// 2. Find value - between 2 and 3 word characters
// 3. Find closing pipe - literal pipe, end of string
// Then, replace all of this with the second capture group ($2); just the value.
// Note that capture groups are 1-indexed.
str_value = str_value
.replace( /(^\|)(\w{2,3})(\|$)/, '$2' );
// build a filter config struct
var skv_this_filter_config = {
'title' : "",
'field_type' : "text",
'field' : key,
'location' : "complex",
'source' : "checkbox",
'filter_words' : "",
'value' : str_value,
'match_words' : "",
'match_type' : str_match_type,
'type' : "include",
'bool_include_nulls' : false,
'is_url_param' : true,
"is_required" : false
};
// handle range sliders if there is a "~" or "|" in the value field`
if( /\||~/g.test( str_value ) ){
str_filter_id = 'include~' + key + '~between~' + str_value.replace( '~' , '|' ) + '~range-slider~false';
skv_this_filter_config.source = 'range-slider';
skv_this_filter_config.match_type = 'between';
skv_this_filter_config.filter_words = str_value.replace( '~' , ' - ' );
// otherwise it's a checkbox
} else {
is_previous_URL_filters = true;
// nb 'false' on the end of this string is for 'bool_include_nulls'
var str_filter_id = 'include~' + key + '~' + str_match_type + '~' + str_value + '~checkbox~false';
}
// if this filter doesn't already exist in our array of filters
if( arr_filters.indexOf( str_filter_id ) === -1 ){
// add the filter to the filter array
arr_filters.push( str_filter_id );
// add the filter to the filter struct
skv_filters[ str_filter_id ] = skv_this_filter_config;
}
}
// set new array/struct of fiters in the session
setStoreItem( 'arr_filters_' + table_id, JSON.stringify( arr_filters ) );
setStoreItem( 'skv_applied_filters_' + table_id, JSON.stringify( skv_filters ) );
}
/*
If this channel has lst_required_filters for this tool in it's channel
overrides then a user is forced to apply a filter to at least one of the
listed fields before being allowed to proceed with the complex table tool
*/
function handleRequiredFilters( skv_data, table_id ){
/*
In the build tool we load an image for each model. On multimake channels
that can involve trying to load a large number of images at once which
can be slow. To avoid this, we ask users on multimake channels to select
a make or a bodystyle before proceeding.
However, if they have one of these filters, either in the url or in the
session, or they have come from the cash or car tool, then we don't make
them choose one.
*/
// helper fn in complex-request.js, will parse and return {} if null
var skv_filters = getFromSession( 'skv_applied_filters_' + table_id, true, {} );
// default to true to cover situations where a filter is not needed
var has_required_filter = true;
// if it's a multimake channel but we haven't been linked from cash or car
if(
skv_data.is_multimake &&
skv_data.arr_required_filters.length &&
!skv_data.from_cash_or_car
){
// at this point we default to false because we need a filter
has_required_filter = false;
// loop over struct of filters and check for presence of required filter
for( var key in skv_filters ){
var re_required_filter = '^' +
skv_data.arr_required_filters.join( '|' ) + '$';
var re = new RegExp( re_required_filter, 'g' );
// check if this filter is one of the required ones
if( re.test( skv_filters[ key ].field ) ){
has_required_filter = true;
}
}
}
// if we needed a required filter and we don't have one
if(
!has_required_filter &&
skv_data.hasOwnProperty( 'required_filter_redirect' )
){
// redirect to this page which allows the user to choose one
// before proceeding
// window.location.href = skv_data.required_filter_redirect;
}
}
/*
Determine if this user has been linked to this complex table tool from a
cash or car calculation, and if so, configure and open a modal which asks
them for some further information in order to set an appropriate monthly
budget
*/
function cashOrCarSetup( skv_data ){
var table_id = skv_data.table.table_id;
var skv_filters = getFromSession( 'skv_applied_filters_' + table_id, true, {} );
var has_price_filter = false;
// loop over the applied filters
for( var filter_id in skv_filters ){
// check if any of them are on the price field
var matches = filter_id.match( /^include~price/gi );
// if the test returns an non-null value and it has length
if(
matches &&
matches.length
){
// if they are, we have a price filter
has_price_filter = true;
}
}
if(
// if we already have a price filter we don't need the modal
!has_price_filter &&
// if this was linked from cash or car
skv_data.from_cash_or_car &&
// and we have access to the cash or car function
typeof window[ 'cashOrCarModal' ] === 'function'
){
// open the cash or car budget config modal
cashOrCarModal();
}
}
/*
Check if a varaible is numeric
taken from https://stackoverflow.com/a/1421988/4293734
*/
function isNumber( n ) {
return !isNaN( parseFloat( n ) ) && !isNaN( n - 0 )
}
var arr_dynamic_handler_fns = [
'attachPaginationHandlers',
'attachRowClickHandler',
'attachHeaderCellClickHandler',
'attachFilterPaneHandlers',
'attachRangeSliderHandlers',
'attachCheckboxHandlers',
'recordModalHandlers',
'attachInputAdjustmentHandlers',
'attachDescriptionsClickHandler',
'attachLinkSwitchHandlers',
'attachToggleableColumnsHandlers',
'attachTrailingRowsClickhandlers',
'attachOverflowHandler'
];
var arr_run_once_handler_fns = [
'attachFilteringClickHandler',
'attachCategoriesHandlers'
];
/**
loops over arrays of strings, and checks if they exist as functions, and if they do, calls them.
*/
function attachHandlers( arr_fns, str_table_id ){
var num_fns = arr_fns.length;
for( var i = 0; i < num_fns; i++ ){
if ( typeof window[ arr_fns[ i ] ] === "function" ) {
window[ arr_fns[ i ] ]( str_table_id );
}
}
}
function setupTables(){
window.arr_tables = [];
var $arr_tables = $( '.channel_table' );
var num_tables = $arr_tables.length;
$arr_tables.each(function(index, el) {
var skv_table_data = $( el ).data();
var table_id = skv_table_data.table_id;
var skv_toggle_functionality = skv_table_data.skv_toggle_functionality;
window.arr_tables.push( table_id );
getRequiredFunctionality( table_id, skv_toggle_functionality );
});
}
function getRequiredFunctionality( table_id, skv_toggle_functionality ){
var skv_toggles = {
"bool_select_row" : {
"function" : "attachRowClickHandler",
"array" : "arr_dynamic_handler_fns"
},
"bool_select_row_by_cell" : {
"function" : "attachRowClickByCellHandler",
"array" : "arr_dynamic_handler_fns"
},
"bool_paginate" : {
"function" : "attachPaginationHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_sort" : {
"function" : "attachHeaderCellClickHandler",
"array" : "arr_dynamic_handler_fns"
},
"bool_description" : {
"function" : "attachDescriptionsClickHandler",
"array" : "arr_dynamic_handler_fns"
},
"bool_filter" : {
"function" : "attachFilteringClickHandler",
"array" : "arr_run_once_handler_fns"
},
"bool_range_sliders" : {
"function" : "attachRangeSliderHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_categories" : {
"function" : "attachCategoriesHandlers",
"array" : "arr_run_once_handler_fns"
},
"bool_checkboxes" : {
"function" : "attachCheckboxHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_record_modals" : {
"function" : "recordModalHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_filter_pane" : {
"function" : "attachFilterPaneHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_input_adjustment" : {
"function" : "attachInputAdjustmentHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_linkswitch" : {
"function" : "attachLinkSwitchHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_toggleable_columns" : {
"function" : "attachToggleableColumnsHandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_trailing_rows" : {
"function" : "attachTrailingRowsClickhandlers",
"array" : "arr_dynamic_handler_fns"
},
"bool_allow_overflow" : {
"function" : "attachOverflowHandler",
"array" : "arr_dynamic_handler_fns"
}
};
window[ table_id ] = window[ table_id ] || {};
window[ table_id ].arr_dynamic_handler_fns = [];
window[ table_id ].arr_run_once_handler_fns = [];
for ( var key in skv_toggle_functionality ){
if(
skv_toggle_functionality[ key ] &&
skv_toggles[ key ]
){
window[ table_id ][ skv_toggles[ key ][ 'array' ] ].push( skv_toggles[ key ][ 'function' ] );
}
}
}
$(document).on('page_reload', function(){
if( window.bool_uiFramework ){
uiFramework.init();
}
var num_tables = window.arr_tables.length;
for( var i = 0; i < num_tables; i++ ){
attachHandlers( window[ window.arr_tables[ i ] ].arr_dynamic_handler_fns, window.arr_tables[ i ] );
hideTableLoadingSpinner( window.arr_tables[ i ] );
}
$(document).trigger( 'responsive-init' );
});
function table_js_init(){
if( typeof window.bool_uiFramework !== 'boolean' ){
window.bool_uiFramework = true;
}
if(window.css_framework === undefined) {
window.css_framework = $('html').data('css_framework');
}
// sessionStorage.setItem( 'arr_filters', JSON.stringify( [] ) );
setupTables();
var num_tables = window.arr_tables.length;
for( var i = 0; i < num_tables; i++ ){
attachHandlers( window[ window.arr_tables[ i ] ].arr_run_once_handler_fns, window.arr_tables[ i ] );
attachHandlers( window[ window.arr_tables[ i ] ].arr_dynamic_handler_fns, window.arr_tables[ i ] );
if ( typeof window[ 'setupSessionFilters' ] === "function" ) {
setupSessionFilters( window.arr_tables[ i ] );
}
}
$(document).trigger( 'table-init' );
}
$(document).ready(function($) {
table_js_init();
});
var entityMap = {
"&": "&",
"<": "<",
">": ">",
'"': '"',
"'": ''',
"/": '/',
"(": '(',
")": ')',
";": ';',
};
/**
escape the value and add a complex filter
*/
function filter( skv_config ){
var skv_local_config = skv_config;
skv_local_config.value = escapeHtml( skv_local_config.value );
addComplexFilter( skv_local_config );
}
/**
replace html special characters with escaped versions
*/
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/();]/g, function (s) {
return entityMap[s];
});
}
/**
builds a filter id from it's component parts and returns it as a string
*/
function buildFilterName( skv_config ){
var arr_filter = [
skv_config.type,
skv_config.field,
skv_config.match_type,
skv_config.value,
skv_config.source,
safelyAddBoolPartToFilterID( skv_config, 'bool_include_nulls' )
];
var filter_id = arr_filter.join( '~' );
var decoded_filter_id = $('').html(filter_id).text();
return decoded_filter_id;
}
/**
replace part of a filter if it isn't already defined
*/
function safelyAddBoolPartToFilterID( skv_filter, part_name ){
return ( typeof skv_filter[ part_name ] !== undefined && skv_filter[ part_name ] );
}
/**
returns a struct of filter parts which gets from a filter id
*/
function analyseFilterName( str_filter_id ){
arr_keys = [ 'str_type', 'str_field_label', 'str_match_type', 'str_value',
'str_filter_source', 'bool_include_nulls', 'is_required' ];
arr_props = str_filter_id.split( '~' );
skv_return = {};
arr_keys.forEach( function( key, index, arr_keys ){
skv_return[ key ] = arr_props[ index ];
});
return skv_return;
}
/**
unhide loading spinner
*/
function showTableLoadingSpinner( str_table_id ){
var $table = $( '#' + str_table_id );
$table.siblings( '.loading-indicator' ).removeClass('is-hidden');
// make table opaque
$table.addClass('table--finder-is-loading');
}
/**
rehide loading spinner
*/
function hideTableLoadingSpinner( str_table_id ){
$( '#' + str_table_id ).siblings( '.loading-indicator' ).addClass('is-hidden');
// remove table opacity
$table.removeClass('table--finder-is-loading');
}
/**
on page load/reload, replace table markup and before/after divs
*/
function replaceTableMarkup( str_table_id, skv_markup_config ){
var $complex_table = $( '.complex-table-tool' );
$( '.complex-tables-before, .complex-tables-after' ).remove();
// if we are replacing the entire table then do so with collated markup
if(skv_markup_config.tool.update) {
$complex_table.replaceWith( buildEntireTool(skv_markup_config) );
} else {
// for actions such as sort or paginate we don't need to change the entire
// table so replace certain elements
if(skv_markup_config.body.update) {
$complex_table.find('.table-wrapper').replaceWith(skv_markup_config.body.html);
}
if(skv_markup_config.filter_pane.update) {
$complex_table.find('.filter-pane-wrapper').replaceWith(skv_markup_config.filter_pane.html);
}
if(skv_markup_config.pagination.update) {
$complex_table.find('.pagination').replaceWith(skv_markup_config.pagination.html);
}
}
// rebuild filter tags
$complex_table.find('.applied-filter-container .field-filter-container').remove();
$complex_table.find('.applied-filter-container .clear-complex-filters').removeClass('is-waiting');
$complex_table.find('.applied-filter-container .clear-complex-filters').removeAttr("disabled");
buildAllFilterTags( str_table_id );
}
/**
* Combine all the elements of the tool/table together to create a new table jquery object
* @skv_markup_config data_type Description
* @return string Jquery object
*/
function buildEntireTool(skv_markup_config) {
// create a new complex table jquery object
var $el_tool = $(skv_markup_config.tool.html);
// findout where to insert markup
$el_first = $el_tool.find('.complex-tables-first');
// check to see if there is an element inserted within the tool html already
// if so insert the html afterwards
// otherwise insert it at the start of the tool div
if($el_first.length) {
$el_first.after(getCollatedMarkup(skv_markup_config));
} else {
$el_tool.prepend(getCollatedMarkup(skv_markup_config));
}
return $el_tool;
}
/**
* Combine elements of the tool/table together
* @skv_markup data_type Description
* @return string Full html
*/
function getCollatedMarkup(skv_markup_config) {
var str_full_html = '';
str_full_html += skv_markup_config.filter_pane.html;
str_full_html += skv_markup_config.body.html;
str_full_html += skv_markup_config.pagination.html;
return str_full_html;
}
/**
show the current tab in the filter pane - generally the last one which was
active before a page reload / markup replacement
*/
function showCurrentTab( str_table_id ){
// the markup for tabs has been rebuilt - so need to manually show the tab we were just on
var tab = getStoreItem( 'current_filter_tab_' + str_table_id );
$( '.filter-pane .filter-control-tabs .js-tab-button' )
.removeClass( 'is-selected' )
.parent().removeClass( 'active' ); //for bootstrap
$( '.filter-pane .vehicle_controls .filter-group' )
.hide();
$( '.filter-pane .vehicle_controls .' + tab + '-filter-group' )
.show();
$( '.filter-control-tabs .js-tab-button[data-tab=' + tab + ']' )
.addClass( 'is-selected' )
.parent().addClass('active');
}
/**
build html for data- attributes
*/
function printDataAttributes( skv_attributes ){
var str_html = '';
for ( var key in skv_attributes ){
str_html += ' data-' + key + '=' + JSON.stringify( skv_attributes[ key ] );
}
return str_html;
}
/**
prints attributes for an HTML element from an object
*/
function printAttr( skv_attributes ){
var html = '';
// loop over properties in parameter
for( var key in skv_attributes ){
// handle attributes like "disabled" and "selected"
if(
key.substring(0, 5) != 'data-' &&
( typeof( skv_attributes[ key ] ) == typeof( true ) )
){
html += ( skv_attributes[ key ] ? key + ' ' : '' );
// all other properties, including data- properties
} else {
html += key + '=' + JSON.stringify( skv_attributes[ key ] ) + ' ';
}
}
return html;
}
/**
build html for SVGs
*/
function printSVG( str_icon_class, str_wrapper_classes ){
var str_use = str_icon_class.replace( '-', '--' );
var str_html = '';
if( typeof str_wrapper_classes !== 'undefined' ){
return '
' + str_html + '
';
}
return str_html;
}
/**
* replaces non-db safe characters with ascii escaped sequences
* @param {string} string to ascii escape
*/
function asciiEncode( string ) {
// handle numbers
string = string.toString();
// to avoid double encoding, decode first
string = asciiDecode( string );
// get an array of dodgy characters
// NB hyphens should go at the beginning or end
var arr_replace = string.match( /[^-a-z0-9 _-~!#+;\/\.]/ig );
// characters that need to be escaped in regex
var arr_chars = [ '.', '[', ']', '(', ')', '/', '\\', '|' ];
// loop over characters to replace if matches returned
arr_replace && arr_replace.forEach( function( replace ){
// if the character to replace needs to be escaped in regex
var idx_replace = 0;
if( arr_chars.indexOf( replace ) != -1 ){
// escape it for regex
replace = '\\' + replace;
idx_replace = 1;
}
var re = new RegExp( replace , 'g');
// replace with ascii encoded character
string = string.replace( re, '' + replace.charCodeAt(idx_replace) + ';' );
});
return string;
}
/**
* replaces ascii escape sequences with their original characters
* @param {string} string string to decode ascii escape sequences
*/
function asciiDecode( string ){
// handle numbers
string = string.toString();
// get an array of ascii escape sequences
var arr_match = string.match( /(\d{1,3});/g );
// replace each with it's original character
arr_match && arr_match.forEach( function( find ){
var replace = String.fromCharCode( find.replace( /[^0-9]/g, '' ) );
string = string.replace( find, replace) ;
});
return string
}
/* ---------- ---------- ---------- ---------- ----------
Add/Remove Local (in page js) Filters
---------- ---------- ---------- ---------- ---------- */
function filterInPage( skv_config ){
var str_field_label = skv_config.skv_filter.str_field_label,
str_field_type = skv_config.skv_filter.str_field_type,
str_filter_type = skv_config.skv_filter.str_type
str_match_type = skv_config.skv_filter.str_match_type,
str_value = skv_config.skv_filter.str_value,
filter_id = buildFilterName( skv_config );
// loop over records
for( var i = 0; i < skv_config.num_records; i++ ){
var $this_row = $( '#' + skv_config.table_id + ' tr[data-row_id="' + (i + 1) + '"]' );
var skv_this_record = $this_row.data('record');
var arr_excluded_by = $this_row.data( 'arr_excluded_by' ) || [];
// Finding rows to hide:
// record match and type is exclude
// or no record match and type is include
var bool_filtered = localFilterRecord( skv_this_record, str_field_label, str_field_type, str_filter_type, str_match_type, str_value );
if( bool_filtered ){
skv_config.skv_filter.arr_excludes_records.push( i + 1 );
arr_excluded_by.push( window[ skv_config.table_id ].num_applied_filters );
$this_row.data( 'arr_excluded_by', arr_excluded_by );
// if this row isn't already hidden, hide it and adjust stats
if( !$this_row.hasClass( 'filter_hide' ) ){
$this_row.addClass( 'filter_hide' );
window[ skv_config.table_id ].num_records_shown--;
window[ skv_config.table_id ].num_records_hidden++;
}
}
}
// update onscreen stats
$( '.filter_wrapper[data-table_id="' + skv_config.table_id + '"] .num_records_shown' )
.text( window[ skv_config.table_id ].num_records_shown );
window[ skv_config.table_id ].skv_applied_filters[ filter_id ] = skv_config.skv_filter;
// don't build a filter tag for range slider filters, they don't need them.
if( !(skv_config.skv_filter.str_filter_source === 'range-slider') ){
buildFilterTag( skv_config );
}
}
function localFilterRecord( skv_record, str_field, str_field_type, str_filter_type, str_match_type, str_value ){
if( str_field === 'all' ){
var skv_this_record = skv_record;
var arr_delete_keys = [ 'record_index', 'arr_excluded_by', 'arr_missing_data', 'bool_filter_hide' ];
arr_delete_keys.forEach( function( this_key, key_index ){
delete skv_this_record[ this_key ];
});
var str_target = JSON.stringify( skv_this_record );
} else {
var str_target = skv_record[ str_field ];
}
var str_trimmed_filter_value = str_value.trim();
if( !isNaN( str_trimmed_filter_value ) ){
str_trimmed_filter_value = +str_trimmed_filter_value;
}
// NOTE that this fn returns a boolean for var bool_record_filtered_out
// therefore returning FALSE means the record is included in the returned set
// and TRUE means the record is filtered out
switch( str_field_type ){
case 'text':
var arr_filter_values = str_trimmed_filter_value.split( ',' );
var num_filter_values = arr_filter_values.length;
var num_matches = 0;
var re_filter_value;
// count matches for each fitler value
arr_filter_values.forEach( function( this_filter_value, filter_value_index ){
switch( str_match_type ){
case 'like':
re_filter_value = new RegExp( this_filter_value.trim(), 'i' );
break;
case 'exact':
re_filter_value = new RegExp( '^' + this_filter_value.trim() + '$', 'i' );
break;
}
num_matches += str_target.match( re_filter_value ) ? 1 : 0;
});
// for includes return true when there are matches
if( str_filter_type === 'include'
&& num_matches === 0 ){
return true;
}
// for excludes return true when there are no matches
if( str_filter_type === 'exclude'
&& num_matches > 0 ){
return true;
}
break;
case 'numeric':
case 'currency':
var numeric_match = false;
switch( str_match_type ){
case 'lt':
numeric_match = ( str_target < str_trimmed_filter_value ) ? true : false;
break;
case 'lte':
numeric_match = ( str_target <= str_trimmed_filter_value ) ? true : false;
break;
case 'eq':
numeric_match = ( str_target == str_trimmed_filter_value ) ? true : false;
break;
case 'gte':
numeric_match = ( str_target >= str_trimmed_filter_value ) ? true : false;
break;
case 'gt':
numeric_match = ( str_target > str_trimmed_filter_value ) ? true : false;
break;
case 'pm10':
case 'pm50':
case 'pm100':
case 'pm500':
var text_num = +str_match_type.substring( 2 );
numeric_match = ( ( Math.abs( str_target - str_trimmed_filter_value ) ) <= text_num ) ? true : false;
break;
}
// for includes return true with numeric_match
if( str_filter_type === 'include'
&& !numeric_match ){
return true;
}
// for excludes return true with no numeric_match
if( str_filter_type === 'exclude'
&& numeric_match ){
return true;
}
break;
}
return false;
}
function removeLocalFilter( table_id, filter_id, skv_config ){
arr_excludes_records = window[ table_id ].skv_applied_filters[ filter_id ].arr_excludes_records;
arr_excludes_records.forEach( function( this_record, array_index ){
$this_row = $( '#' + table_id + ' tr[data-row_id="' + this_record + '"]' );
var arr_excluded_by = $this_row.data('arr_excluded_by') || [];
// remove this filter from the list of filters which prevent this item from showing
arr_excluded_by.splice( arr_excluded_by.indexOf( filter_id ), 1);
$this_row.data( 'arr_excluded_by', arr_excluded_by );
// if no filters now prevent item from showing, show it.
if( arr_excluded_by.length === 0 ){
$this_row.removeClass( 'filter_hide' );
window[ table_id ].num_records_shown++;
window[ table_id ].num_records_hidden--;
}
});
cleanUpFilterStats( table_id, filter_id );
// add a remote filter if a config is specified
if( typeof skv_config !== 'undefined' ){
filter( skv_config );
}
}
/* ---------- ---------- ---------- ---------- ----------
Admin Helper Functions
---------- ---------- ---------- ---------- ---------- */
function sortNumeric(a,b) {
return a - b;
}
/* ---------- ---------- ---------- ---------- ----------
Click Handlers
---------- ---------- ---------- ---------- ---------- */
function attachFilteringClickHandler( str_table_id ){
// Table Filtering buttons click handler
$('.table_control.filter_btn[data-table_id=' + str_table_id + ']').on('click', function( event ){
var table_id = $(this).data('table_id');
var $filter = $('.table_control.filter_input[data-table_id="' + table_id + '"]');
var str_filter_value = $filter.val();
var str_filter_type = $(this).data('filter_type');
var $this_table = $( '#' + table_id );
var str_cache_key = $this_table.data('str_cache_key');
var num_pages = $this_table.data('num_pages')
var num_records = $this_table.data('num_records')
var skv_config = {
"table_id" : table_id,
"str_cache_key" : str_cache_key,
"num_records" : num_records,
"num_pages" : num_pages,
"skv_filter" : {
"str_value" : str_filter_value,
"str_type" : str_filter_type,
"str_match_type" : "like",
"re_filter" : "",
"arr_excludes_records" : [],
"str_location" : "local",
"str_filter_words" : ( str_filter_type === 'include') ? "Include" : "Exclude",
"str_match_words" : "contains",
"str_field_label" : "all",
"str_field_title" : "Any Field",
"str_field_type" : "text",
"str_filter_source" : "search-filter"
}
};
filter( skv_config );
$filter.val('');
});
}
var remote_url = '/page/component/table/remote.cfc';
var arr_filter_opts = [
{ "label" : "include", "title" : "Include", "words" : "Include" },
{ "label" : "exclude", "title" : "Exclude", "words" : "Exclude" }
];
var arr_text_opts = [
{ "label" : "like", "title" : "Like", "words" : "is like" },
{ "label" : "exact", "title" : "Exact", "words" : "is exactly" },
];
var arr_numeric_opts = [
{ "label" : "lt", "title" : "<", "words" : "is less than" },
{ "label" : "lte", "title" : "<=", "words" : "is less than or equal to" },
{ "label" : "eq", "title" : "=", "words" : "is equal to" },
{ "label" : "gte", "title" : ">=", "words" : "is greater than or equal to" },
{ "label" : "gt", "title" : ">", "words" : "is greater than" },
{ "label" : "pm10", "title" : "±10", "words" : "is within 10 of" },
{ "label" : "pm50", "title" : "±50", "words" : "is within 50 of" },
{ "label" : "pm100", "title" : "±100", "words" : "is within 100 of" },
{ "label" : "pm500", "title" : "±500", "words" : "is within 500 of" }
];
function initialiseFieldFilter(){
$ff_wrap = $( '.field_filter_wrapper' );
var skv_fields = $ff_wrap.data( 'skv_fields' );
str_html = '
'
+ '
'
+ '
'
+ '';
$ff_wrap.append( str_html );
}
initialiseFieldFilter();
$( '.field_select' ).change(function(event) {
var $ff_wrap = $( this ).parents( '.field_filter_wrapper' );
var table_id = $ff_wrap.data( 'table_id' );
var selected_label = $( this ).val();
// we've got a field now, make the button useable
$( this )
.parents( '.field_filter_wrapper' )
.find( '.field_select_control' )
.removeAttr( 'disabled' );
});
$( '.field_select_control' ).on('click', function(event) {
var $ff_wrap = $( this ).parents( '.field_filter_wrapper' );
var table_id = $ff_wrap.data( 'table_id' );
var $field_select = $( this ).parents( '.field_filter_wrapper' ).find( '.field_select' )
var selected_label = $field_select.val();
var selected_title = $field_select.find( ':selected' ).data( 'title' );
var selected_type = $field_select.find( ':selected' ).data( 'type' );
var action = $( this ).data( 'action' );
if( action === 'select' ){
$( this )
.data( 'action', 'remove' )
.text( 'Remove' )
.after( '' );
$( this ).siblings( '.field_select' ).attr( 'disabled', true );
var str_html = '';
switch( selected_type ){
case 'text':
str_html = '
';
for( var i = 0; i < num_opts; i ++ ){
str_html += '';
}
str_html += '
';
return str_html;
}
var remote_url = '/page/component/table/remote.cfc';
/* ---------- ---------- ---------- ---------- ----------
Add/Remove filters on Remote Tables
---------- ---------- ---------- ---------- ---------- */
function addRemoteFilter( skv_config ){
$.ajax({
url: remote_url,
type: 'GET',
dataType: 'json',
cache: false,
data: {
method : 'remoteFieldFilter',
jsn_skv_config : JSON.stringify( skv_config ),
returnFormat: 'json'
},
})
.done(function( data ) {
replaceTableElement( skv_config.table_id, data.data[0] );
$( '.pagination[data-table_id="' + skv_config.table_id + '"]' )
.replaceWith( data.data[1] );
displayPaginationControls( skv_config.table_id );
var $this_table = $( '#' + skv_config.table_id );
var num_valid_records = $this_table.data( 'num_valid_records' );
var num_records = $this_table.data( 'num_records' );
var filter_id = data.skv_filter.id;
window[ skv_config.table_id ].num_records_shown = num_valid_records;
window[ skv_config.table_id ].num_records_hidden = (num_records - num_valid_records);
if( typeof data.skv_table.skv_range_slider_config !== 'undefined' ){
updateRangeSliders( data.skv_table.skv_options.arr_header_fields );
}
$(document).trigger('page_reload');
// update onscreen stats
$( '.filter_wrapper[data-table_id="' + skv_config.table_id + '"] .num_records_shown' )
.text( num_valid_records );
window[ skv_config.table_id ].skv_applied_filters[ filter_id ] = data.skv_filter;
if( skv_config.skv_filter.str_filter_source !== 'range-slider' ){
buildFilterTag( skv_config );
}
});
}
function remoteRemoveFilter( str_cache_key, filter_id, table_id, skv_config ){
$.ajax({
url: remote_url,
type: 'GET',
dataType: 'json',
cache: false,
data: {
method : 'remoteRemoveFilter',
str_cache_key : str_cache_key,
filter_id : filter_id,
returnFormat: 'json'
},
})
.done(function( data ) {
replaceTableElement( table_id, data.arr_html[0] );
displayPaginationControls( table_id );
$( '.pagination[data-table_id="' + table_id + '"]' )
.replaceWith( data.arr_html[1] );
var $this_table = $( '#' + table_id );
window[ table_id ].num_records_shown = $this_table.data( 'num_valid_records' );
window[ table_id ].num_records_hidden = ( $this_table.data( 'num_records' ) - window[ table_id ].num_records_shown );
cleanUpFilterStats( table_id, filter_id );
if( typeof data.arr_header_fields !== 'undefined' ){
updateRangeSliders( data.arr_header_fields );
}
$(document).trigger('page_reload');
// add a remote filter if a config is specified
if( typeof skv_config !== 'undefined' ){
skv_config.str_cache_key = $( '#' + table_id ).data( 'str_cache_key' );
filter( skv_config );
}
});
}
/* ---------- ---------- ---------- ---------- ----------
Admin Helper Functions
---------- ---------- ---------- ---------- ---------- */
function displayPaginationControls( table_id ){
var $this_table = $( '#' + table_id );
var num_valid_records = $this_table.data( 'num_valid_records' );
var num_records_per_page = $this_table.data( 'num_records_per_page' );
var num_records = $this_table.data( 'num_records' );
var num_pages = Math.ceil( num_valid_records / num_records_per_page );
// hide pagination controls which link to pages which are now empty after filtering
$( '.btn.table_control.btn_pagination:not(.pagination_meta)' ).each(function( index, el ) {
var page_num = $(this).data( 'page_num' );
if( page_num <= num_pages ){
$(this)
.removeClass( 'filter_hide' )
.addClass( 'filter_show' );
}else{
$(this)
.removeClass( 'filter_show' )
.addClass( 'filter_hide' );
}
});
}
/*
Any table request should use this function.
We take an array of actions, amend the stored config accordingly
and then request the table and handle the response.
each action object in the array must contain 2 keys and may contain a third:
1. "action" which determines what action you want to take on the table
2. "payload" which provide the details of that action
3. (optional) "arr_callbacks" - array of fns fired on successful completion of
the request -g generally after handleresult()
For instance, you could filter and sort the table, by passing this array:
arr_actions = [
{
"action" : "add_filter",
"payload" : "include~model~exact~Edge~checkbox~false"
},
{
"action" : 'sort',
"payload" : {
"str_field" : 'model',
"bool_asc" : false
}
];
*/
function requestTable( arr_actions ){
handleAlerts();
var skv_data = $( '#first_load' ).data();
var skv_table = skv_data.table;
var table_id = skv_table.table_id;
var skv_config = ensureProperties( skv_data.table_config );
var sort_pane_visible = $( '.dropdown-sort-by .dropdown__content' ).is( ':visible' );
var skv_column_visibility = getFromSession( 'skv_column_visibility_' + table_id, true );
var filter_pane_visible = getFilterPaneVisible( skv_data, table_id );
// check to see if any override filters are included in the table setup
// (like with the finder bar) otherwise get filters from the session
if(
skv_config.filters_config.hasOwnProperty('arr_override_filters') &&
skv_config.filters_config.arr_override_filters.length
) {
skv_config.filters_config.arr_filters = skv_config.filters_config.arr_override_filters;
} else {
skv_config.filters_config.arr_filters = getFromSession( 'arr_filters_' + table_id, true, [] );
}
// default to false, overridden in particular circumstances
skv_config.selection_config = skv_config.selection_config || {};
skv_config.selection_config.select_all_checked = false;
setStoreItem( 'filter_pane_visible_' + table_id, filter_pane_visible );
setStoreItem( 'sort_pane_visible_' + table_id, sort_pane_visible );
if( skv_column_visibility !== null ){
skv_config[ 'skv_column_visibility' ] = skv_column_visibility;
}
// ensure callback object is setup
setupCallbackObject();
// complete table actions and return modified config
skv_config = completeActions( skv_config, table_id, arr_actions );
APICall( skv_config, skv_table );
}
/**
* ensure callback object exists on window and has required properties
* @return {void} nothing, sets object in Window
*/
function setupCallbackObject(){
// ensure object exists
Window.skv_ct_callbacks = {};
var arr_props = [ 'pre_request', 'post_request', 'post_markup' ];
// loop over props and ensure existence of arrays
for( var idx in arr_props ){
var prop = arr_props[ idx];
Window.skv_ct_callbacks[ prop ] = Window.skv_ct_callbacks[ prop ] || [];
}
}
/**
* get complex tables data for one page
* @arr_actions array of table actions as outlined above requestTable Function
* @return promise, resolved with table data if the api call
* was successful and rejected with a message if not
*/
function getTableData( actions ){
// ensure actions is an array, even if it's only one item
var arr_actions = actionsToArray( actions );
// get the first action
var skv_action = arr_actions.shift();
// ensure a callbacks array property
if( !skv_action.hasOwnProperty( 'arr_callbacks' ) ){
skv_action.arr_callbacks = [];
}
// callback passed to the table js in order to resolve/reject the promise
// the jsapi data is passed in to all callbacks by default,
// the resolve/reject arguments are bound below
var settlePromise = function( resolve, reject, jsapi ){
// if the table api call failed
if( !jsapi.success ){
// reject the promise with the message
reject( jsapi.msg );
}
// otherwise resolve the promise with table data
resolve( jsapi.data[0] );
}
return new Promise( function( resolve, reject ){
// bind the resolve and reject functions to the callback
skv_action.skv_callbacks.post_request.push( settlePromise.bind( null, resolve, reject ) );
// put action back at the front of the array
arr_actions.unshift( skv_action );
// make the table request with the array of actions
requestTable( arr_actions );
});
}
/**
clear any alerts from previous table actions
NB redirect messages from the url will persist through one table load
*/
function handleAlerts(){
$( '.alert:not( .persist )' ).remove();
$( '.alert' ).removeClass( 'persist' );
}
/**
Logic to determind if we're showing the filter pane
*/
function getFilterPaneVisible( skv_data, table_id ){
var is_mobile_view = getIsMobileView();
var initial_load = getFromSession( 'initial_load_' + table_id, true, true );
var filter_pane_visible = true;
// not all tools have thier own setup struct eg rates panel
if(
initial_load &&
skv_data.hasOwnProperty( 'tool_setup' )
){
// get channel setup struct with lowercase keys for consistency
var skv_tool_setup = JSON.parse(
JSON.stringify( skv_data.tool_setup ).toLowerCase()
);
var show_filter_pane_initial = skv_tool_setup.filters.show_filter_pane_initial;
filter_pane_visible = ( is_mobile_view
? show_filter_pane_initial.mobile
: show_filter_pane_initial.desktop
);
}
// a user is applying a filter, or the page is refreshed
if( !initial_load ){
// get filter_pane_visible from session
filter_pane_visible_session = getFromSession(
'filter_pane_visible_' + table_id, true
);
// and if it exists, set it
if( filter_pane_visible_session !== null ){
filter_pane_visible = filter_pane_visible_session;
}
}
return filter_pane_visible;
}
/**
Init the Ui Frame work if it exists on this channel and
return boolean for mobile view
*/
function getIsMobileView(){
var css_framework = $( 'html' ).data( 'css_framework' );
var mobile_breakpoints = ['mobile', 'gt-mobile', 'gt-phablet'];
var is_mobile_view = false;
// only try and use the uiFramework if we're on a channel which supports it
// and it's available to us
if(
css_framework === 'bones' &&
typeof uiFramework === 'object'
){
uiFramework.init();
is_mobile_view = uiFramework.checkBreakpoints( mobile_breakpoints );
}
return is_mobile_view;
}
/**
Helper function for getting things from Session Storage
*/
function getFromSession( item_name, return_parsed, if_null ){
var value = getStoreItem( item_name );
value = return_parsed ? JSON.parse( value ) : value;
// if value is null and we've got an if_null value, use that
return ( if_null && value === null ) ? if_null : value;
}
/**
our config struct should have these properties but it might not start
with them, so add them if they're missing
*/
function ensureProperties( skv_config ){
var arr_struct_params = [
'filters_config',
'data_config',
'selection_config',
'arr_actions'
];
arr_struct_params.forEach( function( this_param ){
if( !skv_config.hasOwnProperty( this_param ) ){
if(this_param == 'arr_actions') {
skv_config[ this_param ] = [];
} else {
skv_config[ this_param ] = {};
}
}
});
return skv_config;
}
/**
wrap valid single action objects in an array if they weren't already
*/
function actionsToArray( actions ){
if(
!Array.isArray( actions ) &&
actions !== null &&
typeof actions === 'object' &&
actions.hasOwnProperty( 'action' ) &&
actions.hasOwnProperty( 'payload' )
){
actions = [ actions ];
}
return actions;
}
/**
Complete an array of actions and return an array of callbacks
All action functions are stored in complex-actions.js
*/
function completeActions( skv_config, table_id, arr_actions ){
var arr_callbacks = [];
var arr_actions = actionsToArray( arr_actions );
// object literal in place of switch case
var skv_all_actions = {
"sort" : sort_fn,
"paginate" : paginate_fn,
"add_filter" : add_filter_fn,
"remove_filter" : remove_filter_fn,
"clear_filters" : clear_filters_fn,
"selection" : selection_fn,
"first_load" : first_load_fn,
"toggle-column" : toggle_column_fn
};
// loop over our array of actions and apply each
arr_actions.forEach( function( skv_this_action ){
// unrecognised action? nothing to do here
if( !skv_all_actions.hasOwnProperty( skv_this_action.action ) ){
return false;
}
// check if any callbacks are supplied
if(
skv_this_action.hasOwnProperty( 'skv_callbacks' )
){
addCallbackObject( skv_this_action.skv_callbacks )
}
// object literal lookup of function, call it with given args
// all actions are stored in complex-actions.js
skv_config = ( skv_all_actions[ skv_this_action.action ] )(
skv_config,
skv_this_action,
table_id
);
});
// pass the unadultarated arr actions so we know what the user just did
skv_config.arr_actions = arr_actions;
return skv_config;
}
/**
* add a struct of arrays of callbacks to the callback struct on the Window
* @param {Object} skv_callbacks object with arrays of callbacks
* @return {void} nothing, calls another function
*/
function addCallbackObject( skv_callbacks ){
// loop over types of callback
for( var key in skv_callbacks ){
// if this is a type we are expecting
if( Window.skv_ct_callbacks.hasOwnProperty( key ) ){
// add this array of callbacks to the Window
addCallbackArray( key, skv_callbacks[ key ] );
}
}
}
/**
* add an array of callbacks to the callback struct on the Window
* @param {Object} skv_callbacks object with arrays of callbacks
* @return {void} nothing, adds to Window object
*/
function addCallbackArray( type, arr_callbacks ){
// loop over fns in array
for( var idx in arr_callbacks ){
var fn = arr_callbacks[idx];
// check this is a function
if( typeof fn === 'function' ){
// add this fn to this array of callbacks
Window.skv_ct_callbacks[ type ].push( fn );
}
}
}
/**
Make an ajax call to get a complex tables, handle the result
and any callbacks specified
*/
function APICall( skv_config, skv_table ){
var skv_ajax_data = {
"method" : 'getTable',
"returnFormat" : 'json',
"skv_config" : JSON.stringify( skv_config ),
"skv_table" : JSON.stringify( skv_table )
}
// if there's a table ajax call that hasn't returned yet,
// ignore it when it does, as a new one is about to be sent
if( window.obj_table_ajax ){
window.obj_table_ajax.abort();
}
lockControlsForAjax();
window.obj_table_ajax = $.ajax({
"url" : '/page/component/newtable/profile.cfc',
"type" : 'GET',
"dataType" : 'json',
"cache" : false,
"data" : skv_ajax_data
})
.done( triggerCallbacks.bind(null, 'post_request'), handleResult )
.fail( handleFail );
}
/**
* Complex tables callbacks are stored in a struct on the window object
* Ensure the existence of the right array, and then fire them all
* @param type {string} type of callback array to run
* @param jsapi {Object} data returned from complex tables API request
*/
function triggerCallbacks( type, jsapi ){
// if we don't have this array of callbacks, don't try to run them
if(
!Window.hasOwnProperty( 'skv_ct_callbacks' ) ||
!Window.skv_ct_callbacks.hasOwnProperty( type ) ||
!Array.isArray( Window.skv_ct_callbacks[ type ] )
){
return;
}
// loop over array of callbacks
for( var idx in Window.skv_ct_callbacks[ type ] ){
var fn = Window.skv_ct_callbacks[ type ][ idx ];
// check each is a function
if( typeof fn === 'function' ){
// run it, passing in response from tables API request
fn.call( null, jsapi );
}
}
}
/**
These are all actions that can be taken on a complex table.
They are used by complex-request.js.
*/
/**
Sort config
*/
function sort_fn( skv_config, skv_action, table_id ){
skv_config.data_config.skv_sort = skv_action.payload;
return skv_config;
}
/**
Paginate config
*/
function paginate_fn( skv_config, skv_action, table_id ){
skv_config.data_config.skv_pagination = skv_action.payload;
return skv_config;
}
/**
Add Filter config
*/
function add_filter_fn( skv_config, skv_action, table_id ){
// filter_ids have dodgy characters encoded
var filter_id = asciiEncode( skv_action.payload );
var field = filter_id.split( '~' )[1];
var source = filter_id.split( '~' )[4];
// if we're dealing with a range,
if( source === 'range-slider' ){
var arr_filters = skv_config.filters_config.arr_filters;
// get rid of any filters on the same field
arr_filters = arr_filters.filter( function( this_filter_id ){
return field != this_filter_id.split( '~' )[1];
});
skv_config.filters_config.arr_filters = arr_filters;
}
skv_config.filters_config.arr_filters.push( filter_id );
// if we have a select all check box in the column heading,
// this handles the select_all_checked logic
arr_current_selection = JSON.parse(
getStoreItem( 'arr_current_selection_' + table_id )
) || [];
var arr_len = arr_current_selection.length;
if(
arr_len &&
arr_current_selection[ arr_len - 1 ].hasOwnProperty( 'select_all_checked' ) &&
arr_current_selection[ arr_len - 1 ].select_all_checked
){
skv_config.selection_config.select_all_checked = true;
}
return skv_config;
}
/**
Remove Filter config
*/
function remove_filter_fn( skv_config, skv_action, table_id ){
var arr_filters = skv_config.filters_config.arr_filters;
// encode filter_id to locate in array
var array_pos = arr_filters.indexOf( asciiEncode( skv_action.payload ) );
if( array_pos >= 0 ){
skv_config.filters_config.arr_filters.splice( array_pos, 1 );
}
return skv_config;
}
/**
Clear Filters config - nb, we don't want to remove required or hidden filters
*/
function clear_filters_fn( skv_config, skv_action, table_id ){
var skv_applied_filters = getFromSession(
'skv_applied_filters_' + table_id,
true,
{}
);
// we only want to remove 'normal' filters, anything else stays applied
arr_remaining_filters = skv_config.filters_config.arr_filters.filter( function( filter_id ){
// filter ids with dodgy characters are ascii encoded
var encoded_filter_id = asciiEncode( filter_id )
return skv_applied_filters[ encoded_filter_id ].filter_type !== 'normal';
});
skv_config.filters_config.arr_filters = arr_remaining_filters
return skv_config;
}
/**
Row Selection config
*/
function selection_fn( skv_config, skv_action, table_id ){
// get the array of selection actions
arr_current_selection = JSON.parse(
getStoreItem( 'arr_current_selection_' + table_id )
) || [];
var skv_data = $( '#first_load' ).data();
var skv_table_config = skv_data.table_config;
var bool_select_multiple_records = skv_table_config.selection_config.multi_select;
var unique_field = skv_table_config.selection_config.unique_field;
skv_config.selection_config.unique_field = unique_field;
skv_config.selection_config.select_all_checked = skv_action.payload.select_all_checked;
if( bool_select_multiple_records ){
// if we're selecting with a query, get the filters that build it
if( skv_action.payload.qry_select ){
skv_action.payload.arr_filters = JSON.parse(
getStoreItem( 'arr_filters_' + table_id )
) || [];
}
// push this action to the array of selection actions
arr_current_selection.push( skv_action.payload );
} else {
arr_current_selection = [ skv_action.payload ];
}
// and update it in the session
setStoreItem(
'arr_current_selection_' + table_id,
JSON.stringify( arr_current_selection )
);
skv_config.selection_config.arr_selection = arr_current_selection;
return skv_config;
}
/**
First Load config
*/
function first_load_fn( skv_config, skv_action, table_id ){
// no config to change, just get a table with the stored config
return skv_config;
}
/**
Toggle visibility of a column in the table
*/
function toggle_column_fn( skv_config, skv_action, table_id ){
return skv_config;
}
/**
* Process the result of the successful complex tables API request
* @param {Object} jsapi object returned from complex tables API request
* @param {string} status argument from jQuery event handler (not used, but passed by jQ anyway)
* @param {Object} jQXhr jQuery object representing API request
* @return {void} nothing, calls other functions
*/
function handleResult( jsapi, status, jQXhr ){
var skv_data = $( '#first_load' ).data();
var table_id = skv_data.table.table_id;
var str_filter_pane_type = 'default';
// if the ajax was successful but the was an error getting the table,
// display an error to the user
if(
!jsapi.hasOwnProperty( 'success' ) ||
!jsapi.success
){
handleFail( jQXhr );
return false;
}
// get rid of the "loading results..." text
$( '#first_load' ).hide();
var skv_request_data = jsapi.data[0];
handleWarnings( skv_request_data );
setSessionData( skv_request_data, table_id );
var skv_markup_cfg = getMarkupConfig(skv_request_data);
// fn is in filter-common.js - this also triggers buildAllFilterTags()
replaceTableMarkup( table_id, skv_markup_cfg);
// get the filter pane type
if(
skv_data.table.hasOwnProperty('skv_options') &&
skv_data.table.skv_options.hasOwnProperty('filter_panel_cfg_override')
) {
str_filter_pane_type = skv_data.table.skv_options.filter_panel_cfg_override[0].type;
}
// check to see if we're on a finder link/bar table component, if we are
// then we don't need to do any tab handling
if(str_filter_pane_type != 'finder-link') {
handleCurrentTab( table_id );
}
handleFilterPane( table_id );
handleSortpane( table_id, skv_markup_cfg );
table_js_init();
runOptionalFunctions( skv_request_data );
addFiltersToLinks( skv_data );
// fn is in complex-request.js
triggerCallbacks( 'post_markup', jsapi );
$( document ).trigger( 'responsive-init' );
}
/**
* Show any warnings returned by the API
* @param {Object} skv_request_data data returned from table API request
* @return {void} nothing, calls another function
*/
function handleWarnings( skv_request_data ){
// handle any warnings passed back to the frontend
if(
skv_request_data.hasOwnProperty( 'arr_warnings' ) &&
skv_request_data.arr_warnings.length
){
displayAlert( 'warning', skv_request_data.arr_warnings );
}
}
/**
* Update the session data after a successful API call
* @param {Object} skv_request_data data returned from table API request
* @param {string} table_id id of complex table element
* @return {void} nothing, modifies data in session storage
*/
function setSessionData( skv_request_data, table_id ){
var arr_header_fields = skv_request_data.skv_table.skv_options.arr_header_fields;
var skv_session_data = {
"skv_table" : skv_request_data.skv_table,
"skv_config" : skv_request_data.skv_config,
"arr_filters" : skv_request_data.skv_config.filters_config.arr_filters,
"arr_selected_records" : skv_request_data.skv_selection.arr_selected_records,
"arr_current_selection" : skv_request_data.skv_selection.arr_selection || [],
"initial_load" : false,
"skv_column_visibility" : getColumnVisibility( arr_header_fields, table_id )
}
// if we are including the filter pane it means that filters have changed
if(skv_request_data.skv_config.include_filter_pane) {
skv_session_data.skv_applied_filters = skv_request_data.skv_filters;
}
// store this data in the session
for( var key in skv_session_data ){
setStoreItem(
key + '_' + table_id,
JSON.stringify( skv_session_data[ key ] )
);
}
}
/**
* Determine if sort pane should be shown after an API call and setup/show
* @param {string} table_id id of complex table element
* @param {Object} skv_request_data data returned from table API request
* @return {void} nothing, modifies element attributes
*/
function handleSortpane( table_id, skv_markup_cfg ){
// set sort pane visibility to state before ajax
var sort_pane_visible = getFromSession( 'sort_pane_visible_' + table_id, true );
if( skv_markup_cfg.header.update ){
// this is in sort.js
setupSortDropdown( skv_markup_cfg.header.html );
}
// hidden by default, only need to show if it was shown before ajax
if( sort_pane_visible ){
$( '.dropdown-sort-by .dropdown__content' ).show();
}
}
/**
* show or hide the filer pane as appropriate
* @param {string} table_id id of complex table element
* @return {void} nothing, modifies element attributes
*/
function handleFilterPane( table_id ){
var filter_pane_visible = getFromSession( 'filter_pane_visible_' + table_id, true );
// we use different classes to hide things depending on the repo/framework
var css_framework = $( 'html' ).data( 'css_framework' );
var hidden_class = css_framework === 'bones' ? 'is-hidden' : 'hidden';
// create a new regexp object as the /.../ form can't be dynamic
var class_regexp = new RegExp( '\\b' + hidden_class + '.*\\b', 'g' );
// if we've got a filter pane and it's visible
if(
$( '.filter-pane' ).length &&
filter_pane_visible
){
// we have to handle responsive hidden classes like is-hidden-md-down etc
$( '.filter-pane' )[0].className = $( '.filter-pane' )[0]
.className.replace( class_regexp, '' );
} else {
$( '.filter-pane' ).addClass( 'is-hidden' );
}
}
/**
* get, set and show the current filter tab
* @param {string} table_id id of complex table element
* @return {void} nothing, modifies element attributes
*/
function handleCurrentTab( table_id ){
// get the current tab
var tab = getStoreItem( 'current_filter_tab_' + table_id );
// if we don't have a current tab
if( tab == null ){
// get the first tab and store that
var tab = $( '.filter-pane .filter-group' )
.attr( 'data-filter-group' );
setStoreItem( 'current_filter_tab_' + table_id, tab );
}
// show that tab (fn in /table/js/filter-common.js)
showCurrentTab( table_id );
};
/**
* These functions may exist in some circumstances and should only
* be run if they do in fact exist // REPLACE WITH CALLBACKS
* @param {Object} skv_request_data data returned from table API request
* @return {void} nothing, triggers callbacks
*/
function runOptionalFunctions( skv_request_data ){
// struct of functions with array of arguments
skv_fns = {
'recordModalHandlers' : [ skv_request_data.skv_table.skv_options ],
'onPageLoaded' : []
};
// loop over functions
for( var str_fn in skv_fns ){
// if the function exists, call with array of arguments
if( typeof window[ str_fn ] === 'function' ){
window[ str_fn ].apply( this, skv_fns[ str_fn ] );
}
}
}
/**
* This is for the build model page where filters need to be passed on
* in to subsequent build pages in the URL
* @param {Object} skv_data complex tables setup data from the DOM
* @return {void} nothing, modifies certain link hrefs
*/
function addFiltersToLinks( skv_data ){
// we need to pass filters on to any further build page tools
var jsn_arr_filters = getFromSession(
'arr_filters_' + skv_data.table.table_id,
false,
"[]"
);
// so grab links
$( '.data_cell a' ).each( function(){
var href = $( this ).prop( 'href' );
// if they are to build pages
if( href.match( /\/(electric-build|build)\// ) ){
// build new href
var new_href = href + '&arr_filters=' + encodeURI( jsn_arr_filters );
var model_tool_id = -1;
// needed to decode for evans halshaw who had a encoding from a facebook
// link
var curr_url = decodeURIComponent(window.location.href);
// work out where to get model tool id from. if we're on the model page
// get it directly from the tool otherwise its attached to the table config
if(curr_url.match( /\/(electric-build|build)\/model/ )) {
model_tool_id = skv_data.tool.id;
} else {
model_tool_id = skv_data.table_config.meta.model_tool_id;
}
// we need to know the tool id of the model page to get it's
// complex tables profile and then build filters
// if the model_tool_id is -1 we lookup the model tool id on that
// build page
if( model_tool_id != -1 ) {
new_href += '&model_tool_id=' + model_tool_id;
}
if(
skv_data.hasOwnProperty( 'from_cash_or_car' ) &&
skv_data.from_cash_or_car
){
new_href += '&from_cash_or_car=true';
}
// add them to href
$( this ).prop( 'href', new_href );
}
});
}
/**
* Wrapper function for displaying errors
* @param {Object} jQXhr jQuery object representing complex tables API request
* @return {void} nothing
*/
function handleFail( jQXhr ){
clearStore();
// don't show a warning to the user if we aborted a table ajax call -
// another one will be in progress. Nothing is broken.
if( jQXhr.statusText === 'abort' ){
return false;
}
var default_msg = 'An issue has occurred while loading this ' +
'tool. Try reloading the page or visit again later.';
displayAlert( 'error', [ { "msg" : default_msg } ] );
}
/**
* Display an alert to a frontend user
* @param {string} alert_type class of alert
* @param {Object[]} arr_errors array of structs of errors
* @return {void} nothing, displays alert on page
*/
function displayAlert( alert_type, arr_errors ){
// hide "Loading..."
$( '#first_load' ).empty();
var str_class, str_title;
switch( alert_type ){
case 'warning' :
str_class = 'warning';
str_title = 'Alert';
break;
case 'error' :
str_class = 'error';
str_title = 'Something has gone wrong';
break;
}
// build alert html
var str_html_alert = '
';
$( '.complex-table-tool' ).replaceWith( str_html_alert );
return false;
}
$('#first_load').before( str_html_alert );
// $( 'h1' ).after( str_html_alert );
}
/**
* Description
* @skv_markup struct Pass in html
* @return formatted html struct
*/
function getMarkupConfig(skv_data) {
var skv_markup = skv_data.skv_markup;
var skv_functionality = skv_data.skv_table.skv_options.skv_toggle_functionality;
var skv_return = {
'body': {
'update': true,
'html': skv_markup.str_body_html
},
'filter_pane': {
'update': skv_markup.str_html_filter_pane.length ? true : false,
'html': skv_markup.str_html_filter_pane
},
'pagination': {
'update': skv_markup.str_pagination_html.length ? true : false,
'html': skv_markup.str_pagination_html
},
'header': {
'update': skv_markup.str_table_header_html.length ? true : false,
'html': skv_markup.str_table_header_html
},
'tool': {}
};
// if we don't have a filter pane always update the entire tool
if(skv_functionality.bool_filter_pane) {
skv_return.tool.update = (skv_return.filter_pane.update ) ?
true :
false;
} else {
skv_return.tool.update = true;
}
skv_return.tool.html = skv_markup.str_tool_html;
return skv_return;
}
/**
These functions are by a complex table tool when linked to from a cash or
car calculation
*/
/**
Setup a modal for users who have arrived at this tool from a cash or car
calculation
The modal takes the total amount of money the cash or car calculation has
determined the user has available and attempts to convert it in to a useful
monthly budget figure, by asking them to supply annual insurance premiums
and any amount of that money they wish to save
*/
function cashOrCarModal(){
var skv_local_config = getDefaultCalculationValues();
var skv_monthly_budget = calculateMonthlyBudget( skv_local_config );
var arr_included = [ 'expenses' ];
// build html content of the modal: opening paragraph
var html_content = '
You just calculated that £' + skv_local_config.net_cash_available +
' was available to spend over ' + skv_local_config.months + ' months.' +
' Now allow for other costs before viewing the cars available within your budget.
';
html_content += '
';
// if we don't have a figure for insurance, ask for one
if( skv_local_config.insurance_per_month === 0 ){
// add this to the start of the array of included sections
arr_included.splice( 1, 0, 'insurance' );
// add input for insurance
html_content += printNumericInput(
'insurance_per_month',
'Insurance',
'Estimate the insurance premium for your vehicle',
'£',
'per month'
);
}
// add input for any savings
html_content += printNumericInput(
'other_expenses_per_month',
'Other Expenses',
'If you need to hold back some cash for other transport expenses, enter that amount here',
'£',
'per month'
);
html_content += '
';
html_content += '
After ' + arr_included.join( ' and ' ) + ' that leaves' +
' an average amount of £' +
skv_monthly_budget.average + ' to spend per month.
';
html_content += '
However, as there is an initial payment of ' +
skv_local_config.deposit + ' monthly rentals, the available cash ' +
'after deductions is actually divided by ( ' + skv_local_config.deposit +
' + ' + ( skv_local_config.months - 1 ) + ' ). Therefore you need to ' +
' be looking at quoted rentals of £' +
'' + skv_monthly_budget.adjusted +
'.
';
// add primary CTA go button
html_content += '
' +
'' +
'
' ;
// set this html as content in the modal
setContent( html_content );
setTitle( 'Set Monthly Budget' );
// open the modal window
var cash_or_car_modal = $( '#framework_modal' ).remodal();
cash_or_car_modal.open();
// remove closed fn
$(document).off( 'closed', '.remodal' );
// on close clear content
$(document).on('closed', '.remodal', function( event ){
setTitle( 'Loading...' );
setContent( '' );
});
}
/**
Builds a numeric input element group witha label, prepends and appends
*/
function printNumericInput( name, title, subtext, prepend, append ){
var str_html = '' +
'
' +
'
' +
'' + prepend + '' +
'
' +
'' +
'
' +
'' + append + '' +
'
' +
'
';
return str_html;
}
/**
Set modal title
*/
function setTitle( str_title ){
$( '.framework-modal .modal__heading' ).text( str_title );
}
/**
Empty the modal content and set new content
*/
function setContent( str_html ){
var $framework_modal = $( '.framework-modal');
$framework_modal.addClass('modal-monthly-budget');
var $modal_content = $framework_modal.find('.modal__content');
$modal_content
.empty()
.append( str_html );
// add handlers to elements which were added to the modal
addEventHandlers();
}
/**
Attach interaction handlers to elements inside the modal
*/
function addEventHandlers(){
$( '#other_expenses_per_month, #insurance_per_month' )
.on( 'change', inputChangeHandler );
$( '#set_budget' )
.on( 'click', setBudgetHandler );
$( '.framework-modal .btn-close' )
.on( 'click', closeBudgetModal );
}
/**
When inputs are changed, pass thier values to a function to recalculate
the monthly budget
*/
function inputChangeHandler( event ){
// build an item to pass
var skv_config = {};
arr_inputs = [
'other_expenses_per_month',
'insurance_per_month'
];
// loop over array of inputs
arr_inputs.forEach( function( input ){
// get value from inputs
var value = $( '#' + input ).val();
// put thier values in the struct, ensuring that they are numeric
// isNumber() is in page.component.table.js.tool-setup.js
value = ( isNumber( value ) ? parseFloat( value ) : 0 );
// if we have a value, add it to the struct
if( value !== 0 ){
skv_config[ input ] = value;
}
});
// recalculate the budget using this new value
var skv_monthly_budget = calculateMonthlyBudget( skv_config );
// update the ui with the new figure
updateBudgets( skv_monthly_budget );
}
/**
Get default values from the page which are passed in from the URL
*/
function getDefaultCalculationValues(){
// get default data from the page
var skv_data = $( '#first_load' ).data();
var skv_cash_or_car = skv_data.cash_or_car;
var months = parseFloat(skv_data.config.months.value);
var deposit = parseFloat(skv_data.config.deposit.value);
var total_budget = parseFloat(skv_cash_or_car.net_cash_available);
var monthly_budget = total_budget / months;
var insurance_per_month = 0
// check if an insurance figure was passed in and set it if so
if(
skv_cash_or_car.hasOwnProperty( 'insurance_pa' ) &&
parseFloat( skv_cash_or_car.insurance_pa + 0 ) > 0
){
insurance_per_month = skv_cash_or_car.insurance_pa;
}
// default values
var skv_local_config = {
"net_cash_available" : skv_cash_or_car.net_cash_available,
"insurance_per_month" : insurance_per_month,
"other_expenses_per_month" : 0,
"months" : months,
"deposit" : deposit,
"average_monthly_budget" : ( skv_cash_or_car.net_cash_available / months )
}
return skv_local_config;
}
/**
Caclulate a more accurate monthly budget
skv_config should include at least one of the following keys:
skv_config = {
"net_cash_available" : 5000,
"insurance_per_month" : 100,
"other_expenses_per_month" : 250,
"months" : 36,
"deposit" : 6
}
*/
function calculateMonthlyBudget( skv_config ){
// get the default config values from the page
var skv_local_config = getDefaultCalculationValues();
// loop over properties in the local config
for( var key in skv_local_config ){
// if the passed in config contains the key
if( skv_config.hasOwnProperty( key ) ){
// overwrite this property on the local config
skv_local_config[ key ] = parseFloat( skv_config[ key ] );
}
}
// calculate the total spent on insurance
var insurance_total = skv_local_config.insurance_per_month *
( skv_local_config.months );
var other_expenses_total = skv_local_config.other_expenses_per_month *
( skv_local_config.months );
// calculate the total available for the new vehicle
var adjusted_cash_available = skv_local_config.net_cash_available -
( insurance_total + other_expenses_total )
// divide the total available by the number of months
var average_monthly_budget = adjusted_cash_available / skv_local_config.months;
// divide the total available by the number of payments
var adjusted_monthly_budget = adjusted_cash_available /
( ( skv_local_config.months - 1 ) + skv_local_config.deposit );
return {
"average" : average_monthly_budget.toFixed(2),
"adjusted" : adjusted_monthly_budget.toFixed(2)
};
}
/**
Update the monthly budget figure in the UI
*/
function updateBudgets( skv_monthly_budget ){
$( '.average_monthly_budget' )
.empty()
.append( skv_monthly_budget.average );
$( '#set_budget .monthly_budget, .adjusted_monthly_budget' )
.empty()
.append( skv_monthly_budget.adjusted );
$( '#set_budget' ).data( 'monthly_budget', skv_monthly_budget.adjusted );
}
/**
set the monthly budget as a filter on the complex table and then
clear and close the framework modal
*/
function setBudgetHandler(){
var monthly_budget = $( '#set_budget' ).data( 'monthly_budget' );
monthly_budget = Math.ceil( monthly_budget );
var price_range = ( monthly_budget - 20 ) + '|' + monthly_budget;
requestTable({
"action" : 'add_filter',
"payload" : 'include~price~between~' + price_range + '~range-slider~false'
});
setTitle( '' );
setContent( 'Loading...' );
// close the modal window
var cash_or_car_modal = $( '#framework_modal' ).remodal();
cash_or_car_modal.close();
}
//ensure monthly budget class is removed
function closeBudgetModal() {
var $framework_modal = $( '.framework-modal');
$framework_modal.removeClass('modal-monthly-budget');
}
/* ---------- ---------- ---------- ---------- ----------
Add/Remove various filters on Complex Tables
---------- ---------- ---------- ---------- ---------- */
function addComplexFilter( skv_config ){
// one function for table requests
requestTable([
{
"action" : "paginate",
"payload" : {
"num_page" : 1
}
},
{
"action" : "add_filter",
"payload" : skv_config.id
}
]);
}
function removeComplexFilter( str_cache_key, str_filter_id, table_id ){
// one function for table requests
requestTable(
[
{
"action" : "paginate",
"payload" : {
"num_page" : 1
}
},
{
"action" : "remove_filter",
"payload" : str_filter_id
}
]
);
}
function clearFieldFilters( str_label, str_cache_key, table_id ){
// get filters
var arr_filters = JSON.parse( getStoreItem( 'arr_filters_' + table_id ) );
// filter to filters on this field
arr_field_filters = arr_filters.filter( function( filter_id ){
return filter_id.split( '~' )[1] === str_label;
});
var arr_actions = [];
// build array of actions to remove filters on this field
arr_field_filters.forEach( function( str_filter_id ){
arr_actions.push({
"action" : "remove_filter",
"payload" : str_filter_id
});
});
// go back to first page
arr_actions.push(
{
"action" : "paginate",
"payload" : {
"num_page" : 1
}
}
);
// remove array of filters
requestTable( arr_actions );
}
/* ---------- ---------- ---------- ---------- ----------
Session Storage Functions
---------- ---------- ---------- ---------- ---------- */
function addToSession( str_item, type, value, key ){
if( getStoreItem( str_item ) === null ){
switch( type ){
case 'array':
var arr_new = [ value ];
setStoreItem( str_item, JSON.stringify( arr_new ) );
break;
case 'object':
var skv_new = {};
skv_new[ key ] = value;
setStoreItem( str_item, JSON.stringify( skv_new ) );
break;
case 'string':
setStoreItem( str_item, value );
break;
}
} else {
switch( type ){
case 'array':
arr_item = JSON.parse( getStoreItem( str_item ) );
arr_item.push( value );
setStoreItem( str_item, JSON.stringify( arr_item ) );
break;
case 'object':
skv_item = JSON.parse( getStoreItem( str_item ) );
skv_item[ key ] = value;
setStoreItem( str_item, JSON.stringify( skv_item ) );
break;
case 'string':
setStoreItem( str_item, value );
break;
}
}
}
function removeFromSession( str_item, type, value, key ){
if( getStoreItem( str_item ) === null ){
// item doesn't exist so you can't delete anything form it
return false;
} else {
switch( type ){
case 'array':
arr_item = JSON.parse( getStoreItem( str_item ) );
arr_item.splice( arr_item.indexOf( value ), 1 );
setStoreItem( str_item, JSON.stringify( arr_item ) );
break;
case 'object':
skv_item = JSON.parse( getStoreItem( str_item ) );
delete skv_item[ key ];
setStoreItem( str_item, JSON.stringify( skv_item ) );
break;
case 'string':
setStoreItem( str_item, '' );
break;
}
}
}
/* ---------- ---------- ---------- ---------- ----------
Admin Helper Functions
---------- ---------- ---------- ---------- ---------- */
function lockControlsForAjax(){
// show the loading spinner on the actual table
$( '.table-loading-spinner' ).show();
// disable sort buttons
$( '.header_cell .sort .btn' ).prop({ "disabled" : true });
// show loading spinners instead of checkboxes for distincts dropdowns
$( '.checkbox-container label' ).addClass( 'is-waiting' );
$( '.checkbox-container label input' ).off();
$( '.checkbox-container label input' ).prop( 'disabled', 'disabled' );
$( '.filter-control .link-button' ).on( 'click', function(){
event.preventDefault();
});
$( '.filter-control .link-button' ).addClass( 'is-waiting' );
$( '.filter-control .link-button span svg' ).replaceWith( '' );
// $( '.filter-control .link-button span' ).append( '' );
$( '.clear-complex-filters, .filter_chip' ).addClass( 'is-waiting' );
$( '.filter_chip .close' ).empty();
$( '.clear-complex-filters, .filter_chip' ).off();
$( '.clear-complex-filters' ).prop( 'disabled', 'disabled' );
$( '.slider-wrap' ).each( function(){
$( this )[0].setAttribute( 'disabled', true );
$( this ).siblings( '.input-wrap' ).find( '.slider-input' ).prop( 'disabled', true );
});
}
var fp_responsive_class = '';
function attachFilterPaneHandlers( str_table_id ){
// put these in the global scope by not var scoping them
$toggle_sort_btn = $( '.toggle-sort-pane' ),
$sort_pane = $( '.dropdown__content .field_names' );
$toggle_filter_btn = $( '.toggle-filter-pane' ),
$fp = $( '.filter-pane' ),
$fp_visible = $fp.is( ':visible' );
$( '.toggle-filter-pane' ).off();
$( '.toggle-filter-pane' ).on(
'click',
{ "str_table_id" : str_table_id },
filterPaneToggleHandler
);
$( '.filter-pane .filter-control-tabs .js-tab-button' ).on(
'click',
{ "str_table_id" : str_table_id },
filterPaneTabHandler
);
// $toggle_sort_btn.on('click', function(){
// $fp.slideUp( function(){
// $sort_pane.slideToggle();
// });
// });
$( '.clear-complex-filters' ).on(
'click',
{ "str_table_id" : str_table_id },
clearComplexFiltersHandler
);
}
function filterPaneToggleHandler(){
$filter_text = $(this).find('.filter-text');
var str_filter_caption = $filter_text.text();
str_filter_caption = (str_filter_caption == 'Filter') ? 'Close' : 'Filter';
$filter_text.text(str_filter_caption);
if( $fp.hasClass( 'is-hidden' ) ){
fp_responsive_class = 'is-hidden';
}
if( $fp.hasClass( 'is-hidden-md-down' ) ){
fp_responsive_class = 'is-hidden-md-down';
}
if( $fp.hasClass( 'is-hidden-lg-up' ) ){
fp_responsive_class = 'is-hidden-lg-up';
}
// no responsive class: filter pane visible at all sizes
if( !fp_responsive_class.length ){
// slide up the sort panel (thead > tr) before toggling filter panel,
// but only if it was hidden to start off with
// or we're in grid mode
if(
$sort_pane.css( 'display' ) === 'table-row' ||
$sort_pane.css( 'display' ) === 'inline-block' ||
$( '.role_tbody' ).is( 'div' )
){
$fp.slideToggle( setFPSessionState );
} else {
if ($sort_pane.length) {
$sort_pane.slideUp( function(){
$fp.slideToggle( setFPSessionState );
});
} else {
$fp.slideToggle( setFPSessionState );
}
}
} else {
// filter pane hidden at certain sizes
if( $fp.hasClass( fp_responsive_class ) ){
$fp
.css({ 'display' : 'none' })
.removeClass( 'is-hidden' )
.removeClass( fp_responsive_class );
// slide up the sort panel (thead > tr) before sliding down filter panel,
// but only if the sort panel was shown to start off with
if(
$sort_pane.css( 'display' ) === 'table-row' ||
$sort_pane.css( 'display' ) === 'inline-block'
){
$sort_pane.slideUp( function(){
$fp.slideDown( setFPSessionState );
});
} else {
$fp.slideDown( setFPSessionState );
}
} else {
$fp.slideUp( function(){
$fp
.addClass( fp_responsive_class )
.css({ 'display' : 'block' });
setFPSessionState();
});
}
}
}
/**
After the filter panel has been toggled, save it's state in the session.
Then, if the markup is replaced (eg by applying a filter) or the page is
refreshed, we can set it back to the state it was when the user last
interacted with it
*/
function setFPSessionState(){
var table_id = $( '#first_load' ).data( 'table' ).table_id;
fpv = $( '.filter-pane' ).is( ':visible' );
setStoreItem( 'filter_pane_visible_' + table_id, fpv );
}
function filterPaneTabHandler( event ){
$( '.filter-pane .filter-control-tabs .js-tab-button' )
.removeClass( 'is-selected' )
.parent().removeClass( 'active' ); //for bootstrap
$( '.filter-pane .vehicle_controls .filter-group' )
.hide();
var tab = $( this ).data( 'tab' );
setStoreItem( 'current_filter_tab_' + event.data.str_table_id, tab );
$( '.filter-pane .vehicle_controls .' + tab + '-filter-group' ).show();
$( this ).addClass( 'is-selected' );
$( this ).parent().addClass( 'active' ); //for bootstrap
}
function clearComplexFiltersHandler( event ){
var str_table_id = $( this ).data( 'str_table_id' );
var str_cache_key = $( '#' + str_table_id ).data( 'str_cache_key' );
/* ----- Required Filters -----
If there is a "required" filter applied and there is
clear_filters_dest value available to us, go there
instead of clearing the filters in the page
*/
var has_required_filter = false;
var skv_applied_filters = JSON.parse(
getStoreItem(
'skv_applied_filters_' + str_table_id
)
);
for( var key in skv_applied_filters ){
if(
skv_applied_filters[ key ].is_required ||
skv_applied_filters[ key ].filter_type == 'locked'
){
has_required_filter = true;
continue;
}
}
if( has_required_filter ){
var clear_filters_dest = $( '#first_load' )
.data( 'clear_filters_dest' );
if( clear_filters_dest ){
var arr_keys = [
'arr_filters_',
'skv_applied_filters_',
'current_filter_tab_',
'str_cache_key_'
];
arr_keys.forEach( function( key ){
removeStoreItem( key + str_table_id );
});
window.location.href = clear_filters_dest;
}
}
// ---- /required filters -----
$( '.checkbox-container label input' ).prop( 'disabled', 'disabled' );
$( '.checkbox-container label input' ).prop( 'checked', false );
$( '.applied-filter-container .filter_chip' )
.addClass( 'is-waiting' )
.find( '.close' ).empty();
// one function for table requests
requestTable([
{
"action" : "paginate",
"payload" : {
"num_page" : 1
}
},
{
"action" : "clear_filters",
"payload" : ""
}
]);
}
function buildAllFilterTags( table_id ){
arr_filters = JSON.parse(
getStoreItem( 'arr_filters_' + table_id )
) || [];
// not hidden or required - used to determine if we should show 'clear filters' btn
var num_normal_filters = 0;
// the order we want the filter chips in - others will come after this list
arr_tag_order = [
'make',
'model',
'grade',
'derivative',
'engine',
'deposit',
'months',
'mileage',
'maintenance',
'agreementType'
];
// only try to apply filters if there's any in the array and we've got an
// table object in the window scope
if(
arr_filters.length &&
window[table_id]
){
var skv_applied_filters = JSON.parse( getStoreItem(
'skv_applied_filters_' + table_id
));
var skv_label_filters = {};
window[ table_id ].skv_applied_filters = {};
window[ table_id ].num_applied_filters = 0;
// get all the applied filters in to a struct using their fields as keys
for( key in skv_applied_filters ){
var skv_item = skv_applied_filters[ key ];
skv_item.is_applied = false;
skv_item.id = key;
skv_label_filters[ skv_item.field ] = skv_item;
}
// var skv_table = JSON.parse( getStoreItem( 'skv_table_' + table_id ) );
// loop over ordered tags and if there's a corresponding filter, apply it
arr_tag_order.forEach( function( key ){
if( skv_label_filters[ key ] ){
skv_label_filters[ key ].is_applied = true;
var filter_type = skv_label_filters[ key ].filter_type;
num_normal_filters += ( filter_type === 'normal' ? 1 : 0 );
applyFilter( skv_label_filters[ key ], table_id );
}
});
// loop over the filters and if there are any remaining that haven't been
// applied, apply them
for( var key in skv_applied_filters ){
var skv_config = skv_applied_filters[ key ];
if( !skv_config.is_applied ){
var str_this_label = skv_config.field;
var filter_type = skv_applied_filters[ key ].filter_type;
num_normal_filters += ( filter_type === 'normal' ? 1 : 0 );
applyFilter( skv_config, table_id );
}
}
var str_hidden_class = ( window.css_framework == 'bones' )
? 'is-hidden'
: 'hidden';
// only show the clear filters button if there are some clearable filters
if( num_normal_filters ){
$( '.clear-complex-filters' ).removeClass( str_hidden_class );
} else {
// check to see if we want clear filters to take you to another page
var clear_filters_dest = $( '#first_load' )
.data( 'clear_filters_dest' );
// only hide if we don't have a path set when clear filters is clicked
if( !clear_filters_dest ){
$( '.clear-complex-filters' ).addClass( str_hidden_class );
}
}
}
}
function applyFilter( skv_config, table_id ){
// if this is a hidden filter, we don't want a tag, shut it down
if( skv_config.filter_type === 'hidden' ){
return false;
}
var str_filter_id = skv_config.id;
window[ table_id ].skv_applied_filters[ str_filter_id ] = skv_config;
window[ table_id ].num_applied_filters++;
buildFilterTag( table_id, skv_config );
$( '.' + skv_config.field + '-range-slider-wrapper .nulls_checkbox input:checkbox' )
.prop( 'disabled', false );
}
function buildFilterTag( table_id, skv_config ){
var skv_filter_type_icons = {
"locked" : 'lock',
"required" : 'swap',
"normal" : 'remove'
};
var rst_icon = getIcon(
skv_filter_type_icons[ skv_config.filter_type ],
window.css_framework
);
var skv_icon = {
'bones' : '',
'bootstrap' : ''
}
var filter_id = skv_config.id,
str_label = skv_config.field;
skv_config.is_locked = skv_config.filter_type === 'locked' ? true : false;
var chip_classes = 'filter_chip' +
( window.css_framework === 'bootstrap' ? ' btn btn-primary btn-sm' : '' ) +
( skv_config.type === 'include' ? ' filter_include' : ' filter_exclude' ) +
( skv_config.filter_type === 'locked' ? ' filter_locked' : '' );
var skv_chip_span_attr = {
"class" : chip_classes,
"data-filter_id" : filter_id,
"data-location" : skv_config.location,
"data-source" : skv_config.source,
"data-is_required" : skv_config.is_required,
"data-is_locked" : skv_config.is_locked
};
// build filter tags to append
var str_html = '';
/*
IMPORTANT
Although primarily intended for filter words like "includes"
also note that RANGE SLIDERS have their readable value defined in this field
because their value field is a compound value in the form "min|max"
*/
if( typeof skv_config.filter_words !== 'undefined' &&
skv_config.filter_words.toString().length ){
str_html += skv_config.filter_words
// add a space if it's not a range slider eg "includes "
+ ( skv_config.source === 'range-slider' ? '' : ' ' );
}
var this_value = skv_config.value;
// check for "Yes"/"No" values wrapped in pipes to escape coldfusion serialisation bug
var match_result = skv_config.value.toString().match( /^\|(.*)\|$/ );
// if there's a match get the group without the pipes
if( match_result ){
this_value = match_result[1];
}
// we don't show the value for range sliders as it's "min|max", see section immediately above
str_html += ( skv_config.source === 'range-slider' ? '' : this_value );
if(
skv_config.filter_item_text &&
skv_config.bool_filter_item_text_in_chip
){
var filter_item_text = skv_config.filter_item_text;
// remove plural if value is 1
if(skv_config.value == '1') {
filter_item_text = filter_item_text.replace(/s$/g, '');
}
str_html += ' ' + filter_item_text;
}
str_html += (window.css_framework === 'bones') ? '
' : '';
// if field filter container doesn't exist, build it and add it
if( !$( '.applied-filter-container').find( '.' + str_label + '-filter-container' ).length ){
// get title from config, or if that's blank, from the table itself
var str_title = skv_config.title;
var $applied_filter_container = $('.applied-filter-container');
var str_hidden_class = (window.css_framework == 'bones') ? 'is-hidden' : 'hidden';
$applied_filter_container.find('.clear-complex-filters')
.removeClass( str_hidden_class );
// hidden by default
$applied_filter_container.removeClass('is-hidden');
// append filter tags to wrapper div
$applied_filter_container
.append( buildFilterFieldContainer(
str_label,
str_title,
skv_config.field_type,
skv_config.source ) );
} else {
// make sure the container is visible
$( '.applied-filter-container').removeClass('is-hidden');
}
// field filter container definitely now exists, add a value to it
$( '.applied-filter-container .' + skv_config.field + '-filter-container' ).append( str_html );
// when the filter_id gets put in to the DOM, it also gets decoded,
// so in order to find the element the selector must use a decoded filter_id
var ascii_decoded_filter_id = asciiDecode( filter_id );
// attach click handler so that clicking on a filter tag removes filters
$('.applied-filter-container .filter_chip[data-filter_id="' + ascii_decoded_filter_id + '"]' )
// but not if the filter is locked
.not( '[data-is_locked="true"]' )
.on('click', function( event ){
filterTagEventListener( table_id, event.currentTarget.dataset.filter_id );
});
}
function buildFilterFieldContainer( str_field_label, str_field_title, str_field_type, str_filter_source ){
var str_html = '
' + str_field_title + ''
+ '
';
return str_html;
}
function filterTagEventListener( table_id, filter_id ){
$this_filter = $( '.applied-filter-container .filter_chip[data-filter_id="' + filter_id + '"]' );
var skv_data = $this_filter.data();
if( skv_data.is_required ){
requiredFilterModal( table_id, filter_id )
return;
}
$this_filter.addClass( 'is-waiting' );
$this_filter.find( '.close' ).empty();
showTableLoadingSpinner( table_id );
lockControlsForAjax();
var str_label = $this_filter.parents( '.field-filter-container' )
.data( 'field_label' );
var filter_id = skv_data.filter_id;
var str_cache_key = $( '#' + table_id ).data( 'str_cache_key' );
// // remove filter from stored filter struct
// for( str_filter in window[ table_id ].skv_applied_filters ){
// if( str_filter.indexOf( str_label ) > 0 ){
// delete window[ table_id ].skv_applied_filters[ str_filter ];
// window[ table_id ].num_applied_filters--;
// removeFromSession( 'arr_filters_' + table_id, 'array', str_filter );
// removeFromSession( 'skv_applied_filters_' + table_id, 'object', 'n/a', str_filter );
// }
// }
// uncheck the item in the checkbox dropdown area
if( $this_filter.data( 'source' ) === 'checkbox' ){
$( '.checkbox-container label input[data-filter_id="' + filter_id + '"]' )
.prop( 'checked', false );
}
if( skv_data.source === 'range-slider' ){
clearFieldFilters( str_label, str_cache_key, table_id );
} else {
// replace switch with fancy object literal and call relevant function
var skv_remove = {
'local' : function(){
return removeLocalFilter( table_id, filter_id )
},
'complex' : function() {
return removeComplexFilter( str_cache_key, filter_id, table_id )
}
};
skv_remove[ skv_data.location ]();
}
}
function requiredFilterModal( table_id, filter_id ){
var $this_filter = $( '.applied-filter-container .filter_chip[data-filter_id="' + filter_id + '"]' );
var skv_data = $this_filter.data();
var modal = $('.framework-modal');
modal.addClass('modal-required-filter');
var modal_content = modal.find('.modal__content');
var skv_parent_data = $this_filter.parent().data();
var str_label = skv_parent_data.field_label;
var str_title = skv_parent_data.field_title;
var skv_filters = JSON.parse( getStoreItem( 'skv_applied_filters_' + table_id ) );
// NOTE we are relying on this field having distincts. Make sure this is set in the profile.
var skv_filter = skv_filters[ filter_id ];
var str_html = '
Please select an option to filter results.
';
var arr_filter_options = skv_filter.arr_original;
var cnt_arr_filter_options = arr_filter_options.length;
for(var idx = 0; idx < cnt_arr_filter_options; idx++) {
// if the value if of type string then remove its || to handle whether its
// a |Yes| |No| value
arr_filter_options[idx] = stripPipes(skv_filter.arr_original[idx]);
}
str_html += printSelectMarkup({
'arr_options' : arr_filter_options,
'selected_option_value' : stripPipes(skv_filter.value),
'skv_attributes' : {
'id' : str_label,
'name' : str_label,
'data-label' : str_label
},
'str_label' : 'Select a value to filter ' + str_title,
'str_wrap_classes' : str_label + '_select',
'skv_btn' : {
'str_content' : 'Confirm',
'str_icon' : '',
'skv_attributes' : {
'id' : str_label + '_filter_btn',
'class' : 'btn--primary'
}
}
});
modal.find('.modal__heading' ).text( 'Change filter' );
modal_content.html( str_html );
required_filter_remodal = modal.remodal();
required_filter_remodal.open();
$( '#' + str_label + '_filter_btn' ).click( function(){
replaceRequiredFilter( this, table_id, filter_id, required_filter_remodal );
});
}
function replaceRequiredFilter( that, table_id, current_filter_id, required_filter_remodal ){
var str_cache_key = $( '#' + table_id ).data( 'str_cache_key' );
var $select = $( that )
.siblings( '.select' )
.children( 'select' );
var value = $select.val();
var str_label = $select.data( 'label' );
var str_new_filter_id = 'include~'
+ str_label + '~exact~'
+ value + '~checkbox~false';
// one function for table requests
requestTable([
{
"action" : "paginate",
"payload" : {
"num_page" : 1
}
},
{
"action" : "remove_filter",
"payload" : current_filter_id
},
{
"action" : "add_filter",
"payload" : str_new_filter_id
}
]);
required_filter_remodal.close();
//allow modal to disappear before removing class
setTimeout(function() {
required_filter_remodal.$modal.removeClass('modal-required-filter');
}, 500);
}
function getIcon( icon, framework ){
var icons = {
"star-outline" : {
"bones" : {
"class" : 'icon-cd-star-outline',
"svg" : 'icon-cd_star--outline'
},
"bootstrap" : {
"class" : 'glyphicon-star-empty'
}
},
"lock" : {
"bones" : {
"class" : 'icon-cd_lock',
"svg" : 'icon-cd_lock'
},
"bootstrap" : {
"class" : 'glyphicon-lock'
}
},
"remove" : {
"bones" : {
"class" : 'icon-cd-remove',
"svg" : 'icon-cd_remove'
},
"bootstrap" : {
"class" : 'glyphicon-remove'
}
},
"swap" : {
"bones" : {
"class" : 'icon-cd-swap',
"svg" : 'icon-cd_swap'
},
"bootstrap" : {
"class" : 'glyphicon-star-empty'
}
}
}
// default retrun empty strings
var skv_return = {
"class" : '',
"svg" : ''
};
// if the icon is defined, populate the properties
if( icons.hasOwnProperty( icon ) ){
skv_return.class = icons[ icon ][ framework ].class;
skv_return.svg = ( icons[ icon ][ framework ].hasOwnProperty( 'svg' )
? icons[ icon ][ framework ].svg
: ''
);
}
return skv_return;
}
/**
* Remove all the pipes in a string
* @str_value string Filter value
* @return value with all | removed
*/
function stripPipes(val) {
if(typeof val == 'string') {
// get all the pipes
var regex = /\|/g;
// replace all pipes with a blank string
return val.replace(regex, '');
}
return val;
}
function findOrder( num_original, num_divisor ){
num_divisor = typeof num_divisor !== 'undefined' ? num_divisor : 10;
var num_divided = num_original / num_divisor;
if( num_divided >= 10 ){
num_divisor *= 10;
return findOrder( num_original, num_divisor );
} else {
return num_divisor;
}
}
function setupSlider( slider_index, this_slider_wrapper ){
var skv_data = $( this_slider_wrapper ).data(),
str_table_id = skv_data.str_table_id,
str_field_label = skv_data.str_field_label,
str_field_title = skv_data.str_field_title,
str_field_type = skv_data.str_field_type,
original_min = skv_data.original_min,
original_max = skv_data.original_max,
order = findOrder( original_min ) / 10,
min_boundary = ( Math.floor( original_min / order ) ) * order || 0,
max_boundary = ( Math.ceil( original_max / order ) ) * order || 0,
this_slider = $( this_slider_wrapper ).find( '.slider-wrap' )[0],
this_nulls_checkbox = $( this_slider_wrapper ).find( '.nulls_checkbox input:checkbox' ),
filtered_min = '',
filtered_max = '',
is_disabled = false,
$min_input = $( '#' + str_field_label + '-input-box-min'),
$max_input = $( '#' + str_field_label + '-input-box-max'),
num_records = $( '#' + str_table_id ).data( 'num_records' ),
bool_include_nulls = true;
var bool_filtered = ( skv_data.hasOwnProperty( 'filtered_min' ) && skv_data.hasOwnProperty( 'filtered_max' ) );
$min_input.prop( 'disabled', false );
$max_input.prop( 'disabled', false );
// can't create a slider with no range.
// spoof the range, disable the slider and add the (equal) values to the inputs
// if( min_boundary === max_boundary || !num_records ){
if( min_boundary === max_boundary ){
var is_disabled = true;
var min_and_max = min_boundary;
$min_input.off()
$min_input.prop( 'disabled', true );
$max_input.off()
$max_input.prop( 'disabled', true );
min_boundary = 0;
max_boundary = 1;
}
skv_config = {
start: [min_boundary, max_boundary],
connect: true,
step: order,
range: {
'min': min_boundary,
'max': max_boundary
}
};
if( typeof $( this_slider_wrapper ).data( 'filtered_min' ) !== 'undefined' ){
filtered_min = $( this_slider_wrapper ).data( 'filtered_min' );
filtered_max = $( this_slider_wrapper ).data( 'filtered_max' );
filtered_min = ( Math.floor( filtered_min / order ) ) * order;
filtered_max = ( Math.ceil( filtered_max / order ) ) * order;
if( typeof $( this_slider_wrapper ).data( 'actual-position-min' ) !== 'undefined' ){
filtered_min = $( this_slider_wrapper ).data( 'actual-position-min' );
}
if( typeof $( this_slider_wrapper ).data( 'actual-position-max' ) !== 'undefined' ){
filtered_max = $( this_slider_wrapper ).data( 'actual-position-max' );
}
skv_config.start = [ filtered_min, filtered_max ];
}
if( $( this_slider_wrapper ).data( 'is_initialised' ) ){
this_slider.noUiSlider.off();
// slider is setup, so just set the values of other filters.
// only reset filters after a page reload if filters have been set
if( typeof filtered_min !== 'undefined'
&& bool_filtered
&& !is_disabled ){
this_slider.noUiSlider.set([ filtered_min, filtered_max ]);
this_slider.removeAttribute( 'disabled' );
}
} else {
// slider is not setup, so set it up
noUiSlider.create(this_slider, skv_config );
$( this_slider_wrapper ).data( 'is_initialised', true );
if( min_boundary === max_boundary ){
this_slider.noUiSlider.setAttribute( 'disabled', true);
}
}
var input_min = $( '#' + str_field_label + '-input-box-min')[0];
var input_max = $( '#' + str_field_label + '-input-box-max')[0];
// this function updates the input boxes when the sliders are moved
this_slider.noUiSlider.on('update', function( values, handle ) {
var value = values[handle];
// convert to number
if( !isNaN( value ) ){
value = +value;
}
switch( handle ){
case 0:
input_min.value = value;
break;
case 1:
input_max.value = value;
break;
}
});
this_slider.noUiSlider.on('set', function( values, handle ) {
var value = values[handle];
// convert value to number
if( !isNaN( value ) ){
value = +value;
}
$( 'div.slider-wrapper' ).data( 'most_recent_filter', false );
$( 'div.' + str_field_label + '-slider-wrapper' ).data( 'most_recent_filter', true );
var previous_min = $( 'div.' + str_field_label + '-slider-wrapper' ).data( 'filtered_min' );
var previous_max = $( 'div.' + str_field_label + '-slider-wrapper' ).data( 'filtered_max' );
bool_include_nulls = $( this_nulls_checkbox ).is( ':checked' );
// only want to call setupFilter if creating a new filter, not if we're adjusting the slider values based
// on another filter we've already created
switch( handle ){
case 0:
if( value !== previous_min ){
setupFilter( str_table_id, value, 'lte', str_field_label, str_field_title, str_field_type, bool_include_nulls );
}
break;
case 1:
if( value !== previous_max ){
setupFilter( str_table_id, value, 'gte', str_field_label, str_field_title, str_field_type, bool_include_nulls );
}
break;
}
});
if( !is_disabled ){
// these functions update the slider when the input boxes are changed
input_min.addEventListener('change', function(){
this_slider.noUiSlider.set([ this.value, null ]);
});
input_max.addEventListener('change', function(){
this_slider.noUiSlider.set([ null, this.value ]);
});
} else {
// adjust disabled sliders
$max_input.val( min_and_max );
$min_input.val( min_and_max );
this_slider.setAttribute( 'disabled', true );
}
$( this_nulls_checkbox ).on('change', function(event) {
bool_include_nulls = $( this_nulls_checkbox ).is( ':checked' );
setupFilter( str_table_id, '', 'update_nulls', str_field_label, str_field_title, str_field_type, bool_include_nulls );
});
}
function setupFilter( str_table_id, filter_value, filter_match_type, str_field_label, str_field_title, str_field_type, bool_include_nulls ){
showTableLoadingSpinner( str_table_id );
// disable all range sliders and input boxes whilst ajax in progress
$( '.slider-wrap' ).each( function(){
$( this )[0].setAttribute( 'disabled', true );
$( this ).siblings( '.input-wrap' ).find( '.slider-input' ).prop( 'disabled', true );
});
var $this_slider_wrapper = $( '.' + str_field_label + '-range-slider-wrapper' );
var original_min = $this_slider_wrapper.data( 'original_min' );
var original_max = $this_slider_wrapper.data( 'original_max' );
var num_records = $( '#' + str_table_id ).data( 'num_records' );
var num_pages = $( '#' + str_table_id ).data( 'num_pages' );
var str_cache_key = $( '#' + str_table_id ).data( 'str_cache_key' );
var str_location = num_pages === 1 ? 'local' : 'remote';
var bool_complex_table = $( '#' + str_table_id ).data( 'bool_complex_table' );
// in certain situation coldfusion borks this, so check for yes/no
if( typeof bool_complex_table === 'string' ){
bool_complex_table = bool_complex_table.toLowerCase() === 'yes' ? true : false;
}
$this_slider_wrapper.find( '.nulls_checkbox input:checkbox' ).prop( 'disabled', false );
/* ------------------------------------------------------------
Build filter text for chip
------------------------------------------------------------ */
var str_filter_words = '';
var temp_number = '';
var order = findOrder( original_min ) / 10;
var str_extreme_type = 'original';
var str_extreme = 'min';
// if it's 'lt', we're setting min, so we need to /get/ the value for max
if( filter_match_type.substr( 0, 2 ) === 'lt' ){
str_extreme = 'max';
}
if( typeof $this_slider_wrapper.data( 'filtered_' + str_extreme ) !== 'undefined' ){
str_extreme_type = 'filtered';
}
// eg filtered_min original_max etc
temp_number = $this_slider_wrapper.data( str_extreme_type + '_' + str_extreme );
var skv_values = {
"min" : roundNearestDirection( temp_number, order, false ),
"max" : filter_value
};
// this means we've just set the min slider
if( str_extreme === 'max' ){
skv_values.min = filter_value;
skv_values.max = roundNearestDirection( temp_number, order, true );
}
str_filter_words = skv_values.min + ' - ' + skv_values.max;
if( skv_values.min == skv_values.max ){
str_filter_words = skv_values.min;
}
var str_value = skv_values.min + '|' + skv_values.max;
// if we're just updating whether or not we're including nulls, rather than actually changing
// the range values, let's just use the previously determined filter words and value
if( filter_match_type === 'update_nulls' ){
var str_filter_id = $( '.applied-filter-container .' + str_field_label + '-filter-container .filter_chip' ).data( 'filter_id' );
var skv_applied_filters = JSON.parse( getStoreItem( 'skv_applied_filters_' + str_table_id ) );
var skv_filter = skv_applied_filters[ str_filter_id ];
str_filter_words = skv_filter.filter_words;
str_value = skv_filter.value;
}
/* ------------------------------------------------------------ */
var skv_config = {
// "table_id" : str_table_id,
// "str_cache_key" : str_cache_key,
// "num_records" : num_records,
// "num_pages" : num_pages,
// "skv_filter" : {
// "re_filter" : "",
// "arr_excludes_records" : [],
// "str_match_words" : '',
// "id" : str_filter_id,
"value" : str_value,
"type" : 'include',
"match_type" : 'between',
"location" : str_location,
"filter_words" : str_filter_words,
"source" : "range-slider",
"field" : str_field_label,
"title" : str_field_title,
"field_type" : str_field_type,
"bool_include_nulls" : bool_include_nulls,
"is_required" : false
// }
};
skv_config.id = buildFilterName( skv_config );
// if there are already filters in play, we should check and see if we need to remove a similar one
// previously set by the range slider
if( typeof window[ str_table_id ].skv_applied_filters !== 'undefined' ){
for( var this_key in window[ str_table_id ].skv_applied_filters ){
skv_this_filter = window[ str_table_id ].skv_applied_filters[ this_key ];
if( skv_this_filter.source === 'range-slider'
&& skv_this_filter.match_type === skv_config.match_type
&& skv_this_filter.field === skv_config.field ){
// if it is a complex table, adding a range slider filter will
// overwrite another filter of the same type, so don't need to remove
if( !bool_complex_table ){
switch( str_location ){
case 'remote':
remoteRemoveFilter( skv_config.str_cache_key, this_key, str_table_id, skv_config );
break;
default:
removeLocalFilter( str_table_id, this_key, skv_config );
break;
}
} else {
removeFromSession( 'arr_filters_' + str_table_id, 'array', this_key );
removeFromSession( 'skv_applied_filters_' + str_table_id, 'object', 'blah', this_key );
}
cleanUpFilterStats( str_table_id, this_key )
}
}
}
// remove previous filter chip
$( '.applied-filter-container .' + str_field_label + '-filter-container .filter_chip' ).remove();
if( bool_complex_table ){
lockControlsForAjax();
}
// buildFilterTag( skv_config );
filter( skv_config );
}
function roundNearestDirection( number, num_round, bool_up ){
if( bool_up ){
return ( Math.ceil( number / num_round ) ) * num_round;
} else {
return ( Math.floor( number / num_round ) ) * num_round;
}
}
function attachRangeSliderHandlers( str_table_id ){
$( '.slider-wrapper' ).each( setupSlider );
}
function attachCheckboxHandlers( str_table_id ){
$( '.checkbox-wrapper .checkbox-dropdown' ).off();
$( '.checkbox-wrapper .checkbox-container input' ).off();
$( '.selection-control button' ).off();
// $( document ).off();
$( '.checkbox-wrapper .checkbox-container .control.is-waiting' ).removeClass( 'is-waiting' );
$( '.checkbox-wrapper .checkbox-container .control input' ).removeAttr( 'disabled' );
// if clicking outside of open drop downs, close them
// catch a click on the document
$( document ).on( 'click', function( event ) {
// check a dropdown isn't an ancestor of the element being clicked
if( !$( event.target ).closest( '.checkbox-wrapper' ).length ) {
// hide visible dropdowns
$( '.checkbox-container:visible' ).hide();
}
})
$( '.checkbox-wrapper .checkbox-dropdown' )
.on( 'click', function(event) {
var str_this_class = $( this )
.parents( '.checkbox-wrapper' )
.data( 'str_field_label' ) + '-checkbox-wrapper';
// hide other open dropdowns
$( '.checkbox-wrapper:not(.' + str_this_class + ') .checkbox-container:visible' ).hide();
//toggle this dropdown
$( this ).siblings( '.checkbox-container' ).toggle();
//the scrollbar wraps another checkbox-container div around checkbox-container so need to toggle that one aswell
$( this ).siblings( '.checkbox-container' ).find('.checkbox-container').toggle();
scrollbarsetup(event);
});
var skv_table_data = $( '#' + str_table_id ).data( );
var str_cache_key = skv_table_data.str_cache_key;
var num_pages = skv_table_data.num_pages;
var bool_complex_table = skv_table_data.bool_complex_table;
$( '.selection-control button' )
.on( 'click', function(event) {
showTableLoadingSpinner( str_table_id );
clearFieldFilters( $( this ).data( 'label' ), str_cache_key, str_table_id );
});
$( '.checkbox-wrapper .checkbox-container input' ).change(function(event) {
var filter_value = $( this ).val();
var filter_type = 'include';
var bool_checked = $( this ).is( ':checked' )
var skv_checkbox_data = $( this ).parents( '.checkbox-wrapper' ).data();
showTableLoadingSpinner( str_table_id );
if( !bool_checked ){
// re-checked box - need to remove the filter which was excluding this option
$( this ).parents( 'label' ).addClass( 'is-waiting' );
$( '.checkbox-wrapper .checkbox-container .control input' ).prop( 'disabled', 'disabled' );
var filter_id = $( this ).data( 'filter_id' );
// remove the chip if there is one
$( '.applied-filter-container .filter_chip[data-filter_id="' + filter_id + '"]' )
.addClass( 'is-waiting' )
.find( '.close' ).empty();
if( bool_complex_table ){
removeComplexFilter( str_cache_key, filter_id, str_table_id );
} else {
if( num_pages === 1 ){
removeLocalFilter( str_table_id, filter_id );
} else {
remoteRemoveFilter( str_cache_key, filter_id, str_table_id );
}
}
} else {
// un-checked box - need to add a filter to exclude this option
$( this ).data( 'filter_id' );
checkboxfilter( str_table_id, filter_value, filter_type, skv_checkbox_data, this );
}
});
}
function checkboxfilter( str_table_id, filter_value, filter_type, skv_checkbox_data, this_checkbox ){
var skv_table_data = $( '#' + str_table_id ).data();
var str_location = 'remote';
if( skv_table_data.bool_complex_table ){
str_location = 'complex';
} else if( skv_table_data.num_pages === 1 ){
str_location = 'local';
}
var skv_config = {
// "table_id" : str_table_id,
// "str_cache_key" : skv_table_data.str_cache_key,
// "num_records" : skv_table_data.num_records,
// "num_pages" : skv_table_data.num_pages,
// "skv_filter" : {
// "str_match_words" : "",
// "re_filter" : "",
// "arr_excludes_records" : [],
"value" : filter_value,
"type" : filter_type,
"match_type" : 'exact',
"location" : str_location,
"filter_words" : "",
"field" : skv_checkbox_data.str_field_label,
"title" : skv_checkbox_data.str_field_title,
"field_type" : skv_checkbox_data.str_field_type,
"source" : 'checkbox',
"bool_include_nulls" : false,
"is_required" : false
// }
};
skv_config.id = buildFilterName( skv_config );
$( '.checkbox-wrapper .checkbox-container .control input' ).prop( 'disabled', 'disabled' );
filter( skv_config );
}
$(document).on('page_reload', function() {
$('.checkbox-dropdown').on('click', scrollbarsetup);
});
function scrollbarsetup(e) {
$checkbox_dropdown = $(e.currentTarget);
$check_container = $checkbox_dropdown.next();
if(!$check_container.hasClass('scroll-wrapper')) {
$check_container.scrollbar({'autoUpdate' : false});
}
}
/**
Attach description rows click handlers
*/
function attachDescriptionsClickHandler( str_table_id ){
// Description rows button click handler
$('#' + str_table_id + ' [data-for-label]' ).on('click', function( event ){
event.stopImmediatePropagation();
var str_label = $( this ).data( 'for-label' );
showDescriptions( str_table_id, $(this) );
});
}
/**
Handle description row clicks
*/
function showDescriptions( str_table_id, $btn ){
var str_label = $btn.data('for-label');
// i buttons can be in headings or cells
var is_heading_description = $btn.parents('th').length ? true : false;
if(is_heading_description) {
$this_header = $( '#' + str_table_id + ' .field_names .header_cell.' + str_label );
var active =$btn.data( 'active' );
var num_fields = $( '#' + str_table_id ).data( 'num_fields' );
// hide all description rows
$other_buttons = $( '#' + str_table_id + ' .field_names .header_cell:not( .' + str_label + ') [data-for-label]' );
$other_buttons.find( 'svg:first-child' ).removeClass( 'is-hidden' );
$other_buttons.find( 'svg:last-child' ).addClass( 'is-hidden' );
$other_buttons.data( 'active', false );
$( '#' + str_table_id + ' .description_row' ).remove();
// was not active, so now build column description
if( !active ){
$( '#' + str_table_id + ' tr.field_names' )
.after( '
' );
}
// toggle active status
$btn.data( 'active', !active );
// otherwise the info is in a data_cell
} else {
var $el_cell = $btn.parents('.data_cell.' + str_label);
$el_cell.find('.explanation__content').toggleClass('is-hidden');
}
// switch the icon on the button
$btn.find( 'svg' ).toggleClass( 'is-hidden' );
}
/**
complex tables pagination request
*/
function requestPage( table_id, num_page, num_max_pages, num_valid_records ){
// don't do needless ajax calls if user clicks page they are already on
if( num_page == $('#' + table_id ).data('start_page') ){
return false;
}
// one function for table requests
requestTable([{
"action" : "paginate",
"payload" : {
"num_page" : num_page,
"num_max_pages" : num_max_pages,
"num_valid_records" : num_valid_records
}
}]);
}
/**
Attach click handlers to pagination controls
*/
function attachPaginationHandlers( str_table_id ){
// remove handler to avoid having it added twice
$( '.btn_pagination[data-table_id="' + str_table_id + '"]' ).off;
// attach handler to pagination buttons
$( '.btn_pagination[data-table_id="' + str_table_id + '"]' ).on('click', function(event) {
var num_page = $(this).data('page_num');
var num_max_pages = $(this).parents('.pagination').data('max_pages');
var table_id = $(this).data('table_id');
var $this_table = $( '#' + table_id );
var num_valid_records = $this_table.data('num_valid_records');
requestPage( table_id, num_page, num_max_pages, num_valid_records );
});
// remove handler to avoid having it added twice
$( '.pagination[data-table_id="' + str_table_id + '"] .pagination_select_go' ).off;
// attach handler to pagination select 'go' button
$( '.pagination[data-table_id="' + str_table_id + '"] .pagination_select_go' ).on('click', function(event) {
var num_page = $( '.pagination[data-table_id="' + str_table_id + '"] .pagination_type_select .pagination_select' ).val();
var num_max_pages = $(this).parents('.pagination').data('max_pages');
var $this_table = $( '#' + str_table_id );
var table_id = $(this).data('table_id');
var $this_table = $( '#' + table_id );
var num_valid_records = $this_table.data('num_valid_records');
requestPage( str_table_id, num_page, num_max_pages, num_valid_records );
});
}
var remote_url = '/page/component/table/remote.cfc'
function rowClick( table_id, row_id ){
var $this_table = $( '#' + table_id );
var $this_record = $this_table.find( 'tr[data-row_id="' + row_id + '"]' );
var skv_record = $this_record.data('record');
var record_index = skv_record.record_index;
var str_ruid = record_index;
var skv_table_data = $this_table.data();
var bool_complex_table = skv_table_data.bool_complex_table;
if( bool_complex_table ){
var skv_data = $( '#first_load' ).data();
var skv_table = skv_data.table;
var skv_table_config = skv_data.table_config;
// var unique_field = skv_table.skv_options.str_unique_key_field;
var unique_field = skv_table_config.selection_config.unique_field;
var str_ruid = skv_record[ unique_field ];
}
var is_multi_select = $this_table.data( 'bool_select_multiple_records' );
var arr_selected_records = $this_table.data( 'selected_rows' );
var bool_selected_record = $this_record.data( 'selected_record' );
var table_active = !$this_record.parents( 'tbody' ).hasClass( 'inactive' );
// Can only select one row at a time
// make sure no rows are selected - there can only be one selected - deselect it.
if( table_active && !is_multi_select && arr_selected_records.length ){
var previously_selected_row_id = arr_selected_records[0];
var $old_row = $this_table.find( 'tr[data-row_id="' + previously_selected_row_id + '"]' )
var jsn_old_record = $old_row.data('record');
var previous_ruid = $old_row.data( 'ruid' );
$( '.selected_record' )
.removeClass( 'selected_record' )
.data( 'selected_record', false);
arr_selected_records = [];
if( !bool_complex_table ){
$(document).trigger(
'row_selection_event',
[
table_id,
is_multi_select,
// previously_selected_row_id,
// record_index,
// jsn_old_record,
// arr_selected_records,
'deselected',
previous_ruid
]
);
}
}
// this row is not already selected, so select it
// Note that if this row is already selected and we don't have multiple row selection,
// the end result will be that no rows are selected due to the clearing above
if( table_active && !bool_selected_record ){
$this_record
.addClass( 'selected_record' )
.data( 'selected_record', true);
arr_selected_records.push( record_index );
arr_selected_records.sort( sortNumeric );
$(document).trigger(
'row_selection_event',
[
table_id,
is_multi_select,
// row_id,
// record_index,
// skv_record,
// arr_selected_records,
'selected',
str_ruid
]
);
}
if( table_active && is_multi_select && bool_selected_record ){
// can select more than one row
// this row is already selected - we'll unselect it now
$this_record
.removeClass( 'selected_record' )
.data( 'selected_record', false );
arr_selected_records.splice( arr_selected_records.indexOf( record_index ) , 1);
$(document).trigger(
'row_selection_event',
[
table_id,
is_multi_select,
// row_id,
// record_index,
// skv_record,
// arr_selected_records,
'deselected',
str_ruid
]
);
}
$this_table.data( 'selected_rows', arr_selected_records);
}
/**
The callback we want to pass to requestTable() needs the table id,
but we don't have it in the global scope where the fn is defined.
So, we have a wrapper function which injects it and returns the function.
*/
function getSelectionCallback( callback_data ){
return function selectionCallback(){
$( document ).trigger(
'row_selection_complete',
callback_data
);
}
}
$( document ).on( 'row_selection_event', function(
event,
table_id,
is_multi_select,
action_type,
str_ruid
){
var $this_table = $( '#' + table_id );
var skv_table_data = $this_table.data();
var bool_complex_table = skv_table_data.bool_complex_table;
var bool_is_multi_select = false;
if(
is_multi_select === 'YES' ||
is_multi_select === true
){
bool_is_multi_select = true;
}
if( bool_complex_table ){
var skv_data = $( '#first_load' ).data();
var skv_table_config = skv_data.table_config;
var unique_field = skv_table_config.selection_config.unique_field;
callback_data = {
"table_id" : table_id,
"is_multi_select" : bool_is_multi_select,
"action_type" : action_type,
"str_ruid" : str_ruid,
"qry_select" : false
};
requestTable([{
"action" : 'selection',
"payload" : {
"qry_select" : false,
"selection_add" : ( action_type === 'selected' ) ,
"this_ruid" : str_ruid,
"unique_field" : unique_field,
"select_all_checked" : false
},
"skv_callbacks" : { "post_markup" : [ getSelectionCallback( callback_data ) ]}
}]);
}
});
function sortNumeric(a,b) {
return a - b;
}
function attachRowClickHandler( table_id ){
// Row Selection click handler - note, only on horizontal tables
$('#' + table_id + '.horizontal tbody tr').on('click', function(event) {
var row_id = $(this).data('row_id');
rowClick( table_id, row_id );
});
}
function rowSelectionByCell(table_id, action) {
$( '#' + table_id + '.horizontal tbody tr td[data-select_row="true"]' ).each(function(index, value) {
// search for either button or inputs to do the selection or rows
$selectable_el = $(this).find('.btn, input');
// otherwise if there aren't any use the table cell
if( !$selectable_el.length ) {
$selectable_el = $(this);
}
switch( action ){
case 'listen' :
$selectable_el.on( 'click', function( event ) {
var row_id = $( this ).closest( 'tr' ).data( 'row_id' );
rowClick( table_id, row_id );
});
return;
break;
case 'click' :
$selectable_el.click();
break;
}
});
}
function attachRowClickByCellHandler( table_id ){
// Row Selection click handler - note, only on horizontal tables
rowSelectionByCell(table_id, 'listen');
}
$(document).on('all_rows_selection_event', function( event, table_id, is_checked ){
var $this_table = $( '#' + table_id );
var str_cache_key = $this_table.data( 'str_cache_key' );
var bool_complex_table = $( '#' + table_id ).data( 'bool_complex_table' );
var action_type = is_checked ? 'selected' : 'deselected';
if( bool_complex_table ){
var select_all_checked = false;
// it's a query and we're adding to the selection:
// select all should be checked
if( is_checked ){
select_all_checked = true;
}
callback_data = {
"table_id" : table_id,
"is_multi_select" : true,
"action_type" : action_type,
"str_ruid" : '',
"qry_select" : true
};
var skv_this_selection = {
"action" : 'selection',
"payload" : {
"qry_select" : true,
"selection_add" : is_checked,
// "select_multiple_records" : true,
"this_ruid" : '',
"unique_field" : 'id',
"select_all_checked" : select_all_checked
},
"skv_callbacks": { "post_request": [ getSelectionCallback( callback_data ) ] }
};
requestTable([ skv_this_selection ]);
};
});
function addMetaToSession( skv_data, table_id ){
if( typeof skv_data.str_cache_key_modified !== undefined ){
addToSession( 'str_cache_key_' + table_id, 'string', skv_data.str_cache_key_modified );
}
if( typeof skv_data.arr_selected_records !== undefined ){
setStoreItem( 'arr_selected_records' + table_id, JSON.stringify( skv_data.arr_selected_records ) );
}
}
/**
Get an array of selected records from the session,
returns an empty array if not available
*/
function getSelectedRecords( table_id ){
return JSON.parse(
getStoreItem( 'arr_selected_records_' + table_id )
) || [];
}
var remote_url = '/page/component/table/remote.cfc';
function attachHeaderCellClickHandler( str_table_id ){
$( '#' + str_table_id + ' .header_cell, [data-table-id="' + str_table_id + '"] .header_cell').each(function() {
$(this).off();
if($(this).data('select_all_records')) {
// $(this).on( 'click', {str_table_id: str_table_id}, selectAllRecords );
$( this ).on('click', function(event) {
$target_el = $(event.target);
if($target_el.is('input')) {
$select_all_input = $target_el;
} else {
$select_all_input = $( event.target ).find( 'input' );
}
is_select_checked = $select_all_input.is( ':checked' );
//check to see if the table header cell is clicked
if(!$target_el.is('input')) {
is_select_checked = !is_select_checked;
$select_all_input.prop( 'checked', is_select_checked );
}
$(document).trigger( 'all_rows_selection_event', [ str_table_id, is_select_checked ] );
});
} else {
if($(this).hasClass('header_cell--sort-available')) {
$(this).on( 'click', triggerSort );
};
}
});
}
function triggerSort( event ) {
// don't sort if show descriptions is active
if( $(this).hasClass( 'highlight' ) ){
return false;
}
var $header = $( this );
var $table = {};
var table_id = '';
// check to see if the header clicked is from the dropdown or the actual table
// head
var $filter_pane_header = $header.parents('.filter-pane-header');
if($filter_pane_header.length === 0) {
$table = $header.parents( 'table' );
table_id = $table.attr( 'id' );
} else {
table_id = $filter_pane_header.data('table-id');
$table = $('#' + table_id);
}
var str_cache_key = $table.data( 'str_cache_key' ),
bool_already_sorted = $header.data( 'sorted' ),
bool_directional_sort = false,
bool_derived = $header.data( 'bool_derived' ),
bool_complex_table = $( '#' + table_id ).data( 'bool_complex_table' ),
label = $header.data( 'label' ),
sort_field = label,
bool_filter_pane_open = $( '.filter-pane' ).is( ':visible' );
// derived fields and those with custom sql don't exist in the database
// so can't sort by them in a complex table ORDER BY clause
if( bool_complex_table ){
if( $header.data( 'custom_sort_field' ) ){
sort_field = $header.data( 'custom_sort_field' );
} else {
// if this is a derived field and we haven't specified a custom
// sort field, do not attempt to sort on this field. Just exit.
// return false;
}
}
// remove sort data from other fields
$( '#' + table_id + '.channel_table .header_cell:not(.' + label + ' )' )
.data( 'sorted', false)
.removeData( 'sort_direction' );
var bool_asc = true;
if( bool_already_sorted && ( $header.data( 'sort_direction' ) === 'asc' ) ){
bool_asc = false;
}
sortField( $header, str_cache_key, label, sort_field, bool_asc, table_id, bool_directional_sort, bool_filter_pane_open );
}
function triggerDirectionalSort( event ){
event.stopPropagation();
var bool_asc = true;
if( $( this ).parent().hasClass('desc') ){
bool_asc = false;
}
var $table;
if($(this).parents('.filter-pane-header').length > 0) {
var str_table_id = $(this).parents('.filter-pane-header').data('tableId');
$table = $('#' + str_table_id);
} else {
$table = $( this ).parents( 'table' );
}
var $header = $( this ).parents( '.header_cell' ),
// $table = $( this ).parents( 'table' ),
str_cache_key = $table.data( 'str_cache_key' ),
table_id = $table.attr( 'id' ),
label = $header.data( 'custom_sort_field' ) || $header.data( 'label' ),
bool_directional_sort = true,
bool_derived = $header.data( 'bool_derived' ),
bool_complex_table = $( '#' + table_id ).data( 'bool_complex_table' ),
sort_field = label,
bool_filter_pane_open = $( '.filter-pane' ).is( ':visible' );
// derived fields don't exist in the database so can't sort by them in a complex table ORDER BY clause
if( bool_complex_table && bool_derived ){
if( $header.data( 'custom_sort_field' ) ){
sort_field = $header.data( 'custom_sort_field' );
} else {
// if this is a derived field and we haven't specified a custom sort field,
// do not attempt to sort on this field. Just exit.
return false;
}
}
sortField( $header, str_cache_key, label, sort_field, bool_asc, table_id, bool_directional_sort, bool_filter_pane_open );
}
function sortField( $header, str_cache_key, label, sort_field, bool_asc, table_id, bool_directional_sort, bool_filter_pane_open ){
var is_paginated = $( '#' + table_id ).data( 'num_pages' ) !== 1;
var bool_complex_table = $( '#' + table_id ).data( 'bool_complex_table' );
if( is_paginated || bool_complex_table ){
showTableLoadingSpinner( table_id );
var skv_table_data = $( '#' + table_id ).data();
var bool_complex_table = skv_table_data.bool_complex_table;
var num_max_pages = $( '.complex-table-tool .pagination[data-table_id="' + table_id + '"]').data('max_pages');
// if we don't have any pagination set a default
if(!num_max_pages) {
num_max_pages = 1;
}
var num_valid_records = $( '#' + table_id ).data('num_valid_records');
// one function for table requests
requestTable([
{
"action" : "sort",
"payload" : {
"str_field" : sort_field,
"bool_asc" : bool_asc
}
},
{
"action" : "paginate",
"payload" : {
"num_page" : 1,
"num_max_pages" : num_max_pages,
"num_valid_records" : num_valid_records
}
}
]);
} else {
localSort( $header, label, sort_field, bool_asc, table_id, bool_directional_sort, bool_filter_pane_open );
}
}
function setupSortDropdown( str_thead_markup ){
$sort_by_wrap = $('.filter-pane-header .dropdown-sort-by');
$sort_by_wrap.append('
' + str_thead_markup + '
');
if( window.bool_uiFramework ){
uiFramework.setupDropdowns();
$sort_by_wrap.on('click.table', function() {
$(this).siblings('.toggle-columns-wrapper').find('.dropdown__content').css({'display' : 'none'});
});
} else {
$sort_by_wrap.on('click', function() {
$(this).find('.dropdown__content').slideToggle();
});
}
}
function objectSort( a, b ){
if (a.last_nom < b.last_nom){
return -1;
} else if (a.last_nom > b.last_nom){
return 1;
} else {
return 0;
}
}
function arrayKeys( array, key ){
var arr_keys = [];
array.forEach( function( element ){
arr_keys.push( element[ key ] )
});
return arr_keys
}
function localSort( $header, label, sort_field, bool_asc, table_id, bool_directional_sort, bool_filter_pane_open ){
var arr_data = [],
arr_jq_rows = [],
sort_direction = $header.data( 'sort_direction' ),
is_currently_sorted = $header.attr( 'data-sorted' );
// if this is currently sorted, now sort it the other direction
if( is_currently_sorted ){
if( sort_direction === 'asc' ){
sort_direction = 'desc';
} else {
sort_direction = 'asc';
}
}
// if we're deliberately sorting in a given direction, override the reverse sort with the specific direciton
if( bool_directional_sort ){
sort_direction = bool_asc ? 'asc' : 'desc';
}
// set the new sort_direction
$header.data( 'sort_direction', sort_direction );
// get all the records from all the rows, add the row_ids and put in to an array
$( '#' + table_id + ' tbody tr' ).each(function(index, el) {
skv_record = $(this).data( 'record' );
skv_record.id = $(this).data( 'row_id' );
arr_data.push( skv_record );
});
// sort the array of objects by the given key
if( sort_direction === 'asc' ){
arr_data.sort(function(a,b) {return (a[ label ] > b[ label ]) ? 1 : ((b[ label ] > a[ label ]) ? -1 : 0);} );
}else{
arr_data.sort(function(b,a) {return (a[ label ] > b[ label ]) ? 1 : ((b[ label ] > a[ label ]) ? -1 : 0);} );
}
var arr_sorted_rows = arrayKeys( arr_data, 'id' );
var num_records = arr_sorted_rows.length;
// get rows in the sorted order and put jquery row objects in an array
arr_sorted_rows.forEach( function( element ){
arr_jq_rows.push( $( '#' + table_id + ' tbody tr[data-row_id=' + element + ']' ) );
});
// remove current table rows
$( '#' + table_id + ' tbody' ).empty();
// re populate table with sorted rows
for( var i = 0; i < num_records; i++ ){
$( '#' + table_id + ' tbody' ).append( arr_jq_rows[i] );
}
// unset previous sort
$( '#' + table_id + ' thead tr th.sorted .sort-indicator-wrap' ).remove();
$( '#' + table_id + ' thead tr th.sorted' )
.remove('.sort-indicator-wrap')
.removeClass( 'sorted' )
.attr( 'data-sorted', false );
// set current sort
$( '#' + table_id + ' thead tr th.' + label )
.addClass( 'sorted' )
.attr( 'data-sorted', true );
var str_up_or_down = sort_direction == 'asc' ? 'up' : 'down';
var str_direction_markup = '
'
+ ''
+ '
'
$( '#' + table_id + ' thead tr th.' + label + ' .field_header' )
.append(str_direction_markup);
// re do row striping classes
$( '#' + table_id + ' tbody tr' ).removeClass( 'odd even' );
$( '#' + table_id + ' tbody tr:not(.filter_hide):visible:odd' ).addClass( 'odd' );
$( '#' + table_id + ' tbody tr:not(.filter_hide):visible:even' ).addClass( 'even' );
// NOTE - local in-page sorting doesn't remove the thead so there is no need to
// reattach events as there are no new DOM elements.
// BUT it does need to reattach the row listeners, as they are new DOM elements
attachRowClickHandler( table_id );
}
var visible_status = {
hidden_items: false,
force_categories_open: true,
force_categories_closed: false
}
function showHideItemsAndCategories( str_table_id ) {
var $target_table = $( '#' + str_table_id ),
$all_items = $target_table.find( '[data-record]' ),
$trailing_rows = $target_table.find( '.trailing_row' ),
$hidden_items = $target_table.find( '.row_hide_match' ),
$headings = $target_table.find('.category_heading.collapsible-handler'),
$heading_collapse_down = $headings.find('.icon-cd-collapse--down'),
$heading_collapse_up = $headings.find('.icon-cd-collapse--up');
// if categories are hidden, then hide all records
if( visible_status.force_categories_closed ) {
// hide all items
$all_items.addClass( 'is-hidden' );
$trailing_rows.filter(':visible')
.addClass( 'is-hidden' )
.addClass('trailing_row_hidden');
// set all headings status to closed - cycle chevron icons
$headings.data( 'status', 'closed' );
$headings.addClass('isClosed');
$heading_collapse_down.removeClass('is-hidden');
$heading_collapse_up.addClass('is-hidden');
} else {
// show all rows by default, hide the ones we don't need
$all_items.removeClass( 'is-hidden' );
if( visible_status.force_categories_open ) {
// if we're forcing everything open, flag all categories as open
$headings.data( 'status', 'open' );
$headings.removeClass('isClosed');
$heading_collapse_down.addClass('is-hidden');
$heading_collapse_up.removeClass('is-hidden');
// reveal any previously open trailing_rows
$trailing_rows.filter('.trailing_row_hidden')
.removeClass( 'is-hidden' );
} else {
// if not forcing all open, we need to check per category
// for status=closed and then hide those category's rows
$headings.each(function(){
var $category_heading = $( this ),
$category_icon_down = $category_heading.find('.icon-cd-collapse--down'),
$category_icon_up = $category_heading.find('.icon-cd-collapse--up'),
data = $category_heading.data(),
status = data.status,
target_field = data.str_category_field,
target_value = data.str_category_field_value,
target_selector = '[data-' + target_field + '="' + target_value + '"]',
$rows = $target_table.find( target_selector );
if( status === 'closed' ) {
$rows.filter('.trailing_row:visible')
.addClass( 'trailing_row_hidden' );
$rows.addClass( 'is-hidden' );
$category_icon_down.removeClass('is-hidden');
$category_icon_up.addClass('is-hidden');
$(this).addClass('isClosed');
} else {
$rows.filter( '.trailing_row_hidden' )
.removeClass( 'trailing_row_hidden' )
.removeClass( 'is-hidden' );
$category_icon_down.addClass('is-hidden');
$category_icon_up.removeClass('is-hidden');
$(this).removeClass('isClosed');
}
});
}
// hide specially flagged "hidden" rows
// (ie Standards in the Options config)
if( !visible_status.hidden_items ) {
$hidden_items.addClass( 'is-hidden' );
}
}
// work out if empty_categories need revealing
// empty_categories are ones that only contains hidden rows
if( visible_status.hidden_items ) {
$target_table.find( '.empty_category' ).removeClass( 'is-hidden' );
} else {
$target_table.find( '.empty_category' ).addClass( 'is-hidden' );
}
//find the last category heading without is-hidden class and give it last class
$.each($headings, function() {
$(this).removeClass('last_visible_category_heading');
});
$headings_visible = $target_table.find('.category_heading.collapsible-handler').not('.is-hidden');
$headings_visible.last().addClass('last_visible_category_heading');
}
function attachCategoriesHandlers( str_table_id ) {
$( '.toggle-hidden-items[data-table_id=' + str_table_id + ']' ).on('click', function(event) {
// set show_hidden_items true/false
var $chk_active = $( this ).find( '[type="checkbox"]' ),
checked = $chk_active.prop( 'checked' );
if( checked ) {
visible_status.hidden_items = true;
} else {
visible_status.hidden_items = false;
}
showHideItemsAndCategories( str_table_id );
});
$( '.toggle_categories[data-table_id=' + str_table_id + ']' ).on('click', function(event) {
// toggle all categories
var str_action = $( this ).data( 'type' );
if( str_action === 'close' ){
visible_status.force_categories_closed = true;
} else {
visible_status.force_categories_closed = false;
}
visible_status.force_categories_open = !visible_status.force_categories_closed;
showHideItemsAndCategories( str_table_id );
});
$( '#' + str_table_id + ' .category_heading.collapsible-handler' ).on('click', function(event) {
// change status for single category
var str_status = $( this ).data( 'status' );
if( str_status === 'open' ){
$( this ).data( 'status', 'closed');
$( this ).addClass('isClosed');
} else {
$( this ).data( 'status', 'open');
$( this ).removeClass('isClosed');
}
// as soon as you interact directly with a category, need to change the
// global visible_status.force* variables to false
visible_status.force_categories_open = false;
visible_status.force_categories_closed= false;
showHideItemsAndCategories( str_table_id );
});
}
function attachInputAdjustmentHandlers( str_table_id ){
$('.btn-number').off('click');
$('.btn-number').click(function(e){
e.preventDefault();
// fieldName = $(this).attr('data-field');
type = $(this).attr('data-type');
$input = $(this).closest('.input-group-btn').siblings('input');
var current_val = parseInt($input.val());
var new_val = '';
if (!isNaN(current_val)) {
if(type == 'minus') {
if(current_val > $input.attr('min')) {
new_val = current_val - 1;
} else {
new_val = current_val;
}
if(parseInt($input.val()) == $input.attr('min')) {
// $(this).attr('disabled', true);
}
} else if(type == 'plus') {
if(current_val < $input.attr('max')) {
new_val = current_val + 1;
} else {
new_val = current_val;
}
if(parseInt($input.val()) == $input.attr('max')) {
// $(this).attr('disabled', true);
}
}
} else {
new_val = 0;
}
$input.val(new_val);
inputChanged($input);
});
$('.input-number').focusin(function(){
$(this).data('oldValue', $(this).attr('value'));
});
$(".input-number").keyup(function (e) {
//remove last character entered if it isnt a number or .
if (!$(this).val().match(/[0-9|\.]$/)) {
var new_val = $(this).val().slice(0, -1);
$(this).val(new_val);
inputChanged($(this));
} else {
inputChanged($(this));
}
});
function inputChanged($input) {
$row = $input.closest('tr');
record_index = $row.data('row_id');
minValue = parseInt($input.attr('min'));
maxValue = parseInt($input.attr('max'));
valueCurrent = parseInt($input.val());
name = $input.attr('name');
if(valueCurrent >= minValue) {
$(".btn-number[data-type='minus'][data-field='"+name+"']").removeAttr('disabled');
} else {
console.log('Sorry, the minimum value was reached');
$input.val($(this).data('oldValue'));
}
if(valueCurrent <= maxValue) {
$(".btn-number[data-type='plus'][data-field='"+name+"']").removeAttr('disabled');
} else {
console.log('Sorry, the maximum value was reached');
$input.val($(this).data('oldValue'));
}
if(!record_index) {
record_index = -1;
}
$(document).trigger( 'input_adjustment_event', [str_table_id, record_index, $input, $input.val()] );//, arr_selected_records ] );
}
}
// attach click handlers
function attachLinkSwitchHandlers( str_table_id ){
$( '.linkswitch_group :radio' ).off();
$( '.linkswitch_group :radio' ).on(
'click',
{ str_table_id : str_table_id },
linkSwitchClick
);
// to detect whether the switch input changes
// note: this may be a temporary solution until business / personal are real
// filters
$( '.linkswitch_group :radio' ).off();
$( '.linkswitch_group :radio' ).on(
'change',
{ str_table_id : str_table_id },
linkSwitchChange
);
}
// listen for when a the radio input changes in case the user presses the currently
// active input
function linkSwitchChange(event) {
var skv_switch = getLinkSwitchData();
var dev_prefix = getDevPrefix();
var link = '';
if(
event.target.dataset.group === 'agreement_type'
){
window.location.href = window.location.origin
+ window.location.pathname
+ '?agreementType='
+ skv_switch.agreement_type;
}
updateSearchHref(skv_switch);
}
// link switch click handler
function linkSwitchClick( event ){
var skv_switch = getLinkSwitchData();
var dev_prefix = getDevPrefix();
var link = '';
if(
event.target.dataset.group === 'vehicle_type'
){
link = 'http://' + dev_prefix + skv_switch.vehicle_type;
window.location.href = link + window.location.pathname;
}
updateSearchHref(skv_switch);
};
// point the href of the search button in the right direction
function updateSearchHref(skv_switch) {
link = buildLink( skv_switch );
// update search link
$( '.link-wrap .link-button' ).prop({ 'href' : link });
}
// get the prefix if we're on a dev server
function getDevPrefix(){
var dev_prefix = '';
var regex = /d51(\w+)/g;
var is_dev = regex.test( window.location.host );
if( is_dev ){
dev_prefix = window.location.host.match( regex )[0] + '.';
}
return dev_prefix;
}
// get data from link switch radio inputs
function getLinkSwitchData(){
var skv_switch = {};
// build skv_switch
$( '.linkswitch_group :radio:checked' ).each(function(index, el) {
var switch_group = $( el ).data( 'group' );
var value = $(el).val();
skv_switch[ switch_group ] = value;
});
return skv_switch;
}
// build a link to the finder tool on the right domain, with the right agreement type
function buildLink( skv_switch ){
var link = '';
var $van_input = $( '.vehicle_type-filter-control .linkswitch_group input[data-label="van"]' );
var $pch_input = $( '#radiopch' );
if( skv_switch.hasOwnProperty( 'vehicle_type' ) ){
link = 'http://' + skv_switch.vehicle_type;
if( $van_input.is( ':checked' ) ){
$pch_input.prop( 'disabled', 'disabled' );
} else {
$pch_input.removeAttr( "disabled" );
}
} else {
link = window.location.href;
}
if( skv_switch.hasOwnProperty( 'agreement_type' ) ){
link += skv_switch.agreement_type;
var href = $( '.link-button' ).attr( 'href' );
var new_href = '';
if( skv_switch.agreement_type === 'pch' ){
// no van pch
$van_input.prop( 'disabled', 'disabled' );
// switch search button from bch to pch
new_href = href.replace( /\/bch\//, '/pch/' );
} else {
// switch search button from pch to bch
new_href = href.replace( /\/pch\//, '/bch/' );
// van bch is available
$van_input.removeAttr( "disabled" );
}
$( '.link-button' ).attr( 'href', new_href );
}
link += '/finder/';
return link;
}
/**
Attache click handlers to checkboxes which toggle columns
*/
function attachToggleableColumnsHandlers(){
$( '#manage-columns' ).off();
$( '.toggle-columns label' ).off();
// if clicking outside of open drop downs, close them
// catch a click on the document
$( document ).on( 'click', function( event ) {
// check a dropdown isn't an ancestor of the element being clicked
if( !$( event.target ).closest( '.toggle-columns-wrapper' ).length ) {
// hide visible dropdowns
$( '.toggle-columns' ).hide();
}
})
$( '#manage-columns' ).click( manageColumnsClickHandler );
$( '.toggle-columns input' ).change( toggleColumnClickHandler );
}
/**
Handle a click on a button to show manage column pane
*/
function manageColumnsClickHandler(){
var $toggle_columns = $( '.toggle-columns' );
$toggle_columns.toggle();
var $el_sort_by = $toggle_columns.parent().siblings('.dropdown-sort-by');
if($el_sort_by.hasClass('is-open')) {
$el_sort_by.removeClass('is-open');
$el_sort_by.find('.dropdown__content').css({ 'display' : '', 'left' : '' });
}
}
/**
Handle a click on a checkbox to toggle a column
*/
function toggleColumnClickHandler( event ){
var table_id = $( '#first_load' ).data().table.table_id;
var field = event.target.value;
var is_visible = event.target.checked;
var storage_item = 'skv_column_visibility_' + table_id;
var css_framework = $( 'html' ).data( 'css_framework' );
var hidden_class = ( css_framework === 'bones' ? 'is-hidden' : 'hidden' );
// get visibility state from session
var skv_columns = JSON.parse( getStoreItem( storage_item ) );
// update visibility state of this field
if( skv_columns.hasOwnProperty( field ) ){
skv_columns[ field ] = is_visible;
}
// store state change in the session, which we'll use in the next ajax call
setStoreItem( storage_item, JSON.stringify( skv_columns ) );
// field set to visible: remove hidden class
if( is_visible ){
$( '.data_cell.' + field + ', .header_cell.' + field )
.removeClass( hidden_class );
// field hidden: add hidden class
} else {
$( '.data_cell.' + field + ', .header_cell.' + field )
.addClass( hidden_class );
}
//check whether we are now or no longer overflowing to show overflow style indicators
var $table = $('#' + table_id);
var $table_scroll_wrap = $table.parent();
//called in overflow.js
handleOverflow($table_scroll_wrap);
}
/**
Returns a struct of label : visibility for each toggleable column
*/
function getColumnVisibility( arr_header_fields, table_id ){
var skv_columns = {};
// loop over our header fields and for those that are toggleable,
// add their visibility state
arr_header_fields.forEach( function( skv_header ){
// ignore field if not toggleable
if( !skv_header.is_toggleable ){
return false;
}
skv_columns[ skv_header.label ] = skv_header.bool_display;
});
return skv_columns;
}
/**
Attach click handlers to trailing rows toggles
*/
function attachTrailingRowsClickhandlers( table_id ){
// Description rows button click handler
$('#' + table_id + ' .data_cell .toggle_trailing_rows' ).on('click', function( event ){
event.stopImmediatePropagation();
trailingRowsClickhandler( table_id, event );
});
}
/**
Handles clicks on trailing rows toggles
*/
function trailingRowsClickhandler( table_id, event ){
var $action_btn = $( event.target );
var $table = $( '#' + table_id );
var row_id = $action_btn.parents( '.record' ).data( 'row_id' );
var $parent_row = $table.find( '.role_tbody .record[data-row_id=' + row_id + ']' );
var num_fields = $table.data( 'num_fields' );
var tr_id = row_id + '_trailing_row';
var skv_row_data = $parent_row.data();
var row_class = ( $parent_row.hasClass( 'even' ) ? 'even' : 'odd' );
// TODO: maybe we should get these buttons from the table?
// we could have some kind of indicator that if there's more than one that they
// should get put in a trailing row, then hide them with css but take the markup
// when we need to show them in a trailing row?
// build button markup to add to the trailing rows
var configure_btn = buildActionButton(
table_id,
skv_row_data,
'/compare/build/options/?vehicle_id=(:vehicle_id)&arr_filters=(:jsn_filters)',
'Configure'
);
var compare_btn = buildActionButton(
table_id,
skv_row_data,
'/compare/?vehicle_id=(:vehicle_id)',
'Compare'
);
// remove other action rows
$table.find( '.trailing-row' ).remove();
// set other action buttons to active:false when we click on any action button
var other_btns_selector = '.record:not( [data-row_id=' + row_id + '] ) .data_cell.actions_btn .toggle_trailing_rows'
$table.find( other_btns_selector ).data( "active", false );
// if this action button isn't active
if( $action_btn.data( 'active' ) != true ){
// add this action row
$parent_row.after(
'
'
+ '
'
+ '
' + configure_btn + compare_btn + '
'
+ '
'
);
// get the element we just inserted hidden
$trailing_row = $( '#' + tr_id );
// show the element
$trailing_row.slideDown( 'slow' );
// set the button to active
$action_btn.data( "active", true );
// if this button was already active
} else {
// set it to inactive (we've already removed all trailing rows)
$action_btn.data( "active", false );
}
}
/**
Build a link to the options configurator
*/
function buildActionButton( table_id, skv_row_data, action_url, btn_text ){
var skv_resolve_data = {
"vehicle_id" : skv_row_data.record.id,
"jsn_filters" : '"' + getStoreItem( 'arr_filters_' + table_id ) + '"'
};
action_url = resolveShortcuts( action_url, skv_resolve_data );
var action_btn = '' +
'' + btn_text +
printSVG( 'button-right' )
'';
return action_btn;
}
/**
Replace shortcuts in strings with resolved values, provided in a struct
*/
function resolveShortcuts( str_raw, skv_data ){
var str_resolved = str_raw;
for( var key in skv_data ){
var re = new RegExp( '\\(\\:' + key + '\\)', 'ig' );
str_resolved = str_resolved.replace( re, skv_data[ key ] );
}
return str_resolved;
}
//this file will detect when the table is overflowing right or left (or at all)
//attached from table setup
function attachOverflowHandler( str_table_id ){
var $table = $('#' + str_table_id);
var $table_scroll_wrap = $table.parent();
//do an initial check
checkOverflow($table_scroll_wrap);
//window resize
$(window).on('resize', function() {
checkOverflow($table_scroll_wrap);
});
//when scrollbar is being used
$table_scroll_wrap.on('scroll', function() {
checkOverflow($table_scroll_wrap);
});
}
//debounce so we don't fire the function too much
var checkOverflow = debounce(function($table_scroll_wrap) {
handleOverflow($table_scroll_wrap);
}, 100);
function handleOverflow($table_scroll_wrap) {
var skv_is_overflown = isContentOverflown($table_scroll_wrap);
//do some initial tidying up of classes
if(!skv_is_overflown.left) {
$table_scroll_wrap.removeClass('is-overflowing-left--in').removeClass('is-overflowing-left');
}
if(!skv_is_overflown.right) {
$table_scroll_wrap.removeClass('is-overflowing-right--in').removeClass('is-overflowing-right');
}
//if we're not overflowing no need to go further
if(
!skv_is_overflown.left &&
!skv_is_overflown.right
) {
$table_scroll_wrap.removeClass('is-overflowing');
return;
}
//if we are overflowing add relevant classes
if(skv_is_overflown.left) {
$table_scroll_wrap.addClass('is-overflowing');
$table_scroll_wrap.addClass('is-overflowing-left--in');
}
if(skv_is_overflown.right) {
$table_scroll_wrap.addClass('is-overflowing');
$table_scroll_wrap.addClass('is-overflowing-right--in');
}
}
//to check whether we're overflowing on the left or the right (or at all)
function isContentOverflown(element) {
//return whether we are overflowing on the left or the right based on scroll position
var skv_return = {
'left' : false,
'right' : false
};
//we'll allow this amount of px before showing an indicator that we can scroll in a particular direction
var padding = 20;
//check to see if the content is larger than the element
var is_overflown = $(element)[0].scrollWidth > $(element)[0].clientWidth;
//the amount of width that may be hidden
var potentially_hidden_right = $(element)[0].scrollWidth - $(element)[0].clientWidth;
if(is_overflown) {
skv_return['left'] = (element.scrollLeft() - padding) > 0;
skv_return['right'] = element.scrollLeft() < (potentially_hidden_right - padding);
}
return skv_return;
}
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};