Widget.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * The base Widget class, used for loading Web based Niagara Widgets.
 * 
 * @module bajaux/Widget
 * @requires lex
 * @requires jquery
 * @requires bajaux/events
 * @requires bajaux/Properties
 * @requires bajaux/Validators
 */
define([
  'lex!',
  'log!bajaux.Widget',
  'bajaux/events',
  'bajaux/Properties',
  'bajaux/Validators',
  'bajaux/commands/CommandGroup',
  'bajaux/commands/Command',
  'bajaux/spandrel/symbols',
  'jquery',
  'Promise',
  'nmodule/js/rc/tinyevents/tinyevents' ], function (
  lex,
  log,
  events,
  Properties,
  Validators,
  CommandGroup,
  Command,
  symbols,
  $,
  Promise,
  tinyevents) {
  
  "use strict";

  const logWarning = log.warning.bind(log);

  const notInitialized = "Not initialized!";
  const alreadyDestroyed = "Already destroyed!";

  const { IS_ELEMENT_SYMBOL } = symbols;
  const ROOT_CSS_CLASS_SYMBOL = Symbol('rootCssClass');

  let idCount = 0;

  /**
   * A Widget contains the most basic mechanisms necessary for displaying user
   * interface upon a DOM element. It should not be instantiated directly,
   * but rather subclassed with the specific functionality you need.
   *
   * At a bare minimum, either the `doInitialize` or `doLoad` function will need
   * to be implemented by your subclass.

   * @class
   * @alias module:bajaux/Widget
   *
   * @param {Object} [params] a parameters object.
   * @param {Object} [params.properties] properties to add to this editor's
   * underlying `bajaux/Properties` instance.
   * @param {Boolean} [params.enabled] false to disable this editor
   * @param {Boolean} [params.readonly] true to readonly this editor
   * @param {String} [params.formFactor] form factor this editor should use
   * (c.f. `Widget.formfactor`)
   * @param {String} [params.keyName] the key name that bajaux should use to
   * look up lexicon entries for this editor
   * @param {String} [params.moduleName='bajaux'] the module name that
   * bajaux should use to look up lexicon entries for this editor
   * @param {Object} [params.data] optional additional configuration data that
   * may be used on a per-widget basis. This will often be used in conjunction
   * with `fe`.
   *
   * @param {Object} [params.params] in conjunction with `defaults`, allows a
   * Widget subclass to accept parameters passed to the constructor as well as
   * an initial set of default values. See example.
   * @param {Object} [params.defaults] in conjunction with `params`, allows a
   * Widget subclass to define an initial set of default values. See example.
   *
   * @example
   * <caption>Construct a Widget, defining its initial configuration with an
   * object literal.</caption>
   * const w = new Widget({
   *   readonly: true,
   *   properties: { foo: 'bar' },
   *   formFactor: 'compact'
   * });
   *
   * @example
   * <caption>Define a Widget subclass with an initial set of default values,
   * while still allowing parameters to be passed to the constructor. This is
   * the preferred method of calling a Widget super-constructor.</caption>
   *
   * class MyButton extends Widget {
   *   constructor(params) {
   *     super({
   *       params,
   *       defaults: {
   *         moduleName: 'myModule',
   *         keyName: 'MyButton',
   *         properties: { borderRadius: 15, padding: 10 }
   *       }
   *     });
   *   }
   * }
   * const button = new MyButton({ properties: { padding: 20 } });
   * button.properties().getValue('borderRadius'); // 15, from defaults
   * button.properties().getValue('padding'); // 20, from constructor
   *
   * @example
   * <caption>params and defaults will be correctly processed even when
   * subclassing.</caption>
   * class AWidget extends Widget {
   *   constructor(params) {
   *     super({ params, defaults: { properties: { name: 'A' } } });
   *   }
   * }
   * class BWidget extends AWidget {
   *   constructor(params) {
   *     // defaults will be merged in with superclass defaults.
   *     super({ params, defaults: { properties: { name: 'B' } } });
   *   }
   * }
   * new BWidget().properties().getValue('name'); // 'B'
   * new BWidget({ properties: { name: 'Bee' } }).properties().getValue('name'); // 'Bee'
   */
  const Widget = function Widget(params) {
    // note: DO NOT convert to ES6 class - this will break "Widget.apply(this)" pattern everyone uses
    const args = arguments;
    if (!params || typeof params !== 'object') {
      params = {
        moduleName: args[0],
        keyName: args[1],
        formFactor: args[2]
      };
    }
    params = processParams(params);

    const props = new Properties();
    const moduleName = params.moduleName || 'bajaux';
    const keyName = params.keyName || 'widget';
    const formFactor = params.formFactor || Widget.formfactor.max;

    /**
     * Whether this widget is currently in the process of initializing - set to
     * `true` when `initialize` is called, and set to
     * `false` after its callback completes.
     * @private
     * @type {Boolean}
     */
    this.$initializing = false;

    let initResolved = false;
    /**
     * A promise to be resolved after the Widget finishes initializing.
     * @private
     * @type {Promise}
     */
    // eslint-disable-next-line promise/avoid-new
    this.$initPromise = new Promise((resolve) => {
      this.$resolveInit = resolve;
    })
      .then(() => { initResolved = true; });
    this.$initPromise.isResolved = () => initResolved;
    
    /**
     * Indicates whether this widget is enabled or not.
     * @private
     * @type {Boolean}
     */
    this.$enabled = true;
        
    /**
     * An internal array of MixIns registered on this widget.
     * @private
     */
    this.$mixins = [];

    /**
     * Name of the Niagara Module associated with this widget. This is
     * used for looking up values from a lexicon.
     * @private
     * @type {String}
     */     
    this.$moduleName = moduleName;
    
    /**
     * Name of type associated with this widget. This is
     * used for looking up values from a lexicon.
     * @private
     * @type {String}
     */
    this.$keyName = keyName;

    /**
     * The widget's form-factor.
     * @type {String}
     * @private
     */
    this.$formFactor = formFactor;

    /**
     * The Properties for a widget.
     * @private
     * @type {module:bajaux/Properties}
     */
    this.$properties = props;
          
    // By default, use the editor's display name.      
    this.setCommandGroup(new CommandGroup(
      `%lexicon(${ moduleName }:${ keyName }.displayName)%`));

    /**
     * Whether any widgets in this view have been changed.
     * @private
     * @type {Boolean}
     */
    this.$modified = false;
          
    /**
     * Whether this widget is currently in the process of loading - set to
     * `true` when `load` is called, and set to
     * `false` after its callback completes.
     * @private
     * @type {Boolean}
     */
    this.$loading = false;
    
    /**
     * The widget's current value.
     * @private
     */
    this.$value = null;

    /**
     * The widget's collection of Validators.
     * @private
     */
    this.$validators = new Validators(this);

    /**
     * The widget's readonly state.
     * @private
     */
    this.$readonly = false;

    /**
     * This flag is not yet exposed as public API. It is intended to be used
     * only by Tridium developers.
     *
     * When in "quiet mode" the widget will not be as chatty when firing events.
     * Firing a jQuery event isn't free, so in situations when you have huge
     * numbers of Widgets this can help performance. The following events are
     * squelched in quiet mode:
     *
     * - `INITIALIZE_EVENT`
     * - `LOAD_EVENT`
     * - `SAVE_EVENT`
     * - `ENABLE_EVENT`
     * - `DISABLE_EVENT`
     * - `READONLY_EVENT`
     * - `WRITABLE_EVENT`
     * - `LAYOUT_EVENT`
     * - `DESTROY_EVENT`
     *
     * Note that error events like `INITIALIZE_FAIL_EVENT`, and events that are
     * critical for operation like `MODIFY_EVENT`, will still be fired.
     *
     * @private
     * @type {boolean}
     */
    this.$quiet = !!params.$quiet;

    /**
     * The widget's form-factor.
     * @type {String}
     * @private
     */
    this.applyParams(params);
    this.$properties.$modified = false;
    
    props.on(events.PROPERTY_ADDED, (name) => {
      this.trigger(events.PROPERTY_ADDED, name);
    });

    props.on(events.PROPERTY_CHANGED, (names, values, options) => {
      const initialized = this.isInitialized();
      names.forEach((name, i) => {
        const value = values[i];
        this.trigger(events.PROPERTY_CHANGED, name, value, options);
        if (initialized) {
          this.changed(name, value, options);
        }
      });
    });
    
    props.on(events.PROPERTY_REMOVED, (name, prop) => {
      this.trigger(events.PROPERTY_REMOVED, name, prop);
    });

    props.on(events.METADATA_CHANGED, (name, value, options) => {
      this.trigger(events.METADATA_CHANGED, name, value, options);
    });

    tinyevents(this);
  };

  /**
   * Can re-apply certain params that can also be passed to the constructor.
   * @param {Object} [params]
   * @param {Boolean} [params.readonly]
   * @param {Boolean} [params.enabled=true] must explicitly set to false to
   * disable
   * @param {Object} [params.properties]
   * @returns {Promise} promise to be resolved after any
   * `setEnabled`/`setReadonly` work is done. Note that these functions will not
   * be called if the value of `enabled`/`readonly` is not actually changing.
   * @since Niagara 4.10
   */
  Widget.prototype.applyParams = function (params) {
    params = params || {};

    const readonly = !!params.readonly,
      properties = params.properties,
      enabled = getEnabledFromParams(params);

    return Promise.all([
      (readonly !== this.isReadonly()) && this.setReadonly(readonly),
      (enabled !== this.isEnabled()) && this.setEnabled(enabled),
      this.properties().addAll(new Properties(properties))
    ]);
  };

  /**
   * Convenience to process and apply params.
   * The intention of this method is to support older style Widgets that
   * add property defaults in the constructor after applying the Widget constructor.
   * That was causing any incoming property value to be reset by the default.
   * This method maybe removed in the future.
   * 
   * @since Niagara 4.10
   * @private
   * @returns {Promise}
   */
  Widget.prototype.$reapplyParams = function (params) {
    logWarning(this.constructor +
      ' -> Modifying Properties in Widget constructor after super call,' +
      ' may not be supported in the future.' +
      ' Pass "defaults" to the constructor instead.');
    return this.applyParams(processParams(params));
  };

  /**
   * Allows for Widget-specific extension of parameters objects, granting more
   * control over which params are invariant for your Widget subclass and which
   * can be overridden by the constructor. Default behavior is to do a deep
   * extend on any `properties` argument, and shallow extend for other
   * properties.
   *
   * @private
   * @since Niagara 4.10
   * @deprecated since Niagara 4.14 - use `{ params, defaults }` Widget constructor signature instead
   * @param {...object} [obj] zero or more params objects. Properties later in the
   * arguments list will overwrite earlier properties.
   *
   * @returns {object} extended params object
   *
   * @example
   * <caption>When implementing a Widget subclass, I may want certain params to
   * always be applied, while other params may be overridden by the constructor.
   * </caption>
   *
   * class MyWidget extends Widget {
   *   constructor(params) {
   *     super(Widget.$extendParams(
   *     {
   *       properties: { myProp: 'myProp' } // define my default set of properties here...
   *     },
   *     params, // allow the constructor to override them with own values...
   *     {
   *       moduleName: 'MyModule', // and enforce a constant Widget display name.
   *       keyName: 'MyWidget'
   *     }));
   *   }
   * }
   */
  Widget.$extendParams = function () {
    const args = Array.prototype.slice.apply(arguments);
    const result = {};
    const propsObjects = [];
    const defaultsArray = [];
    let moduleName;

    args.forEach((arg) => {
      if (!arg || typeof arg !== 'object') { return; }
      Object.keys(arg).forEach((key) => {
        if (key === '__proto__') { return; }

        if (key === 'moduleName') {
          moduleName = arg[key];
        }

        if (key === 'properties') {
          const props = arg[key];
          if (props) {
            propsObjects.push(props);
          }
        } else if (key === 'defaults') {
          //process these in reverse order to allow the deeper defaults to go last
          defaultsArray.unshift(arg[key]);
        } else {
          result[key] = arg[key];
        }
      });
    });

    if (propsObjects.length) {
      const propObjectsWithDefaults = propsObjects.map((propObjs) => {
        let propsWithDefault = {  };
        const propsWithoutDefault = propObjs instanceof Properties ? propObjs.toObject() : propObjs;
        const allPropNamesAndSymbols = Object.keys(propsWithoutDefault)
          .concat(Object.getOwnPropertySymbols(propsWithoutDefault));

        allPropNamesAndSymbols.forEach((propName) => {
          const originalProp = propsWithoutDefault[propName];

          // Make sure we have a JSON object with a { value: '' } structure
          if (typeof originalProp !== 'object' || originalProp === null ||
            !originalProp.hasOwnProperty('value')) {
            propsWithDefault[propName] = {
              value: originalProp
            };
          } else {
            propsWithDefault[propName] = originalProp;
          }

          let defaultModuleName = moduleName;
          let defaultFound = false;
          defaultsArray.forEach((defaults) => {
            if (defaultFound) {
              return;
            }
            defaultModuleName = defaults && (defaults.moduleName || defaultModuleName);
            
            const defaultProps = defaults && defaults.properties;
            // Provide this prop with a default value.
            if (defaultProps && defaultProps.hasOwnProperty(propName)) {
              defaultFound = true;
              if (defaultProps[propName].hasOwnProperty('value')) {
                propsWithDefault[propName].defaultValue = defaultProps[propName].value;
              } else {
                propsWithDefault[propName].defaultValue = defaultProps[propName];
              }
            } 
            
            if (defaultProps && defaultProps.hasOwnProperty(propName) && defaultModuleName && !propsWithDefault[propName].moduleName) {
              propsWithDefault[propName].moduleName = defaultModuleName;
            }
          });

          if (!defaultFound) {
            propsWithDefault[propName].defaultValue = propsWithDefault[propName].value;
          }
        });

        return propsWithDefault;
      });

      result.properties = Properties.extend(...propObjectsWithDefaults);
    }

    return result;
  };

  Widget.$ROOT_CSS_CLASS_SYMBOL = ROOT_CSS_CLASS_SYMBOL;

  /**
   * Widget form-factors.
   *
   * @see module:bajaux/Widget#getFormFactor
   *
   * @property {String} max A large Widget (e.g. a View).
   * @property {String} compact A medium sized Widget (e.g. a dashboard Widget).
   * @property {String} mini A small Widget (e.g. a Field Editor).
   */
  Widget.formFactor = Widget.formfactor = {
    max: "max",
    compact: "compact",
    mini: "mini"
  };

  /**
   * Widget CSS class names.
   *
   * @property {String} initialized `bajaux-initialized`: applied after the
   * widget is initialized.
   * @property {String} disabled `bajaux-disabled`: applied when the widget is
   * disabled.
   * @property {String} designTime `bajaux-design-time`: Applied to an ancestor
   * DOM element to indicate the widget is running in a graphic design editor.
   * You can use this to style your widget differently between design time and
   * production.
   * @property {String} readonly `bajaux-readonly`: Applied when the widget is
   * marked readonly.b
   * @property {String} max `bajaux-max`: A large Widget (e.g. a View).
   * @property {String} compact `bajaux-compact`: A medium sized Widget (e.g. a
   * dashboard Widget).
   * @property {String} mini `bajaux-mini`: A small Widget (e.g. a Field
   * Editor).
   */
  Widget.css = {
    initialized: "bajaux-initialized",
    disabled: "bajaux-disabled",
    designTime: "bajaux-design-time",
    readonly: "bajaux-readonly",
    max: "bajaux-" + Widget.formfactor.max,
    compact: "bajaux-" + Widget.formfactor.compact,
    mini: "bajaux-" + Widget.formfactor.mini
  };

  const css = Widget.css;
  const unknownErr = Widget.unknownErr = "unknown error";
  const allFormFactorCss = [ css.max, css.compact, css.mini ].join(' ');
  const classesToRemoveOnDestroy = Object.keys(css).map((c) => css[c]);

  /**
   * Generate a unique DOM ID. The ID will include the name of this widget's
   * constructor just for tracing/debugging purposes.
   *
   * @returns {String}
   */
  Widget.prototype.generateId = function () {
    const prefix = this.constructor.name || 'Widget';
    return prefix + '__id__' + idCount++;
  };

  /**
   * Return the widget's form-factor. The form-factor 
   * is normally passed in from the Widget's constructor. However,
   * it can be set from a 'formFactor' property if required. 
   *
   * A widget's form-factor typically doesn't change during a widget's
   * life-cycle.
   *
   * @see module:bajaux/Widget.formfactor
   * 
   * @return {String} The form-factor.
   */
  Widget.prototype.getFormFactor = function () {
    return this.properties().getValue("formFactor", this.$formFactor);
  };

  /**
   * A widget may need to do its own layout calculation. It might need
   * to statically position elements, or show/hide them based
   * on the shape of its container.
   * 
   * This function gives a widget an opportunity to do that. It's called once
   * the Widget has been initialized and once the form factor has changed.
   * Also it may be called when the widget's container changes shape
   * or size, or is shown/hidden.
   * 
   * This method should not typically be overridden. Override
   * {@link module:bajaux/Widget#doLayout|doLayout()} instead.
   *
   * @param {*} [params] as of Niagara 4.10, any parameters passed to
   * `layout()` will also be passed to `doLayout()`.
   * @returns {Promise} A promise that's resolved once the layout has
   * completed.
   */
  Widget.prototype.layout = function (params) {
    if (!this.isInitialized()) { return Promise.resolve(); }

    return Promise.resolve(this.doLayout(params))
      .then(() => {
        // Trigger the layout event once the Widget has been laid out.
        if (!this.$quiet) { this.trigger(events.LAYOUT_EVENT); }
      });
  };

  /**
   * Called when the layout of the Widget changes. This method is designed
   * to be overridden.
   *
   * @param {*} [params] as of Niagara 4.10, any parameters passed to
   * `layout()` will also be passed to `doLayout()`.
   * @returns {*|Promise} This method may optionally return a promise once the
   * Widget has been laid out.
   */
  Widget.prototype.doLayout = function (params) {
  };

  /**
   * Access the widget's display name asynchronously.
   * 
   * By default, this will attempt to access the widget's display name from
   * the originating Lexicon. The Lexicon key should be in the format of
   * `keyName.displayName`. If an entry can't be found then the Type's 
   * name will be used.
   *
   * @returns {Promise} A promise to be resolved with the widget's display name
   */   
  Widget.prototype.toDisplayName = function toDisplayName() {
    const { $moduleName, $keyName } = this;
    
    // Attempt to access the Widget's displayName from the originating module's 
    // Lexicon as 'keyName.displayName'.
    return lex.module($moduleName)
      .then((moduleLex) => moduleLex.get($keyName + ".displayName") || $keyName)
      .catch(() => $keyName);
  };
    
  /**
   * Access the widget's icon asynchronously.
   * 
   * By default this will attempt to access the widget's icon from
   * the originating Lexicon. The Lexicon key should be in the format of
   * `keyName.description`. If an entry can't be found then a blank string
   * will be used.
   * 
   * @returns {Promise} A promise to be resolved with the widget's description
   */   
  Widget.prototype.toDescription = function toDescription() {
    const { $moduleName, $keyName } = this;
    
    // Attempt to access the Widget's description from the originating module's 
    // Lexicon as 'keyName.description'.
    return lex.module($moduleName)
      .then((moduleLex) => moduleLex.get($keyName + ".description") || "")
      .catch(() => '');
  };
  
  /**
   * Access the widget's icon asynchronously.
   * 
   * By default, this will attempt to access the widget's description from
   * the originating Lexicon. The Lexicon key should be in the format of
   * `keyName.icon`. If an entry can't be found then a blank String will be 
   * returned.
   *
   * @returns {Promise} A promise to be resolved with the widget's icon URI.
   */  
  Widget.prototype.toIcon = function toIcon() {
    const { $moduleName, $keyName } = this;

    // Attempt to access the widget's icon ORD from the originating module's 
    // Lexicon as 'keyName.icon'.
    return lex.module($moduleName)
      .then((moduleLex) => {
        const s = moduleLex.get($keyName + ".icon");
        return s ? s.replace(/^module:\/\//, "/module/") : "";
      })
      .catch(() => '');
  };

  /**
   * Return the widget's command group.
   *
   * @returns {module:bajaux/commands/CommandGroup}
   */
  Widget.prototype.getCommandGroup = function getCommandGroup() {
    return this.$commandGroup;
  };
  
  /**
   * Set this widget's command group. Triggers a `bajaux:changecommandgroup`
   * event.
   *
   * @param {module:bajaux/commands/CommandGroup} commandGroup
   */
  Widget.prototype.setCommandGroup = function setCommandGroup(commandGroup) {
    commandGroup.visit((c) => c.jq(this.$jq));
    this.$commandGroup = commandGroup;
    this.trigger(events.command.GROUP_CHANGE_EVENT);
  };
    
  /**
   * Private callback for when the DOM element is changed.
   *
   * @private
   */
  Widget.prototype.domChanged = function domChanged() {
    this.$commandGroup.visit((c) => c.jq(this.$jq));
  };

  /**
   * Return a promise that will only resolve after `initialize` has resolved.
   * Will reject if the widget is destroyed.
   * 
   * @private
   * @returns {Promise}
   */
  Widget.prototype.$initialized = function () {
    if (this.$destroyed) {
      return Promise.reject(new Error(alreadyDestroyed));
    }
    return this.$initPromise;
  };
  
  /**
   * Return true if this Widget is initialized.
   *
   * @returns {Boolean}
   */
  Widget.prototype.isInitialized = function isInitialized() {
    return this.$initPromise.isResolved() && !this.$destroyed;
  };

  /**
   * Return true if this Widget has already been destroyed. After destruction,
   * `initialize()` will always reject: the widget cannot be reused.
   * 
   * @returns {Boolean}
   */
  Widget.prototype.isDestroyed = function isDestroyed() {
    return !!this.$destroyed;
  };
  
  /**
   * Find the Widget instance living in this DOM element.
   *
   * Or: "what Widget is in this element?"
   *
   * @param {JQuery|HTMLElement} el
   * @returns {module:bajaux/Widget|undefined}
   * @since Niagara 4.6
   * @see #owning
   */
  Widget.in = function (el) {
    // noinspection JSValidateTypes
    return $(el).data('widget');
  };

  /**
   * Walk up the DOM tree to find the closest Widget instance. Will find a Widget that is
   * initialized directly into the given element, or is initialized into one of its ancestors.
   *
   * Or: "what Widget is this element in?"
   *
   * @param {JQuery|HTMLElement} el
   * @returns {module:bajaux/Widget|undefined}
   * @since Niagara 4.15
   * @see #in
   */
  Widget.owning = function (el) {
    el = $(el);

    let w;

    do {
      w = Widget.in(el);
      el = el.parent();
    } while (el.length && !(w && !w[IS_ELEMENT_SYMBOL]));

    return w;
  };
  
  /**
   * Initializes the DOM element to be bound to this Widget.
   * 
   * In a nutshell, `initialize` defines the following contract:
   * 
   * * After `initialize` completes and resolves its Promise, the target element will be fully
   *   initialized, structured, and ready to load in a value. It will be accessible by calling
   *   `this.jq()`.
   * * If this is an editor, `load` may not be called until 
   *   `initialize`'s promise is resolved. Attempting 
   *   to load a value prior to initialization will result in failure.
   * * This widget will be set as a jQuery data value on the initialized
   *   DOM element. It can be retrieved by calling `Widget.in(element)`.
   * 
   * `initialize` delegates the actual work of building the
   * HTML structure (if any) to the `doInitialize` function. When
   * subclassing Widget, you should not override `initialize`.
   * `doInitialize` should be overridden.
   * 
   * After `initialize` completes, an  `bajaux:initialize` or
   * `bajaux:initializefail` event will be triggered, as appropriate.
   * 
   * `initialize` is a one-time operation. It will always reject if the widget has already been
   * initialized once, or if it has been destroyed.
   *
   * @see module:bajaux/Widget#doInitialize
   * 
   * @param {JQuery} dom The jQuery DOM element in which this widget should
   * build its HTML (will be passed directly to `doInitialize`)
   * @param {*} [params] optional parameters object to be passed through to
   * `doInitialize`
   * @param {*} [layoutParams] as of Niagara 4.10, optional parameters object to
   * be passed through to `layout`
   * @returns {Promise} A promise to be resolved once the widget has
   * initialized
   */
  Widget.prototype.initialize = function initialize(dom, params, layoutParams) {
    const prom = this.$initialized();
    
    if (prom.isResolved()) {
      return prom;
    }

    if (this.$initializing) {
      return Promise.reject(new Error('already initialized!'));
    }

    if (Widget.in(dom)) {
      return Promise.reject(new Error("DOM element already has 'widget' data!"));
    }

    this.$initializing = true;

    const initializeFail = (err) => {
      err = err || new Error(unknownErr);
      try {
        this.trigger(events.INITIALIZE_FAIL_EVENT, err);
      } catch (ignore) {}

      //roll back the binding if initialization fails.
      delete this.$jq;
      throw err;
    };

    // Once the Command Group has finished loading, initialize the Widget.
    return this.$commandGroup.loading()
      .then(() => {
        // Set this before doInitialize is called so methods like #isDesignTime will
        // work during initialization callbacks.
        this.$jq = dom;
        return this.doInitialize(dom, params);
      })
      .then(() => {
        try {
          dom.data('widget', this)
                 .addClass(Widget.css.initialized);

          this.domChanged();
          if (!this.$quiet) { this.trigger(events.INITIALIZE_EVENT); }
        } catch (eventHandlerErr) {
          dom.removeData('widget')
                 .removeClass(Widget.css.initialized);
          throw eventHandlerErr;
        }
      })
      .then(() => {
        // Remove any of the old form factor classes and add the new one.
        this.$jq
          .removeClass(allFormFactorCss)
          .addClass(Widget.css[this.getFormFactor()])
          .addClass(getRootCssClass(this));
        this.$resolveInit(dom, params);

        // Ensure the widget is laid out once initialized. init promise must resolve, since
        // layout() is a no-op until isInitialized() returns true.
        // we also want to ensure that any listeners for the "initialized" tinyevent get notified
        // *after* isInitialized() returns true. this is to protect the pattern:
        // if (widget.isInitialized()) { doIt(); } else { widget.on('initialized', doIt); }
        return prom.then(() => {
          this.emit('initialized');
          return this.layout(layoutParams);
        });
      }, initializeFail)
      .finally(() => {
        this.$initializing = false;
      });
  };
      
  /**
   * Performs the actual work of initializing the DOM element in which this
   * widget will live. This function should be overridden by subclasses - the 
   * subclass function should append elements to `element` as 
   * necessary and then optionally return a promise.
   * 
   * Most commonly, this will involve building up the HTML structure 
   * necessary to load in a value. If this widget will display/edit a String,
   * for example, `doInitialize` might append a text input element to 
   * the target element. A `DynamicEnum` might include a 
   * `<select>` dropdown. 
   * 
   * In some cases, no initialization may be required at all. This 
   * might be the case if you are binding the widget to an HTML element that is
   * already pre-populated with all the necessary structure to load a value,
   * or maybe `doLoad` will empty out the element completely
   * and rebuild it from scratch every time a new value is loaded. In this
   * case, you do not need to override this method. (However, a widget that
   * overrides neither `doInitialize` nor `doLoad` will not be very useful!)
   * 
   * Tip: the promises returned by `setEnabled` and `setReadonly` can
   * only ever resolve after `initialize` itself resolves. So
   * `return this.setEnabled(enabled)` or `return this.setReadonly(readonly)`
   * from `doInitialize` will result in a deadlock that will never resolve.
   * They may be called, but not returned.
   * 
   * @see module:bajaux/Widget#initialize
   * 
   * @param {JQuery} element The element in which this Widget should build its
   * HTML.
   * @param {Object} [params] Optional params object passed into `initialize()`.
   * @returns {*|Promise} An optional promise to be resolved once the Widget
   * has initialized.
   */
  Widget.prototype.doInitialize = function doInitialize(element, params) {
    
  };

  /**
   * Called to clean up the DOM when the widget is being destroyed.
   *
   * This method can be overridden if DOM clean up needs to be 
   * handled in a different way.
   */
  Widget.prototype.cleanupDom = function cleanupDom() {
    this.jq().empty().off();
  };
  
  /**
   * Indicates that a widget is no longer needed and is in the process of being
   * removed. In this function, subclasses can deallocate any resources, event 
   * handlers, etc. that they may be holding. Delegates the actual work to 
   * `doDestroy`.
   *
   * This method will not typically be overridden. `doDestroy()` should be
   * overridden instead.
   * 
   * Triggers a `bajaux:destroy` or `bajaux:destroyfail` event, as appropriate.
   *
   * Please note, after `doDestroy` has resolved, the DOM will be emptied, 
   * all event handlers will be removed and the 'widget' data stored on the
   * DOM element will be deleted.
   * 
   * @see module:bajaux/Widget#doDestroy
   *
   * @param {object} [params] optional parameters to be passed to `doDestroy`
   * @returns {Promise} A promise to be resolved when the widget has been
   * destroyed
   */
  Widget.prototype.destroy = function destroy(params) {
    const jq = this.$jq;

    const cleanup = () => {
      if (this.$jq) {
        // Strips, removes and cleans up everything after
        // necessary events have fired.
        jq.removeData("widget");
        jq.removeClass(classesToRemoveOnDestroy);
        jq.removeClass(getRootCssClass(this));

        this.cleanupDom();

        this.$jq = null;
      }

      if (this.$properties) {
        this.$properties
          .removeAllListeners(events.PROPERTY_ADDED)
          .removeAllListeners(events.PROPERTY_CHANGED)
          .removeAllListeners(events.PROPERTY_REMOVED);
      }
    };

    // If the Widget isn't initialized then just clean up listeners and mark
    // the widget as destroyed.
    if (!this.isInitialized()) {
      this.$destroyed = true;
      return Promise.try(cleanup);
    }
    
    const cmds = [];
    this.getCommandGroup().visit(function (cmd) {
      if (cmd instanceof Command) {
        cmds.push(cmd);
      }
    });

    this.$destroyed = true;

    return Promise.all([
      Promise.try(() => this.doDestroy(params)),
      Promise.all(cmds.map((cmd) => {
        return Promise.try(() => cmd.setEnabled(false));
      }))
    ])
      .then(() => {
        if (!this.$quiet) { this.trigger(events.DESTROY_EVENT); }
        this.emit('destroyed');
      }, (err) => {
        err = err || new Error(unknownErr);
        this.trigger(events.DESTROY_FAIL_EVENT, err);
        throw err;
      })
      .finally(cleanup);
  };
  
  /**
   * Called by `destroy` so this widget has a chance to clean up after itself
   * and release any resources it is holding.
   * 
   * Notably, any jQuery event handlers registered on child elements of the
   * widget's DOM element should be unregistered here. Also, you may want to
   * remove any CSS classes you've added to the widget's DOM element.
   *  
   * @see module:bajaux/Widget#destroy
   * 
   * @param {Object} [params] Optional params object passed to `destroy()`
   * @returns {*|Promise} An optional promise that's resolved once the widget
   * has been destroyed.
   */
  Widget.prototype.doDestroy = function doDestroy(params) {
    
  };
  
  /**
   * Returns the jQuery DOM element in which this widget has been initialized.
   * If `initialize()` has not yet been called, then this will return `null`.
   *
   * @returns {JQuery|null} the DOM element in which this widget has been
   * initialized, or `null` if not yet initialized.
   */
  Widget.prototype.jq = function jq() {
    return this.$jq || null;
  };
  
  /**
   * Returns this widget's enabled state.
   *
   * @see module:bajaux/Widget#setEnabled
   *
   * @returns {Boolean}
   */
  Widget.prototype.isEnabled = function isEnabled() {
    return this.$enabled;
  };
  
  // noinspection JSCommentMatchesSignature
  /**
   * Set this widget's enabled state.
   *
   * Setting of the internal flag will be synchronous, so `isEnabled` will
   * return the expected value immediately after calling this function. However,
   * the actual work of updating the DOM cannot be performed until after the
   * widget has finished initializing, so this method will return a promise.
   *
   * This method will not typically be overridden. `doEnabled()` should be
   * overridden instead.
   *
   * @see module:bajaux/Widget#isEnabled
   * 
   * @param {Boolean} enabled the new enabled state
   * @returns {Promise} A promise to resolve immediately if `initialize` has
   * not yet been called, that will resolve once the work of `initialize`
   * followed by `doEnabled` have both been completed. It will reject if
   * `initialize` or `doEnabled` fail.
   */
  Widget.prototype.setEnabled = function setEnabled(enabled, ...rest) {
    enabled = !!enabled;

    // Set state change immediately! This is always set regardless of
    // any errors that may be thrown by any callbacks.
    this.$enabled = enabled;
    
    // Only trigger callbacks and events if the Widget has been initialized.
    const prom = this.$initialized()
      .then(() => {
        const enabled = this.$enabled;
        this.jq().toggleClass(Widget.css.disabled, !enabled);
        return Promise.try(() => this.doEnabled(enabled, ...rest))
          .then(() => {
            if (!this.$quiet) {
              this.trigger(enabled ? events.ENABLE_EVENT : events.DISABLE_EVENT);
            }
            return enabled;
          }, (err) => {
            err = err || new Error(unknownErr);
            this.trigger(enabled ? events.ENABLE_FAIL_EVENT : events.DISABLE_FAIL_EVENT, err);
            throw err;
          });
      });

    // If the Widget isn't initialized yet then we don't know when it will be
    // resolved. Therefore, we need to resolve this right now.
    if (!this.isInitialized()) {
      return Promise.resolve(enabled);
    } else {
      return prom;
    }
  };
  
  /**
   * Called when the widget is enabled/disabled.
   *
   * @param {Boolean} enabled the new enabled state.
   * @returns {*|Promise} An optional Promise that can be returned if
   * the state change is asynchronous.
   */
  Widget.prototype.doEnabled = function doEnabled(enabled) {
    
  };

  /**
   * Trigger a widget event. By default, this fires a DOM event on the associated
   * widget's DOM element.
   */
  Widget.prototype.trigger = function trigger(name) {
    if (this.$jq) {
      const passedArgs = Array.prototype.slice.call(arguments, 1);
      const args = [ this ].concat(passedArgs);
      this.$jq.trigger(name, args);
    }
  };
              
  /**
   * Return true if the widget implements the specified MixIn.
   *
   * @param {String} mixin the name of the mixin to test for.
   * @returns {Boolean}
   */
  Widget.prototype.hasMixIn = function hasMixIn(mixin) {
    const mixins = this.$mixins;
    
    for (let i = 0; i < mixins.length; ++i) {
      if (mixins[i] === mixin) {
        return true;
      }
    }
    
    return false;
  };

  /**
   * Return the Properties for a widget.
   * 
   * @returns {module:bajaux/Properties} The Properties for a widget.
   */
  Widget.prototype.properties = function properties() {
    return this.$properties;
  };

  /**
   * Called whenever a Widget's Property is changed.
   *
   * If this Widget is not yet initialized, this is a no-op.
   * 
   * This function should not typically be overridden.
   * {@link module:bajaux/Widget#doChanged|doChanged()} should be overridden
   * instead.
   *
   * @see module:bajaux/Widget#properties
   * 
   * @param  {String} name The name of the Property that's changed.
   * @param  {*} value The new Property value.
   * @returns {Promise}
   */
  Widget.prototype.changed = function changed(name, value) {
    if (name === 'enabled') {
      return this.setEnabled(value).then(() => this.doChanged(name, value));
    } else {
      return Promise.resolve(this.doChanged(name, value));
    }
  };

  /**
   * Called by {@link module:bajaux/Widget#changed|changed()} when a Property
   * is changed.
   *
   * This method is designed to be overridden by any subclasses.
   * 
   * @param  {String} name The name of the Property that's changed.
   * @param  {*} value The new Property value.
   * @returns {Promise|*}
   */
  Widget.prototype.doChanged = function doChanged(name, value) {
  };

  /**
   * Returns true if the Widget is in a graphic design editor.
   *
   * @returns {Boolean}
   */
  Widget.prototype.isDesignTime = function isDesignTime() {
    return this.$jq ? this.$jq.closest("." + Widget.css.designTime).length > 0 : false;
  };

  /**
   * Returns this widget's modified state. 
   *
   * @returns {Boolean}
   */
  Widget.prototype.isModified = function isModified() {
    return this.$modified;
  };
  
  /**
   * Sets this widget's modified or "dirty" status, to indicate that the user
   * has made changes to this widget that may need to be saved.
   *
   * The modification status will only be set if the widget is initialized
   * and the widget is not loading a new value.
   * 
   * Triggers `bajaux:modify` or `bajaux:unmodify` depending on the input value.
   * Any arguments passed to this function after the first will be passed
   * through to the triggered event.
   * 
   * This method should not typically be overridden. 
   * {@link module:bajaux/Widget#doModified|doModified()} should be overridden
   * instead.
   * 
   * @see module:bajaux/Widget#doModified
   * 
   * @param {Boolean|*} modified (a non-Boolean will be checked for truthiness)
   * 
   * @example
   *   <caption>Say I have collection of nested widgets in my DOM element.
   *   Whenever one of those widgets is modified, I want to mark myself
   *   modified but also provide the originally modified editor. For example,
   *   when a Property Sheet is modified, I want to know which row caused the
   *   modification.</caption>
   *     
   *   var that = this;
   *   dom.on(events.MODIFY_EVENT, function (e, modifiedEd) {
   *     that.setModified(true, modifiedEd);
   *     return false;
   *   });
   */
  Widget.prototype.setModified = function setModified(modified) {
    const args = Array.prototype.slice.call(arguments);
    
    if (this.isLoading() || !this.isInitialized()) {
      return;
    }
    
    modified = !!modified;
    this.$modified = modified;
    this.doModified(modified);
    
    args[0] = modified ? events.MODIFY_EVENT : events.UNMODIFY_EVENT;
    this.trigger.apply(this, args);
  };

  /**
   * The actual implementation for `setModified`. This function
   * should do any work necessary when the widget is set to a modified or 
   * "dirty" state - typically enabling a save button, arming a
   * `window.onbeforeunload` handler, etc. Likewise, it should do
   * the opposite when setting modified to false.
   * 
   * Note that this is *synchronous*, as is `setModified`. Async work can be
   * performed, but `setModified` will not wait for it.
   * 
   * @see module:bajaux/Widget#setModified
   * 
   * @param {Boolean} modified
   */
  Widget.prototype.doModified = function doModified(modified) {
  };

  /**
   * Resolve a value from some data. Please note, this will not load the value
   * but will resolve some data that could then be loaded by the widget.
   *
   * By default, this will treat the data as an ORD so it can be resolved via
   * BajaScript.
   *
   * @param {*|String|baja.Ord} data Specifies some data used to resolve a
   * load value so `load(value)` can be called on the widget.
   * @param {Object} [resolveParams] An Object Literal used for ORD resolution.
   * This parameter is designed to be used internally by bajaux and
   * shouldn't be used by developers.
   * @returns {Promise} a promise to be resolved with the value resolved from
   * the given data object
   */
  
  Widget.prototype.resolve = function resolve(data, resolveParams) {

    // eslint-disable-next-line promise/avoid-new
    return new Promise((resolve, reject) => {
      resolveParams = resolveParams || {};


      // Lazily require BajaScript so we don't create a direct dependency on it
      // since this method can be overridden.
      require([ "baja!" ], function (baja) {

        const ordUtil = require('bajaScript/baja/ord/ordUtil');

        const quickResolvePromise = ordUtil.quickResolve(data, resolveParams);
        if (quickResolvePromise) {
          return quickResolvePromise
            .then((ordTarget) => {
              resolve(ordTarget.getObject());
            });
        }

        resolveParams.ok = resolveParams.ok || resolve;
        resolveParams.fail = resolveParams.fail || reject;

        // Resolve the ORD
        //TODO: if resolveParams.ok is truthy, Widget#resolve() will never actually resolve its promise
        baja.Ord.make(String(data)).get(resolveParams);
      });
    });
  };

  /**
   * Updates the widget's HTML with the given value. An widget for editing a
   * string, for example, might load the string into a text input. A view for
   * editing a `DynamicEnum` might programmatically set a `<select>`
   * dropdown's value.
   * 
   * `load()` may not be called until `initialize()` has completed its work.
   * If `initialize()` is not finished, `load()` will reject.
   * 
   * After `load()` completes its work, the value loaded will be accessible
   * via `this.value()`.
   * 
   * `load()` delegates the work of loading the HTML values to `doLoad()`. 
   * Subclasses will typically not override `load`, but more commonly will
   * override `doLoad`.
   * 
   * After `load()` completes, a `bajaux:load` or `bajaux:loadfail` event will
   * be triggered, as appropriate.
   * 
   * While this method is performing its work, `this.isLoading()` will return 
   * `true`.
   *
   * @see module:bajaux/Widget#doLoad
   * @see module:bajaux/Widget#value
   * @see module:bajaux/mixin/batchLoadMixin
   * 
   * @param {*} value The value to be loaded
   * @param {Object} [params] additional parameters to be passed to `doLoad()`
   * @returns {Promise} A promise to be resolved with the loaded value after
   * the widget has been loaded, or rejected if the widget fails to load the
   * value.
   */
  Widget.prototype.load = function load(value, params) {
    const oldValue = this.$value;
                
    if (!this.isInitialized()) {
      //TODO: trigger LOAD_FAIL?
      return Promise.reject(new Error(notInitialized));
    }

    this.$value = value;
    this.$loading = true;

    return Promise.try(() => this.doLoad(value, params))
      .then(() => {
        if (!this.$quiet) { this.trigger(events.LOAD_EVENT); }
        this.emit('loaded', value);
        return value;
      }, (err) => {
        err = err || new Error(unknownErr);
        this.$value = oldValue;
        this.trigger(events.LOAD_FAIL_EVENT, err);
        throw err;
      })
      .finally(() => {
        this.$loading = false;
      });
  };

  /**
   * Loads in a new value, and sets the widget modified as well. Use this
   * convenience method when you wish to load in a new value as if a user had
   * done it, thereby triggering the necessary modify event handlers.
   *
   * @param {*} value
   * @param {*} [params]
   * @returns {Promise.<*>}
   * @since Niagara 4.12
   */
  Widget.prototype.loadAndModify = function (value, params) {
    return this.load(value, params)
      .then((result) => {
        this.setModified(true);
        return result;
      });
  };
  
  /**
   * Performs the actual work of populating the widget's HTML to reflect the
   * input value.
   * 
   * This function should be overridden by subclasses. The subclass function
   * should manipulate the DOM {@link module:bajaux/Widget#jq|jq()} and,
   * optionally,return a promise to indicate that the work of loading the value
   * has completed.
   * 
   * @see module:bajaux/Widget#load
   * @see module:bajaux/Widget#value
   * 
   * @param {*} value The value to be loaded.
   * @param {Object} [params] Optional params object passed to `load()`
   * @returns {Promise} An optional promise that's resolved once the widget has
   * loaded.
   */
  Widget.prototype.doLoad = function doLoad(value, params) {
  };

  /**
   * Check if this widget is currently in the process of loading. This will
   * return `true` immediately after `load` is called, and return `false`
   * after the `load` promise resolves.
   *
   * @returns {Boolean}
   */
  Widget.prototype.isLoading = function isLoading() {
    return this.$loading;
  };
  
  /**
   * Returns the widget's current loaded value. This the value that was last
   * loaded via `load()`. To read a widget's current representation, reflecting
   * any user-entered changes, call `read()`. If no value has been loaded yet,
   * `null` is returned.
   * 
   * @see module:bajaux/Widget#load
   * @see module:bajaux/Widget#doLoad
   * @see module:bajaux/Widget#read
   * 
   * @returns {*|null} the loaded value, or `null` if a value hasn't been
   * loaded yet.
   */
  Widget.prototype.value = function () {
    return this.$value;
  };
  

  /**
   * Saves any outstanding user-entered changes to this widget. Triggers a
   * `bajaux:save` or `bajaux:savefail` event, as appropriate.
   * 
   * In order to save the widget, its current value will be validated using
   * `validate()`, then the validated value will be passed to `doSave()`.
   * 
   * This method will not typically be overridden. 
   * {@link module:bajaux/Widget#doSave|doSave()} should be overridden instead.
   *
   * @see module:bajaux/Widget#doSave
   * @see module:bajaux/mixin/batchSaveMixin
   * 
   * @param {Object} [params] Additional parameters to be passed to `doSave()`
   * 
   * @returns {Promise} A promise to be resolved once the widget has been saved,
   * or rejected if the save fails.
   */
  Widget.prototype.save = function save(params) {
    if (!this.isInitialized()) {
      //TODO: trigger SAVE_FAIL
      return Promise.reject(new Error(notInitialized));
    }
    
    return Promise.resolve(this.validate())
      .then((validValue) => {
        //TODO: always pass an object to avoid if (params) checks?
        return this.doSave(validValue, params);
      })
      .then(() => {
        // Now the widget is saved, unmodify the widget
        this.setModified(false);
        if (!this.$quiet) { this.trigger(events.SAVE_EVENT); }
        //squelch "promise not returned" warnings - MODIFY_EVENT/SAVE_EVENT
        //triggers may kick off promises
        return null;
      }, (err) => {
        err = err || new Error(unknownErr);
        this.trigger(events.SAVE_FAIL_EVENT, err);
        throw err;
      });
  };
  
  /**
   * Performs the actual work of saving the widget. This function should
   * be overridden by subclasses to save the value. The subclass function
   * should save the value and then, optionally, return a promise to indicate
   * that the work of saving the widget has completed.
   *
   * @param {*} validValue The value to be used for saving. This value will have
   * been read from the widget using `read()` and validated using `validate()`.
   * @param {Object} [params] Optional params object passed to `save()`
   * @returns {*|Promise} An optional promise that's resolved once
   * the widget has saved.
   */
  Widget.prototype.doSave = function doSave(validValue, params) {
  };
  
  /**
   * Read the current representation of the widget. For instance, if the widget
   * is made up from two text input boxes, this might resolve an object with
   * two strings from those text boxes.
   * 
   * Note the word "representation" - this function does not necessarily
   * return the widget's actual value, but might assemble a different
   * object, or array, or number, based on current user-entered values.
   * 
   * `read` will not typically be overridden.
   * {@link module:bajaux/Widget#doRead|doRead()} should be overridden instead.
   *
   * @see module:bajaux/Widget#doRead
   * 
   * @returns {Promise} A promise that will be resolved with a value read from
   * the widget as specified by `doRead`, or rejected if the read fails.
   */
  Widget.prototype.read = function read() {
    if (!this.isInitialized()) {
      return Promise.reject(new Error(notInitialized));
    }
    
    return Promise.try(() => this.doRead());
  };
  
  /**
   * Does the work of reading the widget's current representation.
   * 
   * This might mean reading a series of text inputs and assembling their
   * values into an array. It might mean instantiating a copy of the backing
   * `baja.Component` and setting slot values on the new copy.
   * It might mean simply returning the boolean value of a checkbox. If your
   * widget is composed of pure text/HTML and is not actually backed by an
   * external value, it might mean returning <i>nothing</i>.
   * 
   * When saving a modified widget, the output of this function will be passed
   * directly into this widget's validation process, so all your validation
   * steps should be expecting to receive this. It will also be passed to
   * `doSave`, so your `doSave` implementation should also expect this value.
   * 
   * The default behavior of `doRead` is simply to use the widget's current
   * value.
   *
   * @see module:bajaux/Widget#read
   * @returns {*|Promise} The read value, or a promise to be resolved with the
   * read value
   */
  Widget.prototype.doRead = function doRead() {
    return this.value();
  };

  /**
   * Return the widget's Validators.
   *
   * @see module:bajaux/Validators
   * 
   * @return {module:bajaux/Validators}
   */
  Widget.prototype.validators = function validators() {
    return this.$validators;
  };

  /**
   * Read the current value from the widget and validate it.
   *
   * @see module:bajaux/Validators#validate
   * 
   * @return {Promise} A promise to be resolved with the value read from the
   * widget and passed through all validators, or rejected if the value could
   * not be read or validated.
   */
  Widget.prototype.validate = function validate() {
    return this.read()
      .then((value) => {
        return this.validators().validate(value);
      });
  };

  /**
   * Returns this widget's readonly state.
   *
   * @see module:bajaux/Widget#setReadonly
   *
   * @returns {Boolean}
   */
  Widget.prototype.isReadonly = function isReadonly() {
    return this.$readonly;
  };
  
  // noinspection JSCommentMatchesSignature
  /**
   * Set this widget's readonly state.
   * 
   * Setting of the internal flag will be synchronous, so `isReadonly` will
   * return the expected value immediately after calling this function. However,
   * the actual work of updating the DOM cannot be performed until after the
   * widget has finished initializing, so this method will return a promise.
   * 
   * This method will not typically be overridden. `doReadonly()` should be
   * overridden instead.
   *
   * @see module:bajaux/Widget#isReadonly
   * 
   * @param {Boolean} readonly the new readonly state.
   * @returns {Promise} A promise to resolve immediately if `initialize` has
   * not yet been called, that will resolve once the work of `initialize`
   * followed by `doReadonly` have both been completed. It will reject if
   * `initialize` or `doReadonly` fail.
   */
  Widget.prototype.setReadonly = function setReadonly(readonly, ...rest) {
    readonly = !!readonly;

    // Set state change immediately! This is always set regardless of
    // any errors that may be thrown by any callbacks.
    this.$readonly = readonly;
    
    // Only trigger callbacks and events if the widget has been initialized.
    const prom = this.$initialized()
      .then(() => {
        const readonly = this.$readonly;
        this.jq().toggleClass(Widget.css.readonly, readonly);
        return Promise.try(() => this.doReadonly(readonly, ...rest))
          .then(() => {
            if (!this.$quiet) {
              this.trigger(readonly ? events.READONLY_EVENT : events.WRITABLE_EVENT);
            }
            return readonly;
          }, (err) => {
            err = err || new Error(unknownErr);
            this.trigger(readonly ? events.READONLY_FAIL_EVENT : events.WRITABLE_FAIL_EVENT, err);
            throw err;
          });
      });

    // If the widget isn't initialized yet then we don't know when it will be
    // resolved. Therefore, we need to resolve this right now.
    if (!this.isInitialized()) {
      return Promise.resolve(readonly);
    } else {
      return prom;
    }
  };
  
  /**
   * Called when the widget is set to readonly or made writable.
   *
   * @param {Boolean} readonly the new readonly state.
   * @returns {*|Promise} An optional Promise that can be returned if
   * the state change is asynchronous.
   */
  Widget.prototype.doReadonly = function doReadonly(readonly) {
    
  };

  /**
   * Returns an array of child widgets living inside this editor's DOM.
   * This method will specifically *not* return child widgets of child widgets
   * - for instance, if this widget has one child editor for a `baja.Facets`,
   * you will only get a single widget back - it won't recurse down and give
   * you all the tag editors, type editors etc.
   *
   * This is safer and easier than using `$.find()`, which recurses down,
   * or carefully managing strings of `$.children()` calls.
   *
   * Pass in a `jQuery` instance to limit the child editor search to
   * a particular set of elements. Otherwise, will search all child elements
   * of this editor's DOM.
   *
   * If this editor has not initialized yet, you'll just get an empty array
   * back.
   *
   * The returned array will have some utility functions attached that return
   * promises. See example for details.
   *
   * @param {Object|jQuery} [params]
   * @param {JQuery} [params.dom=this.jq().children()] the dom element to search
   * @param {Function} [params.type] the widget type to search for - pass in the
   * actual constructor, for `instanceof` checks
   * @returns {Array.<module:bajaux/Widget>} an array of child widgets
   * @example
   *
   *   var kids = ed.getChildWidgets();
   *   kids.setAllEnabled(false).then(function () {});
   *   kids.setAllModified(false).then(function () {});
   *   kids.setAllReadonly(false).then(function () {});
   *   kids.readAll().then(function (valuesArray) {});
   *   kids.validateAll().then(function (valuesArray) {});
   *   kids.saveAll().then(function () {});
   *   kids.destroyAll().then(function () {});
   *
   * @example
   *
   *   var stringEditors = ed.getChildWidgets({ type: StringEditor });
   *
   * @since Niagara 4.10
   */
  Widget.prototype.getChildWidgets = function (params) {
    params = !params ? {} : params instanceof $ ? { dom: params } : params;
    const dom = params.dom || (this.jq() && this.jq().children());
    return getChildWidgets(dom, params);
  };

  /**
   * Attempts to place the cursor focus on this editor. For instance, if
   * showing a simple string editor in a dialog, it should request focus so
   * that the user can simply begin typing without having to move the mouse
   * over to it and click.
   *
   * Override this as necessary; by default, will place focus on the first
   * `input` or `textarea` element in this editor's element.
   *
   * @since Niagara 4.10
   */
  Widget.prototype.requestFocus = function () {
    if (!this.isReadonly()) {
      $('input:not([readonly]):not(.nofocus), textarea:not([readonly]):not(.nofocus)', this.jq())
        .first().focus();
    }
  };

  function getChildWidgets(dom, params) {
    const { type, filter } = params;

    //this looks overcomplicated but it's actually a fast way of doing it,
    //and it's tough to do safely with pure jQuery selectors.
    function edsOf(dom) {
      return dom.map(function () {
        const $dom = $(this);
        const typeofFilter = typeof filter;
        const widget = (typeofFilter !== 'string' || $dom.is(filter)) && Widget.in($dom);
        if (widget) {
          if (type) {
            return widget instanceof type ? widget : [];
          }

          if (typeofFilter === 'function') {
            return filter(widget) ? widget : [];
          }

          return widget;
        } else {
          return edsOf($dom.children());
        }
      }).get();
    }

    const eds = dom ? edsOf(dom) : [];

    addUtilityFunctions(eds);

    return eds;
  }

  function callOnAll(arr, functionName, args) {
    return Promise.all(arr.map(function (ed) {
      return ed[functionName].apply(ed, args);
    }));
  }

  function mapWhen(fName) {
    return function () {
      const args = Array.prototype.slice.call(arguments);
      return callOnAll(this, fName, args);
    };
  }

  const utilityFunctions = {
    readAll: mapWhen('read'),
    saveAll: mapWhen('save'),
    validateAll: mapWhen('validate'),
    setAllEnabled: mapWhen('setEnabled'),
    setAllModified: mapWhen('setModified'),
    setAllReadonly: mapWhen('setReadonly'),
    destroyAll: mapWhen('destroy'),
    layoutAll: mapWhen('layout')
  };

  function addUtilityFunctions(editorsArray) {
    Object.keys(utilityFunctions).forEach(function (key) {
      editorsArray[key] = utilityFunctions[key];
    });
  }

  function getRootCssClass(widget) {
    const props = widget.properties();
    return props.getValue('rootCssClass') || props.getValue(ROOT_CSS_CLASS_SYMBOL) || '';
  }

  function getEnabledFromParams(params) {
    const { properties } =  params,
      enabledProperty = properties instanceof Properties ? properties.getValue("enabled") : null;
    if (enabledProperty !== null) {
      return enabledProperty !== false;
    } else if (properties && properties.enabled !== undefined) {
      return properties.enabled !== false;
    }
    return params.enabled !== false;
  }

  function processParams(paramsObj) {
    if (!paramsObj) { return {}; }
    const objects = [];
    while (paramsObj) {
      let { params, defaults } = paramsObj;
      if (defaults) {
        replaceDefaultRootCssClass(defaults.properties);
      }
      if (paramsObj) { objects.push(paramsObj); }
      if (defaults) { objects.push(defaults); }
      paramsObj = params;
    }

    return Widget.$extendParams(...objects);
  }

  /**
   * If rootCssClass is specified using `defaults`, replace it with a Symbol to make it invisible
   * to the Px Editor.
   * @param {object} properties default properties object
   */
  function replaceDefaultRootCssClass(properties) {
    if (!properties) { return; }
    const { rootCssClass } = properties;
    if (rootCssClass) {
      delete properties.rootCssClass;
      properties[ROOT_CSS_CLASS_SYMBOL] = rootCssClass;
    }
  }

  return Widget;
});