Source: enyo-x/source/views/list_relations_editor_box.js

/*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true, white:true, strict: false*/
/*global XM:true, XV:true, XT:true, _:true, enyo:true*/

(function () {

  var _events = "change readOnlyChange statusChange refreshView";

  XV.RelationsEditorMixin = enyo.mixin({
    events: {
      onNotify: ""
    },
    published: {
      value: null
    },
    handlers: {
      onValueChange: "controlValueChanged"
    },
    bind: function (action) {
      if (this.value) {
        this.value[action](_events, this.attributesChanged, this);
        this.value[action]("notify", this.notify, this);
        if (this.value.meta) {
          this.value.meta[action]("change", this.attributesChanged, this);
        }
      }
    },
    destroy: function () {
      this.bind("off");
      this.value = null;
      this.inherited(arguments);
    },
    setValue: function (value) {
      this.bind("off");
      this.value = value;
      this.bind("on");
      this.attributesChanged(value);
      if (this.valueChanged) { this.valueChanged(value); }
    }

  }, XV.EditorMixin);

  /**
    @name XV.RelationsEditor
    @class Use to define the editor for {@link XV.ListRelationsEditorBox}.
    @extends XV.Groupbox
  */
  var editor = enyo.mixin({
    name: "XV.RelationsEditor",
    kind: "XV.Groupbox",
  }, XV.RelationsEditorMixin);
  enyo.kind(editor);

  /**
    @name XV.ListRelationsEditorBox
    @class Provides a container in which its components
    are a vertically stacked group of horizontal rows.<br />
    Made up of a header, panels, and a row of navigation buttons.<br />
    Must include a component called `list`.
    List must be of subkind {@link XV.ListRelations}.
    The `value` must be set to a collection of `XM.Model`.
    @extends XV.Groupbox
  */
  enyo.kind(/** @lends XV.ListRelationsEditorBox# */{
    name: "XV.ListRelationsEditorBox",
    kind: "XV.Groupbox",
    classes: "panel xv-relations-editor-box",
    published: {
      attr: null,
      disabled: false,
      value: null,
      title: "",
      parentKey: "",
      listRelations: "",
      editor: null,
      summary: null,
      childWorkspace: null,
      fitButtons: true
    },
    events: {
      onError: "",
      onChildWorkspace: ""
    },
    handlers: {
      onSelect: "selectionChanged",
      onDeselect: "selectionChanged",
      onTransitionFinish: "transitionFinished",
      onValueChange: "controlValueChanged"
    },
    /**
    @todo Document the attrChanged method.
    */
    attrChanged: function () {
      this.$.list.setAttr(this.attr);
    },
    /**
    @todo Document the controlValueChanged method.
    */
    controlValueChanged: function () {
      // this is getting called before the list is created
      if (this.$.list) {
        this.$.list.refresh();
      }
      return true;
    },
    /**
    @todo Document the create method.
    */
    create: function () {
      this.inherited(arguments);
      var editor = this.getEditor(),
        panels,
        control;

      // Header
      this.createComponent({
        kind: "onyx.GroupboxHeader",
        content: this.getTitle()
      });

      // List
      panels = {
        kind: "Panels",
        fit: true,
        arrangerKind: "CollapsingArranger",
        components: [
          {kind: editor, name: "editor"},
          {kind: this.getListRelations(), name: "list",
            attr: this.getAttr()}
        ]
      };
      control = this.createComponent(panels);
      control.setIndex(1);

      // Buttons
      this.createComponent({
        kind: "FittableColumns",
        name: "navigationButtonPanel",
        classes: "xv-buttons",
        components: [
          {kind: "onyx.Button", name: "newButton", onclick: "newItem",
          classes: "icon icon-plus"},
          {kind: "onyx.Button", name: "deleteButton", onclick: "deleteItem",
            disabled: true, classes: "icon-minus"},
          {kind: "onyx.Button", name: "prevButton", onclick: "prevItem",
            disabled: true, classes: "icon-chevron-left"},
          {kind: "onyx.Button", name: "nextButton", onclick: "nextItem",
            disabled: true, classes: "icon-chevron-right"},
          {kind: "onyx.Button", name: "doneButton", onclick: "doneItem",
            disabled: true, classes: "icon-ok"},
          {kind: "onyx.Button", name: "expandButton", ontap: "launchWorkspace",
            classes: "icon-resize-full"}
        ]
      });
      this.$.expandButton.setShowing(_.isString(this.getChildWorkspace()));
    },

    /**
      Marks the model of the selected item to be deleted on save
      and remove it from its parent collection and the Enyo list
    */
    deleteItem: function () {
      var index = this.$.list.getFirstSelected(),
        model = index ? this.$.list.getModel(index) : null;
      this.$.list.getSelection().deselect(index, false);
      model.destroy();
      this.$.list.lengthChanged();
    },
    destroy: function () {
      this.unbind();
      this.getValue().off("add remove", this.valueChanged, this);
      this.inherited(arguments);
    },
    /**
      Disables or enables the view
     */
    disabledChanged: function () {
      this.$.newButton.setDisabled(this.getDisabled());
      // complicated logic we need to disable and enable the
      // done and delete buttons is here:
      this.selectionChanged();
      this.$.expandButton.setDisabled(this.getDisabled());
    },
    /**
      Close the edit session and return to read-only summary view
    */
    doneItem: function () {
      var index = this.$.list.getFirstSelected(),
        selection = this.$.list.getSelection();
      if (this.validate() && index) {
        selection.deselect(index);
        if (this.$.list.$.page0 && !this.$.list.$.page0.hasNode()) {
          XT.log("Warning: page0 doesn't hasNode");
        } else if (this.$.list.$.page1 && !this.$.list.$.page1.hasNode()) {
          XT.log("Warning: page1 doesn't hasNode");
        }
        this.$.list.render();
      }
    },
    error: function (model, error) {
      var inEvent = {
        model: model,
        error: error
      };
      this.doError(inEvent);
    },
    launchWorkspace: function (inSender, inEvent) {
      var index = Number(this.$.list.getFirstSelected());
      this.doChildWorkspace({
        workspace: this.getChildWorkspace(),
        collection: this.getValue(),
        index: index,
        listRelations: this.$.list
      });
      return true;
    },
    /**
      Add a new model to the collection and bring up a blank editor to fill it in
    */
    newItem: function () {
      var collection = this.$.list.getValue(),
        Klass = collection.model,
        model = new Klass(null, {isNew: true}),
        components = this.$.editor.getComponents(),
        scroller,
        length,
        first;
      if (this.validate()) {
        this.$.editor.clear();
        collection.add(model);
        if (collection.comparator) { collection.sort(); }

        // Exclude models marked for deletion
        length = _.filter(collection.models, function (model) {
          return !model.isDestroyed();
        }).length;
        this.$.list.select(length - 1);

        // Scroll to top and set focus on first available widget
        scroller = _.find(components, function (c) {
          return c instanceof enyo.Scroller;
        });
        if (scroller) { scroller.scrollToTop(); }
        first = _.find(components, function (c) {
          return c.attr && !model.isReadOnly(c.attr);
        });
        if (first && first.focus) {
          first.focus();
        }
      }
    },
    /**
      Move to edit the next item in the collection.
    */
    nextItem: function () {
      var index = this.$.list.getFirstSelected() - 0;
      if (this.validate()) {
        this.$.list.select(index + 1);
      }
    },
    /**
      Move to edit the previous line in the collection.
    */
    prevItem: function () {
      var index = this.$.list.getFirstSelected() - 0;
      if (this.validate()) {
        this.$.list.select(index - 1);
      }
    },
    /**
    @todo Document the selectionChanged method.
    */
    selectionChanged: function () {
      var index = this.$.list.getFirstSelected(),
        model = index ? this.$.list.getModel(index) : null,
        K = XM.Model,
        that = this;
      this.unbind();
      this.$.deleteButton.setDisabled(true);
      this.$.doneButton.setDisabled(!index || this.getDisabled());
      if (model) {
        model.on("invalid", this.error, this); // Error event binding
        this.$.editor.setValue(model);
        if (model.isNew() ||
          model.isBusy() && model._prevStatus === K.READY_NEW) {
          this.$.deleteButton.setDisabled(this.getDisabled());
        } else {
          model.used({
            success: function (resp) {
              if (that.$.deleteButton) { // Sometimes the workspace has been closed
                that.$.deleteButton.setDisabled(resp || that.getDisabled());
              }
            }
          });
        }
        if (this.$.panels.getIndex()) { this.$.panels.setIndex(0); }
        this.$.prevButton.setDisabled(index - 0 === 0);
        this.$.nextButton.setDisabled(index - 0 === this.$.list.value.length - 1);
      } else {
        if (!this.$.panels.getIndex()) { this.$.panels.setIndex(1); }
        this.$.prevButton.setDisabled(true);
        this.$.nextButton.setDisabled(true);
      }
    },
    /**
    @todo Document the transitionFinished method.
    */
    transitionFinished: function (inSender, inEvent) {
      if (inEvent.originator.name === 'panels') {
        if (this.$.panels.getIndex() === 1) {
          this.doneItem();
        }
        return true;
      }
    },
    /**
      Remove current model bindings.
    */
    unbind: function () {
      var model = this.$.editor.getValue();
      if (model) {
        model.off("invalid", this.error, this);
      }
    },
    /**
      Returns whether a selected model is validate. If
      none selected returns `true`. If an error is found
      an error event is raised.

      @returns {Boolean}
    */
    validate: function () {
      var list = this.$.list,
        index = list.getFirstSelected() - 0,
        model = isNaN(index) ? false : list.getModel(index),
        error = model ? model.validate(model.attributes) : false;
      if (error) {
        this.error(model, error);
        return false;
      }
      return true;
    },
    /**
    @todo Document the valueChanged method.
    */
    valueChanged: function () {
      var value = this.getValue();
      // Make sure list refreshes if collection changed
      if (value) {
        value.once("add remove", this.valueChanged, this);
      }
      this.$.list.setValue(value);
    }
  });

}());