fe/BaseWidget.js

/**
 * @copyright 2016 Tridium, Inc. All Rights Reserved.
 * @author Logan Byam
 */

/**
 * @module nmodule/webEditors/rc/fe/BaseWidget
 */
define([ 'bajaux/Properties',
        'bajaux/Widget',
        'jquery',
        'Promise',
        'underscore',
        'nmodule/js/rc/asyncUtils/asyncUtils',
        'nmodule/js/rc/switchboard/switchboard' ], function (
         Properties,
         Widget,
         $,
         Promise,
         _,
         asyncUtils,
         switchboard) {

  'use strict';

  var doRequire = asyncUtils.doRequire,
      SHOULD_VALIDATE = 'shouldValidate';

////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////

  var requireFe = _.once(function () {
    return doRequire('nmodule/webEditors/rc/fe/fe');
  });

  const widgetDefaults = () => ({
    moduleName: 'webEditors'
  });


////////////////////////////////////////////////////////////////
// BaseWidget
////////////////////////////////////////////////////////////////

  /**
   * Base class for all `webEditors` widgets. This widget includes lots of
   * sugar for the `Widget` constructor, utilities for managing nested
   * collections of `Widget`s, etc.
   *
   * Note that this widget has no idea what BajaScript is; for more
   * Baja-specific editor functionality, reach for `BaseEditor`.
   *
   * @class
   * @extends module:bajaux/Widget
   * @alias module:nmodule/webEditors/rc/fe/BaseWidget
   * @mixes tinyevents
   * @param {Object} [params]
   * @param {module:bajaux/Properties|Object} [params.properties] properties to
   * add to this editor's underlying `bajaux/Properties` instance. This can be
   * either a `Properties` instance or an object literal.
   * @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='webEditors'] 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`.
   */
  var BaseWidget = function BaseWidget(params) {
    Widget.call(this, { params, defaults: widgetDefaults() });

    //TODO: destroy
    //multiple calls to load() all take effect, but will pile up
    switchboard(this, {
      load: { allow: 'oneAtATime', onRepeat: 'queue' }
    });
  };
  BaseWidget.prototype = Object.create(Widget.prototype);
  BaseWidget.prototype.constructor = BaseWidget;

  /**
   * `VALUE_READY_EVENT` is a completely optional event. You may choose to
   * trigger this event when the user has taken some action to indicate that
   * the given value is the desired one, *without* actually saving the editor.
   *
   * Why would you want to trigger this event? Currently the best reason would
   * be if your editor is shown in a dialog, and it has some kind of selection
   * mechanism (dropdown, radio buttons, list etc.). This would allow the user
   * to simply select a value and allow the dialog to close, without actually
   * making the user click OK to save.
   *
   * For instance, `feDialogs` listens for `VALUE_READY_EVENT` when showing an
   * editor in a dialog. When the event is emitted, the dialog will
   * automatically be saved and closed.
   *
   * To illustrate, an OrdChooser in a dialog will show a station nav tree. When
   * the user double-clicks a file, `VALUE_READY_EVENT` will be emitted.
   * Therefore all the user must do is double-click the file to select it.
   * Without the event, the user would have to click to select the file, then
   * manually go down and click OK to commit the changes.
   *
   * You may also respond to this event when fired directly on the editor
   * itself. Why would you do this? Certain framework modules may invert this
   * pattern, that is, the framework may notify the editor itself that the user
   * has chosen a value he or she is satisfied with, without saving the editor.
   * An example of this is, again, `feDialogs`: when the user clicks OK, the
   * current value will be read and used elsewhere, although the editor may not
   * itself be saved.
   *
   * The two uses refer to the same use case (the user likes this value even
   * without wishing to commit it to the station yet); they just go in two
   * different directions: one is the editor notifying the framework, the other
   * is the framework notifying the editor. Be careful not to re-trigger one in
   * a handler for the other, or you may get an infinite loop!
   *
   * @see module:webEditors/rc/fe/feDialogs
   * @type {string}
   * @example
   *   dom.on('dblclick', '.listItem', function (e) {
   *     var value = $(this).text();
   *     dom.trigger(BaseWidget.VALUE_READY_EVENT, [ value ]);
   *   });
   *
   * @example
   * function MyEditor() {
   *   ...
   *   var that = this;
   *   that.on(VALUE_READY_EVENT, function (newValue) {
   *     console.log('The user has chosen to commit this value: ' + newValue);
   *     //...even though it may not be written to the station yet.
   *     return that.saveToMostRecentlyUsedValues(newValue);
   *   });
   * }
   */
  BaseWidget.VALUE_READY_EVENT = 'BaseWidget:valueReady';

  /**
   * Every `BaseWidget` will add the `editor` class to the element and emit an
   * `initialized` tinyevent when initialized.
   *
   * @param {JQuery} dom
   * @returns {Promise} call to {@link module:bajaux/Widget#initialize}
   */
  BaseWidget.prototype.initialize = function (dom) {
    dom.addClass('editor');
    return Widget.prototype.initialize.apply(this, arguments);
  };


  /**
   * Removes the `editor` class and emits a `destroyed` tinyevent.
   *
   * @returns {Promise} call to {@link module:bajaux/Widget#destroy}
   */
  BaseWidget.prototype.destroy = function () {
    const jq = this.jq();
    //TODO: select for bajaux-initialized kids and log warning if found
    return Widget.prototype.destroy.apply(this, arguments)
      .then(() => jq && jq.removeClass('editor'));
  };

  BaseWidget.prototype.makeChildFor = function (params) {
    var that = this;
    //avoid circular dependency
    return requireFe()
      .then(function (fe) {
        return fe.makeFor(_.extend({
          enabled: that.isEnabled(),
          readonly: that.isReadonly()
        }, params));
      });
  };

  //TODO: revisit buildChildFor:
  //assert child dom is child of my dom?
  //listen for modified events - set self modified if child modified?
  /**
   * Builds a child editor to belong to this parent editor. Currently, this is
   * only an easy way to have a child editor share its parent's enabled and
   * readonly status. Consider this private API until better defined.
   *
   * @private
   * @param params
   * @returns {Promise} promise to be resolved with a child editor
   * instance
   * @see module:nmodule/webEditors/rc/fe/fe
   */
  BaseWidget.prototype.buildChildFor = function (params) {
    var that = this;

    return that.makeChildFor(params)
      .then(function (kid) {
        return requireFe()
          .then(function (fe) {
            return fe.buildFor(params, kid);
          });
      });
  };

  /**
   * String `Property` name used by `shouldValidate()`.
   * @type {string}
   */
  BaseWidget.SHOULD_VALIDATE = SHOULD_VALIDATE;

  /**
   * The `shouldValidate` `Property` should match this value if this editor
   * should always validate on save.
   * @type {number}
   */
  BaseWidget.VALIDATE_ON_SAVE = 1;

  /**
   * The `shouldValidate` `Property` should match this value if this editor
   * should always validate on read.
   * @type {number}
   */
  BaseWidget.VALIDATE_ON_READ = 2;

  /**
   * This provides an extra hook for an editor to declare itself as needing to
   * be validated before saving or not. The default behavior is to return true
   * if this editor is modified, or if a `shouldValidate` `bajaux` `Property`
   * is present and truthy. If neither of these conditions is true, it will
   * check all known child editors, and return true if it has a child editor
   * that should validate.
   *
   * If `flag` is given, then the check against the `shouldValidate`
   * `Property` will return true _only_ if the value bitwise matches the
   * parameter. See `BaseWidget.SHOULD_VALIDATE_ON_SAVE`, etc.
   *
   * @param {Number} [flag]
   *
   * @returns {Boolean}
   */
  BaseWidget.prototype.shouldValidate = function (flag) {
    var prop = this.properties().getValue(SHOULD_VALIDATE);

    if (prop !== null) {
      if (typeof prop === 'boolean') {
        return prop;
      }
      return flag ? !!(flag & prop) : true;
    }

    if (this.isModified()) {
      return true;
    }

    return !!_.find(this.getChildWidgets({ type: BaseWidget }), function (kid) {
      return kid.shouldValidate(flag);
    });
  };

  return BaseWidget;
});