fe/fe.js

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

/*
 * dev note: be super careful about adding dependencies to other editor types
 * here. other editors may depend on this module and it becomes circular
 * dependency city.
 */
define([ 'baja!',
        'log!nmodule.webEditors.rc.fe.fe',
        'Promise',
        'underscore',
        'bajaux/Properties',
        'bajaux/Widget',
        'nmodule/webEditors/rc/fe/registry/StationRegistry',
        'nmodule/webEditors/rc/fe/feUtils',
        'nmodule/webEditors/rc/fe/registry/NiagaraWidgetManager' ], function defineFE(
         baja,
         log,
         Promise,
         _,
         Properties,
         Widget,
         StationRegistry,
         feUtils,
         NiagaraWidgetManager) {

  'use strict';



  /**
   * Functions for registering, looking up, and instantiating editors for
   * certain Baja types.
   *
   * @exports nmodule/webEditors/rc/fe/fe
   */
  const fe = {};

  const deriveSpecifiedConstructor = feUtils.deriveSpecifiedConstructor,
      formFactorToInterface = feUtils.formFactorToInterface,
      toRegistryQuery = feUtils.toRegistryQuery;

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

  const mgr = new NiagaraWidgetManager({
    registry: StationRegistry.getInstance()
  });


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

  function formFactorsToInterfaces(formFactors) {
    return _.map(formFactors || [], formFactorToInterface);
  }

  function getReg() {
    return StationRegistry.getInstance();
  }

////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////

  /**
   * @private
   * @typedef {Object} FeSpecificParams
   * @property {baja.Complex} [complex] if given (with `slot` as well), will
   * instantiate a complex editor that will save changes back to the `Complex`
   * instance.
   * @property {baja.Slot|String} [slot] if given (with `complex` as well), will
   * instantiate an editor for the value at this slot.
   * @property {Object|module:bajaux/Properties} [properties] the bajaux
   * Properties the new widget should have. If the Property `uxFieldEditor` is
   * present, its string value will be used to determine the type of widget to
   * create. This can be a type spec that resolves to a `web:IJavaScript` type,
   * or a RequireJS module ID.
   * @property {Object|baja.Facets} [facets] (Deprecated: use `properties`) additional Facets information to
   * apply to the new widget. Facets will be added as hidden transient
   * bajaux Properties. (If a Property with the same key is also given, it will
   * be overridden by the facet.)
   * @property {Object} [data] an object literal containing any additional
   * values to be passed to the new widget's constructor
   */

  /**
   * This type describes the available parameters to be passed to the various
   * methods on `fe`. These values will be used both to look up the type of the
   * desired editor, and also to construct that editor. In other words, the
   * data to look up a widget will also be used in the same turn to construct
   * that widget. See {@link module:bajaux/Widget}
   *
   * @typedef {module:bajaux/lifecycle/WidgetManager~BuildParams | FeSpecificParams} module:nmodule/webEditors/rc/fe/fe~FeParams
   */

  /**
   * @private
   * @returns {module:nmodule/webEditors/rc/fe/registry/NiagaraWidgetManager} the
   * `WidgetManager` used by `fe` to perform widget lookups
   */
  fe.getWidgetManager = function () {
    return mgr;
  };

  /**
   * This function takes an object literal (passed for `makeFor` or `buildFor`)
   * and performs some processing on it, adding some information needed to
   * build editors.
   *
   * It will take values that Workbench "assumes to be present" in the Context
   * (`timeFormat` and `unitConversion` Facets, which get injected via General
   * Options) and inject them into the `facets` property. This way, editors
   * instantiated via `makeFor` and `buildFor` will always have access to these.
   *
   * Contractual note: the input object should be compatible with the
   * `Widget` constructor; the output object will be as well. That is, if
   * your input object can be used to build a `Widget`, the output object
   * can as well.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/fe/fe~FeParams} params
   * @returns {Promise} promise to be resolved with an object with extra
   * `fe`-related information
   */
  fe.params = function (params) {
    if (params === undefined || params === null) {
      return Promise.reject(new Error('value required'));
    }

    if (typeof params !== 'object') {
      params = { value: params };
    }

    if (params.facets) {
      logWarning(new Error('passing facets param to fe is deprecated ' +
        '- use properties instead'));
    }

    return mgr.buildContext(params)
      .then((cx) => {
        const { value } = cx;
        return _.extend({}, params, cx.constructorParams, {
          getValueToLoad: () => {
            if (typeof value === 'undefined') {
              throw new Error('could not determine value to load');
            }
            return value;
          },
          getWidgetConstructor: () => cx.widgetConstructor || null
        });
      });
  };


  /**
   * Registers a RequireJS module to a baja Type. This takes a RequireJS
   * module ID string which resolves to a module exporting a constructor for a
   * {@link module:bajaux/Widget|Widget} subclass.
   *
   * @param {Type|String} type
   * @param {String} module RequireJS module ID
   * @param {Object} [params]
   * @param {Array.<String>} [params.formFactors] form factors that this editor
   * should support
   * @returns {Promise} promise to be resolved after the module
   * registration is complete. Note that `getDefaultConstructor()`, `makeFor()`,
   * etc. will still work (with some possible extra network calls) before the
   * promise is fully resolved. Promise will be rejected if RequireJS is unable
   * to resolve a given module ID.
   *
   * @example
   *   <caption>Register StringEditor on baja:String, so that it can be used to
   *   build "mini" editors for Strings.</caption>
   *   fe.register('baja:String', 'nmodule/webEditors/rc/fe/baja/StringEditor', {
   *     formFactors: [ Widget.formfactor.mini ]
   *   });
   */
  fe.register = function register(type, module, params) {
    if (typeof module !== 'string') {
      throw new Error('module required');
    }

    var interfaces = formFactorsToInterfaces(params && params.formFactors);

    if (interfaces.indexOf('web:IFormFactor') < 0) {
      interfaces.push('web:IFormFactor');
    }

    return getReg().register(type, {
      rjs: module,
      tags: interfaces
    });
  };

  /**
   * Retrieve the `Widget` constructor function registered for the given Type.
   *
   * @param {String|Type} type
   * @param {Object} [params]
   * @param {Array.<String|Number>} [params.formFactors] describes the form
   * factors that the resolved constructor is required to support. These can
   * be Strings referencing a form factor property on
   * `bajaux/Widget.formfactor`, or the value itself. If a constructor matches
   * *any* of these form factors it will be returned (union, not intersection).
   * @param {Object} [params.properties] pass the widget properties that can help
   * determine 'a' Widget constructor. For example, 'uxFieldEditor'.
   * @returns {Promise.<Function>} a promise to be resolved with the constructor
   * function for the given Type, or with `undefined` if no constructor is
   * registered. Note that if an invalid RequireJS module ID was passed to
   * `fe.register()`, it will still look up the supertype chain in an attempt
   * to resolve *something*.
   *
   * @example
   *   function StringEditor() {} //extends Widget
   *   fe.register('baja:String', StringEditor, {
   *     formFactors: [ Widget.formfactor.mini ]
   *   });
   *
   *   //resolves StringEditor
   *   fe.getDefaultConstructor('baja:String', { formFactors: [ 'mini' ] });
   *
   *   //resolves undefined
   *   fe.getDefaultConstructor('baja:String', { formFactors: [ 'compact' ] });
   *
   * @example
   *   // Will return myModule's SpecialNumericEditor instead of the default webEditors:NumericEditor
   *   fe.getDefaultConstructor('baja:Double', { properties: { uxFieldEditor: 'myModule:SpecialNumericEditor' } });
   */
  fe.getDefaultConstructor = function getDefaultConstructor(type, params) {
    return deriveSpecifiedConstructor(params)
      .then(function (Ed) {
        return Ed || getReg().resolveFirst(type, toRegistryQuery(params));
      });
  };

  /**
   * Retrieve all available widget constructors for the given type.
   *
   * @param {String|Type} type
   * @param {Object} [params]
   * @param {Array.<String>} [params.formFactors]
   * @returns {Promise.<Array.<Function>>} promise to be resolved with an array
   * of constructor functions.
   */
  fe.getConstructors = function getConstructors(type, params) {
    return deriveSpecifiedConstructor(params)
      .then(function (Ed) {
        return Ed ? [ Ed ] : getReg().resolveAll(type, toRegistryQuery(params));
      });
  };


  //TODO: pass in validator functions
  //TODO: register JS validators on BIValidator?
  /**
   * Instantiate a new editor for a value of a particular Type.
   *
   * Note that you will receive a constructed instance of the editor, but
   * it is uninitialized - calling `instantiate()` and `load()` is still your
   * job. (See `buildFor`.)
   *
   * @param {module:nmodule/webEditors/rc/fe/fe~FeParams} params
   *
   * @returns {Promise.<module:bajaux/Widget>} promise to be resolved with an
   * editor instance, or rejected if invalid parameters are given.
   *
   * @example
   *   <caption>Instantiate an editor for a baja value. Note that the workflow
   *   below is easily simplified by using fe.buildFor() instead.</caption>
   *
   *   var myString = 'my string';
   *   fe.makeFor({
   *     value: myString
   *     properties: { multiLine: true }
   *   }).then(function (editor) {
   *     return editor.initialize($('#myStringEditorDiv'))
   *      .then(function () {
   *        return editor.load(myString);
   *      });
   *   });
   */
  fe.makeFor = function makeFor(params) {
    return mgr.makeFor(params);
  };


  /**
   * Instantiates an editor as in `makeFor`, but with the added steps of
   * initializing and loading the editor. When the promise resolves, the
   * editor will be initialized within the DOM, and the passed value will have
   * been loaded into the editor.
   *
   * @param {module:nmodule/webEditors/rc/fe/fe~FeParams} params
   * @param {Object} [params.initializeParams] any additional parameters to be
   * passed to the editor's `initialize` method
   * @param {Object} [params.loadParams] any additional parameters to be
   * passed to the editor's `load` method
   * @param {module:bajaux/Widget} [ed] optionally,
   * pass in an editor instance to just initialize and load that, skipping the
   * `makeFor` step
   * @returns {Promise.<module:bajaux/Widget>} promise to be resolved with the
   * instance of the editor (fully initialized and loaded), or rejected if
   * invalid parameters are given (including missing `dom` parameter).
   *
   * @example
   *   <caption>Build a raw editor for a String</caption>
   *   fe.buildFor({
   *     value: 'my string',
   *     properties: { multiLine: true },
   *     dom: $('#myStringEditorDiv')
   *   }).then(function (editor) {
   *     //editor is now fully initialized and loaded
   *   });
   *
   * @example
   *   <caption>Build an editor for a slot on a component</caption>
   *   var myComponent = baja.$('baja:Component', { mySlot: 'hello world' });
   *
   *   fe.buildFor({
   *     complex: myComponent,
   *     slot: 'mySlot',
   *     properties: { multiLine: true },
   *     dom: ${'#myStringSlotEditorDiv')
   *   }).then(function (editor) {
   *     //editor is now fully initialized and loaded
   *
   *     $('#saveButton').click(function () {
   *       editor.save().then(function () {
   *         alert('your changes are applied to the component');
   *       });
   *     });
   *   });
   *
   * @example
   *   <caption>Build a StringEditor even though you plan to load a different
   *   kind of value into it.</caption>
   *   fe.buildFor({
   *     type: StringEditor,
   *     value: 5,
   *     dom: $('#myStringEditorDiv')
   *   }).then(function (stringEditor) {
   *     //StringEditor better be able to load the value you specified,
   *     //or this will reject instead.
   *   });
   *
   * @example
   *   <caption>Example showing the effects of the uxFieldEditor facet
   *   (BFacets.UX_FIELD_EDITOR). By setting this slot facet to the type spec
   *   of a `BIJavaScript` implementation, you can force the usage of a
   *   particular field editor instead of relying on the agent registration.
   *   </caption>
   *   fe.buildFor({
   *     value: 5,
   *     dom: $('#myStringEditorDiv'),
   *     properties: { uxFieldEditor: 'webEditors:StringEditor' }
   *   }).then(function (stringEditor) {
   *     //uxFieldEditor facet enforced usage of StringEditor instead of the
   *     //default NumericEditor
   *   });
   */
  fe.buildFor = function buildFor(params, ed) {
    return mgr.buildFor(params, ed);
  };

  return fe;
});