fe/baja/BaseEditor.js

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

/**
 * @module nmodule/webEditors/rc/fe/baja/BaseEditor
 */
define([
  'baja!',
  'log!nmodule.webEditors.rc.fe.baja.BaseEditor',
  'bajaux/mixin/mixinUtils',
  'jquery',
  'Promise',
  'underscore',
  'nmodule/webEditors/rc/fe/BaseWidget',
  'nmodule/webEditors/rc/fe/baja/util/compUtils',
  'nmodule/webEditors/rc/fe/baja/util/facetsUtils',
  'nmodule/webEditors/rc/fe/baja/util/typeUtils',

  'css!nmodule/webEditors/rc/fe/webEditors-structure' ], function (
  baja,
  log,
   mixinUtils,
  $,
  Promise,
  _,
  BaseWidget,
  compUtils,
  facetsUtils,
  typeUtils) {

  'use strict';

  const TYPE_CLASS_PREFIX = 'type-';
  const TYPE_CLASS_REGEX = /^type-/;
  const MIXIN_NAME = 'typeClasses';
  const { getMergedSlotFacets } = compUtils;
  const { applyFacets } = facetsUtils;
  const { applyMixin, hasMixin } = mixinUtils;
  const { getSuperTypeChain } = typeUtils;
  const logWarning = log.warning.bind(log);



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

  function isTypeClass(cls) {
    return !!cls.match(TYPE_CLASS_REGEX);
  }

  function removeAllTypeClasses(dom) {
    if (!dom) { return; }
    dom.removeClass(function (i, classList) {
      var allClasses = classList.split(' '),
        typeClasses = _.filter(allClasses, isTypeClass);
      return typeClasses.join(' ');
    });
  }

////////////////////////////////////////////////////////////////
// BaseEditor definition
////////////////////////////////////////////////////////////////

  /**
   * Base class for all `webEditors` editors for Baja values. This editor
   * incorporates all the `Widget` sugar from `BaseWidget` and adds more
   * Baja-specific features on top. Most Niagara field editors should extend
   * from this class.
   *
   * @class
   * @extends module:nmodule/webEditors/rc/fe/BaseWidget
   * @alias module:nmodule/webEditors/rc/fe/baja/BaseEditor
   * @param {Object} [params] Same parameters as
   * {@link module:nmodule/webEditors/rc/fe/BaseWidget BaseWidget}
   * @param {baja.Facets|Object} [params.facets] (Deprecated - use properties)
   * facets to use to customize this editor instance. These facets will
   * typically come from the slot facets of a Baja value being loaded into this
   * BaseEditor. They will be converted into transient bajaux `Properties` and
   * will override any other specified `Properties` with the same name. This can
   * be either a `Facets` instance or an object literal.
   */
  var BaseEditor = function BaseEditor(params) {
    params = params || {};
    var that = this,
      data = params.data;
    BaseWidget.apply(that, arguments);

    that.$base = data && data.ordBase;
    if (params.facets) {
      logWarning(new Error('passing facets param to BaseEditor constructor is' +
        ' deprecated - use properties instead'));
      that.setFacets(params.facets);
    }
  };
  BaseEditor.prototype = Object.create(BaseWidget.prototype);
  BaseEditor.prototype.constructor = BaseEditor;

  //copy across just in case
  BaseEditor.VALUE_READY_EVENT = BaseWidget.VALUE_READY_EVENT;

  BaseEditor.$MIXIN_NAME = MIXIN_NAME;

  /**
   * Convert a BajaScript Type to a corresponding CSS class.
   *
   * @param {String|Type} type spec
   * @returns {String}
   * @example
   *   expect(BaseEditor.typeToClass('baja:String')).toBe('type-baja-String');
   */
  BaseEditor.typeToClass = function (type) {
    return TYPE_CLASS_PREFIX + String(type).replace(':', '-');
  };

  /**
   * Convert the given Facets into hidden, transient `bajaux Properties` and
   * apply them to this editor. In most cases you'll want to use
   * `properties().setValue()` directly, but this method is useful when
   * applying `Complex` slot facets.
   *
   * @param {baja.Facets|Object} facets (a `baja.Facets` instance or an object
   * literal to be converted to `baja.Facets`)
   */
  BaseEditor.prototype.setFacets = function (facets) {
    applyFacets(facets, this);
  };

  function getTypeClasses(type) {
    return _.map(getSuperTypeChain(type), BaseEditor.typeToClass);
  }

  /**
   * Every `BaseEditor` will apply a number of CSS classes to a DOM element
   * when a value is loaded into it:
   *
   * - `editor`
   * - If the loaded value is a Baja value, a number of CSS classes
   *   corresponding to the value's Type and all superTypes. Classes will be
   *   determined using {@link module:nmodule/webEditors/rc/fe/baja/BaseEditor.typeToClass|typeToClass()}.
   *
   * It will also emit a `loaded` tinyevent.
   *
   * @param {baja.Value|*} value
   * @param {Object} [params]
   * @returns {Promise} call to {@link module:bajaux/Widget#load}
   */
  BaseEditor.prototype.load = function (value, params) {
    var that = this;

    return BaseWidget.prototype.load.apply(that, arguments)
      .then(function (result) {
        var dom = that.jq();

        //TODO: switchboard notWhile
        if (!dom) {
          throw new Error('already destroyed');
        }

        that.$updateTypeClasses(value, dom);

        return result;
      });
  };

  /**
   * @private
   * @param {*} value
   * @param {JQuery} dom
   */
  BaseEditor.prototype.$updateTypeClasses = function (value, dom) {
    removeAllTypeClasses(dom);
    if (baja.hasType(value)) {
      dom.addClass(getTypeClasses(value.getType()).join(' '));
    }
  };

  /**
   * Removes all classes added during a call to {@link #load}. Emits a
   * `destroyed` tinyevent.
   *
   * @returns {Promise} call to {@link module:bajaux/Widget#destroy}
   */
  BaseEditor.prototype.destroy = function () {
    const jq = this.jq();
    return BaseWidget.prototype.destroy.apply(this, arguments)
      .then(() => {
        removeAllTypeClasses(jq);
      });
  };

  /**
   * Applies the legacy behavior of applying the "type-moduleName-TypeName" CSS classes when a
   * value is loaded.
   * @private
   * @param {module:bajaux/Widget} widget
   */
  BaseEditor.$mixinTypeClasses = function (widget) {
    if (widget instanceof BaseEditor || hasMixin(widget, MIXIN_NAME)) {
      return;
    }

    applyMixin(widget, MIXIN_NAME);

    const { load, destroy } = widget;
    widget.$updateTypeClasses = widget.$updateTypeClasses || BaseEditor.prototype.$updateTypeClasses;
    widget.load = function (value) {
      return load.apply(this, arguments)
        .then((result) => {
          const dom = this.jq();

          //TODO: switchboard notWhile
          if (!dom) {
            throw new Error('already destroyed');
          }

          this.$updateTypeClasses(value, dom);
          dom.addClass('editor');

          return result;
        });
    };

    widget.destroy = function () {
      const dom = this.jq();
      return destroy.apply(this, arguments)
        .then(() => {
          if (dom) {
            dom.removeClass('editor');
            removeAllTypeClasses(dom);
          }
        });
    };
  };

  /**
   * There are two main cases for using an editor.
   *
   * In one case, you are editing a standalone value. For example, you are
   * prompting the user for a `baja:String`, and the read value will simply be
   * passed into another function for handling - nothing will necessarily be
   * saved up to the station.
   *
   * In the second case, you are editing a slot on a Complex. In this case,
   * when the editor is saved, you want the saved value to be written back to
   * a particular slot on the Complex.
   *
   * This function indicates which case is currently in play. When an editor
   * is a complex slot editor, the load/read semantics are the same, but saving
   * the editor should cause the read value to be written back to the edited
   * Slot.
   *
   * To create a complex slot editor, see `ComplexSlotEditor.make`.
   *
   * @private
   * @returns {boolean}
   * @see module:nmodule/webEditors/rc/fe/baja/ComplexSlotEditor
   */
  BaseEditor.prototype.isComplexSlotEditor = function () {
    return false;
  };

  /**
   * If this editor is a complex slot editor, then when it is saved, the read
   * value will be written back to this Complex.
   *
   * This returns `undefined` by default. It should return a Complex if and only
   * if it is a complex slot editor (`isComplexSlotEditor()` returns true).
   *
   * @private
   * @returns {undefined|baja.Complex} the Complex that saved changes will
   * be written back to
   */
  BaseEditor.prototype.getComplex = function () {
    return undefined;
  };

  /**
   * If this editor is a complex slot editor, then when it is saved, the read
   * value will be written to the backing Complex at this slot.
   *
   * This returns `undefined` by default. It should return a `baja.Slot` if and
   * only if it is a complex slot editor (`isComplexSlotEditor()` returns
   * true).
   *
   * @private
   * @returns {undefined|baja.Slot} the slot on the backing Complex to write
   * saved changes to
   */
  BaseEditor.prototype.getSlot = function () {
    return undefined;
  };

  //TODO: this is exactly the kind of TODO that was called for on makeChildFor.
  //come back and replace ordBase shuffling.
  /**
   * Sometimes, an editor may need to resolve ORDs in order to operate; for
   * instance, resolving the RoleService for a list of roles, the SearchService
   * to perform a search, etc. ORD resolution when BajaScript is in offline mode
   * will often require a mounted base component to resolve the ORD against.
   *
   * This base object may be given as an `ordBase` bajaux `Property`.
   *
   * See: `spaceUtils.resolveService`, etc.
   *
   * @private
   * @returns {Promise.<baja.Component|undefined>} promise to be resolved with a
   * Component to use as a base when resolving ORDs, or `undefined` if not
   * present
   */
  BaseEditor.prototype.getOrdBase = function () {
    return Promise.resolve(
      this.properties().getValue('ordBase') || this.getComplex());
  };

  /**
   * When this editor is a complex slot editor, the facets for that slot will
   * be translated to hidden properties on this editor. By default, the facets
   * will be retrieved directly from the slot (`getSlot()`) on the complex
   * (`getComplex()`). By overriding this function, you can define the way the
   * slot facets are retrieved.
   *
   * Why would you want to override this function? In the Niagara Framework,
   * the `getSlotFacets` method is very often overridden on `BComplex`
   * subclasses, most commonly to pull down facets from a parent component.
   * BajaScript does not necessarily know about those overridden
   * slot facets. By overriding this function instead, you can bring that
   * functionality into your field editors even if it does not yet exist in
   * BajaScript.
   *
   * This should not typically be called directly (hence the `$`) but will be
   * called automatically by the framework when editing a slot on a complex.
   *
   * As of 4.3, this was marked private. BajaScript Type Extensions allow for
   * the overriding of getSlotFacets() in the browser, rendering this function
   * mostly unnecessary.
   *
   * @private
   * @returns {Promise} promise to be resolved with the facets for the
   * configured Complex and Slot, or `baja.Facets.DEFAULT` if not present
   */
  BaseEditor.prototype.$getSlotFacets = function () {
    var complex = this.getComplex(),
      slot = this.getSlot();

    return Promise.resolve((complex && slot && getMergedSlotFacets(complex, slot)) ||
      baja.Facets.DEFAULT);
  };

  /**
   * Currently, only `ComplexSlotEditor` will use this function. It functions
   * as an override point; there is no real reason to call it directly.
   * PropertySheet saves have always called this method.
   * Starting in Niagara 4.14, `Manager` based Dialogs started calling this method.
   * Note that the New and Add Commands on a Manager will call this method, but with an unmounted
   * complex. You can detect this scenario by checking if `this.getComplex().isMounted()` is false.
   * Any behavior in this function that is needed for a mounted Complex should
   * be replicated in your subclass of MgrModel#addInstances.
   * @see module:nmodule/webEditors/rc/wb/mgr/model/MgrModel#addInstances
   *
   * In certain rare use cases, you may want finer control over how the
   * value is saved back to the `Complex`: changing slot flags, for instance,
   * or delegating to a separate service. In this case, override this function
   * to perform the work you need, then return a truthy value to indicate
   * that the work was done.
   *
   * @private
   * @param {baja.Value|module:nmodule/webEditors/rc/fe/baja/util/ComplexDiff} readValue
   * the Baja value to save to `this.getSlot()` on `this.getComplex()`
   * @param {Object} [params] same parameters that may be passed to `save()`
   * @param {baja.comm.Batch} [params.batch] optional batch to use
   * @param {function} [params.progressCallback] if present, `saveToComplex` implementations
   * *must* call it with `batchSaveMixin.COMMIT_READY` after adding any network calls to
   * `params.batch`.
   * @returns {undefined|Promise|*} undefined by default, to indicate
   * that no work was done. When overriding, return a truthy value to
   * short-circuit default `ComplexSlotEditor` save behavior.
   */
  BaseEditor.prototype.saveToComplex = function (readValue, params) {
    return undefined;
  };

  /**
   * Same as `getChildWidgets`, but is limited to instances of `BaseEditor`.
   * @deprecated use `getChildWidgets` instead.
   * @param {Object} [params]
   */
  BaseEditor.prototype.getChildEditors = function (params) {
    params = !params ? {} : params instanceof $ ? { dom: params } : params;
    return this.getChildWidgets(_.extend({
      filter: (ed) => ed instanceof BaseEditor || isHonoraryBaseEditor(ed)
    }, params));
  };

  /**
   * NCCB-68350: this goes away
   * @param {module:bajaux/Widget} ed
   * @returns {boolean}
   */
  function isHonoraryBaseEditor(ed) {
    return ed && ed.$isHonoraryBaseEditor;
  }

  // copy over for API compatibility
  BaseEditor.SHOULD_VALIDATE = BaseWidget.SHOULD_VALIDATE;
  BaseEditor.VALIDATE_ON_SAVE = BaseWidget.VALIDATE_ON_SAVE;
  BaseEditor.VALIDATE_ON_READ = BaseWidget.VALIDATE_ON_READ;

  return (BaseEditor);
});