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',
  '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,
   $,
   Promise,
   _,
   BaseWidget,
   compUtils,
   facetsUtils,
   typeUtils) {

  'use strict';

  const TYPE_CLASS_PREFIX = 'type-';
  const TYPE_CLASS_REGEX = /^type-/;
  const { getMergedSlotFacets } = compUtils;
  const { applyFacets } = facetsUtils;
  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;

  /**
   * 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');
        }

        removeAllTypeClasses(dom);
        if (baja.hasType(value)) {
          dom.addClass(getTypeClasses(value.getType()).join(' '));
        }

        return result;
      });
  };

  /**
   * 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 () {
    removeAllTypeClasses(this.jq());
    return BaseWidget.prototype.destroy.apply(this, arguments);
  };

  /**
   * 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.
   * 'Manager' based Dialogs do not call this method, only PropertySheet saves;
   * any behavior in this function needs to be replicated within the Manager.
   *
   * 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
   * @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({ type: BaseEditor }, params));
  };

  // 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);
});