src/StartPlatz/Bundle/StyleBundle/Resources/views/Bootstrap4/_javascript.ajax-helpers.html.twig line 1

Open in your IDE?
  1. <style>
  2.     [data-template] {
  3.         display: none;
  4.     }
  5.     [data-filtered-out-by] {
  6.         display: none;
  7.     }
  8. </style>
  9. <script type="text/javascript">
  10.     "use strict";
  11.     if (window.ajaxHelpers) {
  12.         throw Error('Ajax helpers loaded multiple times.');
  13.     }
  14.     /*
  15.     * maps toast types to their html classes
  16.     */
  17.     const toastClasses = {
  18.         ERROR: 'ajax-toast-error',
  19.         SUCCESS: 'ajax-toast-success'
  20.     };
  21.     /*
  22.     * Reads the data attributes
  23.     */
  24.     const readDataAttributes = function (elem) {
  25.         const outmap = {};
  26.         const attrs = elem.attributes;
  27.         for (let i = 0; i < attrs.length; i++) {
  28.             outmap[attrs[i].name] = attrs[i].value;
  29.         }
  30.         return outmap;
  31.     };
  32.     /* 
  33.     * Converts from a snake_case string to regex that matches _MACRO_CASE
  34.     * Importantly, the regex will have three capture groups, one just before the match, the match, and one just after
  35.     * e.g. varName -> _VAR_NAME
  36.     */
  37.     const toVarReg = function (str) {
  38.         const reg = '\\b_' + camelToSnake(str).toUpperCase() + '\\b';
  39.         return new RegExp(reg, 'g');
  40.     };
  41.     /* 
  42.     * Projections that can be called from data-use or before actions are taken
  43.     * e.g. <input type="checkbox" data-on-change="..." data-use="checked|toBool" />
  44.     * e.g. <input type="checkbox" data-on-ajax-success="toSuccessToast|addToast" />
  45.     */
  46.     const projections = {};
  47.     projections.not = (val) => !val;
  48.     projections.toBool = function (val) {
  49.         const res = val === "on" || val === "true" || val === 1 || val === true;
  50.         return res;
  51.     };
  52.     projections.toValue = (inputEvent) => inputEvent.target.value;
  53.     projections.toSetUnset = function (val) {
  54.         if (typeof val !== 'boolean') {
  55.             val = projections.toBool(val);
  56.         }
  57.         if (val) {
  58.             return "set";
  59.         }
  60.         return "unset";
  61.     };
  62.     projections.falseyToNullString = (str) => str || 'null';
  63.     projections.toData = function (val) {
  64.         if (typeof val === 'string') {
  65.             return val;
  66.         }
  67.         if (typeof val === 'object') {
  68.             if (val.length != null) {
  69.                 // Typical jQuery response (data, textStatus, request) or (request, textStatus, errorThrown)
  70.                 if (typeof val[2] === 'string' && val[2]) {
  71.                     return val[2];
  72.                 }
  73.                 if (typeof val[0] === 'string' && val[0]) {
  74.                     return val[0];
  75.                 }
  76.             }
  77.         }
  78.         return null;
  79.     };
  80.     projections.toHeaderNotice = function (val) {
  81.         if (typeof val === 'object' && val.length != null) {
  82.             let xhr;
  83.             if (val[0] && val[0].getResponseHeader) {
  84.                 xhr = val[0];
  85.             } else if (val[2] && val[2].getResponseHeader) {
  86.                 xhr = val[2];
  87.             }
  88.             if (xhr != null && xhr.getResponseHeader('X-Notice')) {
  89.                 return xhr.getResponseHeader('X-Notice');
  90.             }
  91.         }
  92.         return null;
  93.     };
  94.     projections.toMessage = function (val) {
  95.         if (typeof val === 'string') {
  96.             return val;
  97.         }
  98.         const hNotice = projections.toHeaderNotice(val);
  99.         if (hNotice != null) {
  100.             return hNotice;
  101.         }
  102.         if (typeof val === 'object') {
  103.             if (val.length != null) {
  104.                 // Typical jQuery response (data, textStatus, request) or (request, textStatus, errorThrown)
  105.                 if (typeof val[0] === 'object'
  106.                     && val[0].responseText) {
  107.                     return val[0].responseText;
  108.                 }
  109.                 if (typeof val[2] === 'string' && val[2]) {
  110.                     return val[2];
  111.                 }
  112.                 if (typeof val[0] === 'string' && val[0]) {
  113.                     return val[0];
  114.                 }
  115.                 if (val[1] === 'error') {
  116.                     return 'Error: Unable to connect to server.';
  117.                 }
  118.                 if (typeof val[1] === 'string' && val[1]) {
  119.                     return val[1];
  120.                 }
  121.             }
  122.             /* Here we could further expand on our standard success/error message format.
  123.             E.g.
  124.                 if (val[0].message) { val.message; }
  125.                 if (val[0].success) { "Success"; }
  126.                 if (val[0].error) { "Error"; }
  127.             */
  128.         }
  129.         return null; // Null/undefined messages will get ignored
  130.     }
  131.     projections.toErrorToast = function (val) {
  132.         let message = projections.toMessage(val);
  133.         if (message == null) {
  134.             message = 'Error';
  135.         }
  136.         return {
  137.             message: message,
  138.             className: toastClasses.ERROR
  139.         };
  140.     }
  141.     projections.toSuccessToast = function (val) {
  142.         let message = projections.toMessage(val);
  143.         if (message == null) {
  144.             message = 'Success';
  145.         }
  146.         return {
  147.             message: message,
  148.             className: toastClasses.SUCCESS
  149.         };
  150.     }
  151.     projections.toStyledToast = function (val) {
  152.         // If is ajax response
  153.         let non200 = false;
  154.         if (typeof val === 'object' && val[0] != null && val[0].status != null) {
  155.             non200 = val[0].status < 200 || val[0].status >= 300;
  156.         }
  157.         let message = projections.toMessage(val);
  158.         if (message == null) {
  159.             return null;
  160.         } // Pass nulls through
  161.         let className = '';
  162.         const lowercase = message.toLowerCase();
  163.         if (non200 || lowercase.startsWith('error') || lowercase.startsWith('internal server error')) {
  164.             className = toastClasses.ERROR;
  165.         } else if (lowercase.startsWith('success')) {
  166.             className = toastClasses.SUCCESS;
  167.         }
  168.         return {
  169.             message: message,
  170.             className: className
  171.         };
  172.     }
  173.     projections.encodeURI = function (val) {
  174.         return encodeURIComponent(val);
  175.     };
  176.     const applyProjections = function (inVal, selectedProjections) {
  177.         return selectedProjections.reduce(function (val, projection) {
  178.             if (!(projection in projections)) {
  179.                 throw Error('Invalid data- projection: '
  180.                     + projection
  181.                     + '. Available projections: '
  182.                     + Object.keys(projections).join(', ')
  183.                 );
  184.             }
  185.             // Run the value through the projection
  186.             return projections[projection](val);
  187.         }, inVal);
  188.     };
  189.     {# /*
  190.     * Convert a kebab-case string to a camelCase one
  191.     * e.g. class-name -> className
  192.     *      inner-html -> innerHTML
  193.     *      namespace-uri -> namespaceURI
  194.     */ #}
  195.     const kebabToCamel = function (str) {
  196.         const acronyms = ['html', 'uri'];
  197.         const parts = str.split('-');
  198.         const tail = parts.slice(1).map(function (t) {
  199.             if (t === '') {
  200.                 return t;
  201.             }
  202.             if (t in acronyms) {
  203.                 return t.toUpperCase();
  204.             }
  205.             return t.charAt(0).toUpperCase() + t.slice(1);
  206.         });
  207.         return parts[0] + tail.join('');
  208.     };
  209.     {# /*
  210.     * Convert a camelCase string to a kebab-case one
  211.     * e.g. className -> class-name
  212.     *      innerHTML -> inner-html
  213.     *      namespaceURI -> namespace-uri
  214.     */ #}
  215.     const camelToKebab = function (str) {
  216.         return str.replace(/([a-z][A-Z])/g, function (_match, p1) {
  217.             return p1.charAt(0) + '-' + p1.charAt(1);
  218.         }).toLowerCase();
  219.     };
  220.     {# /* 
  221.     * Converts kebab-case strings to snake_case ones
  222.     * e.g. class-name -> class_name
  223.     */ #}
  224.     const kebabToSnake = function (str) {
  225.         return str.replace(/\-/g, '_');
  226.     }
  227.     {# /*
  228.     * Convert a camelCase string to a snake_case one
  229.     * e.g. className -> class_name
  230.     *      innerHTML -> inner_html
  231.     *      namespaceURI -> namespace_uri
  232.     */ #}
  233.     const camelToSnake = function (str) {
  234.         return kebabToSnake(camelToKebab(str));
  235.     };
  236.     {# /*
  237.     * Convert all keys of an objects keys to another case
  238.     * e.g. { className: 'someVal' } -> { class_name: 'someVal' }
  239.     */ #}
  240.     const convertObjCase = function (obj, conversion) {
  241.         return Object.keys(obj).reduce(function (newObj, key) {
  242.             newObj[conversion(key)] = obj[key];
  243.             return newObj;
  244.         }, {});
  245.     };
  246.     const parseAction = function (actionNames, actions, attr) {
  247.         const reducedActions = actionNames.split(',').reduce(function (projectedActions, actionName) {
  248.             actionName = actionName.trim();
  249.             if (!actionName) {
  250.                 return  projectedActions;
  251.             }
  252.             const parts = actionName.split('|');
  253.             const name = parts[parts.length - 1]; // Select the last one
  254.             const action = actions[name]
  255.             if (!action) {
  256.                 console.error('Error in data-on-: action "' + name + '" doesn\'t exist! Available actions: ' + Object.keys(actions));
  257.                 return projectedActions;
  258.             }
  259.             const projections = parts.slice(0, -1);
  260.             let usableAttr = attr;
  261.             if (!usableAttr) {
  262.                 usableAttr = 'data-for-' + camelToKebab(name);
  263.             }
  264.             const newProjectedAction = function (e, elem, ctx, eventHandlers) {
  265.                 const attrs = readDataAttributes(elem);
  266.                 const primaryArg = attrs[usableAttr];
  267.                 const updatedPrimary = applyProjections(primaryArg || e, projections);
  268.                 const newPrimaryArg = actionArgs(name, elem, updatedPrimary, attrs);
  269.                 action(newPrimaryArg, elem, ctx, eventHandlers);
  270.             };
  271.             // Check if we should create a toast element
  272.             return projectedActions.concat(newProjectedAction);
  273.         }, []);
  274.         // Return the object
  275.         return function (primaryArg, elem, ctx, eventHandlers) {
  276.             reducedActions.forEach(function (action) {
  277.                 action(primaryArg, elem, ctx, eventHandlers);
  278.             });
  279.         };
  280.     }
  281.     {# /* 
  282.     * Extract data from attributes and element to send to server
  283.     * If you want to request the url https://example.com/add/234
  284.     * You can use the element attributes:
  285.     *       data-on-change-get="https://example.com/_ID" 
  286.     *       data-add-id="234" 
  287.     * Similarly you can use "data-use" to extract something from the element itself:
  288.     *       data-on-change-get="https://example.com/_PROP/_VALUE" 
  289.     *       data-use="value" 
  290.     *       data-use-as-prop="innerHTML" 
  291.     * Finally, if the context is of type form, then the inputs will be read (name / value)
  292.     * and used as a basis for the context
  293.     */ #}
  294.     const getContextData = function (elem) {
  295.         const attrs = readDataAttributes(elem);
  296.         let initialContext = {}
  297.         if (elem.tagName === 'FORM') {
  298.             initialContext = $(elem) // Select the form
  299.                 .serializeArray() //  Serialize inputs
  300.                 .reduce((iCtx, nameVal) => {
  301.                     iCtx[nameVal.name] = nameVal.value;
  302.                     return iCtx;
  303.                 }, {}); // Convert array to obj
  304.         }
  305.         return Object.keys(attrs).reduce(function (data, key) {
  306.             if (key.indexOf('data-use') === 0) {
  307.                 const propParts = attrs[key].split('|');
  308.                 const prop = propParts[0]; // property name
  309.                 const selProjections = propParts.slice(1); // selected projections
  310.                 let varName = prop;
  311.                 if (key.indexOf('data-use-as-') === 0) { // Use an alias
  312.                     varName = key.replace(/^data\-use\-as\-/, '');
  313.                     varName = kebabToCamel(varName);
  314.                 } else if (key !== 'data-use') {
  315.                     throw Error('Invalid data-use property: ' + key);
  316.                 }
  317.                 if (!(prop in elem)) {
  318.                     throw Error('Invalid data-use value: ' + prop);
  319.                 }
  320.                 // Apply all projections
  321.                 const val = applyProjections(elem[prop], selProjections);
  322.                 return writeVar(data, varName, val);
  323.             }
  324.             if (key.indexOf('data-add-') === 0) {
  325.                 let varName = key.replace(/^data\-add\-/, '');
  326.                 const value = attrs[key];
  327.                 varName = kebabToCamel(varName);
  328.                 return writeVar(data, varName, value);
  329.             }
  330.             return data;
  331.         }, initialContext);
  332.     };
  333.     const writeVar = (data, varName, val) => {
  334.         const locationChain = varName.split(/[\[\]]/).filter((t) => t);
  335.         const updatedData = Object.assign({}, data);
  336.         let currentLevel = updatedData;
  337.         locationChain.forEach((name, index) => {
  338.             if (index === locationChain.length-1) {
  339.                 currentLevel[name] = val;
  340.                 return;
  341.             }
  342.             if (!currentLevel[name]) {
  343.                 currentLevel[name]={};
  344.             } else {
  345.                 currentLevel[name]=Object.assign({}, currentLevel[name]);
  346.             }
  347.             currentLevel = currentLevel[name];
  348.         });
  349.         return updatedData;
  350.     }
  351.     /* 
  352.     * "Enhance" the URL by replacing parts of the URL
  353.     * If you want to request the URL https://example.com/add/234
  354.     * You can used the context: { id: 234 }, and URL: https://example.com/add/_ID
  355.     */
  356.     const formatURL = function (url, data) {
  357.         return Object.keys(data).reduce(function (url, key) {
  358.             let val = data[key];
  359.             if (typeof val !== 'string') {
  360.                 val = JSON.stringify(val);
  361.             }
  362.             const newURL = url.replace(toVarReg(key), encodeURIComponent(val));
  363.             return newURL;
  364.         }, url);
  365.     }
  366.     /* 
  367.     * Instantiate a template by replacing parameters in the URL
  368.     * If you want the html: <div>Your ID is: 123</div>
  369.     * You can used the context: { id: 123 }, and template string: "<div>Your ID is: _ID</div>"
  370.     */
  371.     const formatHTML = function (html, data) {
  372.         return Object.keys(data).reduce(function (html, key) {
  373.             let val = data[key];
  374.             if (typeof val !== 'string') {
  375.                 val = JSON.stringify(val);
  376.             }
  377.             const newHTML = html.replace(toVarReg(key), val);
  378.             return newHTML;
  379.         }, html);
  380.     }
  381.     {# /* 
  382.     * Prepares the argument(s) to be passed to actions 
  383.     * When calling actions, often all you need is a single argument and some context
  384.     * However, sometimes you need more than one argument. 
  385.     * This can be accomplished by adding more properties. 
  386.     * E.g. data-for-create-elem-to="..." data-for-create-elem-insert-by=""
  387.     */ #}
  388.     const actionArgs = function (actionName, elem, primaryValue, attrs) {
  389.         const dataFor = 'data-for-' + camelToKebab(actionName) + '-';
  390.         const dataForLen = dataFor.split('-').length - 1;
  391.         const otherKeys = Object.keys(attrs)
  392.             .filter((t) => t.startsWith('data-for-'))
  393.             .map((t) => {
  394.                 if (t.startsWith(dataFor)) {
  395.                     return {attr: t, rep: dataFor};
  396.                 }
  397.                 // Allow for * matches
  398.                 const matchStart = t.split('-').slice(0, dataForLen).join('-');
  399.                 const regex = new RegExp(matchStart.replaceAll('*', '[\\w|-|\\*]*') + '-?');
  400.                 return {attr: t, rep: regex};
  401.             })
  402.             .filter((t) => dataFor.match(t.rep));
  403.         if (otherKeys.length === 0) {
  404.             return primaryValue;
  405.         }
  406.         const args = {0: primaryValue};
  407.         for (let i = 0; i < otherKeys.length; i++) {
  408.             const attrRep = otherKeys[i];
  409.             const shortName = attrRep.attr.replace(attrRep.rep, '');
  410.             const camelName = shortName === '' ? 0 : kebabToCamel(shortName);
  411.             args[camelName] = attrs[attrRep.attr];
  412.         }
  413.         if (Object.keys(args).length === 1 && args[0] != null) {
  414.             return args[0];
  415.         }
  416.         return args;
  417.     };
  418.     {# /* 
  419.     * Creates an event handler for secondary events (e.g. response, ajax-success, ajax-error).
  420.     * It returns a function that receives one (or more) arguments, 
  421.     * and wraps it with the other arguments provided.
  422.     * This allows us to bind to an event like: 
  423.     *   $.get(...).done(secondaryHandler(actionName, elem, _ctx, _eventHandlers))
  424.     */ #}
  425.     const secondaryHandler = function (actionName, elem, ctx, eventHandlers) {
  426.         return function () {
  427.             const action = eventHandlers[actionName];
  428.             if (!action) {
  429.                 return;
  430.             }
  431.             let wrappedVal = arguments;
  432.             if (arguments.length <= 1) {
  433.                 wrappedVal = arguments[0];
  434.             }
  435.             return action(wrappedVal, elem, ctx, eventHandlers);
  436.         };
  437.     }
  438.     const getChangeClassOptions = function (arg, elem, actionName) {
  439.         if (typeof arg === 'string') {
  440.             if (!arg) {
  441.                 throw new Error('Invalid action ' + actionName + ', class name is empty!');
  442.             }
  443.             return {operateOn: elem, className: arg};
  444.         }
  445.         if (arg.select && !$(arg.select)[0]) {
  446.             throw new Error('Invalid action '
  447.                 + actionName
  448.                 + ', selected element (from data-for-*-select: '
  449.                 + arg.select
  450.                 + ') doesn\'t exist!');
  451.         }
  452.         return {
  453.             operateOn: arg.select || elem,
  454.             className: arg[0]
  455.         };
  456.     }
  457.     {# /*
  458.     * Virtual Filter - Decide what should be filtered without modifying the DOM
  459.     * returns attributes to be set or removed 
  460.     */ #}
  461.     const filterAttr = 'data-filtered-out-by';
  462.     const vFilter = function (options, elem) {
  463.         if (typeof options !== 'object') {
  464.             throw Error('Invalid filter action. No named arguments (' + options + ') specified. Did you forget a "data-for-filter-by" attribute?');
  465.         }
  466.         const filterValue = options[0] || options.value;
  467.         const name = options.name || 'general';
  468.         const select = options.select || $(elem).children();
  469.         const by = options.by;
  470.         if (!by && options.byText == null) {
  471.             throw Error('Invalid filter action. No filterBy specified. Did you forget a "data-for-filter-by" attribute?');
  472.         }
  473.         const totalList = $(select);
  474.         const startVisibleLength = totalList.toArray().filter(t => !t.hasAttribute(filterAttr)).length;
  475.         // The filter is being cleared
  476.         if (!filterValue) {
  477.             const setRemove = totalList.toArray().reduce((setRem, item) => {
  478.                 const names = item.getAttribute(filterAttr);
  479.                 if (names == null) {
  480.                     return setRem;
  481.                 }
  482.                 const newNames = names.split(',').filter((t) => t !== name);
  483.                 if (newNames.length === 0) {
  484.                     setRem.remove = setRem.remove.concat({attr: filterAttr, item: item});
  485.                     return setRem;
  486.                 }
  487.                 setRem.set = setRem.set.concat({attr: filterAttr, item: item, value: newNames.join(','), isNew: false});
  488.                 return setRem;
  489.             }, {remove: [], set: []});
  490.             return {
  491.                 totalLength: totalList.length,
  492.                 setRemove: setRemove,
  493.                 startVisibleLength: startVisibleLength,
  494.             };
  495.         }
  496.         // The filter should be added or removed
  497.         let match = 'like';
  498.         const matchFunctions = {
  499.             startsWith: (q, v) => v.startsWith(q),
  500.             like: (q, v) => v.includes(q),
  501.             exact: (q, v) => q === v,
  502.             gt: (q, v) => q > v,
  503.             gte: (q, v) => q >= v,
  504.             lt: (q, v) => q < v,
  505.             lte: (q, v) => q <= v
  506.         };
  507.         const matchOptions = {
  508.             like: options.like,
  509.             exact: options.exact,
  510.             gt: options.gt,
  511.             gte: options.gte,
  512.             lt: options.lt,
  513.             lte: options.lte,
  514.             startsWith: options.startsWith
  515.         };
  516.         const activeMatchOptions = Object.keys(matchOptions).filter((t) => matchOptions[t] != null);
  517.         if (activeMatchOptions.length > 1) {
  518.             throw Error('Invalid filter action. Conflicting match options (' + activeMatchOptions.join(', ') + ')');
  519.         }
  520.         if (activeMatchOptions.length === 1) {
  521.             match = activeMatchOptions[0];
  522.         }
  523.         const lowerFilterValue = options.asInt != null
  524.             ? parseInt(filterValue, 10)
  525.             : filterValue.toLowerCase();
  526.         const setRemove = totalList.toArray().reduce((setRem, item) => {
  527.             let itemVal;
  528.             if (options.byText == null) {
  529.                 const itemCtx = getContextData(item);
  530.                 if (itemCtx[by] == null) {
  531.                     throw Error('Filter error. Item (' + item.outerHTML + ') is missing data context attribute ' + by + '. Did you forget a "data-add-" attribute?');
  532.                 }
  533.                 itemVal = itemCtx[by].toLowerCase();
  534.             } else {
  535.                 itemVal = $(item).text().toLowerCase();
  536.             }
  537.             const names = item.getAttribute(filterAttr);
  538.             if (!matchFunctions[match](lowerFilterValue, itemVal)) {
  539.                 const isNew = names == null;
  540.                 const oldNames = isNew ? [] : names.split(',');
  541.                 if (oldNames.includes(name)) {
  542.                     return setRem;
  543.                 }
  544.                 const newNames = oldNames.concat(name);
  545.                 setRem.set = setRem.set.concat({attr: filterAttr, item: item, value: newNames.join(','), isNew: isNew});
  546.                 return setRem;
  547.             } else {
  548.                 if (names == null) {
  549.                     return setRem;
  550.                 }
  551.                 const newNames = names.split(',').filter((t) => t !== name);
  552.                 if (newNames.length === 0) {
  553.                     setRem.remove = setRem.remove.concat({attr: filterAttr, item: item});
  554.                     return setRem;
  555.                 }
  556.                 setRem.set = setRem.set.concat({attr: filterAttr, item: item, value: newNames.join(','), isNew: false});
  557.                 return setRem;
  558.             }
  559.         }, {set: [], remove: []});
  560.         return {
  561.             totalLength: totalList.length,
  562.             setRemove: setRemove,
  563.             startVisibleLength: startVisibleLength,
  564.         };
  565.     };
  566.     // Primary HTML Events we can listen for
  567.     const events = ['change', 'click', 'mouseover', 'mouseenter', 'mouseout', 'input', 'submit'];
  568.     {# /* 
  569.     * Actions we can take
  570.     * Each action must handle both it's primary duty (e.g. a get request), 
  571.     * and calling secondary event handlers (e.g. the on response event)
  572.     */ #}
  573.     const actions = {};
  574.     actions.call = function (arg, elem, ctx, eventHandlers) {
  575.         if (typeof arg !== 'string') {
  576.             throw Error('No function specified for action call (' + JSON.stringify(arg) + '). Did you forget a "data-for-call" attribute?');
  577.         }
  578.         arg.split(',').forEach((arg) => {
  579.             const funcName = arg.trim();
  580.             if (!funcName) {
  581.                 return;
  582.             }
  583.             // Call globally defined function
  584.             window[funcName]({arg: arg, elem: elem});
  585.         });
  586.     };
  587.     actions.confirm = function (arg, elem, ctx, eventHandlers) {
  588.         // Confirm is synchronous
  589.         if (confirm(arg)) {
  590.             secondaryHandler('confirmed', elem, ctx, eventHandlers)(arg);
  591.             return;
  592.         }
  593.         // Else
  594.         secondaryHandler('not-confirmed', elem, ctx, eventHandlers)(arg);
  595.     };
  596.     actions.consoleLog = function (arg, elem, ctx, eventHandlers) {
  597.         console.log('Log', {
  598.             arg: arg,
  599.             elem: elem,
  600.             ctx: ctx,
  601.             eventHandlers: eventHandlers
  602.         });
  603.     };
  604.     const debounceTimers = {}; // These are global 
  605.     actions.debounce = function (args, elem, ctx, eventHandlers) {
  606.         let name = 'default';
  607.         let time = 1500; // default to 1.5s
  608.         if (typeof args === 'object') {
  609.             name = args.name || elem.getAttribute('id') || elem.getAttribute('name') || name;
  610.             time = args.time || time;
  611.         }
  612.         clearTimeout(debounceTimers[name]);
  613.         debounceTimers[name] = setTimeout(secondaryHandler('debounced', elem, ctx, eventHandlers), time);
  614.     };
  615.     actions.disableInput = function(args, elem, ctx, eventHandlers) { 
  616.         let select = args;
  617.         if (typeof select !== 'string') {
  618.             if (inputTagNames.includes(elem.tagName) || ['BUTTON', 'SUBMIT'].includes(elem.tagName)) {
  619.                 select = elem;
  620.             } else {
  621.                 throw Error('Invalid action disableInput. No selector specified. Did you forget a data-for-disable-input?');
  622.             }
  623.         }
  624.         $(select).prop('disabled', true);
  625.     };
  626.     actions.enableInput = function(args, elem, ctx, eventHandlers) { 
  627.         let select = args;
  628.         if (typeof select !== 'string') {
  629.             if (inputTagNames.includes(elem.tagName) || ['BUTTON', 'SUBMIT'].includes(elem.tagName)) {
  630.                 select = elem;
  631.             } else {
  632.                 throw Error('Invalid action enableInput. No selector specified. Did you forget a data-for-enable-input attribute?');
  633.             }
  634.         }
  635.         $(select).prop('disabled', false);
  636.     };
  637.     actions.get = function (args, elem, ctx, eventHandlers) {
  638.         if (!args) {
  639.             throw Error('No arguments found for ajax get. Did you forget a "data-for-get" attribute?');
  640.         }
  641.         const url = typeof args === 'string' ? args : (args[0] || args.url);
  642.         if (!url) {
  643.             throw Error('No URL found for ajax get. Did you forget a "data-for-get" attribute?');
  644.         }
  645.         const accept = args.accept || 'text/plain';
  646.         $.ajax({
  647.             method: 'get',
  648.             url: formatURL(url, ctx),
  649.             headers: {Accept: accept}
  650.         }).done(secondaryHandler('ajax-success', elem, ctx, eventHandlers))
  651.             .fail(secondaryHandler('ajax-error', elem, ctx, eventHandlers))
  652.             .always(secondaryHandler('response', elem, ctx, eventHandlers));
  653.     };
  654.     actions.post = function (args, elem, ctx, eventHandlers) {
  655.         if (!args) {
  656.             throw Error('No arguments found for ajax post. Did you forget a "data-for-post" attribute?');
  657.         }
  658.         const url = typeof args === 'string' ? args : (args[0] || args.url);
  659.         if (!url) {
  660.             throw Error('No URL found for ajax post. Did you forget a "data-for-post" attribute?');
  661.         }
  662.         const accept = args.accept || 'text/plain';
  663.         const reqData = ctx;
  664.         $.ajax({
  665.             method: 'POST',
  666.             url: url,
  667.             data: reqData,
  668.             headers: {Accept: accept}
  669.         }).done(secondaryHandler('ajax-success', elem, ctx, eventHandlers))
  670.             .fail(secondaryHandler('ajax-error', elem, ctx, eventHandlers))
  671.             .always(secondaryHandler('response', elem, ctx, eventHandlers));
  672.     };
  673.     actions.toggleModal = function(args, elem, _ctx, _eventHandlers) {
  674.         let modal = elem;
  675.         if (args && typeof args === 'string') {
  676.             modal = args;
  677.         }
  678.         $(args).modal('toggle');
  679.     };
  680.     actions.showModal = function(args, elem, _ctx, _eventHandlers) {
  681.         let modal = elem;
  682.         if (args && typeof args === 'string') {
  683.             modal = args;
  684.         }
  685.         $(args).modal('show');
  686.     };
  687.     actions.hideModal = function(args, elem, _ctx, _eventHandlers) {
  688.         let modal = elem;
  689.         if (args && typeof args === 'string') {
  690.             modal = args;
  691.         }
  692.         $(args).modal('hide');
  693.     };
  694.     actions.toggleClass = function (args, elem, _ctx, _eventHandlers) {
  695.         const options = getChangeClassOptions(args, elem, 'toggleClass');
  696.         $(options.operateOn).toggleClass(options.className);
  697.     };
  698.     actions.addClass = function (args, elem, _ctx, _eventHandlers) {
  699.         const options = getChangeClassOptions(args, elem, 'addClass');
  700.         $(options.operateOn).addClass(options.className);
  701.     };
  702.     actions.removeClass = function (args, elem, _ctx, _eventHandlers) {
  703.         const options = getChangeClassOptions(args, elem, 'removeClass');
  704.         $(options.operateOn).removeClass(options.className);
  705.     };
  706.     actions.addToast = function (message, elem, _ctx, _eventHandlers) {
  707.         if (!message) {
  708.             return;
  709.         }
  710.         addToast(message);
  711.     };
  712.     actions.removeEl = function (selector, elem, _ctx, _eventHandlers) {
  713.         $(selector || elem).remove();
  714.     };
  715.     actions.createEl = function (toFrom, elem, ctx, _eventHandlers) {
  716.         let to;
  717.         let from;
  718.         let id;
  719.         let template;
  720.         if (typeof toFrom === "string") {
  721.             from = $(toFrom);
  722.         } else {
  723.             from = $(toFrom.from || toFrom[0]);
  724.             to = toFrom.to;
  725.             id = toFrom.id;
  726.             template = toFrom.template;
  727.         }
  728.         if ((!from || from.length == 0) && !template) {
  729.             throw Error('No template found for createEl. Did you forget a "data-for-create-el" attribute?');
  730.         }
  731.         // It's possible that data-for-create-el-from is set
  732.         //  and not data-for-create-el-to
  733.         if (!to || to.length == 0) {
  734.             if (from) {
  735.                 to = $(from).parent(); // Default to parent of template
  736.             } else {
  737.                 to = $(elem).parent(); // Default to parent of calling
  738.             }
  739.         }
  740.         // Read Template 
  741.         const html = template || from.html();
  742.         if (!html.trim()) {
  743.             throw Error('No template found for createEl. Targeted element is empty!');
  744.         }
  745.         const formattedElements = $.parseHTML(formatHTML(html, ctx));
  746.         // We can insert in a particular place
  747.         const insertBy = toFrom.insertBy || toFrom.byInv;
  748.         if (toFrom.insertFirst != null) {
  749.             if (insertBy) {
  750.                 throw Error('Invalid data-for(s) for createEl. You cannot specify both "insert-first" and "insert-by".');
  751.             }
  752.             $(to).prepend(formattedElements);
  753.             return;
  754.         }
  755.         if (insertBy) {
  756.             // Search for an element
  757.             const valueToInsert = ctx[insertBy];
  758.             if (valueToInsert == null) {
  759.                 throw Error('Insert by property (' + insertBy + ') isn\'t present in the current context! Did you for get a data-add or data-use?');
  760.             }
  761.             const children = $(to).children().toArray();
  762.             // If the insertion is inverted
  763.             let comp = function (a, b) {
  764.                 return a > b;
  765.             };
  766.             if (toFrom.insertByInv) {
  767.                 comp = function (a, b) {
  768.                     return a < b;
  769.                 };
  770.             }
  771.             const insertBefore = children.find((elem) => {
  772.                 const elemCtx = getContextData(elem);
  773.                 // This comparison should (mostly) work for strings and numbers.
  774.                 return elemCtx[toFrom.insertBy] != null && comp(elemCtx[toFrom.insertBy].toLowerCase(), valueToInsert.toLowerCase());
  775.             });
  776.             // If we find a place to insert, insert it
  777.             if (insertBefore != null) {
  778.                 $(formattedElements).id = id;
  779.                 $(formattedElements).insertBefore(insertBefore);
  780.                 return;
  781.             }
  782.         }
  783.         // Insert at end of target's children
  784.         $(to).append(formattedElements);
  785.     };
  786.     actions.updateEl = function (selected, elem, ctx, _eventHandlers) {
  787.         selected = selected || elem;
  788.         if ($(selected).length === 0) {
  789.             throw Error('Invalid updateEl action. Element to update (' + selected + ') not found.');
  790.         }
  791.         selected = $(selected);
  792.         const template = selected.attr('data-render-template');
  793.         if (template == null || $(template).length === 0) {
  794.             throw Error(
  795.                 'Invalid updateEl action. Template (' + template + ') not found. '
  796.                 + 'Does this element have a "data-render-template" attribute?'
  797.             );
  798.         }
  799.         const html = $(template).html().trim();
  800.         const oldContext = getContextData(selected[0]);
  801.         const newContext = Object.assign(oldContext, ctx);
  802.         // Render to the target
  803.         const formattedHTML = formatHTML(html, newContext)
  804.         const formattedElements = $.parseHTML(formattedHTML);
  805.         // Update the context / attributes
  806.         // As long as we're rending to the same typeof object, we can just update it
  807.         if (selected[0].type === formattedElements[0].type) {
  808.             const oldAttrs = readDataAttributes(selected[0]);
  809.             const newAttrs = readDataAttributes(formattedElements[0]);
  810.             selected[0].innerHTML = formattedElements[0].innerHTML;
  811.             // Remove Old
  812.             Object.keys(oldAttrs).forEach(function (key) {
  813.                 if (newAttrs[key] != null) {
  814.                     return;
  815.                 }
  816.                 // These keys are held over
  817.                 if (key === 'id' || key === 'data-render-template') {
  818.                     return;
  819.                 }
  820.                 selected.removeAttr(key);
  821.             });
  822.             // Add new
  823.             Object.keys(newAttrs).forEach(function (key) {
  824.                 selected.attr(key, newAttrs[key]);
  825.             });
  826.         } else { // Here we actually just replace the element
  827.             selected[0].outerHTML = formattedHTML;
  828.         }
  829.     };
  830.     actions.freshEl = function (args, elem, ctx, _eventHandlers) {
  831.         let responseOrHTML = args;
  832.         if (args[0] != null && args.length == null && typeof args !== 'string') {
  833.             responseOrHTML = args[0];
  834.         }
  835.         const html = projections.toData(responseOrHTML);
  836.         const createArgs = Object.assign(args, {template: html});
  837.         actions.createEl(createArgs, elem, ctx, _eventHandlers);
  838.     };
  839.     actions.refreshEl = function (args, elem, ctx, _eventHandlers) {
  840.         let responseOrHTML = args;
  841.         let target = elem;
  842.         if (args.select != null) {
  843.             responseOrHTML = args[0];
  844.             target = $(args.select)[0];
  845.         }
  846.         const html = projections.toData(responseOrHTML);
  847.         target.outerHTML = html;
  848.     };
  849.     actions.grayOut = function (sel, elem, ctx, _eventHandlers) {
  850.         let target = elem;
  851.         if (typeof sel === 'string') {
  852.             target = $(sel)[0] || elem;
  853.         }
  854.         $(target).addClass('grayout');
  855.         $('input, textarea, select', target).prop('disabled', true);
  856.         $('label, [role="button"]', target).addClass('disabled');
  857.     };
  858.     actions.filter = function (options, elem, ctx, _eventHandlers) {
  859.         // Get info on what should be modified
  860.         const filterData = vFilter(options, elem).setRemove;
  861.         // Set the attributes
  862.         filterData.set.forEach((s) => {
  863.             s.item.setAttribute(s.attr, s.value);
  864.         });
  865.         // Remove the attributes
  866.         filterData.remove.forEach((r) => {
  867.             r.item.removeAttribute(r.attr);
  868.         });
  869.     };
  870.     actions.sendForm = function (arg, elem, ctx, eventHandlers) {
  871.         let options = {};
  872.         if (arg != null) {
  873.             if (arg instanceof Event) {
  874.                 arg.preventDefault();
  875.             } else if (arg[0] instanceof Event && !arg.allowDefault) {
  876.                 arg.preventDefault();
  877.                 options = arg;
  878.             }
  879.         }
  880.         const method = (options.method || elem.getAttribute('method') || 'get').toLowerCase();
  881.         const action = options.action || elem.getAttribute('action') || window.location.href;
  882.         if (method === 'get') {
  883.             actions.get(action, elem, ctx, eventHandlers);
  884.         } else {
  885.             actions.post(action, elem, ctx, eventHandlers);
  886.         }
  887.         // Call secondary event handler for submit
  888.         secondaryHandler('form-sent', elem, ctx, eventHandlers)(arg);
  889.     };
  890.     actions.validateForm = function (arg, elem, ctx, eventHandlers) {
  891.         let target = elem;
  892.         if (arg != null) {
  893.             if (arg instanceof Event) {
  894.                 arg.preventDefault();
  895.             } else if (arg[0] instanceof Event && !arg.allowDefault) {
  896.                 arg.preventDefault();
  897.             }
  898.             if (typeof arg === 'string') {
  899.                 target = $(arg)[0];
  900.             } else if (arg.select != null) {
  901.                 target = $(arg.select)[0];
  902.             }
  903.         }
  904.         if (target.checkValidity()) {
  905.             secondaryHandler('valid', elem, ctx, eventHandlers)(arg);
  906.             return;
  907.         }
  908.         secondaryHandler('invalid', elem, ctx, eventHandlers)(arg);
  909.     };
  910.     actions.showFormValidation = function (arg, elem, ctx, eventHandlers) {
  911.         let target = $(elem);
  912.         if (arg != null) {
  913.             if (typeof arg === 'string') {
  914.                 target = $(arg)[0];
  915.             } else if (arg.select != null) {
  916.                 target = $(arg.select)[0];
  917.             }
  918.         }
  919.         $(target).addClass('was-validated');
  920.     };
  921.     actions.resetForm = function (arg, elem, ctx, eventHandlers) {
  922.         let target = elem;
  923.         if (arg != null) {
  924.             if (typeof arg === 'string') {
  925.                 target = $(arg)[0];
  926.             } else if (arg.select != null) {
  927.                 target = $(arg.select)[0];
  928.             }
  929.         }
  930.         if (!target || !target.reset) {
  931.             throw Error('Invalid resetForm action. Target element ' + (target && target.tagName) + ' is not a form and cannot be reset.');
  932.         }
  933.         target.reset();
  934.         $(target).removeClass('was-validated');
  935.     }
  936.     actions.fireTrigger = function (arg, elem, ctx, eventHandlers) {
  937.         const name = arg;
  938.         const triggerActions = triggers[name];
  939.         if (!triggerActions) {
  940.             throw Error('Invalid trigger action. Trigger (' + name + ') not found in: ' + Object.keys(triggers).join(', '));
  941.         }
  942.         for(let i = 0; i < triggerActions.length; i++) {
  943.             const ta= triggerActions[i];
  944.             const context = getContextData(ta.elem);
  945.             ta.action(null, ta.elem, context, ta.moreEvents);
  946.         }
  947.     }
  948.     let triggers = {};
  949.     {# /*
  950.     * Here is the code for actually showing toasts on screen
  951.     */ #}
  952.     const toastQueues = {}; // Keep track of our queues
  953.     const displayToastFor = 5000; // Five seconds
  954.     const nextToasts = function (next) {
  955.         if (next.queue.length === 0) { // Begin hiding the toast
  956.             return;
  957.         }
  958.         let message = next.queue.shift();
  959.         let className = '';
  960.         if (typeof message === 'object') {
  961.             className = message.className || '';
  962.             message = message.message;
  963.         }
  964.         next.slots--;
  965.     // create toast container on the fly
  966.         if (!toastContainer) { createToastContainer(); }
  967.         // Set message and show
  968.         const newToast = createToastElement(message, className);
  969.         $(newToast).css('opacity', 0);
  970.         $(newToast).css('display', 'block');
  971.         toastContainer.appendChild(newToast);
  972.         setTimeout(() => {
  973.             $(newToast).css('opacity', 1);
  974.         }, 100);
  975.         setTimeout(function () {
  976.             $(newToast).css('opacity', 0);
  977.             setTimeout(function () {
  978.                 toastContainer.removeChild(newToast);
  979.             }, 500); // transition time
  980.             next.slots++;
  981.             nextToasts(next);
  982.         }, displayToastFor);
  983.     };
  984.     const addToast = function (message) {
  985.         let queueName = 'info';
  986.         if (typeof message === 'object' && message.className) {
  987.             queueName = message.className;
  988.         }
  989.         if (!toastQueues[queueName]) {
  990.             toastQueues[queueName] = {
  991.                 queue: [],
  992.                 slots: 3,
  993.             };
  994.         }
  995.         const next = toastQueues[queueName];
  996.         next.queue.push(message);
  997.         // Only start if it isn't already processing enough
  998.         if (next.slots > 0) {
  999.             nextToasts(next);
  1000.         }
  1001.     };
  1002.     let toastContainer = null;
  1003.     const createToastContainer = function () {
  1004.         if (toastContainer != null) {
  1005.             return;
  1006.         }
  1007.         toastContainer = document.createElement('div');
  1008.         toastContainer.setAttribute('class', 'ajax-toast-container');
  1009.         document.body.appendChild(toastContainer);
  1010.     };
  1011.     const createToastElement = function (message, className) {
  1012.         const el = document.createElement('div');
  1013.         el.setAttribute('role', 'alert');
  1014.         el.setAttribute('aria-live', 'assertive');
  1015.         el.setAttribute('aria-atomic', 'true');
  1016.         el.setAttribute('class', 'ajax-toast ' + className);
  1017.         el.innerHTML = message;
  1018.         return el;
  1019.     };
  1020.     {#
  1021.     // As action x events need to be searched for to be bound,
  1022.     // and searching for them might cause some performance issues, 
  1023.     // only use these shortcuts, the rest can use normal syntax
  1024.     #}
  1025.     const commonPrimaryActions = [
  1026.         {event: 'click', attr: 'data-on-click-confirm', act: 'confirm'},
  1027.         {event: 'click', attr: 'data-on-click-get', act: 'get'},
  1028.         {event: 'click', attr: 'data-on-click-post', act: 'post'},
  1029.         {event: 'click', attr: 'data-on-click-add-class', act: 'addClass'},
  1030.         {event: 'click', attr: 'data-on-click-remove-class', act: 'removeClass'},
  1031.         {event: 'click', attr: 'data-on-click-toggle-class', act: 'toggleClass'},
  1032.         {event: 'click', attr: 'data-on-click-show-modal', act: 'showModal'},
  1033.         {event: 'click', attr: 'data-on-click-hide-modal', act: 'hideModal'},
  1034.         {event: 'click', attr: 'data-on-click-toggle-modal', act: 'toggleModal'},
  1035.         {event: 'click', attr: 'data-on-click-create-el', act: 'createEl'},
  1036.         {event: 'click', attr: 'data-on-click-update-el', act: 'updateEl'},
  1037.         {event: 'click', attr: 'data-on-click-fire-trigger', act: 'fireTrigger'},
  1038.         {event: 'change', attr: 'data-on-change-get', act: 'get'},
  1039.         {event: 'change', attr: 'data-on-change-post', act: 'post'},
  1040.         {event: 'change', attr: 'data-on-change-toggle-class', act: 'toggleClass'},
  1041.         {event: 'change', attr: 'data-on-change-add-class', act: 'addClass'},
  1042.         {event: 'change', attr: 'data-on-change-remove-class', act: 'removeClass'},
  1043.         {event: 'change', attr: 'data-on-change-create-el', act: 'createEl'},
  1044.         {event: 'change', attr: 'data-on-change-update-el', act: 'updateEl'},
  1045.         {event: 'change', attr: 'data-on-change-fire-trigger', act: 'fireTrigger'},
  1046.         {event: 'submit', attr: 'data-on-submit-confirm', act: 'confirm'},
  1047.     ];
  1048.     const commonSecondaryActions = [
  1049.         {event: 'response', attr: 'data-on-response-update-el', act: 'updateEl'},
  1050.         {event: 'response', attr: 'data-on-response-toast', act: 'toStyledToast|addToast'},
  1051.         {event: 'response', attr: 'data-on-response-header-toast', act: 'toHeaderNotice|toStyledToast|addToast'},
  1052.         {event: 'ajax-success', attr: 'data-on-ajax-success-fresh-el', act: 'freshEl'},
  1053.         {event: 'ajax-success', attr: 'data-on-ajax-success-refresh-el', act: 'refreshEl'},
  1054.         {event: 'ajax-success', attr: 'data-on-ajax-success-fire-trigger', act: 'fireTrigger'},
  1055.         {event: 'ajax-fail', attr: 'data-on-ajax-fail-toast', act: 'toErrorToast|addToast'},
  1056.     ];
  1057.     // Events that can happen as a result of something else
  1058.     const secondaryEvents = ['response', 'ajax-success', 'ajax-error', 'form-sent', 'debounced', 'confirmed', 'not-confirmed', 'valid', 'invalid'];
  1059.     const allSecondaryActions = commonSecondaryActions.concat(secondaryEvents.map(function (eventName) {
  1060.         return {event: eventName, attr: 'data-on-' + eventName, act: null};
  1061.     }));
  1062.     {# /* 
  1063.     * Parses the secondary events from the attributes and turns them 
  1064.     * map a functions that can be called
  1065.     * e.g. data-on-response="get" -> { 'on-response': function(...) { ... } }
  1066.     */ #}
  1067.     const getSecondaryEvents = function (elem) {
  1068.         const attrs = readDataAttributes(elem);
  1069.         return allSecondaryActions.reduce(function (eventMap, eventActionPair) {
  1070.             if (attrs[eventActionPair.attr] == null) {
  1071.                 return eventMap;
  1072.             }
  1073.             const eventName = eventActionPair.event;
  1074.             let actionNames = eventActionPair.act;
  1075.             // If not a shortcut
  1076.             if (!actionNames) {
  1077.                 actionNames = attrs[eventActionPair.attr];
  1078.             }
  1079.             if (!actionNames) {
  1080.                 return eventMap;
  1081.             }
  1082.             const parsed = parseAction(actionNames, actions, eventActionPair.act && eventActionPair.attr);
  1083.             let newAction = parsed;
  1084.             const oldAction = eventMap[eventName];
  1085.             if (oldAction) {
  1086.                 newAction = (e, elem, ctx, eventHandlers) => {
  1087.                     parsed(e, elem, ctx, eventHandlers);
  1088.                     oldAction(e, elem, ctx, eventHandlers);
  1089.                 };
  1090.             }
  1091.             eventMap[eventName] = newAction;
  1092.             return eventMap;
  1093.         }, {});
  1094.     };
  1095.     const attrActionEvents = commonPrimaryActions.concat(
  1096.         events.map(function (e) {
  1097.             // Formal def (e.g. data-on-click="get" data-for-get="...")
  1098.             return {event: e, attr: 'data-on-' + e, act: null};
  1099.         })
  1100.     );
  1101.     const primarySel = attrActionEvents.map(function (t) {
  1102.         return '[' + t.attr + ']';
  1103.     }).concat('[data-ajax]').join(', ');
  1104.     let mutationObserver;
  1105.     const bindToDOMChanges = function (onNodesAdded) {
  1106.         if (mutationObserver) {
  1107.             mutationObserver.disconnect();
  1108.             mutationObserver = null;
  1109.         }
  1110.         // Options for the observer (which mutations to observe)
  1111.         const config = {attributes: false, childList: true, subtree: true};
  1112.         // Callback function to execute when mutations are observed
  1113.         const callback = function (mutationsList, observer) {
  1114.             let changed = [];
  1115.             for (let i = 0; i < mutationsList.length; i++) {
  1116.                 const mutation = mutationsList[i];
  1117.                 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  1118.                     changed = changed.concat(
  1119.                         $(mutation.addedNodes).add(primarySel, mutation.addedNodes).toArray()
  1120.                     );
  1121.                 }
  1122.             }
  1123.             // Remove duplicates
  1124.             changed = changed.filter(function (elem, index, self) {
  1125.                 return self.indexOf(elem) === index;
  1126.             });
  1127.             onNodesAdded(changed);
  1128.         };
  1129.         // Create an observer instance linked to the callback function
  1130.         mutationObserver = new MutationObserver(callback);
  1131.         // Start observing the target node for configured mutations
  1132.         mutationObserver.observe(document, config);
  1133.     };
  1134.     const inputTagNames = ['TEXTAREA', 'INPUT', 'SELECT']
  1135.     const bindToEvents = function (elements) {
  1136.         // By default don't create the toast element
  1137.         elements = $(elements);
  1138.         elements.each(function (_index, elem) {
  1139.             // We're only able to bind to elements
  1140.             if (elem.nodeType !== Node.ELEMENT_NODE) {
  1141.                 return;
  1142.             }
  1143.             const moreEvents = getSecondaryEvents(elem);
  1144.             
  1145.             // Iterate through action-event pairings, find elements, and bind to them
  1146.             for (let i = 0; i < attrActionEvents.length; i++) {
  1147.                 const attrActEv = attrActionEvents[i];
  1148.                 // If the element doesn't have the attribute, it's a pass
  1149.                 if (!elem.hasAttribute(attrActEv.attr)) {
  1150.                     continue;
  1151.                 }
  1152.                 const eventName = attrActEv.event;
  1153.                 const attrName = attrActEv.attr;
  1154.                 let actionNames = attrActEv.act;
  1155.                 // If this is a "normal" binding (e.g. data-on-change="post"), lookup the action name
  1156.                 if (actionNames == null) {
  1157.                     actionNames = elem.getAttribute(attrName);
  1158.                 }
  1159.                 // Action not found
  1160.                 if (actionNames == null) {
  1161.                     console.error("No action found to call when binding to event: " + attrName, elem);
  1162.                     continue;
  1163.                 }
  1164.                 const parsedAction = parseAction(actionNames, actions, attrActEv.act && attrName);
  1165.                 // If someone tries to listen to changes in an empty <span>, error 
  1166.                 // If someone tries to listen to changes in a <span> with <inputs> inside,
  1167.                 // bind to each input
  1168.                 let elems = $(elem);
  1169.                 let formContext = false;
  1170.                 if (['change', 'input'].includes(eventName) && !inputTagNames.includes(elem.tagName)) {
  1171.                     elems = $(inputTagNames.join(', '), elem);
  1172.                     if (elems.length === 0) {
  1173.                         console.error('Attempted to bind to input event:',
  1174.                             eventName + '.',
  1175.                             'With an element (',
  1176.                             elem.tagName,
  1177.                             ') that isn\'t an input, and does\'t have any child inputs.',
  1178.                             elem);
  1179.                         return;
  1180.                     }
  1181.                     formContext = true;
  1182.                 }
  1183.                 // We might listen to more than one element
  1184.                 elems.each(function (_in, childElem) {
  1185.                     // Bind to a primary event - add an event listener
  1186.                     childElem.addEventListener(eventName, function (e) {
  1187.                         let context = getContextData(elem);
  1188.                         // If we've bound to a different element that was included in the attribute
  1189.                         // Apply that child's properties on top of ours
  1190.                         if (childElem !== elem) {
  1191.                             const addContext = getContextData(childElem);
  1192.                             context = Object.assign(context, addContext);
  1193.                         }
  1194.                         parsedAction(e, elem, context, moreEvents);
  1195.                     });
  1196.                     // By default remove browser cached values on input
  1197.                     if ((inputTagNames.includes(childElem.tagName))
  1198.                         && !childElem.hasAttribute('data-no-reset')
  1199.                         && !elem.hasAttribute('data-no-reset')) {
  1200.                         switch (childElem.tagName) {
  1201.                             case 'SELECT':
  1202.                                 let index = $('option', childElem).toArray().findIndex((t) => t.hasAttribute('selected'));
  1203.                                 if (index < 0) {
  1204.                                     index = null;
  1205.                                 }
  1206.                                 childElem.selectedIndex = index;
  1207.                                 break;
  1208.                             default:
  1209.                                 if (['checkbox', 'radio'].includes(childElem.type)) {
  1210.                                     childElem.checked = childElem.hasAttribute('checked');
  1211.                                 } else {
  1212.                                     childElem.value = childElem.getAttribute('value');
  1213.                                 }
  1214.                                 break;
  1215.                         }
  1216.                     }
  1217.                 });
  1218.                 if ('FORM' === elem.tagName && !elem.hasAttribute('data-no-reset')) {
  1219.                     elem.reset();
  1220.                 }
  1221.             } 
  1222.             
  1223.             // Bind to triggers, they have 'dynamic' names
  1224.             const attrs = readDataAttributes(elem);
  1225.             const newTriggers = Object.keys(attrs)
  1226.                 .filter((name) => name.startsWith('data-on-trigger-'))
  1227.                 .map((attrName) => {
  1228.                     const parsed = parseAction(elem.getAttribute(attrName), actions, attrName);
  1229.                     const name = attrName.replace(/^data\-on\-trigger\-?/, '') || 'default';
  1230.                     return {
  1231.                         name: name,
  1232.                         action: parsed,
  1233.                         elem: elem,
  1234.                         moreEvents: moreEvents
  1235.                     };
  1236.                 });
  1237.             for (let i = 0; i < newTriggers.length; i++) {
  1238.                 const trigger = newTriggers[i];
  1239.                 const actionList = triggers[trigger.name] || [];
  1240.                 triggers[trigger.name] = actionList.concat(trigger);
  1241.             }
  1242.         });
  1243.     };
  1244.     // Bind to events in the whole document
  1245.     bindToEvents($(primarySel));
  1246.     // When there's additions to the DOM, bind to those too
  1247.     bindToDOMChanges(bindToEvents);
  1248.     // Export to allow testing/debugging
  1249.     window.ajaxHelpers = {
  1250.         kebabToSnake: kebabToSnake,
  1251.         kebabToCamel: kebabToCamel,
  1252.         camelToKebab: camelToKebab,
  1253.         bindToEvents: bindToEvents,
  1254.         toastContainer: toastContainer,
  1255.         applyProjections: applyProjections,
  1256.         actionArgs: actionArgs,
  1257.         vFilter: vFilter,
  1258.         filterAttr: filterAttr,
  1259.         projections: projections,
  1260.         formatURL: formatURL,
  1261.         formatHTML: formatHTML,
  1262.         getContextData: getContextData,
  1263.         readDataAttributes: readDataAttributes,
  1264.         createToastContainer: createToastContainer,
  1265.         createToastElement: createToastElement,
  1266.         addToast: addToast,
  1267.         toastClasses: toastClasses,
  1268.         triggers: triggers
  1269.     };
  1270.     // Can be used to post changes to single fields to the database. Made to post single fields like in allmeda_pages_set_toggle_ajax_with_data, also compatible with any ajax responding paths.
  1271.     function ajaxPostUpdate(pathUrl, id, field, action, sendToast = true) {
  1272.         $.ajax({
  1273.             url: pathUrl,
  1274.             method: 'Post',
  1275.             data: {
  1276.                 'id' : id,
  1277.                 'field' : field,
  1278.                 'action' : action,
  1279.             },
  1280.             headers: { 'Accept': 'application/json, text/plain' }
  1281.         }).done((data, textStatus, request) => {
  1282.             // Typical jQuery response (data, textStatus, request)
  1283.             if(sendToast) addToast(projections.toStyledToast([data, textStatus, request]));
  1284.             if (data && data.success && data.redirect) {
  1285.                 window.location.href = data.redirect;
  1286.             }
  1287.         })
  1288.             .fail((request, textStatus, errorThrown) => {
  1289.                 // Typical jQuery response (request, textStatus, errorThrown)
  1290.                if(sendToast) addToast(projections.toStyledToast([request, textStatus, errorThrown]));
  1291.             })
  1292.        if(sendToast) createToastContainer();
  1293.     }
  1294. </script>