Source: layout.js

var _ = require("lodash");
var binder = require("./events/binder");

/**
 * Contains the logic for rendering a layout of items
 *
 * Can be subclassed to implement layout methods, or can be passed
 * layout methods in the Layout constructor options.
 *
 * @class
 * @mixes binder
 * @param {?Object} options - layout options
 * @param {?Function} options.getRenders - implementation of getRenders method
 * @param {?Function} options.initializeRender - implementation of initializeRender method
 * @param {?Function} options.loadRender - implementation of loadRender method
 * @param {?Function} options.unloadRender - implementation of unloadRender method
 * @param {?Function} options.getShowAnimation - implementation of getShowAnimation method
 * @param {?Function} options.getHideAnimation - implementation of getHideAnimation method
 * @param {?Function} options.setShowAnimation - implementation of setShowAnimation method
 * @param {?Function} options.setHideAnimation - implementation of setHideAnimation method
 * @returns {Layout}
 */
function Layout(options) {
  if (!(this instanceof Layout)) {
    return new Layout(options);
  }

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

  // Allow the caller to create a Layout implementation without subclassing Layout -
  // just by passing in implementations of the key methods in options
  _.each(this.getOverridables(), function(key) {
    if (options[key]) {
      this[key] = options[key];
    }
  }, this);
}

_.extend(Layout.prototype, binder, /** @lends Layout.prototype */ {
  /**
   * Default options for a Layout instance
   */
  defaultOptions: {
  },

  /**
   * List of method names that can be overridden by passing them in via
   * options properties in the constructor.  You can also override these
   * properties by subclassing {@link Layout}.
   */
  getOverridables: function() {
    return [
      // Render-related methods
      "getRenders",
      "initializeRender",
      "shouldLoadRender",
      "shouldUnloadRender",
      "loadRender",
      "unloadRender",

      // Custom render-related methods
      "getCustomRenders",

      // Animation-related methods
      "getShowAnimation",
      "getHideAnimation",
      "setShowAnimation",
      "setHideAnimation",

      // Canvas/Gesture-related methods
      "getCanvasGestureOptions",
      "getMoveToElementOffsets",

      // Emitter event handlers
      "onViewportItemDrawing",
      "onViewportItemsDrawing",

      "onViewportRenderDrawing",
      "onViewportRenderErasing",
      "onViewportRenderDrawn",
      "onViewportRenderErased",
      "onViewportAllRendersDrawn",

      // TODO: add custom render event handlers?

      "onCanvasBoundsSet",

      "onFrameBoundsSet",

      "onItemCollectionFilterSet",
      "onItemCollectionSortBySet",
      "onItemCollectionReversedSet",
      "onItemCollectionIndexed"
    ];
  },

  /**
   * Events that all Layout instances will bind to, so the layout can receive
   * notifications when certain decks events occur.
   */
  getEmitterEvents: function() {
    return {
      "viewport:item:drawing": "onViewportItemDrawing",
      "viewport:items:drawing": "onViewportItemsDrawing",
      //"viewport:render:drawing": "onViewportRenderDrawing",
      //"viewport:render:erasing": "onViewportRenderErasing",
      //"viewport:render:drawn": "onViewportRenderDrawn",
      //"viewport:render:erased": "onViewportRenderErased",
      "viewport:all:renders:drawn": "onViewportAllRendersDrawn",
      "canvas:bounds:set": "onCanvasBoundsSet",
      "frame:bounds:set": "onFrameBoundsSet",
      "item:collection:filter:set": "onItemCollectionFilterSet",
      "item:collection:sort:by:set": "onItemCollectionSortBySet",
      "item:collection:reversed:set": "onItemCollectionReversedSet",
      "item:collection:indexed": "onItemCollectionIndexed"
    };
  },

  /**
   * Destroys the layout (no-op by default)
   *
   * @return {undefined}
   */
  destroy: _.noop,

  /**
   * Creates the "render" or "renders" for a given {@link Item} during a (re-)drawing cycle.
   *
   * A "render" is basically an instruction to the {@link Viewport} on where and how to draw
   * the item in the * {@link Canvas}.  An {@link Item} can be drawn in the {@link Canvas} one
   * time, or multiple times, which is specified by how many render objects this method returns.
   *
   * The {@link Viewport} invokes this method for an/each {@link Item} when a drawing
   * cycle is happening.  This method should return a "render" object (which is a set of
   * DOM style properties to apply to the render's element, along with animation options,
   * like duration, easing, delay, etc.), or an array of render objects.
   *
   * The Viewport will reconcile any existing renders/elements for the given {@link Item}, and
   * eventually animate an element to the property values listed in "transform", with the
   * animation controlled by the options in "animateOptions".
   *
   * This method is abstract at this level - it must be implemented by either passing
   * an options.getRenders function value into the {@link Layout} constructor, or
   * creating a subclass of {@link Layout} that implements this method on itself, or its
   * prototype.
   *
   * A render object must have "transform" and "animateOptions" properties at a minimum,
   * but can also have any other arbitrary properties that are needed for loading or unloading
   * the render at a later time (e.g. in load/unloadRender).
   *
   * A render might look like this:
   *
   * @abstract
   * @param {!Item} item - Item for which to create renders
   * @param {!Object} options - Other options provided by the {@link Viewport}
   * @returns {(Object|Object[])} - The render object or array of render objects for the {@link Item}
   * @example Example render object created by Layout#getRenders:
   * {
   *   transform: {
   *    top: 20,
   *    left: 20,
   *    width: 200,
   *    height: 150,
   *    rotateZ: 20
   *   },
   *   animateOptions: {
   *    duration: 200,
   *    easing: "easeInOutExpo"
   *   },
   *   someLayoutSpecificProperty: "some value",
   * }
   */
  getRenders: function getRenders(/*item, options*/) {
    throw new Error("Layout#getRenders: not implemented");
  },

  /**
   * Allows the layout to initialize a new render element, when a new render element is needed
   * for a render.
   *
   * @param {Object} render - The render object that was just created
   * @returns {undefined}
   */
  initializeRender: function initializeRender(render, options) {
    // Call unloadRender for this by default, as these methods are probably similar
    this.unloadRender(render, options);
  },

  /**
   * Returns whether the given render should be loaded at the time of invocation.
   * The {@link Layout} can implement this method if there are cases where a render
   * might not be normally loaded, but should be.
   *
   * @param {!Object} render - render object to check for loading
   * @param {!Object} options - hash of all deck-related objects
   * @return {undefined}
   */
  shouldLoadRender: function shouldLoadRender(/*render, options*/) {
    return true;
  },

  /**
   * Contains the logic to load the contents of a render (e.g. load an image in the render element)
   *
   * @param {Object} render - The render object to load
   * @returns {undefined}
   */
  loadRender: function loadRender(/*render, options*/) {
    throw new Error("Layout#loadRender not implemented");
  },

  /**
   * Returns whether the given render should be unloaded at the time of invocation.
   * The {@link Layout} can implement this method if there are cases where a render
   * should be unloaded (e.g. to save on memory).
   *
   * @param {!Object} render - render object to check for unloading.
   * @param {!Object} options - hash of all deck-related objects
   * @return {undefined}
   */
  shouldUnloadRender: function shouldUnloadRender(/*render, options*/) {
    return false;
  },

  /**
   * Contains the logic to unload the contents of a render (e.g. remove the DOM content of a render element)
   *
   * @param {Object} render - The render object to unload
   * @param {Object} options
   * @returns {undefined}
   */
  unloadRender: function unloadRender(/*render, options*/) {
    throw new Error("Layout#unloadRender not implemented");
  },

  /**
   * Event handler which informs the {@link Layout} that a render cycle is about to start for a
   * single {@link Item}.
   *
   * @param {DecksEvent} e - event object
   * @param {string} e.type - the event type
   * @param {Viewport} e.sender - the sender of the event (Viewport instance)
   * @param {Item} e.data - the {@link Item} that is about to be drawn (animated)
   * @return {undefined}
   */
  onViewportItemDrawing: _.noop,

  /**
   * Event handler which informs the {@link Layout} that a render cycle is about to start for a
   * multiple {@link Item}s.
   *
   * @param {DecksEvent} e - event object
   * @param {string} e.type - the event type
   * @param {Viewport} e.sender - the sender of the event (Viewport instance)
   * @param {Item[]} e.data - the {@link Item}s that are about to be drawn (animated)
   * @return {undefined}
   */
  onViewportItemsDrawing: _.noop,

  /**
   * Event handler which informs the {@link Layout} that a render is about to start drawing (animating).
   * The {@link Layout} can use this method to modify the render/element before the animation starts.
   *
   * @param {DecksEvent} e - event object
   * @param {string} e.type - the event type
   * @param {Viewport} e.sender - the sender of the event (Viewport instance)
   * @param {Object} e.data - the "render" object that is about to be drawn (animated)
   * @return {undefined}
   */
  onViewportRenderDrawing: _.noop,

  /**
   * Event handler which informs the {@link Layout} that a render is about to start erasing (animating
   * to a hidden state before being removed).)
   * The {@link Layout} can use this method to modify the render/element before the animation starts.
   *
   * @param {DecksEvent} e - event object
   * @param {string} e.type - the event type
   * @param {Viewport} e.sender - the sender of the event (Viewport instance)
   * @param {Object} e.data - the "render" object that is about to be drawn (animated)
   * @return {undefined}
   */
  onViewportRenderErasing: _.noop,

  /**
   * Event handler which informs the {@link Layout} when a single render has finished animating.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onViewportRenderDrawn: _.noop,

  /**
   * Event handler which informs the {@link Layout} when a single render has finished its hide animation.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onViewportRenderErased: _.noop,

  /**
   * Event handler which informs the {@link Layout} when a all renders in one drawing cycle have finished animating.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onViewportAllRendersDrawn: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link Canvas} bounds have been set.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onCanvasBoundsSet: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link Frame} bounds have been set.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onFrameBoundsSet: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link ItemCollection} filter has been set.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onItemCollectionFilterSet: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link ItemCollection} sort by function has been set.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onItemCollectionSortBySet: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link ItemCollection} reversed flag has been set.
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onItemCollectionReversedSet: _.noop,

  /**
   * Event handler which informs the {@link Layout} when the {@link ItemCollection} has been (re-)indexed..
   *
   * @param {DecksEvent} e - event object
   * @return {undefined}
   */
  onItemCollectionIndexed: _.noop,

  /**
   * Gets the animation to use/merge when showing a render.
   * This would typically be transform properties that ensure the element
   * is fully visible (e.g. opacity: 1, scaleX: 1, scaleY: 1, display: auto, etc.)
   *
   * Override this method in a {@link Layout} subclass or with {@link Layout} options
   * to provide a custom "show" animation.
   */
  getShowAnimation: function getShowAnimation() {
    return {
      transform: {
        //opacity: 1,
        scaleX: 1,
        scaleY: 1,
        rotateZ: 0
      },
      animateOptions: {
        duration: 400,
        display: "auto"
      }
    };
  },

  /**
   * Gets the base animation to use/merge when hiding (removing) a render
   * This is typically the opposite of the show animation transform.  E.g.
   * if the show animation sets opacity: 1, this might set opacity: 0.
   *
   * Override this method in a {@link Layout} subclass or with {@link Layout} options
   * to provide a custom "hide" animation.
   */
  getHideAnimation: function getHideAnimation() {
    return {
      transform: {
        //opacity: 0,
        scaleX: 0,
        scaleY: 0,
        rotateZ: 0
      },
      animateOptions: {
        duration: 400,
        display: "none"
      }
    };
  },

  /**
   * Sets the animation on a render to include the default show animation.
   *
   * @param {!Object} render - render on which to add the show animation
   * @returns {undefined}
   */
  setShowAnimation: function setShowAnimation(render) {
    _.merge(render, this.getShowAnimation());
  },

  /**
   * Sets an animation on a render to remove the render.  The implementation of this method should set
   * transform and animateOptions properties on the render.
   *
   * @param {!Object} render render on which to set animation
   * @return {undefined}
   */
  setHideAnimation: function setHideAnimation(render) {
    _.merge(render, this.getHideAnimation());
  },

  /**
   * Returns an array of render objects, or a single render object, which are not associated
   * with items.  This can be used to draw custom elements on the {@link Canvas}, like divider lines,
   * non-item-associated labels, etc.
   *
   * @param {Object} options - standard layout method options (viewport, frame, etc.)
   * @return {Object[]} - array of custom render objects
   */
  getCustomRenders: function getCustomRenders(/*options*/) {
    return {};
  },

  /**
   * Gets the default gesture handler options to apply to render elements
   *
   * @param {!Object} render - render object
   * @param {!Object} options - standard {@link Layout} method options
   * @return {Object} - {@link GestureHandler} options to apply to the render
   */
  getRenderGestureOptions: function getRenderGestureOptions() {
    return {
      gestures: {
        pan: {
          enabled: false,
          horizontal: true,
          vertical: false
        },
        swipe: {
          enabled: false,
          horizontal: true,
          vertical: false
        }
      },
      snapping: {
        toBounds: true,
        toNearestChildElement: false
      }
    };
  },

  /**
   * Gets the gesture handler options to use for the {@link Canvas} for this {@link Layout}.
   *
   * Each {@link Layout} might call for different {@link Canvas} gestures, like a vertical list Layout
   * might only allow vertical panning/swiping, whereas a horizontal list might only allow horizontal
   * scrolling.
   *
   * Override this method in a {@link Layout} subclass or {@link Layout} options.
   */
  getCanvasGestureOptions: function getCanvasGestureOptions() {
    return {};
  },

  /**
   * Gets the {@link Layout}s preferences for how the canvas is resized when elements are added
   * or removed.
   *
   * @return {Object} - the resize options
   */
  getCanvasBoundsOptions: function getCanvasBoundsOptions() {
    return {
      marginRight: 0,
      marginBottom: 0,
      smartMarginRight: true,
      smartMarginBottom: true,
      preventOverflowHorizontal: false,
      preventOverflowVertical: false,
      preventScrollbarHorizontal: false,
      preventScrollbarVertical: false,
      scrollbarSize: 20
    };
  },

  /**
   * Gets extra offsets to apply when panning to an item
   *
   * @param {!Element} element - element being moved to
   * @return {undefined}
   */
  getMoveToElementOffsets: function getMoveToElementOffsets(/*element*/) {
    return {
      x: 0,
      y: 0
    };
  }
});

module.exports = Layout;