wb/mgr/Manager.js

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

/**
 * @module nmodule/webEditors/rc/wb/mgr/Manager
 */
define([ 'jquery',
        'Promise',
        'underscore',
        'bajaux/Widget',
        'bajaux/util/CommandButtonGroup',
        'nmodule/webEditors/rc/fe/fe',
        'nmodule/webEditors/rc/fe/baja/BaseEditor',
        'nmodule/webEditors/rc/fe/baja/util/typeUtils',
        'nmodule/webEditors/rc/wb/mgr/MgrStateHandler',
        'nmodule/webEditors/rc/wb/mgr/model/MgrColumn',
        'nmodule/webEditors/rc/wb/mgr/model/MgrModel',
        'nmodule/webEditors/rc/wb/mixin/mixinUtils',
        'nmodule/webEditors/rc/wb/mixin/TransferSupport',
        'nmodule/webEditors/rc/wb/table/Table',
        'nmodule/webEditors/rc/wb/table/menu/DefaultTableContextMenu',
        'nmodule/webEditors/rc/util/htmlUtils' ], function (
         $,
         Promise,
         _,
         Widget,
         CommandButtonGroup,
         fe,
         BaseEditor,
         typeUtils,
         MgrStateHandler,
         MgrColumn,
         MgrModel,
         mixinUtils,
         TransferSupport,
         Table,
         DefaultTableContextMenu,
         htmlUtils) {

  'use strict';

  var isComponent = typeUtils.isComponent,
      contextMenuOnLongPress = htmlUtils.contextMenuOnLongPress,
      hasMixin = mixinUtils.hasMixin;

  /**
   * Return the default HTML used for the manager view. If the learn mixin
   * is applied, this will be overridden by a template that describes the
   * multi table split pane markup.
   *
   * @returns {String}
   */
  function defaultHtml(mgr) {
    return '<div class="mainTable">' +
      '<div class="tableContainer"></div>' +
      '</div>' +
      '<div class="showHideMenu mgr-show-hide-menu mgr-show-hide-menu-main"></div>' +
      '<div class="commandContainer"></div>';
  }

  /**
   * API Status: **Development**
   *
   * View for managing groups of components, monitoring their current state
   * and adding/removing components to the group. The concrete manager type
   * must provide the `moduleName` and `keyName` parameters if it requires
   * state to be saved between hyperlinks and page reloads, as these values
   * will be used when generating the key used to index the cached state data.
   *
   * Due to the incubating status of the manager framework, it is *not
   * recommended* that you extend `Manager` directly. Instead, extend
   * `DeviceMgr` or `PointMgr`: these will provide more robust functionality
   * for most use cases.
   *
   * @param {Object} params
   * @param {String} params.moduleName - The module name, used for accessing values from the lexicon
   * and also used to generate the key for saving state information for a manager type.
   * @param {String} params.keyName - The key name, used for accessing values from the lexicon and
   * also used to generate the key for saving state information for a manager type.
   * @see module:nmodule/driver/rc/wb/mgr/DeviceMgr
   * @see module:nmodule/driver/rc/wb/mgr/PointMgr
   *
   * @class
   * @abstract
   * @alias module:nmodule/webEditors/rc/wb/mgr/Manager
   * @extends module:nmodule/webEditors/rc/fe/baja/BaseEditor
   */
  var Manager = function Manager(params) {
    var that = this;
    BaseEditor.apply(that, arguments);
    that.$getStateHandler = _.once(function () {
      return Promise.resolve(params && params.moduleName && params.keyName &&
        that.makeStateHandler(params.moduleName, params.keyName));
    });
    TransferSupport(this);
  };

  Manager.prototype = Object.create(BaseEditor.prototype);
  Manager.prototype.constructor = Manager;

  /**
   * @param {JQuery} dom
   * @returns {Array|null} the selected subject of the table being clicked
   */
  Manager.prototype.getSubject = function (dom) {
    const tableDom = dom.closest('.TableWidget');

    if (!$.contains(this.jq()[0], tableDom[0])) { return null; }

    const table = Widget.in(tableDom);

    if (!table) { return null; }

    return table.getSubject(dom);
  };

  /**
   * Get the main Table widget.
   *
   * @returns {module:nmodule/webEditors/rc/wb/table/Table}
   * @since Niagara 4.6
   */
  Manager.prototype.getMainTable = function () {
    // noinspection JSValidateTypes
    return Widget.in(this.$getMainTableElement()
      .children('.tableContainer').children('table'));
  };

  /**
   * Get the element that holds the main table.
   * @private
   * @returns {jQuery}
   */
  Manager.prototype.$getMainTableElement = function () {
    return this.jq().find('.mainTable');
  };

  /**
   * Get the element that holds the command buttons.
   * @private
   * @returns {jQuery}
   */
  Manager.prototype.$getCommandContainerElement = function () {
    return this.jq().find('.commandContainer');
  };

  /**
   * Get the CommandButtonGroup widget.
   *
   * @private
   * @returns {module:bajaux/util/CommandButtonGroup}
   */
  Manager.prototype.$getCommandButtonGroup = function () {
    // noinspection JSValidateTypes
    return Widget.in(this.$getCommandContainerElement()
      .children('.CommandButtonGroup'));
  };

  /**
   * Set up elements for the main table and command group.
   *
   * @param {JQuery} dom
   * @param {Object} [params] the initialization parameters
   * @returns {Promise}
   */
  Manager.prototype.doInitialize = function (dom, params) {

    // Look for a (private) optional parameter allowing custom HTML
    // markup to be specified for the manager. If not defined, the
    // default HTML will be used, with a single main manager table,
    // show/hide menu and the command button group.

    var that = this,
        html = (params && params.html) || defaultHtml(that);

    dom.html(html).addClass('Manager');
    contextMenuOnLongPress(dom, '.tableContainer');

    // The 'MGR_COMMAND' mixin has the option to not show commands in
    // the bar at the bottom of the view. This function will filter out
    // the ones that aren't required.

    function filterCommands(cmd) {
      return (hasMixin(cmd, 'MGR_COMMAND')) ? cmd.isShownInActionBar() : true;
    }

    return Promise.all([
      fe.buildFor({
        dom: $('<div/>').appendTo(dom.children('.commandContainer')),
        type: CommandButtonGroup,
        value: this.getCommandGroup().filter({ include: filterCommands })
      }),
      that.$getStateHandler().then(function (handler) {
        that.$stateHandler = handler;
      })
    ]);
  };

  /**
   * Private framework override point called when the main model has been created, but before
   * it has been loaded into the main table. The deserialized manager state is passed as a
   * parameter, allowing any model configuration to be restored from the manager state.
   *
   * This is intended for internal framework use only.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} model - the main table model
   * @param {Object} deserializedState
   * @returns {Promise.<*>}
   */
  Manager.prototype.$beforeMainTableLoaded = function (model, deserializedState) {
    return Promise.resolve();
  };

  /**
   * Private framework override point called after the main table model has been loaded
   * into the table widget. The deserialized manager state is passed as a parameter; this
   * is called before the main state restoration is performed.
   *
   * This is intended for internal framework use only.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} model - the main table model
   * @param {Object} deserializedState
   * @returns {Promise.<*>}
   */
  Manager.prototype.$afterMainTableLoaded = function (model, deserializedState) {
    return Promise.resolve();
  };

  /**
   * Get the `MgrModel` backing this manager. This will return `undefined` until
   * `load()` is called. It can safely be called from inside `doLoad()`.
   *
   * @returns {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel}
   * @since Niagara 4.6
   */
  Manager.prototype.getModel = function () {
    return this.$mgrModel;
  };

  /**
   * Abstract method to create the `MgrModel` for the main database table. The method
   * should return a `Promise` that will resolve to a `MgrModel` instance for the
   * `ComponentSource` provided in the parameter. This is used to convert the value
   * being loaded into the widget (e.g. a network) into the model used for the table
   * widget.
   *
   * @abstract
   *
   * @param {baja.Component} value - the value being loaded into the `Widget`.
   * @returns {Promise.<module:nmodule/webEditors/rc/wb/mgr/model/MgrModel>} the model for the main table.
   */
  Manager.prototype.makeModel = function (value) {
    throw new Error('Manager does not provide a makeModel() function.');
  };

  Manager.prototype.load = function (comp) {
    var that = this, args = arguments;
    if (comp === that.value()) { return Promise.resolve(); }

    // Read and deserialize the state from storage, but we don't do the full restore
    // until the end of loading. We get the state at this point so we can find out
    // any state that might need to be restored on the model prior to loading. The
    // full manager state is restored *after* the model has been loaded into the
    // table widget.

    that.$deserializedState = (that.$stateHandler && that.$stateHandler.deserializeFromStorage());

    return Promise.resolve(that.makeModel(comp))
      .then(function (model) {
        that.$mgrModel = model;
        return BaseEditor.prototype.load.apply(that, args);
      })
      .finally(function () {
        if (that.hasOwnProperty('$deserializedState')) {
          delete that.$deserializedState;
        }
      });
  };

  /**
   * Initializes and loads the main table with the `MgrModel`. If overriding,
   * be sure to call the super method.
   *
   * @returns {Promise}
   */
  Manager.prototype.doLoad = function () {
    var that = this,
        jq = this.jq(),
        model = that.getModel(),
        tableContainer = jq.find('.mainTable').children('.tableContainer');

    _.each(model.getColumns(), function (col) {
      if (col instanceof MgrColumn) {
        col.$init(that);
      }
    });

    // Create functions to be passed to the main table that will give us a hook
    // into the cell construction/destruction process, should there be need to
    // override any of the main table's default behavior, such as manipulating
    // the css for a particular view.

    return that.$beforeMainTableLoaded(model, that.$deserializedState)
      .then(function () {
        var mainTable = that.getMainTable();
        return mainTable && mainTable.destroy();
      })
      .then(function () {
        tableContainer.empty();
        return fe.buildFor({
          dom: $('<table class="ux-table no-stripe"></table>').appendTo(tableContainer),
          type: Table,
          value: model,
          initializeParams: {
            buildCell: _.bind(that.buildMainTableCell, that),
            destroyCell: _.bind(that.destroyMainTableCell, that),
            finishBuildingRow: _.bind(that.finishMainTableRow, that)
          }
        });
      })
      .then(function (table) {
        new DefaultTableContextMenu(table).arm(jq, '.mgr-show-hide-menu-main');
        return that.$afterMainTableLoaded(model, that.$deserializedState);
      })
      .then(function () {
        return that.restoreState();
      });
  };

  /**
   * If the loaded `MgrModel` is backed by a mounted Component, then use that
   * Component to resolve ORDs.
   *
   * @returns {Promise.<baja.Component|undefined>}
   */
  Manager.prototype.getOrdBase = function () {
    var component = this.value();
    return Promise.resolve(isComponent(component) ? component : undefined);
  };

  /**
   * Update the height of the main table element so that the command buttons
   * are always visible.
   */
  Manager.prototype.doLayout = function () {
    this.$getMainTableElement().css({
      bottom: this.$getCommandContainerElement().outerHeight()
    });
  };

  /**
   * Overrides the base `destroy` method to give the manager a chance to save its state
   * before the content (such as the child table widgets) is destroyed.
   */
  Manager.prototype.destroy = function () {
    this.saveState();
    return BaseEditor.prototype.destroy.apply(this, arguments);
  };

  /**
   * Destroy child editors, the main table, and its model.
   *
   * @returns {Promise}
   */
  Manager.prototype.doDestroy = function () {
    var that = this,
        model = that.getModel();

    that.jq().removeClass('Manager');

    return that.getChildWidgets().destroyAll()
      .then(function () {
        return (model instanceof MgrModel) && model.destroy();
      });
  };

  /**
   * Invoke the handler created by `makeStateHandler()` to restore the Manager's
   * current state when the Manager is loaded.
   *
   * @returns {Promise}
   */
  Manager.prototype.restoreState = function () {
    var handler = this.$stateHandler,
        state = this.$deserializedState;

    return Promise.resolve(handler && state && this.getModel() && handler.restore(this, state));
  };

  /**
   * Invoke the handler created by `makeStateHandler()` to save the Manager's
   * current state when the Manager is destroyed.
   */
  Manager.prototype.saveState = function () {
    var handler = this.$stateHandler;

    if (handler && this.getModel()) { handler.save(this); }
  };

  /**
   * Make a state handler instance for saving and restoring the Manager's
   * state.
   * @returns {module:nmodule/webEditors/rc/wb/mgr/MgrStateHandler}
   */
  Manager.prototype.makeStateHandler = function (keyName, moduleName) {
    return MgrStateHandler.make(keyName + '.' + moduleName);
  };

  /**
   * Override point, allowing a `Manager` to customize the building of
   * a cell within its main table. This will default to delegating the
   * cell building to the column.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Column} column
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise|*}
   */
  Manager.prototype.buildMainTableCell = function (column, row, dom) {
    return column.buildCell(row, dom);
  };

  /**
   * Override point, allowing a `Manager` to customize the destruction
   * of a cell that it created in `#buildMainTableCell`. The default
   * behavior is to delegate to the column.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Column} column
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise|*}
   */
  Manager.prototype.destroyMainTableCell = function (column, row, dom) {
    return column.destroyCell(row, dom);
  };

  /**
   * Override point, allowing a `Manager` to customize the dom for
   * a `Row` after the cells have been built, but before it is inserted
   * into the main table. This allows sub-classes to perform any CSS
   * customizations they may require at an individual row level.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise}
   */
  Manager.prototype.finishMainTableRow = function (row, dom) {
    return Promise.resolve(dom);
  };

  return Manager;
});