Source: augmentedPresentation.js

/**
* AugmentedPresentation.js - The Presentation Core UI Component and package<br/>
* The <b>Presentation</b> extension adds extensive abilities to the presentation layer.<br/>
* This extension adds:<br/>
* Mediator patterned PubSub Views
* Enhanced Application Object
- PubSub mediation and bootstrapping for Application objects
- CSS Stylesheet registration and injection
- breadcrumb management
* Automatic Tables generated from a JSON schema and data
*
* @author Bob Warren
*
* @requires augmentedjs
* @module Augmented.Presentation
* @version 1.6.1
* @license Apache-2.0
*/
(function(moduleFactory) {
  if (typeof exports === 'object') {
    module.exports = moduleFactory(require('augmentedjs'));
  } else if (typeof define === 'function' && define.amd) {
    define(['augmented'], moduleFactory);
  } else {
    window.Augmented.Presentation = moduleFactory(window.Augmented);
  }
}(function(Augmented) {
  "use strict";
  /**
  * The base namespece for all of the Presentation module.
  * @namespace Presentation
  * @memberof Augmented
  */
  Augmented.Presentation = {};

  /**
  * The standard version property
  * @constant VERSION
  */
  Augmented.Presentation.VERSION = "1.6.1";

  /**
  * A private logger for use in the framework only
  * @private
  */
  const _logger = Augmented.Logger.LoggerFactory.getLogger(
        Augmented.Logger.Type.console, Augmented.Configuration.LoggerLevel);

  /*
  * Mediator View
  */

  /**
  * @property delegateEvents
  * @borrows Augmented.View#delegateEvents
  * @memberof Augmented.View
  */
  var delegateEvents = Augmented.View.prototype.delegateEvents;
  /**
  * @property undelegateEvents
  * @borrows Augmented.View#delegateEvents
  * @memberof Augmented.View
  */
  var undelegateEvents = Augmented.View.prototype.undelegateEvents;

  /**
    * Colleague View - The 'child' view.<br/>
    * Allow to define convention-based subscriptions
    * as an 'subscriptions' hash on a view. Subscriptions
    * can then be easily setup and cleaned.
    *
    * @constructor Augmented.Presentation.Colleague
    * @name Augmented.Presentation.Colleague
    * @memberof Augmented.Presentation
    * @extends Augmented.View
    */
  Augmented.Presentation.Colleague = Augmented.View.extend({
    _mediator: null,

    /**
      * Send a message to the mediator's queue
      * @method sendMessage
      * @param {string} message Message to send
      * @param {object} data Data to send with message
      * @memberof Augmented.Presentation.Colleague
      */
    sendMessage: function(message, data) {
      if (this._mediator) {
        this._mediator.trigger(message, data);
      } else {
        _logger.warn("AUGMENTED: No mediator is available, talking to myself.");
      }
    },

    /**
      * Set the mediator to this colleague
      * @method setMediatorMessageQueue
      * @param {Augmented.Presentation.Mediator} mediator The mediator
      * @memberof Augmented.Presentation.Colleague
      */
    setMediatorMessageQueue: function(mediator) {
      if (this._mediator) {
        // already registered, send a dismiss message
        this._mediator._dismissMe(this);
      }
      this._mediator = mediator;
    },

    /**
      * Remove the mediator from this colleague
      * @method removeMediatorMessageQueue
      * @memberof Augmented.Presentation.Colleague
      */
    removeMediatorMessageQueue: function() {
      this._mediator = null;
    },

    /**
      * Render the template
      * @method renderTemplate
      * @memberof Augmented.Presentation.Colleague
      */
    renderTemplate: function() {
      if (this.el && this.template) {
        Augmented.Presentation.Dom.setValue(this.el, this.template);
      }
    }
  });

  /**
  * Mediator View - The mediator in the Mediator Pattern<br/>
  * The mediator defines the interface for communication between colleague views.
  * Loose coupling between colleague objects is achieved by having colleagues communicate
  * with the Mediator, rather than with each other.
  * <pre>
  * [Mediator]<-----[Colleague]
  *     ^-----------[Colleague]
  * </pre>
  * @constructor Mediator
  * @name Augmented.Presentation.Mediator
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.Colleague
  */
  const abstractMediator = Augmented.Presentation.Mediator = Augmented.Presentation.Colleague.extend({
    /**
    * Default Channel Property
    * @property {string} defaultChannel The default channel for the view
    * @memberof Augmented.Presentation.Mediator
    * @private
    */
    _defaultChannel: "augmentedChannel",

    /**
    * Default identifier Property
    * @property {string} defaultIdentifier The default identifier for the view
    * @memberof Augmented.Presentation.Mediator
    * @private
    */
    _defaultIdentifier: "augmentedIdentifier",

    /**
    * Channels Property
    * @property {object} _channels The channels for the view (object array)
    * @memberof Augmented.Presentation.Mediator
    * @private
    */
    _channels: {},

    /**
    * Colleague Map Property
    * @property {object} _colleagueMap The colleagues observed by index in the channel
    * @memberof Augmented.Presentation.Mediator
    * @private
    */
    _colleagueMap: {},

    /**
    * @property {Object} _subscriptions List of subscriptions
    * @memberof Augmented.Presentation.Colleague
    * @private
    */
    _subscriptions: {},

    /**
    * Extend delegateEvents() to set subscriptions
    * @method delegateEvents
    * @memberof Augmented.Presentation.Colleague
    */
    delegateEvents: function() {
      delegateEvents.apply(this, arguments);
      this.setSubscriptions();
    },

    /**
    * Extend undelegateEvents() to unset subscriptions
    * @method undelegateEvents
    * @memberof Augmented.Presentation.Colleague
    */
    undelegateEvents: function() {
      undelegateEvents.apply(this, arguments);
      this.unsetSubscriptions();
    },

    /**
    * Gets all subscriptions
    * @method getSubscriptions
    * @memberof Augmented.Presentation.Colleague
    * @returns {object} Returns all subscriptions
    */
    getSubscriptions: function() {
      return this._subscriptions;
    },

    /**
    * Subscribe to each subscription
    * @method setSubscriptions
    * @param {Object} [subscriptions] An optional hash of subscription to add
    * @memberof Augmented.Presentation.Colleague
    */
    setSubscriptions: function(subscriptions) {
      if (subscriptions) {
        Augmented.Utility.extend(this._subscriptions || {}, subscriptions);
      }
      subscriptions = subscriptions || this._subscriptions;
      if (!subscriptions || (subscriptions.length === 0)) {
        return;
      }
      // Just to be sure we don't set duplicate
      this.unsetSubscriptions(subscriptions);

      var i = 0, l = subscriptions.length;
      for (i = 0; i < l; i++) {
        var subscription = subscriptions[i];
        var once = false;
        if (subscription.$once) {
          subscription = subscription.$once;
          once = true;
        }
        if (typeof subscription === 'string') {
          subscription = this[subscription];
        }
        this.subscribe(subscription.channel, subscription, this, once);
      }
    },

    /**
    * Unsubscribe to each subscription
    * @method unsetSubscriptions
    * @param {Object} [subscriptions] An optional hash of subscription to remove
    * @memberof Augmented.Presentation.Colleague
    */
    unsetSubscriptions: function(subscriptions) {
      subscriptions = subscriptions || this._subscriptions;
      if (!subscriptions || (subscriptions.length === 0)) {
        return;
      }

      var i = 0, l = subscriptions.length;
      for (i = 0; i < l; i++) {
        var subscription = subscriptions[i];
        var once = false;
        if (subscription.$once) {
          subscription = subscription.$once;
          once = true;
        }
        if (typeof subscription == 'string') {
          subscription = this[subscription];
        }
        this.unsubscribe(subscription.channel, subscription.$once || subscription, this);
      }
    },

    /**
    * Observe a Colleague View - observe a Colleague and add to a channel
    * @method observeColleague
    * @param {Augmented.Presentation.Colleague} colleague The Colleague to observe
    * @param {function} callback The callback to call for this colleague
    * @param {string} channel The Channel to add the pubished events to
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    observeColleague: function(colleague, callback, channel, identifier) {
      if (colleague instanceof Augmented.Presentation.Colleague) {
        if (!channel) {
          channel = this._defaultChannel;
        }
        colleague.setMediatorMessageQueue(this);
        this.subscribe(channel, callback, colleague, false, (identifier) ? identifier : this._defaultIdentifier);
      }
    },

    /**
    * Observe a Colleague View - observe a Colleague and add to a channel and auto trigger events
    * @method observeColleague
    * @param {Augmented.Presentation.Colleague} colleague The Colleague to observe
    * @param {string} channel The Channel to add the pubished events to
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    observeColleagueAndTrigger: function(colleague, channel, identifier) {
      this.observeColleague(
        colleague,
        function() {
          colleague.trigger(arguments[0], arguments[1]);
        },
        channel,
        (identifier) ? identifier : this._defaultIdentifier
      );
    },

    _dismissMe: function(colleague) {
      if (colleague instanceof Augmented.Presentation.Colleague) {
        var channel = this._colleagueMap[colleague], myChannelObject = this._channels[channel];
        this.unsubscribe(channel, myChannelObject.fn, colleague, myChannelObject.identifier);
      }
    },

    /**
    * Dismiss a Colleague View - Remove a Colleague from the channel
    * @method dismissColleague
    * @param {Augmented.Presentation.Colleague} colleague The Colleague to observe
    * @param {function} callback The callback to call on channel event
    * @param {string} channel The Channel events are pubished to
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    dismissColleague: function(colleague, callback, channel, identifier) {
      if (colleague instanceof Augmented.Presentation.Colleague) {
        if (!channel) {
          channel = this._defaultChannel;
        }
        colleague.removeMediatorMessageQueue();
        this.unsubscribe(channel, callback, colleague, identifier);
      }
    },

    /**
    * Dismiss a Colleague View - Remove a Colleague from the channel that has an auto trigger
    * @method dismissColleagueTrigger
    * @param {Augmented.Presentation.Colleague} colleague The Colleague to observe
    * @param {string} channel The Channel events are pubished to
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    dismissColleagueTrigger: function(colleague, channel, identifier) {
      var id = (identifier) ? identifier : this._defaultIdentifier;
      this.dismissColleague(
        colleague,
        function() {
          colleague.trigger(arguments[0], arguments[1]);
        },
        channel,
        id
      );
    },

    /**
    * Subscribe to a channel
    * @method subscribe
    * @param {string} channel The Channel events are pubished to
    * @param {function} callback The callback to call on channel event
    * @param {object} context The context (or 'this')
    * @param {boolean} once Toggle to set subscribe only once
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    subscribe: function(channel, callback, context, once, identifier) {
      if (!this._channels[channel]) {
        this._channels[channel] = [];
      }

      var obj  = {
        fn: callback,
        // TODO: the context set to 'this' may be the source of the edge case mediator instance for a channel
        context: context || this,
        once: once,
        identifier: (identifier) ? identifier : this._defaultIdentifier
      };
      this._channels[channel].push(obj);

      this._colleagueMap[context] = channel;

      this.on(channel, this.publish, context);
    },

    /**
    * Trigger all callbacks for a channel
    * @method publish
    * @param {string} channel The Channel events are pubished to
    * @param {object} N Extra parameter to pass to handler
    * @memberof Augmented.Presentation.Mediator
    */
    publish: function(channel) {
      if (!channel || !this._channels[channel]) {
        _logger.warn("AUGMENTED: Mediator: channel '" + channel + "' doest exist.");
        return;
      }

      var args = [].slice.call(arguments, 1), subscription;
      var i = 0, l = this._channels[channel].length;

      for (i = 0; i < l; i++) {
        subscription = this._channels[channel][i];
        if (subscription) {
          if (subscription.fn) {
            subscription.fn.apply(subscription.context, args);
          }
          if (subscription.once) {
            this.unsubscribe(channel, subscription.fn, subscription.context, subscription.identifier);
            i--;
          }
        } else {
          _logger.warn("AUGMENTED: Mediator: No subscription for channel '" + channel + "' on row " + i);
        }
      }
    },

    /**
    * Cancel subscription
    * @method unsubscribe
    * @param {string} channel The Channel events are pubished to
    * @param {function} callback The function callback regestered
    * @param {object} context The context (or 'this')
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    unsubscribe: function(channel, callback, context, identifier) {
      if (!this._channels[channel]) {
        return;
      }

      var id = (identifier) ? identifier : this._defaultIdentifier;

      var subscription, i = 0;
      for (i = 0; i < this._channels[channel].length; i++) {
        subscription = this._channels[channel][i];
        if (subscription) {
          if (subscription.identifier === id && subscription.context === context) {
            // originally compared function callbacks, but we don't always pass one so use identifier
            this._channels[channel].splice(i, 1);
            i--;

            delete this._colleagueMap[subscription.context];
          }
        } else {
          _logger.warn("AUGMENTED: Mediator: No subscription for channel '" + channel + "' on row " + i);
          //logger.debug("AUGMENTED: Mediator: subscription " + this._channels[channel]);
        }
      }
    },

    /**
    * Subscribing to one event only
    * @method subscribeOnce
    * @param {string} channel The Channel events are pubished to
    * @param {string} subscription The subscription to subscribe to
    * @param {object} context The context (or 'this')
    * @param {string} identifier The identifier for this function
    * @memberof Augmented.Presentation.Mediator
    */
    subscribeOnce: function(channel, subscription, context, identifier) {
      this.subscribe(channel, subscription, context, true, identifier);
    },

    /**
    * Get All the Colleagues for a channel
    * @method getColleagues
    * @param {string} channel The Channel events are pubished to
    * @memberof Augmented.Presentation.Mediator
    * @returns {array} The colleagues for a channel
    */
    getColleagues: function(channel) {
      var c = this.getChannel(channel);
      return (c) ? c.context : null;
    },

    /**
    * Get Channels
    * @method getChannels
    * @memberof Augmented.Presentation.Mediator
    * @returns {object} Returns all the channels
    */
    getChannels: function() {
      return this._channels;
    },

    /**
    * Get a specific channel
    * @method getChannel
    * @param {string} channel The Channel events are pubished to
    * @memberof Augmented.Presentation.Mediator
    * @returns {array} Returns the requested channel or null if nothing exists
    */
    getChannel: function(channel) {
      if (!channel) {
        channel = this._defaultChannel;
      }
      return (this._channels[channel]) ? (this._channels[channel]) : null;
    },

    /**
    * Get the default channel
    * Convenience method for getChannel(null)
    * @method getDefaultChannel
    * @memberof Augmented.Presentation.Mediator
    * @returns {array} Returns the default channel or null if nothing exists
    */
    getDefaultChannel: function() {
      return this.getChannel(this._defaultChannel);
    },

    /**
    * Get the default identifier
    * @method getDefaultIdentifier
    * @memberof Augmented.Presentation.Mediator
    * @returns {string} Returns the default identifier
    */
    getDefaultIdentifier: function() {
      return this._defaultIdentifier;
    }
  });

  const decoratorAttributeEnum = {
    "click": "data-click",
    "func": "data-function",
    "style": "data-style",
    "appendTemplate": "data-append-template",
    "prependTemplate": "data-prepend-template",
    // TODO: not implimented yet
    "appendTemplateEach": "data-append-template-each",
    "prependTemplateEach": "data-prepend-template-each"
  };

  /**
  * Augmented.Presentation.DecoratorView<br/>
  * An MVVM view designed around decorating the DOM with bindings.
  * This concept is designed to decouple the view from the backend contract.
  * Although this is achieved via views in general, the idea is:<br/>
  * <blockquote>As a Javascript Developer, I'd like the ability to decorate HTML and control view rendering without the use of CSS selectors</blockquote>
  * <em>Important to note: This view <strong>gives up</strong> it's template and events!
  * This is because all events and templates are used on the DOM directly.</em><br/>
  * To add custom events, use customEvents instead of 'events'<br/>
  * supported annotations:<br/>
  * <ul>
  * <li>data-click</li>
  * <li>data-function</li>
  * <li>data-style</li>
  * <li>data-append-template</li>
  * <li>data-prepend-template</li>
  * </ul>
  * @constructor Augmented.Presentation.DecoratorView
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.Colleague
  */
  Augmented.Presentation.DecoratorView = Augmented.Presentation.Colleague.extend({
    /**
    * Custom Events Property - merge into built-in events
    * @property customEvents
    * @memberof Augmented.Presentation.DecoratorView
    */
    customEvents: {},
    /**
    * Events Property - Do Not Override
    * @property events
    * @memberof Augmented.Presentation.DecoratorView
    */
    events: function(){
      let _events = (this.customEvents) ? this.customEvents : {};
      if (this.name) {
        _events["change input[" + this.bindingAttribute() + "]"] = "_changed";
        _events["change textarea[" + this.bindingAttribute() + "]"] = "_changed";
        _events["change select[" + this.bindingAttribute() + "]"] = "_changed";
        // regular elements with click bindings
        _events["click *[" + this.bindingAttribute() + "][" + decoratorAttributeEnum.click + "]"] = "_click";
      }
      return _events;
    },
    _changed: function(event) {
      var key = event.currentTarget.getAttribute(this.bindingAttribute());
      var val = event.currentTarget.value;
      if(event.currentTarget.type === "checkbox") {
        val = (event.currentTarget.checked) ? true : false;
      }
      this.model.set(( (key) ? key : event.currentTarget.name ), val);
      this._func(event);
      _logger.debug("AUGMENTED: DecoratorView updated Model: " + JSON.stringify(this.model.toJSON()));
    },
    _click: function(event) {
      var func = event.currentTarget.getAttribute(decoratorAttributeEnum.click);
      if (func && this[func]) {
        this._executeFunctionByName(func, this, event);
      }/* else {
        _logger.debug("AUGMENTED: DecoratorView No function bound or no function exists! " + func);
      }*/
      this._func(event);
    },
    _func: function(event) {
      var func = event.currentTarget.getAttribute(decoratorAttributeEnum.func);
      if (func && this[func]) {
        this._executeFunctionByName(func, this, event);
      } /*else {
        _logger.debug("AUGMENTED: DecoratorView No function bound or no function exists! " + func);
      }*/
    },
    /**
    * Initialize method - Do Not Override
    * @memberof Augmented.Presentation.DecoratorView
    * @method initialize
    */
    initialize: function(options) {
      this.init(options);

      if (!this.model) {
        this.model = new Augmented.Model();
      }
    },
    /**
    * Remove method - Does not remove DOM elements only bindings.
    * @method remove
    * @memberof Augmented.Presentation.DecoratorView
    */
    remove: function() {
      /* off to unbind the events */
      this.undelegateEvents();
      this.off();
      this.stopListening();
      return this;
    },
    /**
    * _executeFunctionByName method - Private
    * @method _executeFunctionByName
    * @memberof Augmented.Presentation.DecoratorView
    * @private
    */
    _executeFunctionByName: function(functionName, context /*, args */) {
      var args = Array.prototype.slice.call(arguments, 2);
      var namespaces = functionName.split(".");
      var func = namespaces.pop();
      for (var i = 0; i < namespaces.length; i++) {
        context = context[namespaces[i]];
      }
      return context[func].apply(context, args);
      //return Augmented.exec(arguments);
    },
    /**
    * bindingAttribute method - Returns the binging data attribute name
    * @method bindingAttribute
    * @memberof Augmented.Presentation.DecoratorView
    * @returns {string} Binding attribute name
    */
    bindingAttribute: function() {
      return "data-" + this.name;
    },
    /**
    * injectTemplate method - Injects a template at a mount point
    * @method injectTemplate
    * @param {string} template The template to inject
    * @param {Element} mount The mount point as Document.Element or String
    * @memberof Augmented.Presentation.DecoratorView
    */
    injectTemplate: function(template, mount) {
      var m = mount;
      if (!mount) {
        mount = this.el;
      }
      if (Augmented.isString(mount)) {
        mount = document.querySelector(mount);
      }
      if (Augmented.isString(template)) {
        // html
        var currentHTML = mount.innerHTML;
        mount.innerHTML = currentHTML + template;
      } else if ((template.nodeType && template.nodeName) &&
      template.nodeType > 0 && !(template.nodeName === "template" || template.nodeName === "TEMPLATE")) {
        // DOM
        mount.appendChild(template);
      } else if (template instanceof DocumentFragment  || template.nodeName === "template" || template.nodeName === "TEMPLATE") {
        // Document Fragment
        Augmented.Presentation.Dom.injectTemplate(template, mount);
      }
      this.delegateEvents();
    },
    /**
    * removeTemplate method - Removes a template (children) at a mount point
    * @method removeTemplate
    * @param {Element} mount The mount point as Document.Element or String
    * @param {boolean} onlyContent Only remove the content not the mount point
    * @memberof Augmented.Presentation.DecoratorView
    */
    removeTemplate: function(mount, onlyContent) {
      if (mount) {
        while (mount.firstChild) {
          mount.removeChild(mount.firstChild);
        }
        if (!onlyContent) {
          var p = mount.parentNode;
          if (p) {
            p.removeChild(mount);
          }
        }
        this.delegateEvents();
      }
    },
    /**
    * boundElement method - returns the bound element from identifier
    * @method boundElement
    * @param {string} id The identifier (not id attribute) of the element
    * @memberof Augmented.Presentation.DecoratorView
    * @example
    * from HTML: <div data-myMountedView="something" id="anything"></div>
    * from JavaScript: var el = this.boundElement("something");
    */
    boundElement: function(id) {
      if (this.el && id) {
        return this.el.querySelector("[" + this.bindingAttribute() + "=" + id + "]");
      }
      return null;
    },
    /**
    * syncBoundElement - Syncs the data of a bound element by firing a change event
    * @method syncBoundElement
    * @param {string} id The identifier (not id attribute) of the element
    * @memberof Augmented.Presentation.DecoratorView
    */
    syncBoundElement: function(id) {
      if (id) {
        var event = new UIEvent("change", {
          "view": window,
          "bubbles": true,
          "cancelable": true
        }), sel = this.boundElement(id);
        if (sel) {
          sel.dispatchEvent(event);
        }
      }
    },
    /**
    * syncAllBoundElements - Syncs the data of all bound elements by firing a change events
    * @method syncAllBoundElements
    * @memberof Augmented.Presentation.DecoratorView
    */
    syncAllBoundElements: function() {
      var elements = this.el.querySelectorAll("[" + this.bindingAttribute() + "]");
      if (elements && elements.length > 0) {
        var i = 0, l = elements.length, event = new UIEvent("change", {
          "view": window,
          "bubbles": true,
          "cancelable": true
        });
        for (i = 0; i < l; i++) {
          elements[i].dispatchEvent(event);
        }
      }
    },
    /**
    * addClass - adds a class to a bount element
    * @method addClass
    * @param {string} id The identifier (not id attribute) of the element
    * @param {string} cls The class to add
    * @memberof Augmented.Presentation.DecoratorView
    */
    addClass: function(id, cls) {
      var myEl = this.boundElement(id);
      myEl.classList.add(cls);
    },
    /**
    * removeClass - remove a class to a bount element
    * @method removeClass
    * @param {string} id The identifier (not id attribute) of the element
    * @param {string} cls The class to remove
    * @memberof Augmented.Presentation.DecoratorView
    */
    removeClass: function(id, cls) {
      var myEl = this.boundElement(id);
      myEl.classList.remove(cls);
    },
    /**
    * bindModelChange method - binds the model changes to functions
    * @method bindModelChange
    * @param {func} func The function to call when changing (normally render)
    * @memberof Augmented.Presentation.DecoratorView
    */
    bindModelChange: function(func) {
      if (!this.model) {
        this.model = new Augmented.Model();
      }
      this.model.on('change', func, this);
    },
    /**
    * syncModelChange method - binds the model changes to a specified bound element
    * @method syncModelChange
    * @param {Element} element The element to bind as Document.Element or string
    * @memberof Augmented.Presentation.DecoratorView
    */
    syncModelChange: function(element) {
      if (!this.model) {
        this.model = new Augmented.Model();
      }
      if (element) {
        this.model.on('change:' + element, this._syncData.bind(this, element), this);
      } else {
        this.model.on('change', this._syncAllData.bind(this, element), this);
      }
    },
    /**
    * _syncData method - syncs the model changes to a specified bound element
    * @method _syncData
    * @param {Element} element The element to bind as Document.Element or string
    * @memberof Augmented.Presentation.DecoratorView
    * @private
    */
    _syncData: function(element) {
      var e = this.boundElement(element);
      if (e) {
        var d = this.model.get(element),
        renderStyle = e.getAttribute(decoratorAttributeEnum.style),
        prependTemplate = e.getAttribute(decoratorAttributeEnum.prependTemplate),
        appendTemplate = e.getAttribute(decoratorAttributeEnum.appendTemplate),
        mount, template;

        if (prependTemplate) {
          mount = document.createElement("div");
          template = Augmented.Presentation.Dom.selector("#" + prependTemplate);
          e.appendChild(mount);
          this.injectTemplate(template, mount);
        }

        if (renderStyle) {
          var ee;
          /*,
          prependTemplateEach = e.getAttribute(decoratorAttributeEnum.prependTemplateEach),
          appendTemplateEach = e.getAttribute(decoratorAttributeEnum.appendTemplateEach),
          pEach = prependTemplateEach ? prependTemplateEach : null,
          aEach = appendTemplateEach ? appendTemplateEach : null;*/

          if (renderStyle === "list" || renderStyle === "unordered-list") {
            ee = Augmented.Presentation.Widget.List(null, d, false);
            Augmented.Presentation.Dom.empty(e);
            e.appendChild(ee);
          } else if (renderStyle === "ordered-list") {
            ee = Augmented.Presentation.Widget.List(null, d, true);
            Augmented.Presentation.Dom.empty(e);
            e.appendChild(ee);
          } else if (renderStyle === "description-list") {
            ee = Augmented.Presentation.Widget.DescriptionList(null, d);
            Augmented.Presentation.Dom.empty(e);
            e.appendChild(ee);
          }
        } else {
          Augmented.Presentation.Dom.setValue(e, ((d) ? d : ""));
        }

        if (appendTemplate) {
          mount = document.createElement("div");
          template = Augmented.Presentation.Dom.selector("#" + appendTemplate);
          e.appendChild(mount);

          this.injectTemplate(template, mount);
        }
      }
    },
    _syncAllData: function() {
      // get all model properties
      var attr = this.model.attributes;
      if (attr) {
        var i = 0, keys = Object.keys(attr), l = keys.length;
        for (i = 0; i < l; i++) {
          this._syncData(keys[i]);
        }
      }
    },
    /**
    * unbindModelChange method - unbinds the model changes to elements
    * @method unbindModelChange
    * @param {func} func The function to call when changing (normally render)
    * @memberof Augmented.Presentation.DecoratorView
    */
    unbindModelChange: function(func) {
      this.model.unBind('change', func, this);
    },
    /**
    * unbindModelSync method - unbinds the model changes to a specified bound element
    * @method unbindModelSync
    * @param {Element} element The element to bind as Document.Element or string
    * @memberof Augmented.Presentation.DecoratorView
    */
    unbindModelSync: function(element) {
      this.model.unBind('change:' + element, this._syncData, this);
    }
  });

  /**
  * Presentation Application - extension of Augmented.Application</br/>
  * Add registration of mediators to the application, breadcrumbs, and stylesheet registration
  * @constructor Augmented.Presentation.Application
  * @memberof Augmented.Presentation
  * @extends Augmented.Application
  */
  const app = Augmented.Presentation.Application = function() {
    Augmented.Application.apply(this, arguments);
    this.Mediators = [];
    this.Stylesheets = [];
    this.breadcrumb = new Augmented.Utility.Stack();

    /**
    * Initialize Event - adds any stylesheets registered
    * @method initialize
    * @memberof Augmented.Presentation.Application
    */
    this.initialize = function() {
      if (this.Stylesheets && this.Stylesheets.length > 0) {
        this.attachStylesheets();
      }
    };
    /**
    * Register a Mediator
    * @method registerMediator
    * @memberof Augmented.Presentation.Application
    * @param {Augmented.Presentation.Mediator} mediator The mediator to register
    */
    this.registerMediator = function(mediator) {
      if (mediator) {
        this.Mediators.push(mediator);
      }
    };
    /**
    * Deregister a Mediator
    * @method deregisterMediator
    * @memberof Augmented.Presentation.Application
    * @param {Augmented.Presentation.Mediator} mediator The mediator to deregister
    */
    this.deregisterMediator = function(mediator) {
      if (mediator) {
        var i = this.Mediators.indexOf(mediator);
        if (i != -1) {
          this.Mediators.splice(i, 1);
        }
      }
    };

    /**
    * Get all Mediators
    * @method getMediators
    * @memberof Augmented.Presentation.Application
    * @returns {array} Returns all Mediators
    */
    this.getMediators = function() {
      return this.Mediators;
    };

    /**
    * Register a stylesheet
    * @method registerStylesheet
    * @memberof Augmented.Presentation.Application
    * @param {string} stylesheet URI of the stylesheet
    */
    this.registerStylesheet = function(s) {
      if (s) {
        this.Stylesheets.push(s);
      }
    };
    /**
    * Deregister a stylesheet
    * @method deregisterStylesheet
    * @memberof Augmented.Presentation.Application
    * @param {string} stylesheet URI of the stylesheet
    */
    this.deregisterStylesheet = function(s) {
      if (s) {
        this.Stylesheets.splice((this.Stylesheets.indexOf(s)), 1);
      }
    };
    /**
    * Attach registered stylesheets to the DOM
    * @method attachStylesheets
    * @memberof Augmented.Presentation.Application
    */
    this.attachStylesheets = function() {
      var headElement = document.getElementsByTagName("head")[0],
      // create a shadow DOM
      shaddowDom = document.createDocumentFragment(),
      i = 0, l = this.Stylesheets.length, link = null;
      for (i = 0; i < l; i++) {
        link = document.createElement("link");
        link.type = "text/css";
        link.rel = "stylesheet";
        link.href = this.Stylesheets[i];
        shaddowDom.appendChild(link);
      }
      // add the shadow to the real DOM
      headElement.appendChild(shaddowDom);
    };
    /**
    * Replace stylesheets then attach registered stylesheets to the DOM
    * @method replaceStylesheets
    * @memberof Augmented.Presentation.Application
    */
    this.replaceStylesheets = function() {
      var links = document.getElementsByTagName("link");
      var i = 0, l = links.length - 1;
      for (i = l; i >= 0; i--) {
        element[i].parentNode.removeChild(element[i]);
      }
      this.attachStylesheets();
    };
    /**
    * Sets the current breadcrumb
    * @method setCurrentBreadcrumb
    * @memberof Augmented.Presentation.Application
    * @param {string} uri The URI of the breadcrumb
    * @param {string} name The name of the breadcrumb
    */
    this.setCurrentBreadcrumb = function(uri, name) {
      if (this.breadcrumb.size() > 1) {
        this.breadcrumb.pop();
      }
      this.breadcrumb.push({ "uri": uri, "name": name });
    };
    /**
    * Gets the current breadcrumb
    * @method getCurrentBreadcrumb
    * @memberof Augmented.Presentation.Application
    * @returns {object} Returns the current breadcrumb
    */
    this.getCurrentBreadcrumb = function() {
      return this.breadcrumb.peek();
    };

    /**
    * Get all the breadcrumbs
    * @method getBreadcrumbs
    * @memberof Augmented.Presentation.Application
    * @returns {array} Returns alls the breadcrumbs
    */
    this.getBreadcrumbs = function() {
      return this.breadcrumb.toArray();
    };
  };

  app.prototype.constructor = app;

  // Tables and Grids

  const tableDataAttributes = {
    name:           "data-name",
    type:           "data-type",
    description:    "data-description",
    index:          "data-index",
    label:          "data-label",
    sortClass:      "sorted"
  };

  const csvTableCompile = function(name, desc, columns, data, del) {
    var csv = "";
    if (!del) {
      del = ",";
    }
    if (columns) {
      var key, obj;
      for (key in columns) {
        if (columns.hasOwnProperty(key)) {
          obj = columns[key];
          csv = csv + key + del;
        }
      }
      csv = csv.slice(0, -1);
      csv = csv + "\n";
    }

    var i, d, dkey, dobj, html = "", l = data.length, t;
    for (i = 0; i < l; i++) {
      d = data[i];
      for (dkey in d) {
        if (d.hasOwnProperty(dkey)) {
          dobj = d[dkey];
          t = (typeof dobj);
          csv = csv + dobj + del;
        }
      }
      csv = csv.slice(0, -1);
      csv = csv + "\n";
    }
    return csv;
  };

  const tsvTableCompile = function(name, desc, columns, data) {
    return csvTableCompile(name, desc, columns, data, "\t");
  };

  const defaultTableCompile = function(name, desc, columns, data, lineNumbers, sortKey, editable, display) {
    var html = "<table " + tableDataAttributes.name + "=\"" + name + "\" " + tableDataAttributes.description + "=\"" + desc + "\">";
    if (name) {
      html = html + "<caption";
      if (desc) {
        html = html + " title=\"" + desc + "\"";
      }
      html = html + ">" + name + "</caption>";
    }
    html = html + "<thead>";
    html = html + defaultTableHeader(columns, lineNumbers, sortKey, display);
    html = html + "</thead><tbody>";
    if (data) {
      if (editable) {
        html = html + editableTableBody(data, columns, lineNumbers, sortKey, display);
      } else {
        html = html + defaultTableBody(data, columns, lineNumbers, sortKey, display);
      }
    }
    html = html + "</tbody></table>";
    return html;
  };

  const defaultTableHeader = function(columns, lineNumbers, sortKey, display) {
    var html = "";
    if (columns) {
      html = html + "<tr>";
      if (lineNumbers) {
        html = html + "<th " + tableDataAttributes.name + "=\"lineNumber\">#</th>";
      }
      var key, obj;
      for (key in columns) {
        if (columns.hasOwnProperty(key)) {
          obj = columns[key];
          html = html + "<th " + tableDataAttributes.name + "=\"" + key + "\" " + tableDataAttributes.description + "=\"" + obj.description + "\" " + tableDataAttributes.type + "=\"" + obj.type + "\"";
          if (sortKey === key) {
            html = html + " class=\"" + tableDataAttributes.sortClass + "\"";
          }
          html = html + ">" + key + "</th>";
        }
      }
      html = html + "</tr>";
    }
    return html;
  };

  const defaultTableBody = function(data, columns, lineNumbers, sortKey, display) {
    var i, d, dkey, dobj, html = "", l = data.length, t;
    for (i = 0; i < l; i++) {
      d = data[i];
      html = html + "<tr>";
      if (lineNumbers) {
        html = html + "<td class=\"label number\">" + (i+1) + "</td>";
      }
      for (dkey in d) {
        if (d.hasOwnProperty(dkey)) {
          dobj = d[dkey];
          t = (typeof dobj);
          html = html + "<td " + tableDataAttributes.type + "=\"" + t + "\" class=\"" + t;
          if (sortKey === dkey) {
            html = html + " " + tableDataAttributes.sortClass;
          }
          html = html + "\">" + dobj + "</td>";
        }
      }
      html = html + "</tr>";
    }
    return html;
  };

  const formatValidationMessages = function(messages) {
    let html = "";
    if (messages && messages.length > 0) {
      html = html + "<ul class=\"errors\">";
      const l = messages.length;
      let i = 0, ii = 0;
      for (i = 0; i < l; i++) {
        const ll = messages[i].errors.length;
        for (ii = 0; ii < ll; ii++) {
          html = html + "<li>" + messages[i].errors[ii] + "</li>";
        }
      }
      html = html + "</ul>";
    }
    return html;
  };

  const AbstractAutoTable = Augmented.Presentation.DecoratorView.extend({
    /**
    * The linkable property - enable links in a row (only works in non-editable tables)
    * @property {boolean} linkable enable/disable linking a row
    * @memberof Augmented.Presentation.AutomaticTable
    */
    linkable: false,
    /**
    * The links property - setup linking structure for links in a row
    * @property {boolean} linkable enable/disable linking a row
    * @example links: {
    * wholeRow: false, // link whole row vs column
    * column: "name", // name of column
  	*	link: "rowLink" // callback
  	* }
    * @memberof Augmented.Presentation.AutomaticTable
    */
  	links: {
      wholeRow: true,
      column: "",
  		link: "rowLink"
  	},
    /**
    * The default rowlink function callback called by row to format a link
    * @method rowlink
    * @param {array} row The row data
    * @returns {string} Returns the link uri
    * @memberof Augmented.Presentation.AutomaticTable
    */
    rowLink: function(row) {
      return "";
    },
    /**
    * The selectable property - enable selecting a row in table
    * @property {boolean} selectable enable/disable selecting a row
    * @memberof Augmented.Presentation.AutomaticTable
    */
    selectable: false,
    /**
    * The sortable property - enable sorting in table
    * @property {boolean} sortable enable sorting in the table
    * @memberof Augmented.Presentation.AutomaticTable
    */
    sortable: false,
    /**
    * The sortStyle property - setup the sort API
    * @property {string} sortStyle setup the sort API
    * @memberof Augmented.Presentation.AutomaticTable
    */
    sortStyle: "client",
    /**
    * The sortKey property
    * @property {string} sortKey sorted key
    * @private
    * @memberof Augmented.Presentation.AutomaticTable
    */
    sortKey: null,
    /**
    * Sort the tabe by a key (sent via a UI Event)
    * @method sortBy
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {string} key The key to sort by
    */
    sortBy: function(key) {
      if (key && ( (this.editable) || (!this.editable && this.sortKey !== key))) {
        this.sortKey = key;
        this.collection.sortByKey(key);
        this.refresh();
      }
    },

    /**
      * Fields to display - null will display all
      * @method display
      * @memberof Augmented.Presentation.AutomaticTable
      */
    display: null,

    // pagination
    /**
    * The renderPaginationControl property - render the pagination control
    * @property {boolean} renderPaginationControl render the pagination control
    * @memberof Augmented.Presentation.AutomaticTable
    */
    renderPaginationControl: false,
    /**
    * The paginationAPI property - setup the paginatin API to use
    * @property {Augmented.PaginationFactory.type} paginationAPI the pagination API to use
    * @memberof Augmented.Presentation.AutomaticTable
    */
    paginationAPI: null,
    /**
    * The name property
    * @property {string} name The name of the table
    * @memberof Augmented.Presentation.AutomaticTable
    */
    name: "",
    /**
    * The description property
    * @property {string} description The description of the table
    * @memberof Augmented.Presentation.AutomaticTable
    */
    description: "",
    /**
    * Return the current page number
    * @method currentPage
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {number} The current page number
    */
    currentPage: function() {
      return this.collection.currentPage;
    },
    /**
    * Return the total pages
    * @method totalPages
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {number} The total pages
    */
    totalPages: function() {
      return this.collection.totalPages;
    },
    /**
    * Advance to the next page
    * @method nextPage
    * @memberof Augmented.Presentation.AutomaticTable
    */
    nextPage: function() {
      this.collection.nextPage();
      this.refresh();
    },
    /**
    * Return to the previous page
    * @method previousPage
    * @memberof Augmented.Presentation.AutomaticTable
    */
    previousPage: function() {
      this.collection.previousPage();
      this.refresh();
    },
    /**
    * Go to a specific page
    * @method goToPage
    * @param {number} page The page to go to
    * @memberof Augmented.Presentation.AutomaticTable
    */
    goToPage: function(page) {
      this.collection.goToPage(page);
      this.refresh();
    },
    /**
    * Return to the first page
    * @method firstPage
    * @memberof Augmented.Presentation.AutomaticTable
    */
    firstPage: function() {
      this.collection.firstPage();
      this.refresh();
    },
    /**
    * Advance to the last page
    * @method lastPage
    * @memberof Augmented.Presentation.AutomaticTable
    */
    lastPage: function() {
      this.collection.lastPage();
      this.refresh();
    },

    // local storage

    /**
    * The localStorage property - enables localStorage
    * @property {boolean} localStorage The localStorage property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    localStorage: false,
    /**
    * The localStorageKey property - set the key for use in storage
    * @property {string} localStorageKey The localStorage key property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    localStorageKey: "augmented.localstorage.autotable.key",

    // editable

    /**
    * The editable property - enables editing of cells
    * @property {boolean} editable The editable property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    editable: false,

    /**
    * Edit a cell at the row and column specified
    * @method editCell
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {number} row The row
    * @param {number} col The column
    * @param {any} value The value to set
    */
    editCell: function(row, col, value) {
      if (row && col) {
        var model = this.collection.at(row), name = this.columns[col];
        if (model && name) {
          model.set(name, value);
        }
      }
    },
    /**
    * Copy a cell at the row and column  to another
    * @method copyCell
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {number} row1 The 'from' row
    * @param {number} col1 The 'from' column
    * @param {number} row2 The 'to' row
    * @param {number} col2 The 'to' column
    */
    copyCell: function(row1, col1, row2, col2) {
      if (row1 && col1 && row2 && col2) {
        var model1 = this.collection.at(row1), name1 = this.columns[col1],
        model2 = this.collection.at(row);
        if (model1 && name1 && model2) {
          model2.set(name1, value1);
        }
      }
    },
    /**
    * Clear a cell at the row and column specified
    * @method clearCell
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {number} row The row
    * @param {number} col The column
    */
    clearCell: function(row, col) {
      this.editCell(row, col, null);
    },

    // standard functionality

    /**
    * The crossOrigin property - enables cross origin fetch
    * @property {boolean} crossOrigin The crossOrigin property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    crossOrigin: false,
    /**
    * The lineNumber property - turns on line numbers
    * @property {boolean} lineNumbers The lineNumbers property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    lineNumbers: false,
    /**
    * The columns property
    * @property {object} columns The columns property
    * @private
    * @memberof Augmented.Presentation.AutomaticTable
    */
    _columns: {},
    /**
    * The URI property
    * @property {string} uri The URI property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    uri: null,
    /**
    * The data property
    * @property {array} data The data property
    * @memberof Augmented.Presentation.AutomaticTable
    * @private
    */
    data: [],
    /**
    * The collection property
    * @property {Augmented.PaginatedCollection} collection The collection property
    * @memberof Augmented.Presentation.AutomaticTable
    * @private
    */
    collection: null,
    /**
    * The initialized property
    * @property {boolean} isInitalized The initialized property
    * @memberof Augmented.Presentation.AutomaticTable
    */
    isInitalized : false,
    /**
    * Initialize the table view
    * @method initialize
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {object} options The view options
    * @returns {boolean} Returns true on success of initalization
    */
    initialize: function(options) {
      this.init();

      if (!this.model) {
        this.model = new Augmented.Model();
      }

      if (this.collection) {
        this.collection.reset();
      }
      if (options) {
        if (options.paginationAPI) {
          this.paginationAPI = options.paginationAPI;
        }
        if (!this.collection && this.paginationAPI) {
          this.collection = Augmented.PaginationFactory.getPaginatedCollection(this.paginationAPI);
          this.paginationAPI = this.collection.paginationAPI;
          this.localStorage = false;
        } else if (!this.collection && this.localStorage) {
          this.collection = new Augmented.LocalStorageCollection();
        } else if (!this.collection) {
          this.collection = new Augmented.Collection();
        }
        if (options.schema) {
          // check if this is a schema vs a URI to get a schema
          if (Augmented.isObject(options.schema)) {
            this.schema = options.schema;
          } else {
            // is a URI?
            let parsedSchema = null;
            try {
              parsedSchema = JSON.parse(options.schema);
              if (parsedSchema && Augmented.isObject(parsedSchema)) {
                this.schema = parsedSchema;
              }
            } catch(e) {
              _logger.warn("AUGMENTED: AutoTable parsing string schema failed.  URI perhaps?");
            }
            if (!this.schema) {
              this.retrieveSchema(options.schema);
              this.isInitalized = false;
            }
          }
        }

        if (options.el) {
          this.el = options.el;
        }

        if (options.uri) {
          this.uri = options.uri;
          this.collection.url = options.uri;
        }

        if (options.data && (Array.isArray(options.data))) {
          this.populate(options.data);
        }

        if (options.renderPaginationControl) {
          this.renderPaginationControl = options.renderPaginationControl;
        }

        if (options.selectable) {
          this.selectable = options.selectable;
        }

        if (options.sortable) {
          this.sortable = options.sortable;
        }

        if (options.lineNumbers) {
          this.lineNumbers = options.lineNumbers;
        }

        if (options.editable) {
          this.editable = options.editable;
        }

        if (options.localStorageKey && !options.uri) {
          this.localStorageKey = options.localStorageKey;
          this.uri = null;
        }
      }

      if (this.collection && this.uri) {
        this.collection.url = this.uri;
      }
      if (this.collection) {
        this.collection.crossOrigin = this.crossOrigin;
      }
      if (this.schema) {
        if (this.schema.title && this.name === "") {
          this.name = this.schema.title;
        }
        if (this.schema.description) {
          this.description = this.schema.description;
        }

        if (!this.isInitalized) {
          this._columns = this.schema.properties;
          this.collection.schema = this.schema;
          this.isInitalized = true;
        }

      } else {
        this.isInitalized = false;
        return false;
      }

      return this.isInitalized;
    },

    /**
    * Fetch the schema from the source URI
    * @method retrieveSchema
    * @param uri {string} the URI to fetch from
    * @memberof Augmented.Presentation.AutomaticTable
    */
    retrieveSchema: function(uri){
      const that = this;
      let schema = null;
      Augmented.ajax({
        url: uri,
        contentType: 'application/json',
        dataType: 'json',
        success: function(data, status) {
          if (typeof data === "string") {
            schema = JSON.parse(data);
          } else {
            schema = data;
          }
          const options = { "schema": schema };
          that.initialize(options);
        },
        failure: function(data, status) {
          _logger.warn("AUGMENTED: AutoTable Failed to fetch schema!");
        }
      });
    },
    /**
    * Fetch the data from the source URI
    * @method fetch
    * @memberof Augmented.Presentation.AutomaticTable
    */
    fetch: function() {
      // TODO: should be a promise
      this.showProgressBar(true);

      const view = this;

      const successHandler = function() {
        view.showProgressBar(false);
        view.sortKey = null;
        view.populate(view.collection.toJSON());
        view.refresh();
      };

      const failHandler = function() {
        view.showProgressBar(false);
        view.showMessage("AutomaticTable fetch failed!");
      };

      this.collection.fetch({
        reset: true,
        success: function(){
          successHandler();
        },
        error: function(){
          failHandler();
        }
      });
    },

    /**
    * Save the data to the source
    * This only functions if the table is editable
    * @method save
    * @param {boolean} override Save even if not editable
    * @returns Returns true if succesfull
    * @memberof Augmented.Presentation.AutomaticTable
    */
    save: function(override) {
      if (this.editable || override) {
        this.showProgressBar(true);

        const view = this;

        const successHandler = function() {
          view.showProgressBar(false);
          return true;
        };

        const failHandler = function() {
          view.showProgressBar(false);
          view.showMessage("AutomaticTable save failed!");
          _logger.warn("AUGMENTED: AutomaticTable save failed!");
          return false;
        };

        this.collection.save({
          reset: true,
          success: function(){
            successHandler();
          },
          error: function(){
            failHandler();
          }
        });
      }
      return false;
    },

    /**
    * Populate the data in the table
    * @method populate
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {array} source The source data array
    */
    populate: function(source) {
      if (source && Array.isArray(source)) {
        this.sortKey = null;
        this.data = source;
        this.collection.reset(this.data);
      }
    },
    /**
    * Clear all the data in the table
    * @method clear
    * @memberof Augmented.Presentation.AutomaticTable
    */
    clear: function() {
      this.sortKey = null;
      this.data = [];
      this.collection.reset(null);
    },
    /**
    * Refresh the table (Same as render)
    * @method refresh Refresh the table
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {object} Returns the view context ('this')
    * @see Augmented.Presentation.AutomaticTable.render
    */
    refresh: function() {
      return this.render();
    },
    /**
    * Render the table
    * @method render Renders the table
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {object} Returns the view context ('this')
    */
    render: function() {
      return this;
    },

    /**
    * Save Cell Event
    * @private
    */
    saveCell: function(event) {
      const key = event.target, model = this.collection.at(parseInt(key.getAttribute(tableDataAttributes.index)));
      let value = key.value;
      if ((key.getAttribute("type")) === "number") {
        value = parseInt(key.value);
      }
      model.set(key.getAttribute(tableDataAttributes.name), value);
    },

    /**
    * @private
    */
    bindCellChangeEvents: function() {
      var myEl = (typeof this.el === 'string') ? this.el : this.el.localName;
      var cells = [].slice.call(document.querySelectorAll(myEl + " table tr td input"));
      var i=0, l=cells.length;
      for(i=0; i < l; i++) {
        cells[i].addEventListener("change", this.saveCell.bind(this), false);
      }
      // bind the select boxes as well
      cells = [].slice.call(document.querySelectorAll(myEl + " table tr td select"));
      i=0;
      l=cells.length;
      for(i=0; i < l; i++) {
        cells[i].addEventListener("change", this.saveCell.bind(this), false);
      }
    },

    /**
    * @private
    */
    unbindCellChangeEvents: function() {
      var myEl = (typeof this.el === 'string') ? this.el : this.el.localName;
      var cells = [].slice.call(document.querySelectorAll(myEl + " table tr td input"));
      var i=0, l=cells.length;
      for(i=0; i < l; i++) {
        cells[i].removeEventListener("change", this.saveCell, false);
      }
      // unbind the select boxes as well
      cells = [].slice.call(document.querySelectorAll(myEl + " table tr td select"));
      i=0;
      l=cells.length;
      for(i=0; i < l; i++) {
        cells[i].removeEventListener("change", this.saveCell, false);
      }
    },

    /**
    * Export the table data in requested format
    * @method exportTo Exports the table
    * @param {string} type The type requested (csv or html-default)
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {string} The table data in requested format
    */
    exportTo: function(type) {
      var e = "";
      if (type === "csv") {
        e = csvTableCompile(this.name, this.description, this._columns, this.collection.toJSON());
      } else if (type === "tsv") {
        e = tsvTableCompile(this.name, this.description, this._columns, this.collection.toJSON());
      } else {
        // html
        e = defaultTableCompile(this.name, this.description, this._columns, this.collection.toJSON(), false, null);
      }
      return e;
    },

    /**
    * @private
    */
    unbindPaginationControlEvents: function() {
      if (this.pageControlBound) {
        var myEl = (typeof this.el === 'string') ? this.el : this.el.localName;
        var first = document.querySelector(myEl + " div.paginationControl span.first");
        var previous = document.querySelector(myEl + " div.paginationControl span.previous");
        var next = document.querySelector(myEl + " div.paginationControl span.next");
        var last = document.querySelector(myEl + " div.paginationControl span.last");
        if (first) {
          first.removeEventListener("click", this.firstPage, false);
        }
        if (previous) {
          previous.removeEventListener("click", this.previousPage, false);
        }
        if (next) {
          next.removeEventListener("click", this.nextPage, false);
        }
        if (last) {
          last.removeEventListener("click", this.lastPage, false);
        }
        this.pageControlBound = false;
      }
    },

    /**
    * @private
    */
    pageControlBound: false,

    /**
    * @private
    */
    bindPaginationControlEvents: function() {
      if (!this.pageControlBound) {
        var myEl = (typeof this.el === 'string') ? this.el : this.el.localName;
        var first = document.querySelector(myEl + " div.paginationControl span.first");
        var previous = document.querySelector(myEl + " div.paginationControl span.previous");
        var next = document.querySelector(myEl + " div.paginationControl span.next");
        var last = document.querySelector(myEl + " div.paginationControl span.last");
        if (first) {
          first.addEventListener("click", this.firstPage.bind(this), false);
        }
        if (previous) {
          previous.addEventListener("click", this.previousPage.bind(this), false);
        }
        if (next) {
          next.addEventListener("click", this.nextPage.bind(this), false);
        }
        if (last) {
          last.addEventListener("click", this.lastPage.bind(this), false);
        }
        this.pageControlBound = true;
      }
    },

    /**
    * @private
    */
    deriveEventTarget: function(event) {
      var key = null;
      if (event) {
        key = event.target.getAttribute(tableDataAttributes.name);
      }
      return key;
    },
    /**
    * @private
    */
    sortByHeaderEvent: function(event) {
      var key = this.deriveEventTarget(event);
      this.sortBy(key);
    },
    /**
    * @private
    */
    unbindSortableColumnEvents: function()  {
      if (this.el && this.sortable) {
        var list;
        if (typeof this.el === 'string') {
          list = document.querySelectorAll(this.el + " table tr th");
        } else {
          list = document.querySelectorAll(this.el.localName + " table tr th");
        }
        var i = 0, l = list.length;
        for (i = 0; i < l; i++) {
          list[i].removeEventListener("click", this.sortByHeaderEvent, false);
        }
      }
    },
    /**
    * @private
    */
    bindSortableColumnEvents: function()  {
      if (this.el && this.sortable) {
        var list;
        if (typeof this.el === 'string') {
          list = document.querySelectorAll(this.el + " table tr th");
        } else {
          list = document.querySelectorAll(this.el.localName + " table tr th");
        }
        var i = 0, l = list.length;
        for (i = 0; i < l; i++) {
          if (list[i].getAttribute(tableDataAttributes.name) === "lineNumber") {
            // Do I need to do something?
          } else {
            list[i].addEventListener("click", this.sortByHeaderEvent.bind(this), false);
          }
        }
      }
    },

    /**
    * An overridable template compile
    * @method compileTemplate
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {string} Returns the template
    */
    compileTemplate: function() {
      return "";
    },
    /**
    * Sets the URI
    * @method setURI
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {string} uri The URI
    */
    setURI: function(uri) {
      this.uri = uri;
    },
    /**
    * Sets the schema
    * @method setSchema
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {object} schema The JSON schema of the dataset
    */
    setSchema: function(schema) {
      this.schema = schema;
      this._columns = schema.properties;
      this.collection.reset();
      this.collection.schema = schema;

      if (this.uri) {
        this.collection.url = this.uri;
      }
    },

    /**
    * Enable/Disable the progress bar
    * @method showProgressBar
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {boolean} show Show or Hide the progress bar
    */
    showProgressBar: function(show) {
      if (this.el) {
        var e = (typeof this.el === 'string') ? document.querySelector(this.el) : this.el;
        var p = e.querySelector("progress");
        if (p) {
          p.style.display = (show) ? "block" : "none";
          p.style.visibility = (show) ? "visible" : "hidden";
        }
      }
    },
    /**
    * Show a message related to the table
    * @method showMessage
    * @memberof Augmented.Presentation.AutomaticTable
    * @param {string} message Some message to display
    */
    showMessage: function(message) {
      if (this.el) {
        var e = (typeof this.el === 'string') ? document.querySelector(this.el) : this.el;
        var p = e.querySelector("p[class=message]");
        if (p) {
          p.innerHTML = message;
        }
      }
    },
    /**
    * Validate the table
    * @method validate
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {boolean} Returns true on success of validation
    */
    validate: function() {
      var messages = (this.collection) ? this.collection.validate() : null;
      if (!this.collection.isValid() && messages && messages.messages) {
        this.showMessage(formatValidationMessages(messages.messages));
      } else {
        this.showMessage("");
      }
      return messages;
    },
    /**
    * Is the table valid
    * @method isValid
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {boolean} Returns true if valid
    */
    isValid: function() {
      return (this.collection) ? this.collection.isValid() : true;
    },
    /**
    * Remove the table and all binds
    * @method remove
    * @memberof Augmented.Presentation.AutomaticTable
    */
    remove: function() {
      /* off to unbind the events */
      this.undelegateEvents();
      this.off();
      this.stopListening();

      Augmented.Presentation.Dom.empty(this.el);

      return this;
    },
    /**
    * Gets the selected models
    * @method getSelected
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {Array} Returns array of selected rows (models)
    */
    getSelected: function() {
      const keys = Object.keys(this.model.attributes), l = keys.length, selected = [];
      let i = 0;
      for (i = 0; i < l; i++) {
        if (keys[i].includes("row-") && this.model.attributes[keys[i]] === true) {
          const n = Number(keys[i].substring(4));
          selected.push(this.collection.at(n));
        }
      }
      return selected;
    },
    /**
    * Gets the selected row indexes
    * @method getSelectedIndex
    * @memberof Augmented.Presentation.AutomaticTable
    * @returns {Array} Returns array of selected rows (indexes)
    */
    getSelectedIndex: function() {
      const keys = Object.keys(this.model.attributes), l = keys.length, selected = [];
      let i = 0;
      for (i = 0; i < l; i++) {
        if (keys[i].includes("row-") && this.model.attributes[keys[i]] === true) {
          selected.push(Number(keys[i].substring(4)));
        }
      }
      return selected;
    },
    /**
    * Removes the models
    * @method removeRows
    * @param {Array} rows Models of the rows to remove
    * @memberof Augmented.Presentation.AutomaticTable
    */
    removeRows: function(rows) {
      const l = rows.length;
      let i = 0;
      for (i = 0; i < l; i++) {
        const model = rows[i];
        if (!model.url) {
          model.url = this.uri + "/" + model.id;
        }
        model.destroy();
      }
    }
  });

  const directDOMTableCompile = function(el, name, desc, columns, data, lineNumbers, sortKey, editable, display, selectable, linkable, linksConfig, linkCallback) {
    const table = document.createElement("table"), thead = document.createElement("thead"), tbody = document.createElement("tbody");
    let n, t;

    // Binding
    table.setAttribute("data-" + name, name);

    table.setAttribute(tableDataAttributes.name, name);
    table.setAttribute(tableDataAttributes.description, desc);
    if (name) {
      n = document.createElement("caption");
      if (desc) {
        n.setAttribute("title", desc);
      }
      t = document.createTextNode(name);
      n.appendChild(t);
      table.appendChild(n);
    }
    directDOMTableHeader(thead, columns, lineNumbers, sortKey, display, selectable);
    table.appendChild(thead);
    table.appendChild(tbody);
    if (data) {
      if (editable) {
        directDOMEditableTableBody(tbody, data, columns, lineNumbers, sortKey, display, selectable, linkable, linksConfig, linkCallback);
      } else {
        directDOMTableBody(tbody, data, columns, lineNumbers, sortKey, display, selectable, linkable, linksConfig, linkCallback);
      }
    }
    el.appendChild(table);
  };

  const directDOMTableHeader = function(el, columns, lineNumbers, sortKey, display, selectable) {
    if (columns && el) {
      const tr = document.createElement("tr");
      let n, t, key, obj;
      if (selectable) {
        n = document.createElement("th");
        n.setAttribute(tableDataAttributes.name, "select");
        t = document.createTextNode("\u274f");
        n.appendChild(t);
        tr.appendChild(n);
      }

      if (lineNumbers) {
        n = document.createElement("th");
        n.setAttribute(tableDataAttributes.name, "lineNumber");
        t = document.createTextNode("#");
        n.appendChild(t);
        tr.appendChild(n);
      }

      for (key in columns) {
        let displayCol = true;
        if (display !== null) {
            displayCol = (display.indexOf(key) !== -1);
        }

        if (displayCol && columns.hasOwnProperty(key)) {
          obj = columns[key];

          n = document.createElement("th");
          n.setAttribute(tableDataAttributes.name, key);
          n.setAttribute(tableDataAttributes.description, obj.description);
          n.setAttribute(tableDataAttributes.type, obj.type);
          if (sortKey === key) {
            n.classList.add(tableDataAttributes.sortClass);
          }

          t = document.createTextNode(key);
          n.appendChild(t);
          tr.appendChild(n);
        }
      }
      el.appendChild(tr);
    }
  };

  const directDOMTableBody = function(el, data, columns, lineNumbers, sortKey, display, selectable, name, linkable, linksConfig, linkCallback) {
    const l = data.length;
    let i, d, dkey, dobj, t, td, tn, tr, cobj;

    for (i = 0; i < l; i++) {
      d = data[i];
      tr = document.createElement("tr");

      if (selectable) {
        td = document.createElement("td");
        td.setAttribute(tableDataAttributes.name, "select");
        tn = document.createElement("input");
        tn.type = "checkbox";
        tn.name = String(i);
        tn.value = String(i);
        // Binding
        tn.setAttribute("data-" + name, "row-" + i);

        td.appendChild(tn);
        td.classList.add("label", "select");
        tr.appendChild(td);
      }

      if (lineNumbers) {
        td = document.createElement("td");
        tn = document.createTextNode(String(i + 1));
        td.appendChild(tn);
        td.classList.add("label", "number");
        tr.appendChild(td);
      }
      for (dkey in columns) {
        let displayCol = true;
        if (display !== null) {
            displayCol = (display.indexOf(dkey) !== -1);
        }
        if (displayCol && d.hasOwnProperty(dkey)) {
          dobj = d[dkey];
          t = (typeof dobj);
          td = document.createElement("td");
          tn = document.createTextNode(dobj);

          if (linkable && linksConfig && linkCallback &&
              ((linksConfig.column === dkey) || (linksConfig.wholeRow)) ) {
            const a = document.createElement("a");
            //a.title = "my title text";
            a.href = linkCallback(d);
            a.appendChild(tn);
            td.appendChild(a);
          } else {
            td.appendChild(tn);
          }

          td.classList.add(t);
          if (sortKey === dkey) {
            td.classList.add(tableDataAttributes.sortClass);
          }
          td.setAttribute(tableDataAttributes.type, t);
          td.setAttribute(tableDataAttributes.label, dkey);
          tr.appendChild(td);
        }
      }
      el.appendChild(tr);
    }
  };

  const directDOMEditableTableBody = function(el, data, columns, lineNumbers, sortKey, display, selectable, name) {
    const l = data.length, ln = lineNumbers;
    let i, d, dkey, dobj, t, td, tn, tr, input, cobj;
    for (i = 0; i < l; i++) {
      d = data[i];
      tr = document.createElement("tr");

      if (selectable) {
        td = document.createElement("td");
        td.setAttribute(tableDataAttributes.name, "select");
        tn = document.createElement("input");
        tn.type = "checkbox";
        tn.name = String(i);
        tn.value = String(i);
        td.appendChild(tn);
        td.classList.add("label", "select");
        tr.appendChild(td);
      }

      if (ln) {
        td = document.createElement("td");
        tn = document.createTextNode(String(i + 1));
        td.appendChild(tn);
        td.classList.add("label", "number");
        tr.appendChild(td);
      }

      for (dkey in d) {
        let displayCol = true;
        if (display !== null) {
            displayCol = (display.indexOf(dkey) !== -1);
        }

        if (displayCol && d.hasOwnProperty(dkey)) {
          cobj = (columns[dkey]) ? columns[dkey] : {};
          dobj = d[dkey];

          t = (typeof dobj);

          td = document.createElement("td");
          td.classList.add(t);
          if (sortKey === dkey) {
            td.classList.add(tableDataAttributes.sortClass);
          }
          td.setAttribute(tableDataAttributes.type, t);
          td.setAttribute(tableDataAttributes.label, dkey);
          // input field

          if (t === "object") {
            if (Array.isArray(dobj)) {
              let iii = 0, lll = dobj.length, option, tOption;
              input = document.createElement("select");
              for (iii = 0; iii < lll; iii++) {
                option = document.createElement("option");
                option.setAttribute("value", dobj[iii]);
                tOption = document.createTextNode(dobj[iii]);
                option.appendChild(tOption);
                input.appendChild(option);
              }
            } else {
              input = document.createElement("textarea");
              input.value = JSON.stringify(dobj);
            }
          } else if (t === "boolean") {
            input = document.createElement("input");
            input.setAttribute("type", "checkbox");
            if (dobj === true) {
              input.setAttribute("checked", "checked");
            }
            input.value = dobj;
          } else if (t === "number") {
            input = document.createElement("input");
            input.setAttribute("type", "number");
            input.value = dobj;
          } else if (t === "string" && cobj.enum) {
            input = document.createElement("select");
            var iiii = 0, llll = cobj.enum.length, option2, tOption2;
            for (iiii = 0; iiii < llll; iiii++) {
              option2 = document.createElement("option");
              option2.setAttribute("value", cobj.enum[iiii]);
              tOption2 = document.createTextNode(cobj.enum[iiii]);
              if (dobj === cobj.enum[iiii]) {
                option2.setAttribute("selected", "selected");
              }
              option2.appendChild(tOption2);
              input.appendChild(option2);
            }
          } else if (t === "string" && (cobj.format === "email")) {
            input = document.createElement("input");
            input.setAttribute("type", "email");
            input.value = dobj;
          } else if (t === "string" && (cobj.format === "uri")) {
            input = document.createElement("input");
            input.setAttribute("type", "url");
            input.value = dobj;
          } else if (t === "string" && (cobj.format === "date-time")) {
            input = document.createElement("input");
            input.setAttribute("type", "datetime");
            input.value = dobj;
          } else {
            input = document.createElement("input");
            input.setAttribute("type", "text");
            input.value = dobj;
          }

          if (t === "string" && cobj.pattern) {
            input.setAttribute("pattern", cobj.pattern);
          }

          if (cobj.minimum) {
            input.setAttribute("min", cobj.minimum);
          }

          if (cobj.maximum) {
            input.setAttribute("max", cobj.maximum);
          }

          if (t === "string" && cobj.minlength) {
            input.setAttribute("minlength", cobj.minlength);
          }

          if (t === "string" && cobj.maxlength) {
            input.setAttribute("maxlength", cobj.maxlength);
          }

          input.setAttribute(tableDataAttributes.name, dkey);
          input.setAttribute(tableDataAttributes.index, i);

          // Binding
          input.setAttribute("data-" + name, name);

          td.appendChild(input);

          tr.appendChild(td);
        }
      }
      el.appendChild(tr);
    }
  };

  /*
  * << First | < Previous | # | Next > | Last >>
  */
  const directDOMPaginationControl = function(el, currentPage, totalPages) {
    let d, n, t;
    d = document.createElement("div");
    d.classList.add("paginationControl");

    n = document.createElement("span");
    n.classList.add("first");
    t = document.createTextNode("<< First");
    n.appendChild(t);
    d.appendChild(n);

    n = document.createElement("span");
    n.classList.add("previous");
    t = document.createTextNode("< Previous");
    n.appendChild(t);
    d.appendChild(n);

    n = document.createElement("span");
    n.classList.add("current");
    t = document.createTextNode(currentPage + " of " + totalPages);
    n.appendChild(t);
    d.appendChild(n);

    n = document.createElement("span");
    n.classList.add("next");
    t = document.createTextNode("Next >");
    n.appendChild(t);
    d.appendChild(n);

    n = document.createElement("span");
    n.classList.add("last");
    t = document.createTextNode("Last >>");
    n.appendChild(t);
    d.appendChild(n);

    el.appendChild(d);
  };

  /**
  * Augmented.Presentation.DirectDOMAutomaticTable<br/>
  * Uses direct DOM methods vs cached HTML<br/>
  * Creates a table automatically via a schema for defintion and a uri/json for data
  * @constructor Augmented.Presentation.DirectDOMAutomaticTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  * @example
  * var myAt = Augmented.Presentation.AutomaticTable.extend({ ... });
  * var at = new myAt({
  *     schema : schema,
  *     el: "#autoTable",
  *     crossOrigin: false,
  *     sortable: true,
  *     lineNumbers: true,
  *     editable: true,
  *     uri: "/example/data/table.json"
  * });
  */
  Augmented.Presentation.AutomaticTable =
  Augmented.Presentation.DirectDOMAutomaticTable =
  AbstractAutoTable.extend({
    theme: "material",

    setTheme: function(theme) {
      const el = ((typeof this.el === 'string') ? document.querySelector(this.el) : this.el),
      e = el.querySelector("table");
      if (e) {
        e.setAttribute("class", theme);
      }
      this.theme = theme;
    },
    render: function() {
      if (!this.isInitalized) {
        _logger.warn("AUGMENTED: AutoTable Can't render yet, not initialized!");
        return this;
      }
      let e;
      if (this.template) {
        // refresh the table body only
        this.showProgressBar(true);
        if (this.el) {
          e = (typeof this.el === 'string') ? document.querySelector(this.el) : this.el;
          let tbody = e.querySelector("tbody"), thead = e.querySelector("thead");
          if (e) {
            if (this.sortable) {
              this.unbindSortableColumnEvents();
            }
            if (this.editable) {
              this.unbindCellChangeEvents();
            }
            if (this._columns && (Object.keys(this._columns).length > 0)){
              while (thead.hasChildNodes()) {
                thead.removeChild(thead.lastChild);
              }
              directDOMTableHeader(thead, this._columns, this.lineNumbers, this.sortKey, this.display, this.selectable);
            } else {
              while (thead.hasChildNodes()) {
                thead.removeChild(thead.lastChild);
              }
            }

            if (this.collection && (this.collection.length > 0)){
              while (tbody.hasChildNodes()) {
                tbody.removeChild(tbody.lastChild);
              }
              if (this.editable) {
                directDOMEditableTableBody(tbody, this.collection.toJSON(), this._columns, this.lineNumbers, this.sortKey, this.display, this.selectable, this.name, this.linkable, this.links, this[this.links.link]);
              } else {
                // links not supported
                directDOMTableBody(tbody, this.collection.toJSON(), this._columns, this.lineNumbers, this.sortKey, this.display, this.selectable, this.name);
              }
            } else {
              while (tbody.hasChildNodes()) {
                tbody.removeChild(tbody.lastChild);
              }
            }
          }
        } else if (this.$el) {
          _logger.warn("AUGMENTED: AutoTable doesn't support jquery, sorry, not rendering.");
        } else {
          _logger.warn("AUGMENTED: AutoTable no element anchor, not rendering.");
        }
      } else {
        this.template = "notused";
        this.showProgressBar(true);

        if (this.el) {
          e = (typeof this.el === 'string') ? document.querySelector(this.el) : this.el;
          if (e) {
            // progress bar
            let n = document.createElement("progress"),
            t = document.createTextNode("Please wait.");
            n.appendChild(t);
            e.appendChild(n);

            // the table
            directDOMTableCompile(e, this.name, this.description, this._columns, this.collection.toJSON(), this.lineNumbers, this.sortKey, this.editable, this.display, this.selectable, this.name, this.linkable, this.links);

            // pagination control
            if (this.renderPaginationControl) {
              directDOMPaginationControl(e, this.currentPage(), this.totalPages());
            }

            // message
            n = document.createElement("p");
            n.classList.add("message");
            e.appendChild(n);
          }
        } else if (this.$el) {
          _logger.warn("AUGMENTED: AutoTable doesn't support jquery, sorry, not rendering.");
        } else {
          _logger.warn("AUGMENTED: AutoTable no element anchor, not rendering.");
        }

        if (this.renderPaginationControl) {
          this.bindPaginationControlEvents();
        }
      }
      this.delegateEvents();

      if (this.sortable) {
        this.bindSortableColumnEvents();
      }

      if (this.editable) {
        this.bindCellChangeEvents();
      }

      this.showProgressBar(false);

      this.setTheme(this.theme);

      return this;
    }
  });

  /**
    * Augmented.Presentation.AutomaticTable<br/>
    * Creates a table automatically via a schema for defintion and a uri/json for data
    * @constructor Augmented.Presentation.AutomaticTable
    * @extends Augmented.Presentation.DecoratorView
    * @memberof Augmented.Presentation
    */

  /**
  * Augmented.Presentation.AutoTable
  * Shorthand for Augmented.Presentation.AutomaticTable
  * @constructor Augmented.Presentation.AutoTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.AutoTable = Augmented.Presentation.AutomaticTable;

  /**
  * Augmented.Presentation.BigDataTable
  * Instance class preconfigured for sorting and pagination
  * @constructor Augmented.Presentation.BigDataTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.BigDataTable = Augmented.Presentation.DirectDOMAutomaticTable.extend({
    renderPaginationControl: true,
    lineNumbers: true,
    sortable: true
  });

  /**
  * Augmented.Presentation.EditableTable
  * Instance class preconfigured for editing
  * @constructor Augmented.Presentation.EditableTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.EditableTable = Augmented.Presentation.DirectDOMAutomaticTable.extend({
    editable: true,
    lineNumbers: true
  });

  /**
  * Augmented.Presentation.EditableBigDataTable
  * Instance class preconfigured for editing, sorting, and pagination
  * @constructor Augmented.Presentation.EditableBigDataTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.EditableBigDataTable = Augmented.Presentation.DirectDOMAutomaticTable.extend({
    renderPaginationControl: true,
    lineNumbers: true,
    sortable: true,
    editable: true
  });

  /**
  * Augmented.Presentation.LocalStorageTable
  * Instance class preconfigured for local storage-based table
  * @constructor Augmented.Presentation.LocalStorageTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.LocalStorageTable = Augmented.Presentation.DirectDOMAutomaticTable.extend({
    renderPaginationControl: false,
    lineNumbers: true,
    sortable: true,
    editable: false,
    localStorage: true
  });

  /**
  * Augmented.Presentation.EditableLocalStorageTable
  * Instance class preconfigured for editing, sorting, from local storage
  * @constructor Augmented.Presentation.EditableLocalStorageTable
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.EditableLocalStorageTable = Augmented.Presentation.DirectDOMAutomaticTable.extend({
    renderPaginationControl: false,
    lineNumbers: true,
    sortable: true,
    editable: true,
    localStorage: true
  });

  /**
  * Augmented.Presentation.Spreadsheet
  * Instance class preconfigured for editing for use as a Spreadsheet.<br/>
  * If a propery for length is not specified, it will buffer 10 lines for editing.
  * @constructor Augmented.Presentation.Spreadsheet
  * @extends Augmented.Presentation.AutomaticTable
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.Spreadsheet = Augmented.Presentation.AutomaticTable.extend({
    renderPaginationControl: false,
    lineNumbers: true,
    sortable: true,
    editable: true,
    /**
    * @propery {number} columns Defines a set of columns in the spreadsheet
    * @memberof Augmented.Presentation.AutomaticTable
    */
    columns: 5,
    /**
    * @propery {number} rows Defines a set of rows in the spreadsheet
    * @memberof Augmented.Presentation.AutomaticTable
    */
    rows: 10,
    /**
    * Initialize the table view
    * @method initialize
    * @memberof Augmented.Presentation.Spreadsheet
    * @param {object} options The view options
    * @returns {boolean} Returns true on success of initalization
    */
    initialize: function(options) {
      this.init();

      if (this.collection) {
        this.collection.reset();
      } else if (!this.collection && this.localStorage) {
        this.collection = new Augmented.LocalStorageCollection();
      } else if (!this.collection) {
        this.collection = new Augmented.Collection();
      }
      if (options) {

        if (options.schema) {
          // check if this is a schema vs a URI to get a schema
          if (Augmented.isObject(options.schema)) {
            this.schema = options.schema;
          } else {
            // is a URI?
            var parsedSchema = null;
            try {
              parsedSchema = JSON.parse(options.schema);
              if (parsedSchema && Augmented.isObject(parsedSchema)) {
                this.schema = parsedSchema;
              }
            } catch(e) {
              _logger.warn("AUGMENTED: AutoTable parsing string schema failed.  URI perhaps?");
            }
            if (!this.schema) {
              this.retrieveSchema(options.schema);
              this.isInitalized = false;
              //return false;
            }
          }
        }

        if (options.el) {
          this.el = options.el;
        }

        if (options.uri) {
          this.uri = options.uri;
          this.collection.url = options.uri;
        }

        if (options.data && (Array.isArray(options.data))) {
          this.populate(options.data);
        }

        if (options.sortable) {
          this.sortable = options.sortable;
        }

        if (options.lineNumbers) {
          this.lineNumbers = options.lineNumbers;
        }

        if (options.localStorageKey && !options.uri) {
          this.localStorageKey = options.localStorageKey;
          this.uri = null;
        }
      }

      if (this.collection && this.uri) {
        this.collection.url = this.uri;
      }
      if (this.collection) {
        this.collection.crossOrigin = this.crossOrigin;
      }
      if (this.schema) {
        if (this.schema.title) {
          this.name = this.schema.title;
        }
        if (this.schema.description) {
          this.description = this.schema.description;
        }

        if (!this.isInitalized) {
          this._columns = this.schema.properties;
          this.collection.schema = this.schema;
        }
      } else {
        //very basic schema
        this.schema = {
          "$schema": "http://json-schema.org/draft-04/schema#",
          "title": "untitled",
          "type": "object",
          "description": "",
          "properties": {
          }
        };

        var i = 0;

        for (i = 0; i < this.columns; i++) {
          this.schema.properties[String.fromCharCode(65 + i)] = {
            "description": "",
            "type": "string"
          };
        }

        this._columns = this.schema.properties;
        this.collection.schema = this.schema;
      }

      //buffer
      this._generate();
      this.collection.set(this.data);

      this.isInitalized = true;

      return this.isInitalized;
    },
    _generate: function() {
      if (this.schema && this.schema.properties) {
        var i = 0, ii = 0, keys = Object.keys(this.schema.properties), l = keys.length, obj = {};
        this.data = [];
        for (ii = 0; ii < this.rows; ii++) {
          obj = {};
          for (i = 0; i < l; i++) {
            obj[keys[i]] = "";
          }
          this.data.push(obj);
        }
      }
    }
  });


  /**
  * DOM related functions - Same as Augmented.Presentation.Dom
  * @namespace D
  * @memberof Augmented
  */

  /**
  * DOM related functions
  * @namespace Dom
  * @memberof Augmented.Presentation
  */
  Augmented.D = Augmented.Presentation.Dom = {
    /**
    * Gets the height of the browser viewport
    * @method getViewportHeight
    * @returns {number} The height of the viewport
    * @memberof Augmented.Presentation.Dom
    */
    getViewportHeight: function() {
      return window.innerHeight;
    },
    /**
    * Gets the width of the browser viewport
    * @method getViewportWidth
    * @returns {number} The width of the viewport
    * @memberof Augmented.Presentation.Dom
    */
    getViewportWidth: function() {
      return window.innerWidth;
    },
    /**
    * Sets the value of an element<br/>
    * Will detect the correct method to do so by element type
    * @method setValue
    * @param {Node} el Element or string of element selector
    * @param {string} value Value to set (or HTML)
    * @param {boolean} onlyText Value will set as text only
    * @memberof Augmented.Presentation.Dom
    */
    setValue: function(el, value, onlyText) {
      if (el) {
        value = (value) ? value : "";
        var myEl = this.selector(el);
        if (myEl && (myEl.nodeType === 1) && (myEl.nodeName === "select" || myEl.nodeName === "SELECT")) {
          // Select box
          _logger.debug("Select box (not supported) set to - " + value);
        } else if (myEl && (myEl.nodeType === 1) &&
        (myEl.nodeName === "input" || myEl.nodeName === "INPUT" ||
        myEl.nodeName === "textarea" || myEl.nodeName === "TEXTAREA")) {
          myEl.value = value;
        } else if (myEl && (myEl.nodeType === 1)) {
          if (onlyText){
            myEl.innerText = value;
          } else {
            myEl.innerHTML = value;
          }
        }
      }
    },
    /**
    * Gets the value of an element<br/>
    * Will detect the correct method to do so by element type
    * @method getValue
    * @param {Node} el Element or string of element selector
    * @returns {string} Returns the value of the element (or HTML)
    * @memberof Augmented.Presentation.Dom
    */
    getValue: function(el) {
      if (el) {
        var myEl = this.selector(el);

        if (myEl && (myEl.nodeType === 1) &&
        (myEl.nodeName === "input" || myEl.nodeName === "INPUT" ||
        myEl.nodeName === "textarea" || myEl.nodeName === "TEXTAREA" ||
        myEl.nodeName === "select" || myEl.nodeName === "SELECT")) {
          return myEl.value;
        } else if (myEl && (myEl.nodeType === 1)) {
          return myEl.innerHTML;
        }
      }
      return null;
    },
    /**
    * Selector function<br/>
    * Supports full query selection
    * @method selector
    * @param {string} query Element or string of element selector
    * @returns {Node} Returns the element (or first of type)
    * @memberof Augmented.Presentation.Dom
    */
    selector: function(query) {
      if (query) {
        return Augmented.isString(query) ? document.querySelector(query) : query;
      }
      return null;
    },
    /**
    * Selectors function<br/>
    * Supports full query selection
    * @method selectors
    * @param {string} query Element or string of element selector
    * @returns {NodeList} Returns all the nodes selected
    * @memberof Augmented.Presentation.Dom
    */
    selectors: function(query) {
      if (query) {
        return Augmented.isString(query) ? document.querySelectorAll(query) : query;
      }
      return null;
    },
    /**
    * Query function<br/>
    * Supports full query selection but acts like jQuery
    * @method query
    * @param {string} query Element or string of element selector
    * @param {Node} el Element to start from (optional)
    * @returns {NodeList|Node} Returns all the nodes selected
    * @memberof Augmented.Presentation.Dom
    */
    query: function(query, el) {
      if (query) {
        var d = document;
        if (el) {
          d = Augmented.Presentation.Dom.selector(el);
        }

        var nodelist = Augmented.isString(query) ? d.querySelectorAll(query) : query;

        if (nodelist.length === 1) {
          return nodelist[0];
        }
        return nodelist;
      }
      return null;
    },
    /**
    * Hides an element
    * @method hide
    * @param {Node} el Element or string of element selector
    * @memberof Augmented.Presentation.Dom
    */
    hide: function(el) {
      var myEl = this.selector(el);
      if (myEl) {
        myEl.style.display = "none";
        myEl.style.visibility = "hidden";
      }
    },
    /**
    * Shows an element
    * @method show
    * @param {Node} el Element or string of element selector
    * @param {string} display Value to set for 'display' property (optional)
    * @memberof Augmented.Presentation.Dom
    */
    show: function(el, display) {
      var myEl = this.selector(el);
      if (myEl) {
        myEl.style.display = (display) ? display : "block";
        myEl.style.visibility = "visible";
      }
    },
    /**
    * Sets the class attribute (completely)
    * @method setClass
    * @param {Node} el Element or string of element selector
    * @param {string} cls the class value
    * @memberof Augmented.Presentation.Dom
    */
    setClass: function(el, cls) {
      var myEl = this.selector(el);
      if (myEl) {
        myEl.setAttribute("class", cls);
      }
    },
    /**
    * Adds a class attribute
    * @method addClass
    * @param {Node} el Element or string of element selector
    * @param {string} cls the class value
    * @memberof Augmented.Presentation.Dom
    */
    addClass: function(el, cls) {
      var myEl = this.selector(el);
      if (myEl) {
        myEl.classList.add(cls);
      }
    },
    /**
    * Remove a class attribute
    * @method removeClass
    * @param {Node} el Element or string of element selector
    * @param {string} cls the class value
    * @memberof Augmented.Presentation.Dom
    */
    removeClass: function(el, cls) {
      var myEl = this.selector(el);
      if (myEl) {
        myEl.classList.remove(cls);
      }
    },
    /**
    * Empty a element container
    * @method empty
    * @param {Node} el Element or string of element selector
    * @memberof Augmented.Presentation.Dom
    */
    empty: function(el) {
      this.setValue(el, "", true);
    },
    /**
    * injectTemplate method - Injects a template element at a mount point
    * @method injectTemplate
    * @param {string} template The template selector
    * @param {Node} mount The mount point as Document.Element or String
    * @memberof Augmented.Presentation.Dom
    */
    injectTemplate: function(template, mount) {
      var t = this.selector(template), el = this.selector(mount);
      if (t && el) {
        var clone = document.importNode(t.content, true);
        el.appendChild(clone);
      }
    }
  };

  /**
  * Augmented jQuery-like selectors usinge native selectors</br/>
  * Will return a nodelist for all selections unless only one is found.
  * @method $
  * @memberof Augmented
  * @borrows Augmented.Presentation.Dom.query
  * @example
  * $("#myElement");
  * $("section#main header");
  * - or start from Element:
  * $("header", mainSectionEl);
  */
  Augmented.$ = Augmented.Presentation.Dom.query;

  /**
  * Widgets and small presentation modules
  * @namespace Widget
  * @memberof Augmented.Presentation
  */
  Augmented.Presentation.Widget = {
    /**
    * List widget - renders a standard list
    * @method List
    * @param {string} id The id of the parent to attach the list
    * @param {Object} data The data to render
    * @param {Array} data The data to render
    * @param {string} binding The binding (used for decorator and optional)
    * @param {boolean} ordered True if the list should be ordered
    * @returns {Element} Returns a DOM element as a list
    * @memberof Augmented.Presentation.Widget
    */
    List: function(id, data, ordered, binding) {
      var list = (ordered) ? document.createElement("ol") : document.createElement("ul"), i = 0, l, li, t, d;
      if (id) {
        list.setAttribute("id", id);
      }

      if (binding && id) {
        list.setAttribute("data-" + binding, id);
      }

      if (data && Array.isArray(data)) {
        l = data.length;
        for (i = 0; i < l; i++) {
          li = document.createElement("li");
          li.setAttribute("data-index", i);
          t = document.createTextNode(String(data[i]));
          li.appendChild(t);
          list.appendChild(li);
        }
      }
      return list;
    },
    /**
    * DescriptionList widget - renders a description list
    * @method DescriptionList
    * @param {string} id The id of the parent to attach the list
    * @param {Object} data The data to render
    * @param {string} binding The binding (used for decorator and optional)
    * @returns {Element} Returns a DOM element as a description list
    * @memberof Augmented.Presentation.Widget
    */
    DescriptionList: function(id, data, binding) {
      var list = document.createElement("dl"), i = 0, l, dd, dt, t, keys, key;
      if (id) {
        list.setAttribute("id", id);
      }

      if (binding && id) {
        list.setAttribute("data-" + binding, id);
      }

      if (data && Augmented.isObject(data)) {
        keys = Object.keys(data);
        l = keys.length;
        for (i = 0; i < l; i++) {
          dt = document.createElement("dt");
          t = document.createTextNode(String(keys[i]));
          dt.appendChild(t);
          list.appendChild(dt);

          key = data[keys[i]];
          dd = document.createElement("dd");
          t = document.createTextNode(String(key));
          dd.appendChild(t);
          list.appendChild(dd);
        }
      }
      return list;
    },
    /**
    * DataList widget - renders a data list
    * @method DataList
    * @param {string} id The id of the parent to attach the list
    * @param {Array} data The data to render
    * @param {string} binding The binding (used for decorator and optional)
    * @returns {Element} Returns a DOM element as a data list
    * @memberof Augmented.Presentation.Widget
    */
    DataList: function(id, data, binding) {
      var list = document.createElement("datalist"), i = 0, l, o;
      if (id) {
        list.setAttribute("id", id);
      }

      if (binding && id) {
        list.setAttribute("data-" + binding, id);
      }

      if (data && Array.isArray(data)) {
        l = data.length;
        for (i = 0; i < l; i++) {
          o = document.createElement("option");
          o.value = String(data[i]);
          list.appendChild(o);
        }
      }
      return list;
    },
    /**
    * Input widget - renders an input or simular based on type
    * @method Input
    * @param {object} field Field property object (required)
    * @param {string} name The name of the field
    * @param {string} value The value to preset
    * @param {string} id The id of the field
    * @param {boolean} required If the field is required
    * @param {string} binding The binding (used for decorator and optional)
    * @returns {Element} Returns a DOM element as an input
    * @memberof Augmented.Presentation.Widget
    */
    Input: function(field, name, value, id, required, binding) {
      if (!field) {
        return null;
      }
      var input, dobj = ((value) ? value : ""), cobj = field, t = field.type;

      if (t === "object") {
        if (Array.isArray(dobj)) {
          var iii = 0, lll = dobj.length, option, tOption;
          input = document.createElement("select");
          for (iii = 0; iii < lll; iii++) {
            option = document.createElement("option");
            option.setAttribute("value", dobj[iii]);
            tOption = document.createTextNode(dobj[iii]);
            option.appendChild(tOption);
            input.appendChild(option);
          }
        } else {
          input = document.createElement("textarea");
          input.value = JSON.stringify(dobj);
        }
      } else if (t === "boolean") {
        input = document.createElement("input");
        input.setAttribute("type", "checkbox");
        if (dobj === true) {
          input.setAttribute("checked", "checked");
        }
        input.value = dobj;
      } else if (t === "number" || t === "integer") {
        input = document.createElement("input");
        input.setAttribute("type", "number");
        input.value = dobj;
      } else if (t === "string" && cobj.enum) {
        input = document.createElement("select");
        var iiii = 0, llll = cobj.enum.length, option2, tOption2;
        for (iiii = 0; iiii < llll; iiii++) {
          option2 = document.createElement("option");
          option2.setAttribute("value", cobj.enum[iiii]);
          tOption2 = document.createTextNode(cobj.enum[iiii]);
          if (dobj === cobj.enum[iiii]) {
            option2.setAttribute("selected", "selected");
          }
          option2.appendChild(tOption2);
          input.appendChild(option2);
        }
      } else if (t === "string" && (cobj.format === "email")) {
        input = document.createElement("input");
        input.setAttribute("type", "email");
        input.value = dobj;
      } else if (t === "string" && (cobj.format === "uri")) {
        input = document.createElement("input");
        input.setAttribute("type", "url");
        input.value = dobj;
      } else if (t === "string" && (cobj.format === "date-time")) {
        input = document.createElement("input");
        input.setAttribute("type", "datetime");
        input.value = dobj;
      } else {
        input = document.createElement("input");
        input.setAttribute("type", "text");
        input.value = dobj;
      }

      if (t === "string" && cobj.pattern) {
        input.setAttribute("pattern", cobj.pattern);
      }

      if (cobj.minimum) {
        input.setAttribute("min", cobj.minimum);
      }

      if (cobj.maximum) {
        input.setAttribute("max", cobj.maximum);
      }

      if (t === "string" && cobj.minlength) {
        input.setAttribute("minlength", cobj.minlength);
      }

      if (t === "string" && cobj.maxlength) {
        input.setAttribute("maxlength", cobj.maxlength);
      }

      if (required) {
        input.setAttribute("required", "true");
      }

      if (name) {
        input.setAttribute("name", name);
      }

      if (id) {
        input.setAttribute("id", id);
      }

      if (binding && name) {
        input.setAttribute("data-" + binding, name);
      }

      return input;
    }
  };



  /**
  * A controller to handle a group of views.  The api is handled simular to views for use in a router.
  * @constructor Augmented.Presentation.ViewController
  * @memberof Augmented.Presentation
  * @extends Augmented.Object
  */
  Augmented.Presentation.ViewController = Augmented.Object.extend({
    _views: [],
    /**
    * initialize - an API for the start of the controller.  It is intended to add initializers here
    * @method initialize
    * @memberof Augmented.Presentation.ViewController
    */
    initialize: function() {},
    /**
    * render - an API for the render of the controller.  It is intended to add view render methods here
    * @method render
    * @memberof Augmented.Presentation.ViewController
    */
    render: function() {},
    /**
    * remove - an API for the end of the controller.  It is intended to add view removal and cleanup here
    * @method remove
    * @memberof Augmented.Presentation.ViewController
    */
    remove: function() {},
    /**
    * manageView - manage a view
    * @method manageView
    * @param {Augmented.View} view An instance of a view to manage
    * @memberof Augmented.Presentation.ViewController
    */
    manageView: function(view) {
      this._views.push(view);
    },
    /**
    * removeAllViews - cleans up all views known (calling thier remove method)
    * @method removeAllViews
    * @memberof Augmented.Presentation.ViewController
    */
    removeAllViews: function() {
      var i = 0, l = this._views.length;
      for (i = 0; i < l; i++) {
        this._views[i].remove();
      }
      this._views.splice(0);
      //this._views = [];
    },
    /**
    * getViews - get the instances of the views known
    * @method getViews
    * @returns {Array} Returns an array of view instances
    * @memberof Augmented.Presentation.ViewController
    */
    getViews: function () {
      return this._views;
    }
  });

  /**
  * A automatic dialog view - creates a dialog with simple configurations to customize
  * @constructor Augmented.Presentation.DialogView
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.DecoratorView
  */
  Augmented.Presentation.DialogView = Augmented.Presentation.DecoratorView.extend({
    /**
    * name property - the name of the dialog (required)
    * @property name
    * @memberof Augmented.Presentation.DialogView
    */
    name: "dialog",
    /**
    * title property - the title of the dialog
    * @property title
    * @memberof Augmented.Presentation.DialogView
    */
    title: "",
    /**
    * body property - the body of the dialog, handled by setBody method
    * @property body
    * @memberof Augmented.Presentation.DialogView
    */
    body: "",
    /**
    * style property - the style (form, alert, bigForm, or whatever class you want)
    * @property style
    * @memberof Augmented.Presentation.DialogView
    */
    style: "form",
    /**
    * buttons object property - the buttons to match to functions
    * @property buttons
    * @memberof Augmented.Presentation.DialogView
    */
    buttons: {
      //name : callback
    },
    /**
    * template - sets content of the dialog, handled internally
    * @method template
    * @memberof Augmented.Presentation.DialogView
    */
    template: function() {
      return "<div class=\"blur\"><dialog class=\"" + this.style + "\"><h1>" + this.title + "</h1>" + this.body + this._getButtonGroup() + "</dialog></div>";
    },
    /**
    * setBody - sets the body content of the dialog
    * @method setBody
    * @param {String} body A string value of th body (supports HTML)
    * @memberof Augmented.Presentation.DialogView
    */
    setBody: function(body) {
      this.body = body;
    },
    _getButtonGroup: function() {
      var html = "<div class=\"buttonGroup\">", i = 0, keys = Object.keys(this.buttons), l = (keys) ? keys.length: 0;

      for (i = 0; i < l; i++) {
        html = html + "<button data-" + this.name + "=\"" + this.buttons[keys[i]] + "\"data-click=\"" + this.buttons[keys[i]] + "\">" + keys[i] + "</button>";
      }

      return html + "</div>";
    },
    /**
    * render - render the dialog
    * @method render
    * @memberof Augmented.Presentation.DialogView
    */
    render: function() {
      Augmented.Presentation.Dom.setValue(this.el, this.template());
      this.delegateEvents();
      this.trigger("open");
      return this;
    },
    // built-in callbacks

    /**
    * cancel - standard built-in cancel callback.  Calls close method by default
    * @method cancel
    * @param {Event} event Event passed in
    * @memberof Augmented.Presentation.DialogView
    */
    cancel: function(event) {
      this.close(event);
    },
    /**
    * open - standard built-in open callback.  Calls render method by default
    * @method open
    * @param {Event} event Event passed in
    * @memberof Augmented.Presentation.DialogView
    */
    open: function(event) {
      this.render();
    },
    /**
    * close - standard built-in close callback.  Closes the dialog, triggers the 'close' event
    * @method close
    * @param {Event} event Event passed in
    * @memberof Augmented.Presentation.DialogView
    */
    close: function(event) {
      this.trigger("close");
      Augmented.Presentation.Dom.empty(this.el, true);
    }
  });

  /**
  * A automatic comfirmation dialog view - creates a dialog with yes no buttons
  * @constructor Augmented.Presentation.ConfirmationDialogView
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.DialogView
  */
  Augmented.Presentation.ConfirmationDialogView = Augmented.Presentation.DialogView.extend({
    buttons: {
      //name : callback
      "yes": "yes",
      "no": "no"
    },
    style: "alert"
  });

  /**
  * A automatic alert dialog view - creates a dialog with cancel button and a message
  * @constructor Augmented.Presentation.AlertDialogView
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.DialogView
  */
  Augmented.Presentation.AlertDialogView = Augmented.Presentation.DialogView.extend({
    buttons: {
      //name : callback
      "cancel": "cancel"
    },
    style: "alert"
  });

  const formCompile = function(e, name, description, fields, model, required, binding, display) {
    const form = document.createElement("form"), fs = document.createElement("formset"), keys = Object.keys(fields), l = ((display) ? display.length: keys.length);
    let t, i, input, lb, req;

    form.appendChild(fs);

    if (name) {
      const lg = document.createElement("legend");
      t = document.createTextNode(name);
      if (description) {
        const att = document.createAttribute("title");
        att.value = description;
        lg.setAttributeNode(att);
      }
      lg.appendChild(t);
      fs.appendChild(lg);
    }
    if (!display) {
      display = keys;
    }

    for (i = 0; i < l; i++) {
      let displayCol = true;
      if (display !== null) {
          displayCol = (keys.indexOf(display[i]) !== -1);
      }

      if (displayCol) {
        req = (required.indexOf(display[i]) !== -1);
        lb = document.createElement("label");
        lb.setAttribute("for", display[i]);
        t = document.createTextNode(display[i]);
        lb.appendChild(t);
        fs.appendChild(lb);

        input = Augmented.Presentation.Widget.Input(fields[display[i]], display[i], model[display[i]], display[i], req, binding);
        if (input) {
          fs.appendChild(input);
        }
      }
    }
    e.appendChild(form);
  };

  /**
  * A automatic form view created from a JSON Schema
  * @constructor Augmented.Presentation.AutomaticForm
  * @memberof Augmented.Presentation
  * @extends Augmented.Presentation.DecoratorView
  */
  Augmented.Presentation.AutomaticForm = Augmented.Presentation.DecoratorView.extend({
    // standard functionality

    /**
    * The crossOrigin property - enables cross origin fetch
    * @property {boolean} crossOrigin The crossOrigin property
    * @memberof Augmented.Presentation.AutomaticForm
    */
    crossOrigin: false,
    /**
    * The fields property
    * @property {object} fields The fields property
    * @private
    * @memberof Augmented.Presentation.AutomaticForm
    */
    _fields: {},
    /**
    * The URI property
    * @property {string} uri The URI property
    * @memberof Augmented.Presentation.AutomaticForm
    */
    uri: null,
    /**
    * The model property
    * @property {Augmented.Model} model The model property
    * @memberof Augmented.Presentation.AutomaticForm
    */
    model: null,
    /**
    * The initialized property
    * @property {boolean} isInitalized The initialized property
    * @memberof Augmented.Presentation.AutomaticForm
    */
    isInitalized : false,
    /**
    * The title property
    * @property {string} title The title of the form
    * @memberof Augmented.Presentation.AutomaticForm
    */
    title: null,
    /**
    * The name property
    * @property {string} name The name of the form
    * @memberof Augmented.Presentation.AutomaticForm
    */
    name: "",
    /**
    * The description property
    * @property {string} description The description of the form
    * @memberof Augmented.Presentation.AutomaticForm
    */
    description: "",
    /**
    * The required fields property
    * @property {Array} _required The required fields
    * @memberof Augmented.Presentation.AutomaticForm
    * @private
    */
    _required: [],

    /**
      * Fields to display - null will display all
      * @method display
      * @memberof Augmented.Presentation.AutomaticForm
      */
    display: null,

    /**
    * Initialize the form view<br/>
    * pass clearForm = true to start a fresh form
    * @method initialize
    * @memberof Augmented.Presentation.AutomaticForm
    * @param {object} options The view options
    * @returns {boolean} Returns true on success of initalization
    */
    initialize: function(options) {
      this.init(options);

      if (this.model && options && options.clearForm) {
        this.model.clear();
      } else if (!this.model) {
        this.model = new Augmented.Model();
      }
      if (options) {
        if (options.schema) {
          // check if this is a schema vs a URI to get a schema
          if (Augmented.isObject(options.schema)) {
            this.schema = options.schema;
          } else {
            // is a URI?
            const parsedSchema = null;
            try {
              parsedSchema = JSON.parse(options.schema);
              if (parsedSchema && Augmented.isObject(parsedSchema)) {
                this.schema = parsedSchema;
              }
            } catch(e) {
              _logger.warn("AUGMENTED: AutoForm parsing string schema failed.  URI perhaps?");
            }
            if (!this.schema) {
              this._retrieveSchema(options.schema);
              this.isInitalized = false;
            }
          }
        }

        if (options.el) {
          this.el = options.el;
        }

        if (options.uri) {
          this.uri = options.uri;
        }

        if (options.data && (Augmented.isObject(options.data))) {
          this.model.set(options.data);
        }
        if (options.title) {
          this.title = options.title;
        }
        if (options.description) {
          this.description = options.description;
        }
      }

      if (this.model && this.uri) {
        this.model.url = this.uri;
      }
      if (this.model) {
        this.model.crossOrigin = this.crossOrigin;
      }
      if (this.schema) {
        if (this.schema.title && (!this.name)) {
          this.name = this.schema.title;
        }
        if (this.schema.description && !this.description) {
          this.description = this.schema.description;
        }

        if (this.schema.required) {
          this._required = this.schema.required;
        } else {
          this._required = [];
        }

        if (!this.isInitalized) {
          this._fields = this.schema.properties;
          this.model.schema = this.schema;
          this.isInitalized = true;
        }
      } else {
        this.isInitalized = false;
        return false;
      }

      return this.isInitalized;
    },
    _retrieveSchema: function(uri){
      var that = this;
      var schema = null;
      Augmented.ajax({
        url: uri,
        contentType: "application/json",
        dataType: "json",
        success: function(data, status) {
          if (typeof data === "string") {
            schema = JSON.parse(data);
          } else {
            schema = data;
          }
          var options = { "schema": schema };
          that.initialize(options);
        },
        failure: function(data, status) {
          _logger.warn("AUGMENTED: AutoForm Failed to fetch schema!");
        }
      });
    },

    /**
    * Sets the URI
    * @method setURI
    * @memberof Augmented.Presentation.AutomaticForm
    * @param {string} uri The URI
    */
    setURI: function(uri) {
      this.uri = uri;
    },
    /**
    * Sets the schema
    * @method setSchema
    * @memberof Augmented.Presentation.AutomaticForm
    * @param {object} schema The JSON schema of the dataset
    */
    setSchema: function(schema) {
      this.schema = schema;
      this._fields = schema.properties;
      this.model.reset();
      this.model.schema = schema;

      if (this.uri) {
        model.url = this.uri;
      }
    },

    /**
    * Enable/Disable the progress bar
    * @method showProgressBar
    * @memberof Augmented.Presentation.AutomaticForm
    * @param {boolean} show Show or Hide the progress bar
    */
    showProgressBar: function(show) {
      if (this.el) {
        var e = Augmented.D.selector(this.el);
        var p = e.querySelector("progress");
        if (p) {
          p.style.display = (show) ? "block" : "none";
          p.style.visibility = (show) ? "visible" : "hidden";
        }
      }
    },
    /**
    * Show a message related to the form
    * @method showMessage
    * @memberof Augmented.Presentation.AutomaticForm
    * @param {string} message Some message to display
    */
    showMessage: function(message) {
      if (this.el) {
        var e = Augmented.D.selector(this.el);
        var p = e.querySelector("p[class=message]");
        if (p) {
          p.innerHTML = message;
        }
      }
    },
    /**
    * Validate the form
    * @method validate
    * @memberof Augmented.Presentation.AutomaticForm
    * @returns {boolean} Returns true on success of validation
    */
    validate: function() {
      var messages = (this.model) ? this.model.validate() : null;
      if (!this.model.isValid() && messages && messages.messages) {
        this.showMessage(formatValidationMessages(messages.messages));
      } else {
        this.showMessage("");
      }
      return messages;
    },
    /**
    * Is the form valid
    * @method isValid
    * @memberof Augmented.Presentation.AutomaticForm
    * @returns {boolean} Returns true if valid
    */
    isValid: function() {
      return (this.model) ? this.model.isValid() : true;
    },
    /**
    * Render the form
    * @method render Renders the form
    * @memberof Augmented.Presentation.AutomaticForm
    * @returns {object} Returns the view context ('this')
    */
    render: function() {
      if (!this.isInitalized) {
        _logger.warn("AUGMENTED: AutomaticForm Can't render yet, not initialized!");
        return this;
      }

      this.template = null;//"notused";
      this.showProgressBar(true);

      if (this.el) {
        const e = Augmented.Presentation.Dom.selector(this.el);
        if (e) {
          if (this.theme) {
            Augmented.Presentation.Dom.addClass(e, this.theme);
          }
          // progress bar
          let n = document.createElement("progress"), t = document.createTextNode("Please wait.");
          n.appendChild(t);
          e.appendChild(n);

          // the form
          formCompile(e,
            ((this.title) ? this.title : this.name),
            this.description,
            this._fields,
            this.model.toJSON(),
            this._required,
            this.name,
            this.display);

          this._formEl = Augmented.Presentation.Dom.query("form", this.el);

          // message
          n = document.createElement("p");
          n.classList.add("message");
          e.appendChild(n);
        }
      } else if (this.$el) {
        _logger.warn("AUGMENTED: AutomaticForm doesn't support jquery, sorry, not rendering.");
        this.showProgressBar(false);
        return;
      } else {
        _logger.warn("AUGMENTED: AutomaticForm no element anchor, not rendering.");
        this.showProgressBar(false);
        return;
      }

      this.delegateEvents();

      this.syncAllBoundElements();

      this.showProgressBar(false);
      return this;
    },
    /**
    * Reset the form
    * @method reset
    * @memberof Augmented.Presentation.AutomaticForm
    * @returns {object} Returns the view context ('this')
    */
    reset: function() {
      if (this._formEl) {
        this._formEl.reset();
        this.model.reset();
      }
    },
    /**
    * Populate the form
    * @method populate
    * @param {object} data Data to fill in
    * @memberof Augmented.Presentation.AutomaticForm
    * @returns {object} Returns the view context ('this')
    */
    populate: function(data) {
      this.model.set(data);
    },
    /**
    * Remove the form and all binds
    * @method remove
    * @memberof Augmented.Presentation.AutomaticForm
    */
    remove: function() {
      /* off to unbind the events */
      this.undelegateEvents();
      this.off();
      this.stopListening();

      Augmented.Presentation.Dom.empty(this.el);

      return this;
    }
  });

  // data structure = { id: "itemID", "click": "event", "icon": "web", "title": "something", "spacer": false }
  const buildMenuItems = function(name, data) {
    let items = "";
    if (name && data && data.length !== 0) {
      const l = data.length;
      let i = 0;
      for (i = 0; i < l; i++) {
        if (data[i].spacer) {
          items = items + '<div class="spacer"></div>';
        } else {
          items = items + '<div id="' + data[i].id + '" data-' + name + '="' + data[i].id + '" data-click="' + data[i].click + '">' +
          ( (data[i].icon) ? ('<i class="material-icons md-dark">' + data[i].icon + '</i>') : '' ) + data[i].title + '</div>';
        }
      }
    }
    return items;
  };

  /**
    * Component - Large UI Components
    * @namespace Augmented.Presentation.Component
    * @memberof Augmented.Presentation
    */
  Augmented.Presentation.Component = {};

  /**
    * A View Component
    * @constructor Augmented.Presentation.Component.View
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.DecoratorView
    */
  Augmented.Presentation.Component.View = Augmented.Presentation.DecoratorView.extend({
    /**
      * The template property
      * @property template
      * @memberof Augmented.Presentation.Component.View
      */
    template: "",
    /**
      * Simple render method - renders the template
      * @method render
      * @memberof Augmented.Presentation.Component.View
      */
    render: function() {
      Augmented.Presentation.Dom.setValue(this.el, this.template);
  		return this;
    },
    /**
    * Remove the table and all binds
    * @method remove
    * @memberof Augmented.Presentation.Component.View
    */
    remove: function() {
      /* off to unbind the events */
      this.undelegateEvents();
      this.off();
      this.stopListening();

      Augmented.Presentation.Dom.empty(this.el);

      return this;
    }
  });

  /**
    * A Header Component
    * @constructor Augmented.Presentation.Component.Header
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.Component.View
    */
  Augmented.Presentation.Component.Header = Augmented.Presentation.Component.View.extend({
    /**
      * A title property
      * @property title
      * @memberof Augmented.Presentation.Component.Header
      */
    title: "",
    /**
      * A subTitle property
      * @property subTitle
      * @memberof Augmented.Presentation.Component.Header
      */
    subTitle: ""
  });

  /**
    * A Notfication Center Component
    * @constructor Augmented.Presentation.Component.NotificationCenter
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.Component.View
    */
  Augmented.Presentation.Component.NotificationCenter = Augmented.Presentation.Component.View.extend({

  });

  /**
    * A Component Manager
    * @constructor Augmented.Presentation.Component.Header
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.Mediator
    */
  Augmented.Presentation.Component.Manager = Augmented.Presentation.Mediator.extend({
    manageComponent: function(component) {
      return this.observeColleagueAndTrigger(component, component.name, component.name);
    },
    unmanageComponent: function(component) {
      return this.dismissColleagueTrigger(component, component.name, component.name);
    }

  });

  /**
    * An abstract tooldbar Component, designed to be extended
    * @constructor Augmented.Presentation.Component.AbstractToolbar
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.Component.View
    * @abstract
    */
  Augmented.Presentation.Component.AbstractToolbar = Augmented.Presentation.Component.View.extend({
    /**
      * The model property
      * @property {Augmented.Model} model The model property
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      */
    model: null,
    /**
      * The initialized property
      * @property {boolean} isInitalized The initialized property
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      */
    isInitalized: false,
    /**
      * The menuitems property
      * @property {array} menuItems The initialized property
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      */
    menuItems: null,

    /**
      * Initialize the view
      * @method initialize
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      * @param {object} options The view options
      * @returns {boolean} Returns true on success of initalization
      */
    initialize: function(options) {
      if (!this.menuItems) {
        this.menuItems = [];
      }
      this.init();

      if (this.model) {
        this.model.clear();
      } else {
        this.model = new Augmented.Model();
      }
      if (options) {
        if (options.el) {
          this.el = options.el;
        }
        if (options.data && (Augmented.isObject(options.data))) {
          this.model.set(options.data);
        }
        if (options.title && (Augmented.isString(options.title))) {
          this.title = options.title;
        }
        if (options.menuItems && (Augmented.isObject(options.menuItems))) {
          this.menuItems = options.menuItems;
        }
      }
      if (this.el && this.name) {
        this.isInitalized = true;
      }
      _logger.debug("initialized " + this.isInitalized);
      _logger.debug("name " + this.name + " el " + this.el);
      return this.isInitalized;
    },
    /**
      * @method addItem - Adds an item to the menu
      * @param id {string} The id of the itemID
      * @param click {string} The bound click method to call
      * @param icon {string} The icon name (webfont)
      * @param title {string} The title of the itemID
      * @param spacer {boolean} Sets a spacer item vs text (not clickable)
      * @example addItem({"itemID", "event", "web", "something", false });
      * @example addItem({"space", null, null, null, true });
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      */
    addItem: function(id, click, icon, title, spacer) {
      if (!spacer) {
        this.menuItems.push({ "id": id, "click": click, "icon": icon, "title": title, "spacer": spacer });
      } else {
        this.addSpacer();
      }
    },
    /**
      * @method addSpacer - Adds a spacer item to the menu
      * @example addSpacer();
      * @memberof Augmented.Presentation.Component.AbstractToolbar
      */
    addSpacer: function() {
      this.menuItems.push({ "id": null, "click": null, "icon": null, "title": null, "spacer": true });
    }
  });


  /**
    * A Hamburger Menu View
    * @constructor Augmented.Presentation.Component.HamburgerMenu
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.AbstractToolbar
    */
  Augmented.Presentation.Component.HamburgerMenu = Augmented.Presentation.Component.AbstractToolbar.extend({
    /**
      * The title property
      * @property {string} title The title property
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      */
    title: "",

    /**
      * The menuitems property
      * @property {array} menuItems The initialized property
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      */

    /**
      * @method addItem - Adds an item to the menu
      * @param id {string} The id of the itemID
      * @param click {string} The bound click method to call
      * @param icon {string} The icon name (webfont)
      * @param title {string} The title of the itemID
      * @param spacer {boolean} Sets a spacer item vs text (not clickable)
      * @example addItem({"itemID", "event", "web", "something", false });
      * @example addItem({"space", null, null, null, true });
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      */

    /**
      * @method addSpacer - Adds a spacer item to the menu
      * @example addSpacer();
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      */

    /**
      * Initialize the Hamburger menu view
      * @method initialize
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      * @param {object} options The view options
      * @returns {boolean} Returns true on success of initalization
      */

    /**
      * Render the Hamburger Menu
      * @method render Renders the Hamburger
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      * @returns {object} Returns the view context ('this')
      */
    render: function() {
      if (!this.isInitalized) {
        _logger.warn("AUGMENTED: Hamburger Can't render yet, not initialized!");
        return this;
      }

      this.template = null;//"notused";

      if (this.el) {
        var e = Augmented.Presentation.Dom.selector(this.el);
        if (e) {
          // the menu
          Augmented.Presentation.Dom.addClass(e, "wrapper");
          e.setAttribute("data-" + this.name, "hamburger");

          e.innerHTML =
            '<section class="material-design-hamburger" data-' + this.name + '="hamburgerClickRegion">' +
            '<div class="material-design-hamburger__icon" data-' + this.name + '="hamburgerIcon" data-click="toggle">' +
            '<i class="material-icons md-light">menu</i>' +
            '</div></section>' +
            '<section class="menu menu--off" data-' + this.name + '="hamburgerMenu">' +
            '<div>' + this.title + '</div>' +
            buildMenuItems(this.name, this.menuItems) +
            '</section>';
        }
      } else if (this.$el) {
        _logger.warn("AUGMENTED: Hamburger doesn't support jquery, sorry, not rendering.");

        return;
      } else {
        _logger.warn("AUGMENTED: Hamburger no element anchor, not rendering.");

        return;
      }

      this.delegateEvents();

      this.syncAllBoundElements();
      return this;
    },

    /**
      * Toggle the Hamburger menu view - deprecated
      * @method hamburger
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      * @deprecated
      */
    hamburger: function() {
      this.toggle();
    },
    /**
      * Toggle the Hamburger menu view
      * @method toggle
      * @memberof Augmented.Presentation.Component.HamburgerMenu
      */
    toggle: function() {
      if (!this.modal) {
        const menu = this.boundElement("hamburgerMenu");
        const r = this.boundElement("hamburgerClickRegion");
        r.classList.toggle("model");
        menu.classList.toggle("menu--on");
      }
    }
  });

  // data structure = { id: "itemID", "click": "event", "icon": "web", "title": "something", "spacer": false }
  const buildToolbarItems = function(name, data) {
    let items = "";
    if (name && data && data.length !== 0) {
      const l = data.length;
      let i = 0;
      for (i = 0; i < l; i++) {
        if (data[i].spacer) {
          items = items + '<div class="tools spacer"></div>';
        } else {
          items = items + '<div id="' + data[i].id + '" data-' + name + '="' + data[i].id + '" data-click="' + data[i].click + '" class="tools" title="' + data[i].title + '">' +
          ( (data[i].icon) ? ('<i class="material-icons md-dark">' + data[i].icon + '</i>') : data[i].title ) + '</div>';
        }
      }
    }
    return items;
  };

  /**
    * A Toolbar View
    * @constructor Augmented.Presentation.Component.Toolbar
    * @memberof Augmented.Presentation.Component
    * @extends Augmented.Presentation.AbstractToolbar
    */
  Augmented.Presentation.Component.Toolbar = Augmented.Presentation.Component.AbstractToolbar.extend({
    /**
      * Render the Toolbar
      * @method render Renders the Toolbar
      * @memberof Augmented.Presentation.Component.Toolbar
      * @returns {object} Returns the view context ('this')
      */
    render: function() {
      if (!this.isInitalized) {
        _logger.warn("AUGMENTED: Toolbar Can't render yet, not initialized!");
        return this;
      }

      this.template = null;//"notused";

      if (this.el) {
        var e = Augmented.Presentation.Dom.selector(this.el);
        if (e) {
          // the menu
          Augmented.Presentation.Dom.addClass(e, "toolbar");
          e.setAttribute("data-" + this.name, "toolbar");
          e.innerHTML = buildToolbarItems(this.name, this.menuItems);
        }
      } else if (this.$el) {
        _logger.warn("AUGMENTED: Toolbar doesn't support jquery, sorry, not rendering.");
        return;
      } else {
        _logger.warn("AUGMENTED: Toolbar no element anchor, not rendering.");
        return;
      }

      this.delegateEvents();

      this.syncAllBoundElements();
      return this;
    }
  });

  return Augmented.Presentation;
}));