Source: deck.js

var _ = require("lodash");
var binder = require("./events").binder;
var hasEmitter = require("./events").hasEmitter;
var DecksEvent = require("./events").DecksEvent;
var Item = require("./item");
var ItemCollection = require("./itemcollection");
var Layout = require("./layout");
var Canvas = require("./canvas");
var Frame = require("./frame");
var Viewport = require("./viewport");
var validate = require("./utils/validate");

/**
 * Creates the {@link Deck} object, which is the top-level API for managing the decks system.
 * Contains all of the coordinating objects for managing items, collections of items,
 * viewports, layouts, etc.
 *
 * @class
 * @mixes binder
 * @mixes hasEmitter
 * @param {!Object} options - Deck options
 * @param {?Object} [options.config={}] - Deck configuration settings
 * @param {?boolean} [options.config.debugEvents=false] - Whether to log events to the console
 * @param {?boolean} [options.config.debugDrawing=false] - Whether to log drawing actions to the console
 * @param {?boolean} [options.config.debugGestures=false] - Whether to log gesture info to the console
 * @param {?(Object|Emitter)} [options.emitter={}] - Emitter instance or options
 * @param {!Object} options.animator - Object with animate function (like VelocityJS)
 * @param {?(Object|ItemCollection)} [options.itemCollection=[]] - ItemCollection instance or options
 * @param {!(Object|Layout)} options.layout - Layout instance or options
 * @param {!(Object|Frame)} options.frame - Frame instance or options
 * @param {?(Object|Canvas)} [options.canvas={}] - Canvas instance or options
 * @param {?(Object|Viewport)} [options.viewport={}] - Viewport instance or options
 */
function Deck(options) {
  if (!(this instanceof Deck)) {
    return new Deck(options);
  }

  options = _.merge({}, this.defaultOptions, options);

  validate.isEnabled = !!options.config.validation;

  this.setEmitter(options.emitter || {});
  this.setConfig(options.config || {});
  this.setAnimator(options.animator);
  this.setItemCollection(options.itemCollection || options.items || []);
  this.setFilter(options.filter);
  this.setSortBy(options.sortBy);
  this.setReversed(!!options.reversed);
  this.setLayout(options.layout);
  this.setFrame(options.frame);
  this.setCanvas(options.canvas || {});
  this.setViewport(options.viewport || {});

  this.bind();

  this.emit(DecksEvent("deck:ready", this));
}

_.extend(Deck.prototype, binder, hasEmitter, /** @lends Deck.prototype */ {
  /**
   * Default global {@link Deck} options.
   */
  defaultOptions: {
    config: {
      frameClassName: "decks-frame",
      canvasClassName: "decks-canvas",
      itemClassName: "decks-item",
      customRenderClassName: "decks-custom-render",
      debugEvents: false,
      debugDrawing: false,
      debugGestures: false,
      debugLoading: false,
      validation: true
    }
  },

  /**
   * Events to bind to on the shared {@link Emitter}.
   */
  getEmitterEvents: function getEmitterEvents() {
    return {
      "*": "onAnyEmitterEvent"
    };
  },

  /**
   * Events to bind to on the {@link ItemCollection}
   */
  getItemCollectionEvents: function getItemCollectionEvents() {
    return {
      "*": "onAnyItemCollectionEvent"
    };
  },

  /**
   * Binds the {@link Emitter} and {@link ItemCollection} event handlers.
   *
   * @return {undefined}
   */
  bind: function bind() {
    this.bindEvents(this.emitter, this.getEmitterEvents());

    if (this.itemCollection.emitter !== this.emitter) {
      this.bindEvents(this.itemCollection, this.getItemCollectionEvents());
    }
  },

  /**
   * Unbinds the {@link Emitter} and {@link ItemCollection} event handlers.
   *
   * @return {undefined}
   */
  unbind: function unbind() {
    this.unbindEvents(this.emitter, this.getEmitterEvents());

    if (this.itemCollection.emitter !== this.emitter) {
      this.unbindEvents(this.itemCollection, this.getItemCollectionEvents());
    }
  },

  /**
   * Binds the {@link Layout} {@link Emitter} events.
   *
   * @return {undefined}
   */
  bindLayout: function bindLayout() {
    this.layout.bindEvents(this.emitter, this.layout.getEmitterEvents());
  },

  /**
   * Unbinds the {@link Layout} {@link Emitter} events.
   *
   * @return {undefined}
   */
  unbindLayout: function unbindLayout() {
    this.layout.unbindEvents(this.emitter, this.layout.getEmitterEvents());
  },

  /**
   * Binds all {@link GestureHandler}s and {@link GestureHandlerGroup}s.
   *
   * @return {undefined}
   */
  bindGestures: function bindGestures() {
    this.canvas.bindGestures();
    this.viewport.bindGestures();
  },

  /**
   * Binds all {@link GestureHandler}s and {@link GestureHandlerGroup}s.
   *
   * @return {undefined}
   */
  unbindGestures: function unbindCanvasGestureHandler() {
    this.canvas.unbindGestures();
    this.viewport.unbindGestures();
  },

  /**
   * Destroys the {@link Deck} and all sub-components.  The {@link Deck} is no longer
   * usable after calling this.
   *
   * @return {undefined}
   */
  destroy: function destroy() {
    this.unbind();
    this.unbindLayout();

    this.itemCollection.destroy();
    this.layout.destroy();
    this.frame.destroy();
    this.canvas.destroy();
    this.viewport.destroy();
  },

  /**
   * Enables drawing (if it had previously been disabled with {@link Viewport#disableDrawing}
   *
   * @return {undefined}
   */
  enableDrawing: function enableDrawing() {
    this.viewport.enableDrawing();
  },

  /**
   * Disables drawing (re-enable by calling {@link Viewport#enableDrawing}
   *
   * @return {undefined}
   */
  disableDrawing: function disableDrawing() {
    this.viewport.disableDrawing();
  },

  /**
   * Gets {@link Item}s from the {@link ItemCollection}
   *
   * @param {?Function} [filter=undefined] - optional filter function which takes an {@link Item}
   * @return {undefined}
   */
  getItems: function getItems(filter) {
    return this.itemCollection.getItems(filter);
  },

  /**
   * Gets an {@link Item} by {@link Item} id
   *
   * @param id
   * @return {undefined}
   */
  getItem: function getItem(id) {
    return this.itemCollection.getItem(id);
  },

  /**
   * Adds an {@link Item} to the {@link ItemCollection}
   *
   * @param item
   * @param options
   * @return {undefined}
   */
  addItem: function addItem(item, options) {
    this.itemCollection.addItem(item, options);
  },

  /**
   * Adds {@link Item}s to the {@link ItemCollection}
   *
   * @param items
   * @param options
   * @return {undefined}
   */
  addItems: function addItems(items, options) {
    this.itemCollection.addItems(items, options);
  },

  /**
   * Removes an {@link Item} from the {@link ItemCollection}.
   *
   * @param item
   * @param options
   * @return {undefined}
   */
  removeItem: function removeItem(item, options) {
    this.itemCollection.removeItem(item, options);
  },

  /**
   * Clears all {@link Item}s from the {@link ItemCollection}
   *
   * @param options
   * @return {undefined}
   */
  clear: function clear(options) {
    this.itemCollection.clear(options);
  },

  /**
   * Sets a filter function on the {@link ItemCollection}.  Items that do not pass the filter
   * function will have their {@link Item} index set to -1, which may cause the renders for the
   * {@link Item} to be hidden on the next draw cycle.
   *
   * @param filter
   * @return {undefined}
   */
  setFilter: function setFilter(filter, options) {
    this.itemCollection.setFilter(filter, options);
  },

  /**
   * Sets a sort by function on the {@link ItemCollection}.  The sort by function will be run over
   * the {@link ItemCollection} and may cause the indices to change on zero or more {@link Item}s.
   * This triggers a redraw for any {@link Item} whose index changes.
   *
   * @param sortBy
   * @return {undefined}
   */
  setSortBy: function setSortBy(sortBy, options) {
    this.itemCollection.setSortBy(sortBy, options);
  },

  /**
   * Sets a reversed flag on the {@link ItemCollection} which reverses all the indices of the {@link Item}s.
   * This triggers a redraw if any {@link Item} indices change.
   *
   * @param isReversed
   * @return {undefined}
   */
  setReversed: function setReversed(isReversed, options) {
    this.itemCollection.setReversed(isReversed, options);
  },

  /**
   * Requests a manual redraw and reload cycle on all the items.
   *
   * This is normally not needed, as decks will attempt to always redraw and reload
   * whenever necessary, but can be used to force a redraw/reload to happen.
   *
   * @return {undefined}
   */
  draw: function draw() {
    this.emit(DecksEvent("deck:draw", this));
  },

  /**
   * Requests that the {@link Frame} re-calculate it's bounds.  If the bounds have changed,
   * it will trigger a redraw, which allows the {@link Viewport} to request new renders from
   * the {@link Layout} which might result in renders moving to fit in the new {@link Frame} size.
   *
   * @return {undefined}
   */
  resize: function resize() {
    this.emit(DecksEvent("deck:resize", this));
  },

  /**
   * Pans the {@link Canvas} to the given {@link Item}, with an optional render id (defaults to the first render element).
   *
   * @param {!(Item|string)} itemOrItemId - the item or item id to pan to
   * @param {?(string|number)} [renderIdOrIndex=0] - the render id or index of the element to pan to
   * * (defaults to the first render element for the item)
   * @return {undefined}
   */
  panToItem: function panToItem(itemOrItemId, renderIdOrIndex) {
    var item;
    if (itemOrItemId instanceof Item) {
      item = itemOrItemId;
    } else if (_.isString(itemOrItemId)) {
      item = this.getItem(itemOrItemId);
    } else if (_.isNumber(itemOrItemId)) {
      item = this.getItem("" + itemOrItemId);
    } else if (_.has(itemOrItemId, "id")) {
      var id = itemOrItemId.id;
      if (_.isNumber(id)) {
        id = "" + id;
      }
      item = this.getItem(id);
    }
    this.viewport.panToItem(item, renderIdOrIndex);
  },

  /**
   * Sets a new {@link Layout}, and pans to the given {@link Item} when the new layout draw cycle completes.
   *
   * @param {Layout} layout - new layout to set
   * @param {!(Item|string)} itemOrItemId - item or item id
   * @param {?(string|number)} [renderIdOrIndex=0] - render id or index (defaults to the first render for the {@link Item})
   * @return {undefined}
   */
  setLayoutAndPanToItem: function setLayoutAndPanToItem(layout, itemOrItemId, renderIdOrIndex) {
    var self = this;

    // Listen for the completion of the next drawing cycle (this should be emitted when
    // the drawing cycle for new layout completes).  At that point, pan to the item.
    self.once("viewport:all:renders:drawn", function() {
      self.panToItem(itemOrItemId, renderIdOrIndex);
    });

    self.setLayout(layout);
  },

  /**
   * Sets the config object.
   *
   * @param config
   * @return {undefined}
   */
  setConfig: function setConfig(config) {
    validate(config, "config", { isPlainObject: true, isNotSet: this.config });

    this.config = config;

    this.emit(DecksEvent("deck:config:set", this, this.config));
  },

  /**
   * Sets the animator object.
   *
   * @param animator
   * @return {undefined}
   */
  setAnimator: function setAnimator(animator) {
    validate(animator, "animator", { isPlainObject: true, isNotSet: this.animator });

    this.animator = animator;

    this.emit(DecksEvent("deck:animator:set", this, this.animator));
  },

  /**
   * Sets the {@link ItemCollection}.
   *
   * @param itemCollection
   * @return {undefined}
   */
  setItemCollection: function setItemCollection(itemCollection) {
    validate(itemCollection, "itemCollection", { isRequired: true, isNotSet: this.itemCollection });

    if (!(itemCollection instanceof ItemCollection)) {
      itemCollection = new ItemCollection(itemCollection);
    }

    this.itemCollection = itemCollection;

    this.emit(DecksEvent("deck:item:collection:set", this, this.layout));
  },

  /**
   * Sets the {@link Layout}
   *
   * @param layout
   * @return {undefined}
   */
  setLayout: function setLayout(layout) {
    validate(layout, "layout", { isRequired: true });

    if (this.layout === layout) {
      return;
    }

    this.emit(DecksEvent("deck:layout:setting", this, { oldLayout: this.layout, newLayout: layout }));

    // Unbind the previous layout from emitter events
    if (this.layout) {
      this.unbindLayout();
    }

    if (!(layout instanceof Layout)) {
      layout = new Layout(layout);
    }

    this.layout = layout;
    this.bindLayout();

    this.emit(DecksEvent("deck:layout:set", this, this.layout));
  },

  /**
   * Sets the {@link Frame}
   *
   * @param frame
   * @return {undefined}
   */
  setFrame: function setFrame(frame) {
    validate(frame, "frame", { isRequired: true, isNotSet: this.frame });

    if (!(frame instanceof Frame)) {
      _.extend(frame, {
        emitter: this.emitter,
        config: this.config,
        animator: this.animator
      });
      frame = new Frame(frame);
    }

    this.frame = frame;

    this.emit(DecksEvent("deck:frame:set", this, this.frame));
  },

  /**
   * Sets the {@link Canvas}
   *
   * @param canvas
   * @return {undefined}
   */
  setCanvas: function setCanvas(canvas) {
    validate(canvas, "canvas", { isRequired: true, isNotSet: this.canvas });

    if (!(canvas instanceof Canvas)) {
      _.extend(canvas, {
        emitter: this.emitter,
        config: this.config,
        animator: this.animator,
        layout: this.layout
      });
      canvas = new Canvas(canvas);
    }

    this.canvas = canvas;

    this.emit(DecksEvent("deck:canvas:set", this, this.canvas));
  },

  /**
   * Sets the {@link Viewport}
   *
   * @param viewport
   * @return {undefined}
   */
  setViewport: function setViewport(viewport) {
    validate(viewport, "viewport", { isRequired: true, isNotSet: this.viewport });

    if (!(viewport instanceof Viewport)) {
      _.extend(viewport, {
        emitter: this.emitter,
        config: this.config,
        animator: this.animator,
        deck: this,
        itemCollection: this.itemCollection,
        layout: this.layout,
        frame: this.frame,
        canvas: this.canvas
      });
      viewport = new Viewport(viewport);
    }

    this.viewport = viewport;

    this.emit(DecksEvent("deck:viewport:set", this, this.viewport));
  },

  /**
   * Called on any {@link Emitter} event.
   *
   * @param e
   * @return {undefined}
   */
  onAnyEmitterEvent: function onAnyEmitterEvent(e) {
    if (this.config.debugEvents) {
      console.log("Deck#onAnyEmitterEvent:", e);
    }
  },

  /**
   * Called on any {@link ItemCollection} event.
   *
   * @param e
   * @return {undefined}
   */
  onAnyItemCollectionEvent: function onAnyItemCollectionEvent(e) {
    // Forward itemCollection events on the main Deck emitter
    this.emit(e);
  }
});

module.exports = Deck;