var $ = jQuery.noConflict();
var loc = window.history.location || window.location;
var compiled = {};

/* -------------------------------------------------------------- */
/*
 * jQuery throttle / debounce - v1.1 - 3/7/2010
 * http://benalman.com/projects/jquery-throttle-debounce-plugin/
 * 
 * Copyright (c) 2010 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */
(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);
/* -------------------------------------------------------------- */

/*!
 * History API JavaScript Library v4.2.10
 *
 * Support: IE8+, FF3+, Opera 9+, Safari, Chrome and other
 *
 * Copyright 2011-2018, Dmitrii Pakhtinov ( spb.piksel@gmail.com )
 *
 * http://spb-piksel.ru/
 *
 * MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Update: 2018-04-15 13:54
 */
(function(factory) {
  if (typeof define === 'function' && define['amd']) {
    if (typeof requirejs !== 'undefined') {
      // https://github.com/devote/HTML5-History-API/issues/73
      var r = requirejs,
          rndKey = '[history' + (new Date()).getTime() + ']';
      var onError = r['onError'];
      factory.toString = function() {
        return rndKey;
      };
      r['onError'] = function(err) {
        if (err.message.indexOf(rndKey) === -1) {
          onError.call(r, err);
        }
      };
    }
    define([], factory);
  }
  // commonJS support
  if (typeof exports === "object" && typeof module !== "undefined") {
    module['exports'] = factory();
  } else {
    // execute anyway
    return factory();
  }
})(function() {
  // Define global variable
  var global = (typeof window === 'object' ? window : this) || {};
  // Prevent the code from running if there is no window.history object or library already loaded
  if (!global.history || "emulate" in global.history) return global.history;
  // symlink to document
  var document = global.document;
  // HTML element
  var documentElement = document.documentElement;
  // symlink to constructor of Object
  var Object = global['Object'];
  // symlink to JSON Object
  var JSON = global['JSON'];
  // symlink to instance object of 'Location'
  var windowLocation = global.location;
  // symlink to instance object of 'History'
  var windowHistory = global.history;
  // new instance of 'History'. The default is a reference to the original object instance
  var historyObject = windowHistory;
  // symlink to method 'history.pushState'
  var historyPushState = windowHistory.pushState;
  // symlink to method 'history.replaceState'
  var historyReplaceState = windowHistory.replaceState;
  // if the browser supports HTML5-History-API
  var isSupportHistoryAPI = isSupportHistoryAPIDetect();
  // verifies the presence of an object 'state' in interface 'History'
  var isSupportStateObjectInHistory = 'state' in windowHistory;
  // symlink to method 'Object.defineProperty'
  var defineProperty = Object.defineProperty;
  // new instance of 'Location', for IE8 will use the element HTMLAnchorElement, instead of pure object
  var locationObject = redefineProperty({}, 't') ? {} : document.createElement('a');
  // prefix for the names of events
  var eventNamePrefix = '';
  // String that will contain the name of the method
  var addEventListenerName = global.addEventListener ? 'addEventListener' : (eventNamePrefix = 'on') && 'attachEvent';
  // String that will contain the name of the method
  var removeEventListenerName = global.removeEventListener ? 'removeEventListener' : 'detachEvent';
  // String that will contain the name of the method
  var dispatchEventName = global.dispatchEvent ? 'dispatchEvent' : 'fireEvent';
  // reference native methods for the events
  var addEvent = maybeBindToGlobal(global[addEventListenerName]);
  var removeEvent = maybeBindToGlobal(global[removeEventListenerName]);
  var dispatch = maybeBindToGlobal(global[dispatchEventName]);
  // default settings
  var settings = {"basepath": '/', "redirect": 0, "type": '/', "init": 0};
  // key for the sessionStorage
  var sessionStorageKey = '__historyAPI__';
  // Anchor Element for parseURL function
  var anchorElement = document.createElement('a');
  // last URL before change to new URL
  var lastURL = windowLocation.href;
  // Control URL, need to fix the bug in Opera
  var checkUrlForPopState = '';
  // for fix on Safari 8
  var triggerEventsInWindowAttributes = 1;
  // trigger event 'onpopstate' on page load
  var isFireInitialState = false;
  // if used history.location of other code
  var isUsedHistoryLocationFlag = 0;
  // store a list of 'state' objects in the current session
  var stateStorage = {};
  // in this object will be stored custom handlers
  var eventsList = {};
  // stored last title
  var lastTitle = document.title;
  // store a custom origin
  var customOrigin;

  /**
   * Properties that will be replaced in the global
   * object 'window', to prevent conflicts
   *
   * @type {Object}
   */
  var eventsDescriptors = {
    "onhashchange": null,
    "onpopstate": null
  };

  /**
   * Fix for Chrome in iOS
   * See https://github.com/devote/HTML5-History-API/issues/29
   */
  var fastFixChrome = function(method, args) {
    var isNeedFix = global.history !== windowHistory;
    if (isNeedFix) {
      global.history = windowHistory;
    }
    method.apply(windowHistory, args);
    if (isNeedFix) {
      global.history = historyObject;
    }
  };

  /**
   * Properties that will be replaced/added to object
   * 'window.history', includes the object 'history.location',
   * for a complete the work with the URL address
   *
   * @type {Object}
   */
  var historyDescriptors = {
    /**
     * Setting library initialization
     *
     * @param {null|String} [basepath] The base path to the site; defaults to the root "/".
     * @param {null|String} [type] Substitute the string after the anchor; by default "/".
     * @param {null|Boolean} [redirect] Enable link translation.
     */
    "setup": function(basepath, type, redirect) {
      settings["basepath"] = ('' + (basepath == null ? settings["basepath"] : basepath))
        .replace(/(?:^|\/)[^\/]*$/, '/');
      settings["type"] = type == null ? settings["type"] : type;
      settings["redirect"] = redirect == null ? settings["redirect"] : !!redirect;
    },
    /**
     * @namespace history
     * @param {String} [type]
     * @param {String} [basepath]
     */
    "redirect": function(type, basepath) {
      historyObject['setup'](basepath, type);
      basepath = settings["basepath"];
      if (global.top == global.self) {
        var relative = parseURL(null, false, true)._relative;
        var path = windowLocation.pathname + windowLocation.search;
        if (isSupportHistoryAPI) {
          path = path.replace(/([^\/])$/, '$1/');
          if (relative != basepath && (new RegExp("^" + basepath + "$", "i")).test(path)) {
            windowLocation.replace(relative);
          }
        } else if (path != basepath) {
          path = path.replace(/([^\/])\?/, '$1/?');
          if ((new RegExp("^" + basepath, "i")).test(path)) {
            windowLocation.replace(basepath + '#' + path.
              replace(new RegExp("^" + basepath, "i"), settings["type"]) + windowLocation.hash);
          }
        }
      }
    },
    /**
     * The method adds a state object entry
     * to the history.
     *
     * @namespace history
     * @param {Object} state
     * @param {string} title
     * @param {string} [url]
     */
    pushState: function(state, title, url) {
      var t = document.title;
      if (lastTitle != null) {
        document.title = lastTitle;
      }
      historyPushState && fastFixChrome(historyPushState, arguments);
      changeState(state, url);
      document.title = t;
      lastTitle = title;
    },
    /**
     * The method updates the state object,
     * title, and optionally the URL of the
     * current entry in the history.
     *
     * @namespace history
     * @param {Object} state
     * @param {string} title
     * @param {string} [url]
     */
    replaceState: function(state, title, url) {
      var t = document.title;
      if (lastTitle != null) {
        document.title = lastTitle;
      }
      delete stateStorage[windowLocation.href];
      historyReplaceState && fastFixChrome(historyReplaceState, arguments);
      changeState(state, url, true);
      document.title = t;
      lastTitle = title;
    },
    /**
     * Object 'history.location' is similar to the
     * object 'window.location', except that in
     * HTML4 browsers it will behave a bit differently
     *
     * @namespace history
     */
    "location": {
      set: function(value) {
        if (isUsedHistoryLocationFlag === 0) isUsedHistoryLocationFlag = 1;
        global.location = value;
      },
      get: function() {
        if (isUsedHistoryLocationFlag === 0) isUsedHistoryLocationFlag = 1;
        return locationObject;
      }
    },
    /**
     * A state object is an object representing
     * a user interface state.
     *
     * @namespace history
     */
    "state": {
      get: function() {
        if (typeof stateStorage[windowLocation.href] === 'object') {
          return JSON.parse(JSON.stringify(stateStorage[windowLocation.href]));
        } else if(typeof stateStorage[windowLocation.href] !== 'undefined') {
          return stateStorage[windowLocation.href];
        } else {
          return null;
        }
      }
    }
  };

  /**
   * Properties for object 'history.location'.
   * Object 'history.location' is similar to the
   * object 'window.location', except that in
   * HTML4 browsers it will behave a bit differently
   *
   * @type {Object}
   */
  var locationDescriptors = {
    /**
     * Navigates to the given page.
     *
     * @namespace history.location
     */
    assign: function(url) {
      if (!isSupportHistoryAPI && ('' + url).indexOf('#') === 0) {
        changeState(null, url);
      } else {
        windowLocation.assign(url);
      }
    },
    /**
     * Reloads the current page.
     *
     * @namespace history.location
     */
    reload: function(flag) {
      windowLocation.reload(flag);
    },
    /**
     * Removes the current page from
     * the session history and navigates
     * to the given page.
     *
     * @namespace history.location
     */
    replace: function(url) {
      if (!isSupportHistoryAPI && ('' + url).indexOf('#') === 0) {
        changeState(null, url, true);
      } else {
        windowLocation.replace(url);
      }
    },
    /**
     * Returns the current page's location.
     *
     * @namespace history.location
     */
    toString: function() {
      return this.href;
    },
    /**
     * Returns the current origin.
     *
     * @namespace history.location
     */
    "origin": {
      get: function() {
        if (customOrigin !== void 0) {
          return customOrigin;
        }
        if (!windowLocation.origin) {
          return windowLocation.protocol + "//" + windowLocation.hostname + (windowLocation.port ? ':' + windowLocation.port: '');
        }
        return windowLocation.origin;
      },
      set: function(value) {
        customOrigin = value;
      }
    },
    /**
     * Returns the current page's location.
     * Can be set, to navigate to another page.
     *
     * @namespace history.location
     */
    "href": isSupportHistoryAPI ? null : {
      get: function() {
        return parseURL()._href;
      }
    },
    /**
     * Returns the current page's protocol.
     *
     * @namespace history.location
     */
    "protocol": null,
    /**
     * Returns the current page's host and port number.
     *
     * @namespace history.location
     */
    "host": null,
    /**
     * Returns the current page's host.
     *
     * @namespace history.location
     */
    "hostname": null,
    /**
     * Returns the current page's port number.
     *
     * @namespace history.location
     */
    "port": null,
    /**
     * Returns the current page's path only.
     *
     * @namespace history.location
     */
    "pathname": isSupportHistoryAPI ? null : {
      get: function() {
        return parseURL()._pathname;
      }
    },
    /**
     * Returns the current page's search
     * string, beginning with the character
     * '?' and to the symbol '#'
     *
     * @namespace history.location
     */
    "search": isSupportHistoryAPI ? null : {
      get: function() {
        return parseURL()._search;
      }
    },
    /**
     * Returns the current page's hash
     * string, beginning with the character
     * '#' and to the end line
     *
     * @namespace history.location
     */
    "hash": isSupportHistoryAPI ? null : {
      set: function(value) {
        changeState(null, ('' + value).replace(/^(#|)/, '#'), false, lastURL);
      },
      get: function() {
        return parseURL()._hash;
      }
    }
  };

  /**
   * Just empty function
   *
   * @return void
   */
  function emptyFunction() {
    // dummy
  }

  /**
   * Prepares a parts of the current or specified reference for later use in the library
   *
   * @param {string} [href]
   * @param {boolean} [isWindowLocation]
   * @param {boolean} [isNotAPI]
   * @return {Object}
   */
  function parseURL(href, isWindowLocation, isNotAPI) {
    var re = /(?:([a-zA-Z0-9\-]+\:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/;
    if (href != null && href !== '' && !isWindowLocation) {
      var current = parseURL(),
          base = document.getElementsByTagName('base')[0];
      if (!isNotAPI && base && base.getAttribute('href')) {
        // Fix for IE ignoring relative base tags.
        // See http://stackoverflow.com/questions/3926197/html-base-tag-and-local-folder-path-with-internet-explorer
        base.href = base.href;
        current = parseURL(base.href, null, true);
      }
      var _pathname = current._pathname, _protocol = current._protocol;
      // convert to type of string
      href = '' + href;
      // convert relative link to the absolute
      href = /^(?:\w+\:)?\/\//.test(href) ? href.indexOf("/") === 0
        ? _protocol + href : href : _protocol + "//" + current._host + (
        href.indexOf("/") === 0 ? href : href.indexOf("?") === 0
          ? _pathname + href : href.indexOf("#") === 0
          ? _pathname + current._search + href : _pathname.replace(/[^\/]+$/g, '') + href
        );
    } else {
      href = isWindowLocation ? href : windowLocation.href;
      // if current browser not support History-API
      if (!isSupportHistoryAPI || isNotAPI) {
        // get hash fragment
        href = href.replace(/^[^#]*/, '') || "#";
        // form the absolute link from the hash
        // https://github.com/devote/HTML5-History-API/issues/50
        href = windowLocation.protocol.replace(/:.*$|$/, ':') + '//' + windowLocation.host + settings['basepath']
          + href.replace(new RegExp("^#[\/]?(?:" + settings["type"] + ")?"), "");
      }
    }
    // that would get rid of the links of the form: /../../
    anchorElement.href = href;
    // decompose the link in parts
    var result = re.exec(anchorElement.href);
    // host name with the port number
    var host = result[2] + (result[3] ? ':' + result[3] : '');
    // folder
    var pathname = result[4] || '/';
    // the query string
    var search = result[5] || '';
    // hash
    var hash = result[6] === '#' ? '' : (result[6] || '');
    // relative link, no protocol, no host
    var relative = pathname + search + hash;
    // special links for set to hash-link, if browser not support History API
    var nohash = pathname.replace(new RegExp("^" + settings["basepath"], "i"), settings["type"]) + search;
    // result
    return {
      _href: result[1] + '//' + host + relative,
      _protocol: result[1],
      _host: host,
      _hostname: result[2],
      _port: result[3] || '',
      _pathname: pathname,
      _search: search,
      _hash: hash,
      _relative: relative,
      _nohash: nohash,
      _special: nohash + hash
    }
  }

  /**
   * Detect HistoryAPI support while taking into account false positives.
   * Based on https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
   */
  function isSupportHistoryAPIDetect(){
    var ua = global.navigator.userAgent;
    // We only want Android 2 and 4.0, stock browser, and not Chrome which identifies
    // itself as 'Mobile Safari' as well, nor Windows Phone (issue #1471).
    if ((ua.indexOf('Android 2.') !== -1 ||
      (ua.indexOf('Android 4.0') !== -1)) &&
      ua.indexOf('Mobile Safari') !== -1 &&
      ua.indexOf('Chrome') === -1 &&
      ua.indexOf('Windows Phone') === -1)
    {
      return false;
    }
    // Return the regular check
    return !!historyPushState;
  }

  /**
   * This method attempts to bind a function to global.
   *
   * @param {Function} [func] The function to be bound
   * @return {Function} Returns the bound function or func
   */
  function maybeBindToGlobal(func) {
    if (func && global &&
        global['EventTarget'] &&
        typeof global['EventTarget'].prototype.addEventListener === 'function' &&
        typeof func.bind === 'function') {
      return func.bind(global);
    }
    return func;
  }

  /**
   * Initializing storage for the custom state's object
   */
  function storageInitialize() {
    var sessionStorage;
    /**
     * sessionStorage throws error when cookies are disabled
     * Chrome content settings when running the site in a Facebook IFrame.
     * see: https://github.com/devote/HTML5-History-API/issues/34
     * and: http://stackoverflow.com/a/12976988/669360
     */
    try {
      sessionStorage = global['sessionStorage'];
      sessionStorage.setItem(sessionStorageKey + 't', '1');
      sessionStorage.removeItem(sessionStorageKey + 't');
    } catch(_e_) {
      sessionStorage = {
        getItem: function(key) {
          var cookie = document.cookie.split(key + "=");
          return cookie.length > 1 && cookie.pop().split(";").shift() || 'null';
        },
        setItem: function(key, value) {
          var state = {};
          // insert one current element to cookie
          if (state[windowLocation.href] = historyObject.state) {
            document.cookie = key + '=' + JSON.stringify(state);
          }
        }
      }
    }

    try {
      // get cache from the storage in browser
      stateStorage = JSON.parse(sessionStorage.getItem(sessionStorageKey)) || {};
    } catch(_e_) {
      stateStorage = {};
    }

    // hang up the event handler to event unload page
    addEvent(eventNamePrefix + 'unload', function() {
      // save current state's object
      sessionStorage.setItem(sessionStorageKey, JSON.stringify(stateStorage));
    }, false);
  }

  /**
   * This method is implemented to override the built-in(native)
   * properties in the browser, unfortunately some browsers are
   * not allowed to override all the properties and even add.
   * For this reason, this was written by a method that tries to
   * do everything necessary to get the desired result.
   *
   * @param {Object} object The object in which will be overridden/added property
   * @param {String} prop The property name to be overridden/added
   * @param {Object} [descriptor] An object containing properties set/get
   * @param {Function} [onWrapped] The function to be called when the wrapper is created
   * @return {Object|Boolean} Returns an object on success, otherwise returns false
   */
  function redefineProperty(object, prop, descriptor, onWrapped) {
    var testOnly = 0;
    // test only if descriptor is undefined
    if (!descriptor) {
      descriptor = {set: emptyFunction};
      testOnly = 1;
    }
    // variable will have a value of true the success of attempts to set descriptors
    var isDefinedSetter = !descriptor.set;
    var isDefinedGetter = !descriptor.get;
    // for tests of attempts to set descriptors
    var test = {configurable: true, set: function() {
      isDefinedSetter = 1;
    }, get: function() {
      isDefinedGetter = 1;
    }};

    try {
      // testing for the possibility of overriding/adding properties
      defineProperty(object, prop, test);
      // running the test
      object[prop] = object[prop];
      // attempt to override property using the standard method
      defineProperty(object, prop, descriptor);
    } catch(_e_) {
    }

    // If the variable 'isDefined' has a false value, it means that need to try other methods
    if (!isDefinedSetter || !isDefinedGetter) {
      // try to override/add the property, using deprecated functions
      if (object.__defineGetter__) {
        // testing for the possibility of overriding/adding properties
        object.__defineGetter__(prop, test.get);
        object.__defineSetter__(prop, test.set);
        // running the test
        object[prop] = object[prop];
        // attempt to override property using the deprecated functions
        descriptor.get && object.__defineGetter__(prop, descriptor.get);
        descriptor.set && object.__defineSetter__(prop, descriptor.set);
      }

      // Browser refused to override the property, using the standard and deprecated methods
      if (!isDefinedSetter || !isDefinedGetter) {
        if (testOnly) {
          return false;
        } else if (object === global) {
          // try override global properties
          try {
            // save original value from this property
            var originalValue = object[prop];
            // set null to built-in(native) property
            object[prop] = null;
          } catch(_e_) {
          }
          // This rule for Internet Explorer 8
          if ('execScript' in global) {
            /**
             * to IE8 override the global properties using
             * VBScript, declaring it in global scope with
             * the same names.
             */
            global['execScript']('Public ' + prop, 'VBScript');
            global['execScript']('var ' + prop + ';', 'JavaScript');
          } else {
            try {
              /**
               * This hack allows to override a property
               * with the set 'configurable: false', working
               * in the hack 'Safari' to 'Mac'
               */
              defineProperty(object, prop, {value: emptyFunction});
            } catch(_e_) {
              if (prop === 'onpopstate') {
                /**
                 * window.onpopstate fires twice in Safari 8.0.
                 * Block initial event on window.onpopstate
                 * See: https://github.com/devote/HTML5-History-API/issues/69
                 */
                addEvent('popstate', descriptor = function() {
                  removeEvent('popstate', descriptor, false);
                  var onpopstate = object.onpopstate;
                  // cancel initial event on attribute handler
                  object.onpopstate = null;
                  setTimeout(function() {
                    // restore attribute value after short time
                    object.onpopstate = onpopstate;
                  }, 1);
                }, false);
                // cancel trigger events on attributes in object the window
                triggerEventsInWindowAttributes = 0;
              }
            }
          }
          // set old value to new variable
          object[prop] = originalValue;

        } else {
          // the last stage of trying to override the property
          try {
            try {
              // wrap the object in a new empty object
              var temp = Object.create(object);
              defineProperty(Object.getPrototypeOf(temp) === object ? temp : object, prop, descriptor);
              for(var key in object) {
                // need to bind a function to the original object
                if (typeof object[key] === 'function') {
                  temp[key] = object[key].bind(object);
                }
              }
              try {
                // to run a function that will inform about what the object was to wrapped
                onWrapped.call(temp, temp, object);
              } catch(_e_) {
              }
              object = temp;
            } catch(_e_) {
              // sometimes works override simply by assigning the prototype property of the constructor
              defineProperty(object.constructor.prototype, prop, descriptor);
            }
          } catch(_e_) {
            // all methods have failed
            return false;
          }
        }
      }
    }

    return object;
  }

  /**
   * Adds the missing property in descriptor
   *
   * @param {Object} object An object that stores values
   * @param {String} prop Name of the property in the object
   * @param {Object|null} descriptor Descriptor
   * @return {Object} Returns the generated descriptor
   */
  function prepareDescriptorsForObject(object, prop, descriptor) {
    descriptor = descriptor || {};
    // the default for the object 'location' is the standard object 'window.location'
    object = object === locationDescriptors ? windowLocation : object;
    // setter for object properties
    descriptor.set = (descriptor.set || function(value) {
      object[prop] = value;
    });
    // getter for object properties
    descriptor.get = (descriptor.get || function() {
      return object[prop];
    });
    return descriptor;
  }

  /**
   * Wrapper for the methods 'addEventListener/attachEvent' in the context of the 'window'
   *
   * @param {String} event The event type for which the user is registering
   * @param {Function} listener The method to be called when the event occurs.
   * @param {Boolean} capture If true, capture indicates that the user wishes to initiate capture.
   * @return void
   */
  function addEventListener(event, listener, capture) {
    if (event in eventsList) {
      // here stored the event listeners 'popstate/hashchange'
      eventsList[event].push(listener);
    } else {
      // FireFox support non-standart four argument aWantsUntrusted
      // https://github.com/devote/HTML5-History-API/issues/13
      if (arguments.length > 3) {
        addEvent(event, listener, capture, arguments[3]);
      } else {
        addEvent(event, listener, capture);
      }
    }
  }

  /**
   * Wrapper for the methods 'removeEventListener/detachEvent' in the context of the 'window'
   *
   * @param {String} event The event type for which the user is registered
   * @param {Function} listener The parameter indicates the Listener to be removed.
   * @param {Boolean} capture Was registered as a capturing listener or not.
   * @return void
   */
  function removeEventListener(event, listener, capture) {
    var list = eventsList[event];
    if (list) {
      for(var i = list.length; i--;) {
        if (list[i] === listener) {
          list.splice(i, 1);
          break;
        }
      }
    } else {
      removeEvent(event, listener, capture);
    }
  }

  /**
   * Wrapper for the methods 'dispatchEvent/fireEvent' in the context of the 'window'
   *
   * @param {Event|String} event Instance of Event or event type string if 'eventObject' used
   * @param {*} [eventObject] For Internet Explorer 8 required event object on this argument
   * @return {Boolean} If 'preventDefault' was called the value is false, else the value is true.
   */
  function dispatchEvent(event, eventObject) {
    var eventType = ('' + (typeof event === "string" ? event : event.type)).replace(/^on/, '');
    var list = eventsList[eventType];
    if (list) {
      // need to understand that there is one object of Event
      eventObject = typeof event === "string" ? eventObject : event;
      if (eventObject.target == null) {
        // need to override some of the properties of the Event object
        for(var props = ['target', 'currentTarget', 'srcElement', 'type']; event = props.pop();) {
          // use 'redefineProperty' to override the properties
          eventObject = redefineProperty(eventObject, event, {
            get: event === 'type' ? function() {
              return eventType;
            } : function() {
              return global;
            }
          });
        }
      }
      if (triggerEventsInWindowAttributes) {
        // run function defined in the attributes 'onpopstate/onhashchange' in the 'window' context
        ((eventType === 'popstate' ? global.onpopstate : global.onhashchange)
          || emptyFunction).call(global, eventObject);
      }
      // run other functions that are in the list of handlers
      for(var i = 0, len = list.length; i < len; i++) {
        list[i].call(global, eventObject);
      }
      return true;
    } else {
      return dispatch(event, eventObject);
    }
  }

  /**
   * dispatch current state event
   */
  function firePopState() {
    var o = document.createEvent ? document.createEvent('Event') : document.createEventObject();
    if (o.initEvent) {
      o.initEvent('popstate', false, false);
    } else {
      o.type = 'popstate';
    }
    o.state = historyObject.state;
    // send a newly created events to be processed
    dispatchEvent(o);
  }

  /**
   * fire initial state for non-HTML5 browsers
   */
  function fireInitialState() {
    if (isFireInitialState) {
      isFireInitialState = false;
      firePopState();
    }
  }

  /**
   * Change the data of the current history for HTML4 browsers
   *
   * @param {Object} state
   * @param {string} [url]
   * @param {Boolean} [replace]
   * @param {string} [lastURLValue]
   * @return void
   */
  function changeState(state, url, replace, lastURLValue) {
    if (!isSupportHistoryAPI) {
      // if not used implementation history.location
      if (isUsedHistoryLocationFlag === 0) isUsedHistoryLocationFlag = 2;
      // normalization url
      var urlObject = parseURL(url, isUsedHistoryLocationFlag === 2 && ('' + url).indexOf("#") !== -1);
      // if current url not equal new url
      if (urlObject._relative !== parseURL()._relative) {
        // if empty lastURLValue to skip hash change event
        lastURL = lastURLValue;
        if (replace) {
          // only replace hash, not store to history
          windowLocation.replace("#" + urlObject._special);
        } else {
          // change hash and add new record to history
          windowLocation.hash = urlObject._special;
        }
      }
    } else {
      lastURL = windowLocation.href;
    }
    if (!isSupportStateObjectInHistory && state) {
      stateStorage[windowLocation.href] = state;
    }
    isFireInitialState = false;
  }

  /**
   * Event handler function changes the hash in the address bar
   *
   * @param {Event} event
   * @return void
   */
  function onHashChange(event) {
    // https://github.com/devote/HTML5-History-API/issues/46
    var fireNow = lastURL;
    // new value to lastURL
    lastURL = windowLocation.href;
    // if not empty fireNow, otherwise skipped the current handler event
    if (fireNow) {
      // if checkUrlForPopState equal current url, this means that the event was raised popstate browser
      if (checkUrlForPopState !== windowLocation.href) {
        // otherwise,
        // the browser does not support popstate event or just does not run the event by changing the hash.
        firePopState();
      }
      // current event object
      event = event || global.event;

      var oldURLObject = parseURL(fireNow, true);
      var newURLObject = parseURL();
      // HTML4 browser not support properties oldURL/newURL
      if (!event.oldURL) {
        event.oldURL = oldURLObject._href;
        event.newURL = newURLObject._href;
      }
      if (oldURLObject._hash !== newURLObject._hash) {
        // if current hash not equal previous hash
        dispatchEvent(event);
      }
    }
  }

  /**
   * The event handler is fully loaded document
   *
   * @param {*} [noScroll]
   * @return void
   */
  function onLoad(noScroll) {
    // Get rid of the events popstate when the first loading a document in the webkit browsers
    setTimeout(function() {
      // hang up the event handler for the built-in popstate event in the browser
      addEvent('popstate', function(e) {
        // set the current url, that suppress the creation of the popstate event by changing the hash
        checkUrlForPopState = windowLocation.href;
        // for Safari browser in OS Windows not implemented 'state' object in 'History' interface
        // and not implemented in old HTML4 browsers
        if (!isSupportStateObjectInHistory) {
          e = redefineProperty(e, 'state', {get: function() {
            return historyObject.state;
          }});
        }
        // send events to be processed
        dispatchEvent(e);
      }, false);
    }, 0);
    // for non-HTML5 browsers
    if (!isSupportHistoryAPI && noScroll !== true && "location" in historyObject) {
      // scroll window to anchor element
      scrollToAnchorId(locationObject.hash);
      // fire initial state for non-HTML5 browser after load page
      fireInitialState();
    }
  }

  /**
   * Finds the closest ancestor anchor element (including the target itself).
   *
   * @param {HTMLElement} target The element to start scanning from.
   * @return {HTMLElement} An element which is the closest ancestor anchor.
   */
  function anchorTarget(target) {
    while (target) {
      if (target.nodeName === 'A') return target;
      target = target.parentNode;
    }
  }

  /**
   * Handles anchor elements with a hash fragment for non-HTML5 browsers
   *
   * @param {Event} e
   */
  function onAnchorClick(e) {
    var event = e || global.event;
    var target = anchorTarget(event.target || event.srcElement);
    var defaultPrevented = "defaultPrevented" in event ? event['defaultPrevented'] : event.returnValue === false;
    if (target && target.nodeName === "A" && !defaultPrevented) {
      var current = parseURL();
      var expect = parseURL(target.getAttribute("href", 2));
      var isEqualBaseURL = current._href.split('#').shift() === expect._href.split('#').shift();
      if (isEqualBaseURL && expect._hash) {
        if (current._hash !== expect._hash) {
          locationObject.hash = expect._hash;
        }
        scrollToAnchorId(expect._hash);
        if (event.preventDefault) {
          event.preventDefault();
        } else {
          event.returnValue = false;
        }
      }
    }
  }

  /**
   * Scroll page to current anchor in url-hash
   *
   * @param hash
   */
  function scrollToAnchorId(hash) {
    var target = document.getElementById(hash = (hash || '').replace(/^#/, ''));
    if (target && target.id === hash && target.nodeName === "A") {
      var rect = target.getBoundingClientRect();
      global.scrollTo((documentElement.scrollLeft || 0), rect.top + (documentElement.scrollTop || 0)
        - (documentElement.clientTop || 0));
    }
  }

  /**
   * Library initialization
   *
   * @return {Boolean} return true if all is well, otherwise return false value
   */
  function initialize() {
    /**
     * Get custom settings from the query string
     */
    var scripts = document.getElementsByTagName('script');
    var src = (scripts[scripts.length - 1] || {}).src || '';
    var arg = src.indexOf('?') !== -1 ? src.split('?').pop() : '';
    arg.replace(/(\w+)(?:=([^&]*))?/g, function(a, key, value) {
      settings[key] = (value || '').replace(/^(0|false)$/, '');
    });

    /**
     * hang up the event handler to listen to the events hashchange
     */
    addEvent(eventNamePrefix + 'hashchange', onHashChange, false);

    // a list of objects with pairs of descriptors/object
    var data = [locationDescriptors, locationObject, eventsDescriptors, global, historyDescriptors, historyObject];

    // if browser support object 'state' in interface 'History'
    if (isSupportStateObjectInHistory) {
      // remove state property from descriptor
      delete historyDescriptors['state'];
    }

    // initializing descriptors
    for(var i = 0; i < data.length; i += 2) {
      for(var prop in data[i]) {
        if (data[i].hasOwnProperty(prop)) {
          if (typeof data[i][prop] !== 'object') {
            // If the descriptor is a simple function, simply just assign it an object
            data[i + 1][prop] = data[i][prop];
          } else {
            // prepare the descriptor the required format
            var descriptor = prepareDescriptorsForObject(data[i], prop, data[i][prop]);
            // try to set the descriptor object
            if (!redefineProperty(data[i + 1], prop, descriptor, function(n, o) {
              // is satisfied if the failed override property
              if (o === historyObject) {
                // the problem occurs in Safari on the Mac
                global.history = historyObject = data[i + 1] = n;
              }
            })) {
              // if there is no possibility override.
              // This browser does not support descriptors, such as IE7

              // remove previously hung event handlers
              removeEvent(eventNamePrefix + 'hashchange', onHashChange, false);

              // fail to initialize :(
              return false;
            }

            // create a repository for custom handlers onpopstate/onhashchange
            if (data[i + 1] === global) {
              eventsList[prop] = eventsList[prop.substr(2)] = [];
            }
          }
        }
      }
    }

    // check settings
    historyObject['setup']();

    // redirect if necessary
    if (settings['redirect']) {
      historyObject['redirect']();
    }

    // initialize
    if (settings["init"]) {
      // You agree that you will use window.history.location instead window.location
      isUsedHistoryLocationFlag = 1;
    }

    // If browser does not support object 'state' in interface 'History'
    if (!isSupportStateObjectInHistory && JSON) {
      storageInitialize();
    }

    // track clicks on anchors
    if (!isSupportHistoryAPI) {
      document[addEventListenerName](eventNamePrefix + "click", onAnchorClick, false);
    }

    if (document.readyState === 'complete') {
      onLoad(true);
    } else {
      if (!isSupportHistoryAPI && parseURL()._relative !== settings["basepath"]) {
        isFireInitialState = true;
      }
      /**
       * Need to avoid triggering events popstate the initial page load.
       * Hang handler popstate as will be fully loaded document that
       * would prevent triggering event onpopstate
       */
      addEvent(eventNamePrefix + 'load', onLoad, false);
    }

    // everything went well
    return true;
  }

  /**
   * Starting the library
   */
  if (!initialize()) {
    // if unable to initialize descriptors
    // therefore quite old browser and there
    // is no sense to continue to perform
    return;
  }

  /**
   * If the property history.emulate will be true,
   * this will be talking about what's going on
   * emulation capabilities HTML5-History-API.
   * Otherwise there is no emulation, ie the
   * built-in browser capabilities.
   *
   * @type {boolean}
   * @const
   */
  historyObject['emulate'] = !isSupportHistoryAPI;

  /**
   * Replace the original methods on the wrapper
   */
  global[addEventListenerName] = addEventListener;
  global[removeEventListenerName] = removeEventListener;
  global[dispatchEventName] = dispatchEvent;

  return historyObject;
});


function prefixedEventListener(element, type, callback) {
	var pfx = ['webkit', 'moz', 'MS', 'o', ''];
	for (var p = 0; p < pfx.length; p++) {
		if (!pfx[p]) type = type.toLowerCase();
		element.addEventListener(pfx[p]+type, callback, false);
	}
}

function debounce(func, delay) {
    let timeout;
    return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

jQuery.cachedScript = function( url, options ) {
  options = jQuery.extend( options || {}, {
    dataType: 'script',
    cache: true,
    url: url
  });
  return jQuery.ajax( options );
};

function queueNoty(fn) {
	if (typeof noty_queue == 'undefined') noty_queue = [];
	if (typeof noty_loaded != 'undefined' && noty_loaded) {
		fn();
	} else {
		noty_queue.push(fn);
	}
};

function getUrlParameter(name, source = null) {
    if (source === null) source = location.search;
	name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
	var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
	var results = regex.exec(source);
	return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
};

function antispam(domain, user, display, xclass) {
	if (!display) display = user + '@' + domain;
	if (xclass) {
		xclass = ' class="' + xclass + '"';
	} else {
		xclass = '';
	}
	document.write('<a href="mailto:' + user + '@' + domain + '"' + xclass +'>' + display + '</a>');
}

function getCookie(name) {
	var re = new RegExp(name + "=([^;]+)");
	var value = re.exec(document.cookie);
	return (value != null) ? unescape(value[1]) : null;
}

function is_touch_device() {
	return 'ontouchstart' in window        // works on most browsers 
		|| navigator.maxTouchPoints;       // works on IE10/11 and Surface
}

function registerMacro(macroname, content_id) {
	let $source = $('#' + content_id);
	if ($source.length) {
		return Handlebars.registerPartial(macroname, $source.html());
	}
    console.log('Unable to load partial "' + macroname + '" from #' + content_id);
}

function escapeHtml(text) {
	var map = {
		'&': '&amp;',
		'<': '&lt;',
		'>': '&gt;',
		'"': '&quot;',
		"'": '&#039;'
	};

	return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

$(function() {
	Handlebars.registerHelper('indirect', function(root, fieldname, options) {
		var value = typeof root[fieldname] == 'undefined' ? '' :  root[fieldname];
		return new Handlebars.SafeString(value);
	});
	Handlebars.registerHelper('ifindirect', function(root, fieldname, options) {
		var value = typeof root[fieldname] == 'undefined' ? false :  root[fieldname];
		if (value) {
			return options.fn(this);
		} else {
			return options.inverse(this);
		}
	});
	Handlebars.registerHelper('select', function(root, fieldname, entryvalue, options) {
		var value = typeof root[fieldname] == 'undefined' ? '' :  root[fieldname];
		return new Handlebars.SafeString(value == entryvalue ? ' selected': '');
	});
	Handlebars.registerHelper('ifeq', function (a, b, options) {
        if (typeof a == 'number' && typeof b == 'string') {
            a = a.toString();
        }
		if (a == b) {
			return options.fn(this);
		} else {
			return options.inverse(this);
		}
	});

	Handlebars.registerHelper('compare', function (lvalue, operator, rvalue, options) {
		if (arguments.length < 3) {
			throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
		}
		
		if (options === undefined) {
			options = rvalue;
			rvalue = operator;
			operator = "===";
		}
		
		var operators = {
			'==': function (l, r) { return l == r; },
			'===': function (l, r) { return l === r; },
			'!=': function (l, r) { return l != r; },
			'!==': function (l, r) { return l !== r; },
			'<': function (l, r) { return l < r; },
			'>': function (l, r) { return l > r; },
			'<=': function (l, r) { return l <= r; },
			'>=': function (l, r) { return l >= r; },
			'typeof': function (l, r) { return typeof l == r; }
		};
		
		if (!operators[operator]) {
			throw new Error("Helper 'compare': unknown operator '" + operator + "'");
		}
		
		var result = operators[operator](lvalue, rvalue);
		if (result) {
			return options.fn(this);
		} else {
			return options.inverse(this);
		}

	});	
	Handlebars.registerHelper('encode', function(value, options) {
		return new Handlebars.SafeString(encodeURIComponent(value));
	});

	noty_loaded = (typeof noty == 'function');
	if (!noty_loaded) {
		$.cachedScript('/static/jquery.noty.packaged.min.js' ).done(function( script, textStatus ) {
			noty_loaded = true;
			if (typeof noty_queue == 'undefined') noty_queue = [];
			while (noty_queue.length > 0) {
				noty_queue.pop()();
			}
		}).fail(function(jqXHR, textStatus, error) {
			console.log( "Unable to load noty.js: " + error );
		}); 
	}
	if (is_touch_device()) {
		$('body').addClass('touch');
	}
	
	// non-UserAgent browser detection
	function _getEnvironment() {
		var env = {};
		
		var tmp = document.documentMode, e, isIE;
		try {
			document.documentMode = "";
		} catch(e){ 
		};

		// If document.documentMode is a number, then it is a read-only property, and so
		// we have IE 8+.
		// Otherwise, if conditional compilation works, then we have IE < 11.
		// Otherwise, we have a non-IE browser.
		isIE = typeof document.documentMode == "number" || new Function("return/*@cc_on!@*/!1")( );

		// Switch back the value to be unobtrusive for non-IE browsers.
		try {
			document.documentMode = tmp;
		} catch(e){ 
		};
		
//		env.p = window.innerHeight > window.innerWidth ? 1 : 0;
		env.t = is_touch_device() ? 1 : 0;
		env.sr = screen.width + 'x' + screen.height;
		env.sd = screen.colorDepth;
		env.ofs = - new Date().getTimezoneOffset();
		if (typeof window.chrome !== 'undefined') {
			env.b = 'chrome';
		} else if (typeof window.opera !== 'undefined') {
			env.b = 'opera';
		} else if (typeof InstallTrigger !== 'undefined') {
			env.b = 'firefox';
		} else if (!isIE && !!window.StyleMedia) {
			env.b = 'edge';
		} else if (isIE) {
			env.b = 'ie';
		} else {
			env.b = 'other';
		}
		return env;
	}

	function _assembleEnvironment() {
		var env = _getEnvironment();
		return '?ofs=' + env.ofs + '&b=' + env.b + '&t=' + env.t + '&sr=' + env.sr + '&sd=' + env.sd;
	}
//	var session = getCookie('PHPSESSID').replace('/', '-');
	var session = '';
//	$.cachedScript('/member/.locale/' + session + _assembleEnvironment());

});

function Main(PAGESIZE) {
	var _this = this;
	var cbk_navigation = null;
	var cbk_host;
	var category_items = null;
	var visible_items = [];
	var current_page = 1;
	var current_category = 0;
	var $editform;
	let $jobqueue;
	var $dialog = null;

	function templateRendered($container, pagedata) {
		var $special;
		_this.afterPageLoad(pagedata, $container);
		_this.renderDataTables(pagedata, $container);

		if (typeof pagedata.hilite != 'undefined') {
			if (pagedata.hilite.regex){
				var re = new RegExp(pagedata.hilite.regex, 'gi');
			} else {
				var re = pagedata.hilite.text;
			}
			$special = $('textarea.hilite');
			$special.each(function() {
				$(this).highlightWithinTextarea({highlight: re});
			});
		}
		
		var hash = document.location.hash;
		if (hash.length > 1) {
			_checkHash(hash.substr(1));
		}
	}

	function _retrieveAssociatedDescription(select_data, value) {
		let result = value;
		$.each(select_data, function( index, category ) {
			if (typeof category.item != 'undefined')
				for(let i = 0; i < category.item.length; i++) {
					if (category.item[i].key == value) {
						result = category.item[i].caption;
						return false;
					}
				}
		});
		return result;
	}

	function _promptforchange($field, value, settings) {
		let $form = $field.find('form');
		if (confirm('Do you wish to save this item?')) {
			$form.submit();
		} else {
			$field[0].reset();
		}
	}

	if (typeof $.editable != 'undefined') {
		let oldReset = $.editable.types['defaults'].reset;
		$.editable.types['text'].plugin = function(settings, original) {
			$editform = this;
		}
		$.editable.types['defaults'].reset = function(settings, original) {
			$(original).removeClass('edit');
			oldReset.apply(this, [settings, original]);
		}
	}
	
	this.attachEditable = function($container, pagedata = null) {

        $('.bind-editable', $container).each(function() {

			let $element = $(this);

			function _triggerFieldChanged(value, settings) {
				let category = this.dataset.category || '';
				$(document).trigger( 'editable:' + category, [ this, value, settings ] );
				return value;
			}

			let type = this.dataset.type;
			let params = {
				placeholder: 'no value',
				tooltip: "Click to edit...",
				cssclass: 'inline-editing',
				style: "inherit",
			};
			let target = $element.data('target');
			if (target !== null) {
				let fn = window[target];
                if (typeof fn == 'function') params.target = fn;
			}
			switch(type) {
				case 'select3':
				case 'select':
					let select_data = this.dataset.select;
					if (typeof select_data == 'string') {
						select_data = jQuery.parseJSON(select_data);
					} else if (pagedata !== null) {
						let field = this.dataset.field;
						if (typeof pagedata['list_' + field] != 'undefined') {
							select_data = pagedata['list_' + field];
						}
					}
					Object.assign(params, { 
						data: select_data,
						type: type,
						onblur: function (value, settings) { 
							return _promptforchange($element, value, settings);
						},
						callback: function (value, settings) { 
							if (type == 'select3') {
								$(this).html( _retrieveAssociatedDescription(settings.data, value) );
								$element.attr('data-value', value);
							} else {
								$(this).html(settings.data[value]);
							}
						},
					});
						
					$element.editable(_triggerFieldChanged, params);
					break;

				case 'text':		// the extra functions allow for html content
					$element.editable(_triggerFieldChanged,{ 
						type: 'textarea',
						tooltip: "Click to edit...",
						cssclass: 'inline-editing',
						style: "inherit",
						placeholder: 'no value',
						width: 810,
						height: 350,
						onblur: function (value, settings) { 
							return _promptforchange($element, value, settings);
						},
						onedit: function (settings, self) {
							let $this = $(this);
							settings.data = $this.text();
						},
						callback: function (result, settings) { 
							result = result.replace(/<!--\?/g, '<?').replace(/\?-->/g, '?>');
							settings.data = escapeHtml(result);
							$(this).html(settings.data);
						},
					});
					break;

				case 'password':
					$element.editable(_triggerFieldChanged,{ 
						tooltip: "Click to edit password...",
						cssclass: 'inline-editing',
						style: "inherit",
						type: type,
						placeholder: 'no value',
						onblur: function (value, settings) {		// ', e' is non-standard param we have added to editable.js
							//var $related = $(e.relatedTarget);
							//if ($related.is('button.button-eye')) return false;
							var $form = $element.find('form');
							$form.submit();
							return true;
						}, 
						callback: function (result, settings) { 
							var placeholder = '';
							var $this = $(this);
							for(var i =0; i < result.length; i++) {
								placeholder += '&#9679;'
							}
							$this.html(placeholder);
							$this.data('value', result);
						},
						data: function(initial, settings) {		// data when editing
							return $element.data('value');
						}
					});
					break;

				default:
					$element.editable(_triggerFieldChanged, params);
					/*
					$element.editable(_triggerFieldChanged,{ 
						placeholder: 'no value',
						tooltip: "Click to edit...",
						cssclass: 'inline-editing',
						style: "inherit",
						onedit: function (settings, self) {
							$element.addClass('edit');
						},
						onblur: function (value, settings) {
					        var reset    = $.editable.types[settings.type].reset 
										|| $.editable.types['defaults'].reset;
							let self = this;
							setTimeout(function() {
								reset.apply($editform, [settings, self]);
							}, 500);

						}
					}); */
				
			}
		});
		
	}
	
	this.renderTable = function($table, options) {
		if (options.buttons) {
			if (typeof options.dom == 'undefined') options.dom = 'lfrtipB';
			for(let i = options.buttons.length - 1; i >= 0; i--) {
				if (typeof options.buttons[i] == 'object') {
					if (typeof options.buttons[i].href != 'undefined') {
						let cn = options.buttons[i].className || '';
						if (cn.indexOf('ajax') < 0 && cn.indexOf('json-request') < 0) {
							options.buttons[i].action = function(e, dt, node, config) {
								window.location.href = config.href;
							};
						}
					} else if (typeof options.buttons[i].callback != 'undefined') {
						options.buttons[i].action = function(e, dt, node, config) {
							let form = config.form || '';
							if (form != '') {
								form = $(form).serialize();
								$.ajax({
									dataType: 'json',
									async: true,
									url: config.callback,
									data: form,
									type: 'POST',
									success: function(data) {
										_this.displayPage(data);
									}
								});
							} else {
								$.ajax({
									dataType: 'json',
									async: true,
									url: config.callback,
									type: 'GET',
									crossDomain: true,
									success: function(data) {
										_this.displayPage(data);
									}
								});
							}
						};
					}
					options.buttons[i].init = function(dt, node, config) {
						if (typeof options.buttons[i].hint != 'undefined') {
							node.attr('title', config.hint);
						};
						if (typeof options.buttons[i].href != 'undefined') {
							node.attr('href', config.href);
						};
					}
				}
			}
		}
	
		let original = $table[0];
		if (original.dtable) return;		// already initialized

		let ajax = $table.attr('ajax');		
		let datatable = {};
		Object.assign(datatable, options);
		if (typeof ajax != 'undefined' && ajax != '') {
			datatable.ajax = ajax;
		}
		datatable.initComplete = function(settings, json) {
			$(document).triggerHandler('loaded.datatables', [$table]);	// datatables is displayed
			if ($.editable) {
				_this.attachEditable($table);
			}
		};
		if(datatable.columns) {
			datatable.columns.forEach((element) => {
				if (element.render && typeof element.render == 'string' ) {
					let fn = window[element.render];
					if (typeof fn == 'function') element.render = fn;
				}
			});
		}
		try {
			let table = $table.DataTable(datatable);
			$('input[type="search"]', $table.parent())
				.attr('autocomplete', 'off')
				.attr('autocorrect', 'off')
				.attr('autocapitalize', 'off')
				.attr('spellcheck', 'false');
			original.dtable = table;
		} catch (error) {
			console.error(error.message);
			original.dtable = undefined;
		} finally {
		}
	}

	this.renderDataTables = function(data, $container) {
		var $tables = $('table.data-class', $container);
		$tables.each(function() {
			let $table = $(this);
			let tablekey = $table.attr('for');
			if (tablekey !== null && typeof tablekey != 'undefined' && typeof data[tablekey] != 'undefined') {
				_this.renderTable($table, data[tablekey]);
			} else if (typeof data.datatable != 'undefined') {
				_this.renderTable($table, data.datatable);
			}
		});
		return $tables.length;
	}

	function _getFont(treeId, node) {
		if (node.isDisabled) {
			return {color:'#999', textTransform: 'italic'};
		} else {
			return {};
		}
	}
	
	function _onDrop(event, treeId, treeNodes, targetNode, moveType) {
		if (targetNode === null) return;
		
		var ids = [];
		for(var i = 0; i < treeNodes.length; i++) {
			ids.push(treeNodes[i].id);
		}

		_treeEdit(treeId, {
			action: 'move',
			from: ids.join(','),
			dest: targetNode.id,
			type: moveType
		});
	}
	
	function _treeEdit(treeId, data) {
		var $tree = $('#' + treeId);
		var href = $tree.attr('href');
		_this.loadURL(href, data);
	}

	this.loadURL = function(href, data) {
		$.ajax({
			dataType: 'json',
			async: true,
			url: href,
			data: data,
			type: 'GET'
		}).done(function(data) {
			_this.displayPage(data);
		});
	}
	
	function _onEdit(event, treeId, treeNode) {	// remove, rename, deactivate
		_treeEdit(treeId, {
			action: 'edit',
			dest: treeNode.id,
		});
	}
	
	function _checkHash(hash) {
		$('a.anchor').each(function() {
			var $anchor = $(this);
			var name = $anchor.attr('name');
			if (name == hash) {
				$('div.sections').addClass('hidden');
				var $div = $anchor.closest('div.sections');
				$div.removeClass('hidden');
				return false;
			}
		});
	}

	function _loadTree($tree) {
		var href = $tree.attr('href');
		var setting = {
			async: {
				enable: true,
				url: href,
				autoParam:['id', 'name', 'level'],
				type: 'GET'
			},
			view: {
				showIcon: false,
				showTitle: false,
				nameIsHTML: true,
				fontCss: _getFont
			},
			callback: {
				onAsyncSuccess: _OnAsyncSuccess		// display of the tree is done.
			}
		};
		if ($tree.hasClass('editable')) {
			setting.edit = {
				enable: true,
				showRemoveBtn: false,
				showRenameBtn: false
			};
			setting.callback.onDrop = _onDrop;
			setting.callback.onDblClick = _onEdit;
		}
		$.fn.zTree.init($tree, setting);
	}

	function _OnAsyncSuccess(event, treeId, treeNode, msg) {
		var $active = $('ul.ztree a.select-cat.active');
		if ($active.length) $active[0].scrollIntoView();
	};
	
	function _fetchOrder($list) {
		var list = [];
		var callback = $list.attr('callback');
		$('li.photo-item', $list).each(function() {
			var id = this.dataset.id;
			list.push(id);
		});
		$.ajax({
			dataType: 'json',
			data: {items: list},
			async: true,
			url: callback,
			type: 'POST'
		}).done(function(data) {
		});
		
	}
	
	// is the url handled by this page?
	function _isLocal(href) {
		if (href.indexOf('://') > -1 && href.substr(0, cbk_host.length) != cbk_host) return false;
		let element = parseUri(href);
		
		if (typeof cbk_navigation != 'undefined' && cbk_navigation !== null ) {
			for(let i = 0; i < cbk_navigation.length; i++) {
				let uri = cbk_navigation[i];
				let c = occurrences(uri, '/')
				if (c == 1) {
					if (uri == element.path) return true;
				} else if (uri == element.path.substr(0, uri.length)) {
					return true;		// found it
				}
			}
		}
		return false;
	}
	
	this.closeDialog = function() {
		// if dialog is visible, close it
		try {
			if ($dialog !== null) {
				if ($dialog.dialog('isOpen')) {
					$dialog.dialog('close');
				}
				$dialog.dialog('destroy');
				$dialog = null;
			}
		} catch(e) {
		}
	}
	
	this.performTask = function (href, addHistory = false) {
		if (addHistory) {
			history.pushState(null, null, href);	// change url
		}
		$.ajax({
			dataType: 'json',
			async: true,
			url: href,
			data: {exec: 1},
			type: 'GET',
			crossDomain: true,
		}).done(function(data) {
			if (typeof data.result != 'undefined' && (!data.result || data.result == -9999)) {		// if success, close popupform
				_this.closeDialog();
			}
			_this.displayPage(data);
		});
	}
	
    this.loadTemplate = function(id) {
        let $source = $('#' + id);
        if ($source.length) {
			return Handlebars.compile($source.html());
		}
		if (typeof compiled[id] == 'undefined' && typeof window.HANDLEBAR != 'undefined' && typeof window.HANDLEBAR[id] != 'undefined') {
			let precomp = 'return ' + window.HANDLEBAR[id].content;
			let pre = (new Function(precomp)());
			compiled[id] = Handlebars.template(pre);
		}
		if (typeof compiled[id] == 'undefined') {
			console.log('Unable to load template #' + id);
			return null;
		}
		return compiled[id];
    }
    
	this.displayPopup = function(data) {		// as modal

        let popup = data.popup;
        let template = _this.loadTemplate(popup.template);
        if (template === null) return;

        $dialog = $('div.popup-dialog').first();
        popup.width = popup.width || 500;
        let param = {
            //title: popup.title,
            dialogClass: popup.class || 'dialog-class',
            //modal: true,
//            closeOnEscape: true,
			open: function( event, ui ) {
                data._source = 'popup';
				templateRendered($dialog, data);
			},
			create: function( event, ui ) {		// replace <button> with <a>
				let $container = $dialog.parent().find('.ui-dialog-buttonpane');
				$container.find('button').each(function(idx, elem) {
					let node = param.buttons[idx];
					if (node.icon) {
						let child = elem.innerHTML;
						if (child.indexOf('ui-button-icon') < 0) {
							elem.innerHTML = node.icon + ' ' + child;
						}
					}
				});
				
				$container.find('button[href]').each(function() {
					//let events = $._data(this, 'events');
					this.outerHTML = this.outerHTML.replace('<button', '<a').replace('</button>', '</a>').replace('ui-button', 'ui-button ui-button-href');	// this removes all events
				});
				$container.on('click', '.ui-button-href', function(e) {	// close 
					_this.closeDialog();
				});
			}
        };
		Object.assign(param, popup);
		param.buttons = [];
        if (popup.height) param.minHeight = data.popup.height;
        let hasUpdate = false;
		for(let property in popup.buttons) {
			if (property == 'update') hasUpdate = true;
			param.buttons.push({
				text: popup.buttons[property],
				click: function(e) {
					$(document).trigger('dialog:' + property, param);
				},
			});
			
		}
		if (popup.actions || false) {
			hasUpdate = true;
			$.each(popup.actions, function(index, element) {
				element.click = function(e) {
					e.preventDefault();
					var button = e.target;
					var action  = $(button).attr('action');
					if (action !== undefined) {
						$(document).trigger('dialog:' + action, param);
					} else {
						switch(button.dataset.action) {
							case 'update':
							case 'submit':
								$dialog.find('form').submit();
								return;
							case 'cancel':
								_this.closeDialog();
								return false;
							default:
								$dialog.trigger('click:' + button.dataset.action, e);
								return false;
						}
					}
				}
				//let type = element['data-action'];
				//let text = ...
				param.buttons.push(element);
			});
			delete param.actions;
		} else if (popup.button || false) {
			if (!Array.isArray(popup.button) && typeof popup.button != 'object') {
				popup.button = [popup.button];
			}
			$.each(popup.button, function(index, element) {
				var label, type, node;
				if ($.isNumeric(index)) {
					label = element;
					type = 'submit';
				} else {
					label = index;
					type = element;
				}
				if (type == 'update' ||type == 'submit' ) {
					hasUpdate = true;
				}
				node = {
					text: label,
					'data-action': type,
					click: function(e) {
						e.preventDefault();
						var button = e.target;
						switch(button.dataset.action) {
							case 'submit':
								$dialog.find('form').submit();
								return;
							case 'cancel':
								_this.closeDialog();
								return false;
							default:
								$dialog.trigger('click.' + button.dataset.action, e);
								return false;
						}
					}
				}
				if (typeof type == 'object') {
					node['data-action'] = type.action;
					delete type.action;
					node = {...node, ...type};
				}
				param.buttons.push(node);
			});		
		}
		
        if (!hasUpdate) {
            param.buttons.push({
                text: 'Update',
                click: function(e) {
                    $(document).trigger('dialog:update', param);
                },
            });
        }
        $dialog
            .html(template(param))
            .dialog(param);
		let $footer = $('.ui-dialog-buttonpane', $dialog.parent());
		$footer.find('.footers').remove();
		if (popup.footer) {
			$footer.prepend('<div class="footers">' + data.popup.footer + '</div>');
		}
		_this.afterPageLoad($dialog);
        $(document).trigger('dialog', param);
        return $dialog;
	}

	function _submitForm($form) {
		let data = $form.serializeArray();
		$(document).trigger( 'formsubmit', [ $form, data ] );

		let action = $form.attr('action') || '';
		if (action == '') action = document.location.href;
		
		$.ajax({
			dataType: 'json',
			data: data,
			async: true,	// 20170419
			url: action,
			type: 'POST',
			crossDomain: true,
		}).done(function(data) {
			if (!data.result || data.result == -9999) {		// if success, close popupform
				_this.closeDialog();
			}
			_this.displayPage(data);
		});
	}
	this.submitForm = _submitForm;
	
	function _showFieldMessage(fielddata) {
		$.each(fielddata, function(key, value) {
			// locate field
			var id, msg, 
				$message = null;
			var $field = $('[name="' + key + '"]').first();
			if (!$field.length) $field = $('#' + key);
			if ($field.length) {

				// update border (what for radio buttons?)
				switch(value.state) {
					case 0:
						$field.removeClass('state1 state2 state3');
						break;
					case 1:
						$field.addClass('state1').removeClass('state2 state3');
						break;
					case 2:
						$field.addClass('state2').removeClass('state1 state3');
						break;
					case 3:
						$field.addClass('state3').removeClass('state1 state2');
						break;
				}

				id = $field.get(0).id;
			}
			if (typeof value.value != 'undefined') $field.val(value.value);
			if (typeof value.className != 'undefined') $field.addClass(value.className);

			if (typeof value.message != 'undefined') {
				// locate message area
				if (typeof id != 'undefined') {
					$message = $('.status-message[for="' + id + '"]').first();
				}
				if ($message === null || !$message.length) {
					$message = $('.status-message[for="' + key + '"]').first();
				}
				if (value.message == '?') {
					value.message = 'This field must have a value';
					var placeholder = $field.attr('placeholder');
					if (typeof placeholder != 'undefined') {
						value.message = placeholder + ' must have a value';
					} else if (typeof id != 'undefined') {
						var $label = $('label[for="' + id + '"]').first();
						if ($label.length) {
							value.message = $label.text() + ' must have a value';
						}
					}
				}
				$message.html(value.message);
			}
		});
	}

	function _checkRequiredEntry($input, showNoError) {
		var fielddata = {};
		var x = $input.val().trim();
		var key = $input.attr('name');
		if (x == '') {
			fielddata[key] = {state: 1, message: '?'};
			_showFieldMessage(fielddata);
		} else if (showNoError) {
			fielddata[key] = {state: 0, message: ''};
			_showFieldMessage(fielddata);
		}
		return x != '';
	}


	function _sortableChanged($sortable) {
		var output = '';
		var linked = $sortable.attr('for');
		linked = $('#' + linked);
		$sortable.find('li').each(function() {
			if (output != '') output += ';';
			output += this.dataset.value;
		});
		linked.val(output);
	}

	this.afterPageLoad = function(pagedata, $container) {
		if ($.editable) {
			_this.attachEditable($container, pagedata);
		}

		try{
			$('input.droparea,.bind-droparea', $container).droparea({
				'instructions': 'drag-and-drop a file, or click here to select it',
				'init' : function(result){
				},
				'start' : function(area){
					area.find('.error').remove(); 
				},
				'error' : function(result, input, area){
					$('<div class="error">').html(result.error).prependTo(area); 
					return 0;
				},
				'complete' : function(result, file, input, area) {
					_this.displayPage(result);
				}
			});
		} catch(e) {
		}
		$('input.autocomp', $container).each(function() {
			var $this = $(this);
			var src = $this.attr('src');
			var associated = $this.attr('for');
			var $assoc = $('#' + associated);
			$this.autocomplete({
				select: function( event, ui ) {
					event.preventDefault();
					$this.val(ui.item.label);
					$assoc.val(ui.item.value);
				},
				focus: function( event, ui ) {
					event.preventDefault();
					$this.val(ui.item.label);
					$assoc.val(ui.item.value);
				},
				change: function( event, ui ) {
					if (ui === null) $assoc.val('0');		// nothing selected
				},
				source: src
			});
		});
		$('select.jquery-ui-select', $container).each(function() {
			$(this).selectmenu();
		});
		var $special = $('ul.ztree', $container);
		$special.each(function() {
			_loadTree($(this));
		});
        $('.dropdown', $container).hover(
			function(){
				$(this).addClass('active');
				$(this).children('.sub-menu').slideDown(100);
			},
			function(){
				$(this).removeClass('active');
				$(this).children('.sub-menu').slideUp(100);
			}
        );
		$special = $('div.vertical-panel', $container);
		if ($special.length) {
			$special.tabs({
				activate: function( event, ui ) {
					var $active = $('#activetab');
					if ($active.length) {
						$active.val(ui.newPanel[0].id);
					}
				}
			}).addClass( "ui-tabs-vertical ui-helper-clearfix" );
			$('li', $special).removeClass( "ui-corner-top" ).addClass( "ui-corner-left" );
		}
		$special = $('ul.cms-sortable', $container);
		$special.each(function() {
			var $sortable = $(this);
			$sortable.sortable({
				grid: [10,10],
				zIndex: 200,
				forcePlaceholderSize: false,
				update: function( event, ui ) {
					_sortableChanged($sortable);
				}
			}).disableSelection();
		});
	}
	
	this.displayErrors = function(errors) {
		for(var i = 0; i < errors.length; i++) {
			queueNoty(function() {
				noty({
					text        : errors[i],
					type        : 'error',
					dismissQueue: true,
					timeout     : 10000,
					closeWith   : ['click'],
					layout      : 'top',
					theme       : 'defaultTheme',
					maxVisible  : 10
				});
			});
		}
	}

	function _setFocus(id) {
		var $element = $('#' + id);
		$element.focus();
	}
    
    this.showStatus = function(data) {
		if (!data.result) {
			_this.closeDialog();
        } else if (data.result == -9999) {
            queueNoty(function() {
                noty({
                    text        : data.message,
                    type        : 'success',
                    dismissQueue: true,
                    timeout     : 4000,
                    closeWith   : ['click'],
                    layout      : 'top',
                    theme       : 'defaultTheme',
                    maxVisible  : 10
                });
            });
        } else if (data.result == -9998) {
            queueNoty(function() {
                noty({
                    text        : data.message,
                    type        : 'success',
                    dismissQueue: true,
                    timeout     : 1500,
                    closeWith   : ['click'],
                    layout      : 'top',
                    theme       : 'defaultTheme',
                    maxVisible  : 10
                });
            });
        } else if (data.result != -9997) {				// don't show a message - don't close dialog
            queueNoty(function() {
                noty({
                    text        : data.message + ' [' + data.result + ']',
                    type        : 'error',
                    dismissQueue: true,
                    timeout     : 10000,
                    closeWith   : ['click'],
                    layout      : 'top',
                    theme       : 'defaultTheme',
                    maxVisible  : 10
                });
            });
        }
    }
	function _setDocumentClass(body, classes) {
		let cls = (body.className == '') ? [] : body.className.split(' ');
		let special = Array.from(typeof _this.significantClasses != 'undefined' ? _this.significantClasses : []); 
		special.push('page-outer', 'page-inner');
		for(let i = 0; i < cls.length; i++) {
			if (cls[i] != '' && special.indexOf(cls[i]) == -1) {
				body.classList.remove(cls[i]);
			}
		}
		cls = classes.split(' ');
		for(let i = 0; i < cls.length; i++) {
			body.classList.add(cls[i]);
		}
		// template class is added later
	}
	

	this.displayPage = function(data) {
		current_category = 0;
		
		if (typeof data._tab != 'undefined') window.activepage = data;
		if (typeof data.significant != 'undefined') _this.significantClasses = data.significant;	// this should not be used
		
		if (typeof data.result != 'undefined' && data.result) {
            _this.showStatus(data);
		}
		
		if (typeof data.samepage != 'undefined') {
			cbk_navigation = data.samepage;
			cbk_host = data.host;
		}

		var redirect = data.redirect || '';
		if (redirect === true) {
			window.location.reload(true);
		} else if (redirect != '') {
			if (cbk_navigation !== null && _isLocal(redirect)) {
				history.pushState(null, null, redirect);	// change url
				_this.performTask(redirect);
			} else { 
				window.location.href = redirect;
			}
            return;
		}
		
		var page_title = data.page_title || '';
		if (page_title != '' && document.title != page_title) {
			document.title = page_title;
		}
		if (typeof data.pageBefore != 'undefined') eval(data.pageBefore + '(data);');
		$(document).triggerHandler('page.before', data);

		if (typeof data.className != 'undefined') {
			_setDocumentClass(document.body, data.className);
		}

        var $content = $('.body').first();
		var body = data.body || '';
		var template = data.template || '';
		if (typeof data.category_items != 'undefined') {
			category_items = data.category_items;
			current_category = data.current_category || 0;
		}
		if (body != '') {
			$content.html(body);
            data._source = 'body';
			templateRendered($content, data);
		} else if (template != '') {
            let template_data = _this.loadTemplate(template);
            if (template_data !== null) {
				$content.html(template_data(data));
				body = $('.page-outer')[0];
				body.classList.add(template);
                data._source = 'template';
				templateRendered($content, data);
			}
		} else if (typeof data.datatable != 'undefined') {
			templateRendered($content, data);
		}
		if (typeof data.popup != 'undefined') $content = this.displayPopup(data);
		if (typeof data.field != 'undefined') _showFieldMessage(data.field);
		if (typeof data.html != 'undefined') this.updateHtml(data);
		if (typeof data.errors != 'undefined') _this.displayErrors(data.errors);
		if (typeof data.refresh != 'undefined' && data.refresh) {
			$(document).triggerHandler('page.refresh', data);
		}
        $('input.component-autocomplete', $content).each(function() {
            var $this = $(this);
            var $target = $this.closest('[data-autocomplete]');
            $this.autocomplete({
                source: function( request, response ) {
                    $.ajax( {
                        url: $target.data('autocomplete'),
                        dataType: 'json',
                        cache: false,
                        data: {
                            q: request.term
                        },
                        success: function( data ) {
                            response( data );
                        }
                    });
                },
                minLength: 2,
                select: function( event, ui ) {
                    $this.val(ui.item);
                }
            });
        });
		$('.bind-slider', $content).each(function() {
			let data = {
				change: function( event, ui ) {
					let newvalue = ui.value;
					let $input = $(ui.handle).closest('.control-group').find('input[type="hidden"]');
					$input.val(newvalue);
				},
			};
			let value = $(this).attr('value');
			if (value != undefined && value != '') {
				data.value = parseInt(value);
			}
			$(this).slider(data);
		});
		
		if (typeof data.refresh == 'undefined') {
			$(document).triggerHandler('loaded.template', [data, $content]);		// javascript to be executed once page has loaded
		}
		if (data.execute) {
			let fn = window[data.execute];
			if (typeof fn == 'function') fn(data);
		}

		if (typeof data.setfocus != 'undefined') _setFocus(data.setfocus);
	};
	
	this.initialize = function(data) {
		var pg = getUrlParameter('pg');
		if (pg !== null) {
			pg = parseInt(pg);
			if (pg < 1) pg = 1;
			current_page = pg;
		}
		_this.displayPage(data);
		if (typeof data.template == 'undefined' && (typeof data.body == 'undefined' || data.body === null) ) {
			_this.afterPageLoad(data, $('.body').first());
		}
	}

	function _updateElementHtml_element(data, entry) {
		let table_api;
		var $content = $('#' + entry.field);
		if (typeof entry.value != 'undefined') {
			$content.html(entry.value);
		} else if (typeof entry.src != 'undefined') {
			if ($content[0].tagName == 'IMG') {
				$content.attr('src', entry.src);
			}
		} else if (typeof entry.action != 'undefined') {
			switch(entry.action) {
				case 'datatable.refresh':		// only if ajax specified
					table_api = $content.DataTable();
					//console.log(table_api);
					table_api.ajax.reload(null, false);
					break;

				case 'datatable.reload':
					table_api = $content.DataTable();
					table_api.clear().rows.add(data.html.data).draw();
					break;

				case 'remove':
					$content.remove();
					break;
			}
		} else {
			let template = _this.loadTemplate(entry.template);
			if (template !== null) $content.html(template(data));
		}
		if (typeof entry.className != 'undefined') {
			$content.addClass(entry.className);
		}
	}

	this.updateHtml = function(data) {
		if (data.html.field) {
			_updateElementHtml_element(data, data.html);
		} else {
			for(let i = 0; i < data.html.length; i++) {
				_updateElementHtml_element(data, data.html[i]);
			}
		}
		
	}
	
	function _addReturnLink(link) {
		var element = parseUri(link.href);
		var this_page = parseUri(document.location.href);
		var current = this_page.path;
		if (this_page.query != '') current += '?' + this_page.query;
		if (this_page.anchor != '') current += '#' + this_page.anchor;
		link.href = element.path + '?return=' + encodeURIComponent(current);
	}

	function _socialbutton($icon, href) {
		var classes = $icon[0].className.split(' ');
		$.ajax({
			dataType: 'json',
			data: {items: classes},
			async: true,
			url: href,
			type: 'GET'
		}).done(function(data) {
			_this.displayPage(data);
		});
		
	}
	
	function _navButton(fragment, caption, isEnabled, isCurrent, pageNum) {
		var div = document.createElement('div');
		var cls = 'page-button';
		cls += isEnabled ? ' page-enable' : ' page-disable';
		if (isCurrent) cls += ' page-current';
		div.className = cls;
		div.innerHTML = caption;
		div.dataset.page = pageNum;
		fragment.appendChild(div);
	}

	function _selectDefaultTZ(data) {
		var key, p,
			$tzone = $('#tzone'),
			ofs = - new Date().getTimezoneOffset();
		var z = $tzone.val();
		if (z == '') {
			var x = $tzone[0];
			ofs += '';
			for(var i = 0; i < x.options.length; i++) {
				key = x.options[i].value;
				p = key.indexOf('/');
				if (key.substr(0, p) == ofs) {
					x.value = key;
					break;
				}
			}
		}
	}
	
	function _positionMobileNavigation() {
		let $div = $('.page-outer');
		if (!$div.length) $div = $('.pagecontainer').first();
		if ($div.length) {
			let ps = $div.offset();
			$('#slide-menu').css('left', ps.left);
		}
	}
	
	function htmlToElement(html) {
		var t = document.createElement('template');
		t.innerHTML = html;
		return t.content.firstChild;
	}

	function _getSelectedEntries($button) {
	// get values
		var $select = $button.closest('.ui-dialog').find('select');
		var selectedValues = $select.val() || [];
		
		var $input = $('#' + _this.linked);
		var $sortable = $('ul.cms-sortable[for="' + _this.linked + '"]');
		var current = $input.val();
		
	// add them to the page list, and to the input
		$.each(selectedValues, function(k, value) {
			if (current != '') current += ';';
			current += value;
			var elements = value.split('=', 2);
			var li = htmlToElement('<li data-value="' + value + '"><span class="cms-action"><i class="cms-icon cms-trash"></i></span>' + elements[1] + '</li>');
			$sortable.get(0).appendChild(li);
		});
		$input.val(current);
		// refresh sortable
	}
	
	function _NavigationTransitionEnd() {
		var $menu = $('#slide-menu');
		if ($('body').hasClass('menu-active')) {
		} else {
			$menu.hide();
		}
	}
	
	function _AddToSortable($button) {
		_this.linked = $button.attr('for');
		var href = $button.attr('href');

		var data = {
			order: $('#' + _this.linked).val(),
			page: '',			// current page - should be removed from list
		}
		$.ajax({
			dataType: 'json',
			data: data,
			async: true,	// 20170419
			url: href,
			type: 'POST',
			crossDomain: true,
		}).done(function(data) {
			if (typeof data.popup != 'undefined') {
				_this.displayPopup(data);
			} else {
			//_this.displayPage(data);	// display popup
			}

		});
	}
    
    // get a list of all the fields that have changed
    this.getChangedFields = function getChangedFields($form) {
        let result = [];
        return result;
    }
	
	var timer = null;
	var $popup = null;

	$(function() {
		if ($.editable) {
			$.editable.addInputType('password', {
				element : function(settings, original) {
					let $input = $('<input class="passwordView" />');
					if (settings.width  != 'none') { $input.attr('width', settings.width);  }
					if (settings.height != 'none') { $input.attr('height', settings.height); }
					$input.attr('type', 'password');
					$input.attr('autocomplete','off');
					$(this).append($input);
					$(document).triggerHandler('loaded.password', $input);		// add PasswordView 
					
					let $button = $input.parent().find('button');
					$button.on('blur', function(e) {
						let $related = $(e.relatedTarget);
						if ($related.is('input.passwordView')) return false;	// clicked within input area - still active
						let $form = $input.closest('form.inline-editing');
						$form.submit();
						return true;
					});
					return $input;
				}
			});

			$.editable.addInputType('select3', {
				element : function(settings, original) {
					let $select = $('<select />').addClass('half');
					$(this).append($select);
					return $select;
				},
				content : function(json, settings, original) {
					/* If it is string assume it is json. */
					const fragment = document.createDocumentFragment();	// this will be the optgroups
					$.each(json, function( index, category ) {
						if (typeof category.item != 'undefined') {
							let optgroup = document.createElement('optgroup');
							optgroup.label = category.caption;
							for(let i = 0; i < category.item.length; i++) {
								let opt = document.createElement('option');
								opt.textContent = category.item[i].caption;
								opt.value = category.item[i].key;
								optgroup.appendChild(opt);
							}
							fragment.appendChild(optgroup);
						}
					});
					$('select', this)[0].appendChild(fragment);
					
					let value = original.dataset.value;
					$('option[value="' + value + '"]', $('select', this)).attr('selected', 'selected');
					
					/* Submit on change if no submit button defined. */
					if (!settings.submit) {
						let form = this;
						$('select', this).change(function() {
							form.submit();
						});
					}
				}
			});
		}
		_positionMobileNavigation();
	});
	
	function processHandlebarEntry_next() {
		if (_this.$jobqueue.length) {
			let $first = _this.$jobqueue.first();
			_this.$jobqueue = _this.$jobqueue.slice(1);
			processHandlebarEntry($first);
		} else {
			alert('process complete');
		}
	}
	
	function processHandlebarEntry($entry) {
		let name = $entry.data('name');
		let auth = $entry.data('auth');
		let hash = $entry.data('hash');

        let $source = $('#' + name);
        if (!$source.length) {
			$entry.text('Error: ' + name + ' was not found');
			return false;	// move onto next
		}
		$entry.html('Processing <b>' + name + '</b>...');

		let compiled = Handlebars.precompile($source.html());
		
		// send to back end
		$.ajax({
			dataType: 'json',
			async: true,
			url: '/~/pkpcore/handlebars',
			data: {
				name: name,
				auth: auth,
				hash: hash,
				content: compiled,
			},
			type: 'POST'
		}).done(function(data) {
			if (data && data.result) {
				$entry.html('<span style="color:#900">Error <b>' + name + '</b>: ' + data.message + '</span>');
			} else {
				$entry.html('<span style="color:#090">Successfully processed <b>' + name + '</b></span>');
			}
			processHandlebarEntry_next();
		});
		
		//		process next item, if present
		//		otherwise display finished text
	}
	
	this.rebuildHandlebars = function() {
		this.$jobqueue = $('.bind-progress-hb');
		processHandlebarEntry_next();
	};
	
	$(window).on('hashchange', function() {
		var hash = document.location.hash;
		if (hash.length > 1) {
			_checkHash(hash.substr(1));
		}
	}).on('resize', function() {
		_positionMobileNavigation();
	}).on('popstate', function(e) {
		if (e.originalEvent.state === null) {
			if (cbk_navigation !== null && _isLocal(loc.href)) {
				_this.performTask(loc.href);
			} else { 
				window.location.href = loc.href;
			}
		}
	});
	
	$(document).on('click', '.singleclick', function(event) {   // with history
		var $this = $(this);
		var href = $this.attr('href');
		if (typeof href != 'undefined') {
            event.preventDefault();
			if (typeof cbk_navigation != 'undefined' && _isLocal(href)) {
				history.pushState(null, null, href);
				_this.performTask(href);
			} else {
				window.location.href = href;
			}
		}

	}).on('click', '.ajax', function(event) {                   // with history
		var $this = $(this);
		var href = $this.attr('href');
		if (typeof href != 'undefined') {
			event.preventDefault();
			history.pushState(null, null, href);
			_this.performTask(href);
		}

	}).on('click', '.json-request', function(event) {			// without history
		var $this = $(this);
		var href = $this.attr('href');
		if (typeof href != 'undefined') {
			event.preventDefault();
			_this.performTask(href);
		}

    }).on('click', '.action-confirm', function(event) {
		var $this = $(this);
        var message = $this.attr('confirm');
        if (!message) message = 'Are you sure?';
        if (!confirm(message)) {
            event.preventDefault();
        }


	}).on('focusout', 'input.required', function(event) {
		var $input = $(this);
		if (!$input.hasClass('monitor-change')) {
			_checkRequiredEntry($input, true);
		} else if (_checkRequiredEntry($input, false)) {
			$input.triggerHandler('recheck');
		}

	}).on('click', 'a.btn-login', function(event) {
		_addReturnLink(this);

	}).on('click', 'div.photo-container', function(event) {
		_selectImage($(this), this.dataset.id);

	}).on('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', '#pagecontent', function(event) {
		_NavigationTransitionEnd();
		
	}).on('click', 'a.submit, button.submit', function(event) {
		var $button = $(this);
		var target = $button.attr('target');
		if (typeof target == 'undefined') 
			event.preventDefault();
		var $form = $button.closest('form');
		var info = this.dataset.value || '';
		if (info != '') {
			var element = $button.attr('for');
			$('#' + element).val(info);
		}
		if ($form.hasClass('json')) {
			$form.submit();
		} else {
			$form[0].submit();
		}

	}).on('submit', 'form.json', function(event) {
		event.preventDefault();
		_submitForm($(this));

	}).on('dialog:update', function(event, data) {
		let $form = $('div.' + data.dialogClass).find('form');
		if ($form.length && !$form.hasClass('custom')) {
			event.preventDefault();
			$form.submit();
		}

	}).on('click', 'span.icon-actions .fa-times-circle-o', function(event) {
		var callback = $(this).attr('callback');
		_this.performTask(callback);

	}).on('click', '.s-sharing .btn-out', function(event) {
		var $top = $(this).closest('.s-sharing');
		var $icon = $(this).find('i');
		_socialbutton($icon, $top.data('href'));

	}).on('selectmenuchange', '#sort', function(event, ui) {
		_changeSorting($(this), ui.item);

	}).on('click', '.nav-item', function(event) {
		var $menu = $(this).closest('li.dropdown');
		$menu.children('.sub-menu').slideDown(200);
		$menu.addClass('active');

	}).on('click', '.mobile-nav i', function(event) {
		event.preventDefault();
		var body = $('body');
		var $menu = $('#slide-menu');
		var ok = body.hasClass('menu-active');
		ok ? body.removeClass('menu-active') : body.addClass('menu-active');
		if (!ok) {
			$menu.show();
		}

	}).on('click', '.cms-sortable-add', function(event) {
		event.preventDefault();
		var $button = $(this);
		_AddToSortable($button);
		
	}).on('click', '.cms-selection', function(event) {
		event.preventDefault();
		_getSelectedEntries($(this));
		_this.closeDialog();
		event.stopImmediatePropagation();

	}).on('click', '.cms-trash', function(event) {
		var $sortable = $(this).closest('ul');
		$(this).closest('li').remove();
		_sortableChanged($sortable);
		
	}).on('keydown', '.submit-enter', function(event) {
		var keyCode = event.keyCode || event.which;
		if (keyCode == 13) {
			$(this).closest('form').find('a.submit').click();
		};
	});

}

main = new Main(20);

function rebuildHandlebars() {
	main.rebuildHandlebars();
}

//////////////////////////////////////////////////
// parseUri 1.2.2
// (c) Steven Levithan <stevenlevithan.com>
// MIT License

function parseUri (str) {
	var	o   = parseUri.options,
		m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
		uri = {},
		i   = 14;

	while (i--) uri[o.key[i]] = m[i] || "";

	uri[o.q.name] = {};
	uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
		if ($1) uri[o.q.name][$1] = $2;
	});

	return uri;
};

parseUri.options = {
	strictMode: false,
	key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
	q:   {
		name:   "queryKey",
		parser: /(?:^|&)([^&=]*)=?([^&]*)/g
	},
	parser: {
		strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
		loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	}
};
//////////////////////////////////////////////////

function antispam(domain, user, display, xclass) {
	if (!display) display = user + '@' + domain;
	if (xclass) {
		xclass = ' class="' + xclass + '"';
	} else {
		xclass = '';
	}
	document.write('<a href="mailto:' + user + '@' + domain + '"' + xclass +'>' + display + '</a>');
}

function htmlspecialchars(text) {
		var map = {
			'&': '&amp;',
			'<': '&lt;',
			'>': '&gt;',
			'"': '&quot;',
		};

		return text.replace(/[&<>"]/g, function(m) { return map[m]; });
}

// https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
function occurrences(string, subString, allowOverlapping) {

    string += '';
    subString += '';
    if (subString.length <= 0) return (string.length + 1);

    var n = 0,
        pos = 0,
        step = allowOverlapping ? 1 : subString.length;

    while (true) {
        pos = string.indexOf(subString, pos);
        if (pos >= 0) {
            n++;
            pos += step;
        } else break;
    }
    return n;
}


window.document.addEventListener('pkp', function(e) {
	main.displayPage(e.detail);
}, false);
window.addEventListener('message', function(event) {
	if (event.data == 'install.complete') {
		setTimeout(() => {
			window.location.reload(true);
		}, 4000);

	}
});

$(window).on('beforeunload', function(){
    let $body = $('body');
    if (!$body.hasClass('are-you-sure')) return;    // no changes

    // have we made any changes on the page?
    let $form = $('form').first();
    let changed_fields = main.getChangedFields($form);
    if (!changed_fields.length) return; // no fields changed
    
    return true;    // changes have been made
});

