/* * 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 = $('