Source: src/main/javascript/oracle/oj/ojfilmstrip/ojfilmstrip.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2015, Oracle and/or its affiliates.
 * All rights reserved.
 */

/**
 * @ojcomponent oj.ojFilmStrip
 * @augments oj.baseComponent
 * @since 1.1.0
 * 
 * @classdesc
 * <h3 id="filmStripOverview-section">
 *   JET FilmStrip Component
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#filmStripOverview-section"></a>
 * </h3>
 * 
 * <p>Description: Container component that lays out its children in a single 
 * row or column across logical pages and allows navigating through them.
 * 
 * <p>A JET FilmStrip can be created from any 
 * <code class="prettyprint"><div></code> element that contains multiple 
 * direct child elements.  All child elements are expected to be the same size.
 * <p>FilmStrip will layout the child items across multiple logical pages and 
 * allow for changing between logical pages.  When the component is resized, 
 * relayout will occur and the number of pages and number of items shown per 
 * page may change.
 * 
 * <pre class="prettyprint">
 * <code>
 * <div id="filmStrip" data-bind="ojComponent: {component: 'ojFilmStrip'}">
 *   <div id="item1" class="my-filmstrip-item">Alpha</div>
 *   <div id="item2" class="my-filmstrip-item">Beta</div>
 *   <div id="item3" class="my-filmstrip-item">Gamma</div>
 *   <div id="item4" class="my-filmstrip-item">Delta</div>
 *   <div id="item5" class="my-filmstrip-item">Epsilon</div>
 *   <div id="item6" class="my-filmstrip-item">Zeta</div>
 * </div>
 * </code></pre>
 * 
 * <p>JET FilmStrip and ConveyorBelt look similar, but are intended to be used
 * for different purposes.
 * <p>Use FilmStrip when you want to:
 * <ul>
 * <li>layout a set of items across discrete logical pages</li>
 * <li>control which and how many items are shown</li>
 * <li>hide items outside the current viewport from tab order and screen reader</li>
 * </ul>
 * <p>Use ConveyorBelt when you want to:
 * <ul>
 * <li>handle overflow without showing a scrollbar</li>
 * <li>keep all items accessible via tabbing and readable by a screen reader</li>
 * </ul>
 *
 *
 * <h3 id="touch-section">
 *   Touch End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#touch-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"touchDoc"}
 * 
 * 
 * <h3 id="keyboard-section">
 *   Keyboard End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#keyboard-section"></a>
 * </h3>
 * 
 * {@ojinclude "name":"keyboardDoc"}
 * 
 * 
 * <h3 id="keyboard-appdev-section">
 *   Keyboard Application Developer Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#keyboard-appdev-section"></a>
 * </h3>
 * 
 * <p>FilmStrip is for layout only.  Providing keyboard support for the child 
 * items in the filmstrip is the responsibility of the application developer, if
 * the items do not already support keyboard interaction.  This could be done, 
 * for example, by specifying <code class="prettyprint">tabindex</code> on each 
 * item to enable tab navigation among them.  
 *
 * <p>Only the child items of the FilmStrip that are visible in the current 
 * logical page can be tabbed to.  Items outside the current logical page are
 * hidden from the tab order.  
 * 
 * 
 * <h3 id="accessibility-section">
 *   Accessibility
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#accessibility-section"></a>
 * </h3>
 *
 * <p>FilmStrip is for layout only.  It is the responsibility of the 
 * application developer to make the items in the filmstrip accessible.  Sighted 
 * keyboard-only users need to be able to access the items in the layout just by
 * using the keyboard.  It is up to the child items of the FilmStrip to 
 * support keyboard navigation.  
 *
 * <p>Only the child items of the FilmStrip that are visible in the current 
 * logical page can be read by a screen reader.  Items outside the current 
 * logical page are hidden from a screen reader.  
 * 
 * <p>FilmStrip assigns itself the WAI-ARIA 
 * <code class="prettyprint">role</code> of 
 * <code class="prettyprint">"region"</code>.  The application <b>must</b> 
 * specify a WAI-ARIA label for the FilmStrip in the DOM so that screen reader
 * users will understand the purpose of the FilmStrip.  
 * 
 * <p>The application <b>must</b> provide accessible controls for navigating 
 * through the logical pages of the FilmStrip for use by screen reader users, 
 * because the arrow keys may be intercepted by the screen reader itself.  The
 * navigation arrows built into FilmStrip can be used, or if they are hidden,
 * then alternative means, such as a paging control, must be provided.  
 * 
 * 
 * <h3 id="perf-section">
 *   Performance
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#perf-section"></a>
 * </h3>
 * 
 * <h4>Lazy Rendering</h4>
 * <p>If there are many items in the FilmStrip or complex content in each item,
 * it is recommended to implement lazy rendering of item content.  The application
 * should keep track of which items have rendered their content.  Items that are
 * initially visible in the FilmStrip should render their content from the start.
 * The application should then listen to events from the FilmStrip's 
 * <code class="prettyprint">PagingModel</code> in order to render content for
 * items that are paged into view later.  
 * 
 * 
 * <h3 id="rtl-section">
 *   Reading direction
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#rtl-section"></a>
 * </h3>
 * 
 * <p>As with any JET component, in the unusual case that the directionality 
 * (LTR or RTL) changes post-init, the FilmStrip must be 
 * <code class="prettyprint">refresh()</code>ed.
 * 
 * 
 * <h3 id="pseudos-section">
 *   Pseudo-selectors
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#pseudos-section"></a>
 * </h3>
 * 
 * <p>The <code class="prettyprint">:oj-filmstrip</code> pseudo-selector can 
 * be used in jQuery expressions to select JET FilmStrip.  For example:
 * 
 * <pre class="prettyprint">
 * <code>$( ":oj-filmstrip" ) // selects all JET FilmStrips on the page
 * $myEventTarget.closest( ":oj-filmstrip" ) // selects the closest ancestor that is a JET FilmStrip
 * </code></pre>
 * 
 * 
 * <h3 id="jqui2jet-section">
 *   JET for jQuery UI developers
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#jqui2jet-section"></a>
 * </h3>
 * 
 * <p>Event names for all JET components are prefixed with "oj", instead of 
 * component-specific prefixes like "filmStrip".  
 * 
 * <!-- - - - - Above this point, the tags are for the class.
 *              Below this point, the tags are for the constructor (initializer). - - - - - - -->
 * 
 * @desc Creates a JET FilmStrip. 
 * @example <caption>Initialize the FilmStrip with no options specified:</caption>
 * $( ".selector" ).ojFilmStrip();
 * 
 * @example <caption>Initialize the FilmStrip with some options specified:</caption>
 * $( ".selector" ).ojFilmStrip( { "orientation": "vertical" } );
 * 
 * @example <caption>Initialize the FilmStrip via the JET 
 * <code class="prettyprint">ojComponent</code> binding:</caption>
 * <div id="filmStrip" data-bind="ojComponent: { component: 'ojFilmStrip', orientation: 'vertical'}">
 */
(function()
{
oj.__registerWidget("oj.ojFilmStrip", $['oj']['baseComponent'],
{
  defaultElement: "<div>",
  widgetEventPrefix: "oj",

  options:
  {
    /** 
     * Specify the maximum number of child items to show in a logical page.
     * A value of 0 (the default) means that FilmStrip will show as many items
     * per logical page as will fit based on the component and item sizes.
     * 
     * <p>FilmStrip may show fewer than the specified number of items when 
     * <code class="prettyprint">maxItemsPerPage</code> is set to a value other 
     * than 0 if the component size is smaller than what would be required to 
     * show that many items.  
     *
     * @expose 
     * @memberof! oj.ojFilmStrip
     * @instance
     * @type {number}
     * @default <code class="prettyprint">0</code>
     * @see #getItemsPerPage
     *
     * @example <caption>Initialize the FilmStrip with the 
     * <code class="prettyprint">maxItemsPerPage</code> option specified:</caption>
     * $( ".selector" ).ojFilmStrip( { "maxItemsPerPage": 3 } );
     * 
     * @example <caption>Get or set the <code class="prettyprint">maxItemsPerPage</code> 
     * option after initialization:</caption>
     * // getter
     * var maxItemsPerPage = $( ".selector" ).ojFilmStrip( "option", "maxItemsPerPage" );
     * 
     * // setter
     * $( ".selector" ).ojFilmStrip( "option", "maxItemsPerPage", 3 );
     */
    maxItemsPerPage: 0,
    
    /** 
     * Specify the orientation of the FilmStrip.
     *
     * @expose 
     * @memberof! oj.ojFilmStrip
     * @instance
     * @type {string}
     * @ojvalue {string} "horizontal" Orient the FilmStrip horizontally.
     * @ojvalue {string} "vertical" Orient the FilmStrip vertically.
     * @default <code class="prettyprint">"horizontal"</code>
     *
     * @example <caption>Initialize the FilmStrip with the 
     * <code class="prettyprint">orientation</code> option specified:</caption>
     * $( ".selector" ).ojFilmStrip( { "orientation": "vertical" } );
     * 
     * @example <caption>Get or set the <code class="prettyprint">orientation</code> 
     * option after initialization:</caption>
     * // getter
     * var orientation = $( ".selector" ).ojFilmStrip( "option", "orientation" );
     * 
     * // setter
     * $( ".selector" ).ojFilmStrip( "option", "orientation", "vertical" );
     */
    orientation: "horizontal",
    
    /** 
     * Specify the child item whose logical page should be displayed.  The
     * position of the item on the logical page is not guaranteed.
     * 
     * <p>This option can be set to either the string id of the item or the 
     * numeric 0-based index of the item.  
     * 
     * <p>FilmStrip will automatically update the value of this option when the 
     * logical page is changed to be the first item on the new logical page.  
     * When the value is updated automatically, preference will be given to use 
     * a string id, if possible.  
     * 
     * <p>When the component is resized, FilmStrip will preserve the value of 
     * this option to show the new logical page on which the item is located.  
     *
     * @expose 
     * @memberof! oj.ojFilmStrip
     * @instance
     * @type {(string|number)}
     * @default <code class="prettyprint">0</code>
     *
     * @example <caption>Initialize the FilmStrip with the 
     * <code class="prettyprint">currentItem</code> option specified:</caption>
     * $( ".selector" ).ojFilmStrip( { "currentItem": 3 } );
     * 
     * @example <caption>Get or set the <code class="prettyprint">currentItem</code> 
     * option after initialization:</caption>
     * // getter
     * var currentItem = $( ".selector" ).ojFilmStrip( "option", "currentItem" );
     * 
     * // setter
     * $( ".selector" ).ojFilmStrip( "option", "currentItem", 3 );
     */
    currentItem: 0,
    
    /**
     * Specify the placement of the navigation arrows.
     *
     * @expose 
     * @memberof! oj.ojFilmStrip
     * @instance
     * @type {string}
     * @ojvalue {string} "adjacent" Arrows are outside, adjacent to the filmStrip 
     *          content.  The arrows are still inside the bounds of the filmStrip
     *          component itself.
     * @ojvalue {string} "overlay" Arrows are inside, overlaying the filmStrip content.
     * @default <code class="prettyprint">"adjacent"</code>
     *
     * @example <caption>Initialize the FilmStrip with the 
     * <code class="prettyprint">arrowPlacement</code> option specified:</caption>
     * $( ".selector" ).ojFilmStrip( { "arrowPlacement": "overlay" } );
     * 
     * @example <caption>Get or set the <code class="prettyprint">arrowPlacement</code> 
     * option after initialization:</caption>
     * // getter
     * var arrowPlacement = $( ".selector" ).ojFilmStrip( "option", "arrowPlacement" );
     * 
     * // setter
     * $( ".selector" ).ojFilmStrip( "option", "arrowPlacement", "overlay" );
     */
    arrowPlacement: "adjacent",
    
    /** 
     * Specify the visibility of the navigation arrows.
     *
     * @expose 
     * @memberof! oj.ojFilmStrip
     * @instance
     * @type {string}
     * @ojvalue {string} "visible" Arrows are visible.
     * @ojvalue {string} "hidden" Arrows are hidden.
     * @ojvalue {string} "hover" Arrows are visible when the mouse is over the 
     *          filmStrip, and hidden otherwise.
     * @ojvalue {string} "auto" Behaves as if the value were 
     *          <code class="prettyprint">visible</code> when the 
     *          <code class="prettyprint">arrowPlacement</code> 
     *          option is set to <code class="prettyprint">adjacent</code>, and
     *          <code class="prettyprint">hover</code> when the 
     *          <code class="prettyprint">arrowPlacement</code> 
     *          option is set to <code class="prettyprint">overlay</code>.
     * @default <code class="prettyprint">"auto"</code>
     *
     * @example <caption>Initialize the FilmStrip with the 
     * <code class="prettyprint">arrowVisibility</code> option specified:</caption>
     * $( ".selector" ).ojFilmStrip( { "arrowVisibility": "visible" } );
     * 
     * @example <caption>Get or set the <code class="prettyprint">arrowVisibility</code> 
     * option after initialization:</caption>
     * // getter
     * var arrowVisibility = $( ".selector" ).ojFilmStrip( "option", "arrowVisibility" );
     * 
     * // setter
     * $( ".selector" ).ojFilmStrip( "option", "arrowVisibility", "visible" );
     */
    arrowVisibility: "auto"
    
    /**
     * FilmStrip inherits the <code class="prettyprint">disabled</code> 
     * option from its superclass, but does not support it in order to avoid 
     * tight coupling between a FilmStrip and its contents.  Setting this
     * option on FilmStrip has no effect.
     * 
     * <p><b>WARNING:</b> Applications should not depend on this behavior 
     * because we reserve the right to change it in the future in order to 
     * support <code class="prettyprint">disabled</code> and propagate it to 
     * child components of FilmStrip.  
     * 
     * @member
     * @name disabled
     * @memberof oj.ojFilmStrip
     * @instance
     * @type {boolean}
     * @default <code class="prettyprint">false</code>
     */
    // disabled option declared in superclass, but we still want the above API doc

    // Events

    /**
     * Triggered when the filmStrip is created.
     *
     * @event
     * @name create
     * @memberof oj.ojFilmStrip
     * @instance
     * @property {Event} event <code class="prettyprint">jQuery</code> event object
     * @property {Object} ui Currently empty
     *
     * @example <caption>Initialize the filmStrip with the <code class="prettyprint">create</code> callback specified:</caption>
     * $( ".selector" ).ojFilmStrip({
     *     "create": function( event, ui ) {}
     * });
     *
     * @example <caption>Bind an event listener to the <code class="prettyprint">ojcreate</code> event:</caption>
     * $( ".selector" ).on( "ojcreate", function( event, ui ) {} );
     */
    // create event declared in superclass, but we still want the above API doc
  },

  /**
   * After _ComponentCreate and _AfterCreate, 
   * the widget should be 100% set up. this._super should be called first.
   * @override
   * @protected
   * @instance
   * @memberof oj.ojFilmStrip
   */
  _ComponentCreate: function() // Override of protected base class method.  
  {
    //call superclass first
    this._super();
    
    var elem = this.element;  
    elem
      .addClass("oj-filmstrip oj-component")
      .attr("tabindex", 0)
      .attr("role", "region");
    
    //ensure a unique id for use with aria-labelledby on navigation arrows
    elem.uniqueId();
    
    //log warning message when "disabled" option set
    var options = this.options;
    if (options.disabled)
      oj.Logger.warn(_WARNING_DISABLED_OPTION);
    
    //throw error if "orientation" option set to invalid value
    if (options.orientation !== _HORIZONTAL && options.orientation !== _VERTICAL)
      throw new Error(_ERROR_INVALID_ORIENTATION + options.orientation);
    
    //throw error if "arrowPlacement" option set to invalid value
    if (options.arrowPlacement !== _ADJACENT && 
        options.arrowPlacement !== _OVERLAY)
      throw new Error(_ERROR_INVALID_NAV_ARROW_PLACEMENT + options.arrowPlacement);
    
    //throw error if "arrowVisibility" option set to invalid value
    if (options.arrowVisibility !== _VISIBLE && 
        options.arrowVisibility !== _HIDDEN && 
        options.arrowVisibility !== _HOVER && 
        options.arrowVisibility !== _AUTO)
      throw new Error(_ERROR_INVALID_NAV_ARROW_VISIBILITY + options.arrowVisibility);
    
    this.touchEventNamespace = this.eventNamespace + "Touch";
    this.mouseEventNamespace = this.eventNamespace + "Mouse";
    this.keyEventNamespace = this.eventNamespace + "Key";
    this.navArrowHoverableEventNamespace = this.eventNamespace + "NavArrowHoverable";
    
    this._setup(true);
  },

  // This method currently runs at create, init, and refresh time (since refresh() is called by _init()).
  /**
   * Refreshes the visual state of the FilmStrip. JET components require a 
   * <code class="prettyprint">refresh()</code> or re-init after the DOM is 
   * programmatically changed underneath the component.
   * 
   * <p>This method does not accept any arguments.
   * 
   * @expose 
   * @memberof oj.ojFilmStrip
   * @instance
   * 
   * @example <caption>Invoke the <code class="prettyprint">refresh</code> method:</caption>
   * $( ".selector" ).ojFilmStrip( "refresh" );
   */
  refresh: function() // Override of public base class method.  
  {
    this._super();
    
    this._setup(false);
  },
  
  /**
   * Get the actual number of items shown per logical page.  
   * <p>The value returned by this method may be different from the value of the
   * <code class="prettyprint">maxItemsPerPage</code> option.
   * @returns {number} The actual number of items shown per logical page.
   * 
   * @expose 
   * @memberof oj.ojFilmStrip
   * @instance
   * @see #maxItemsPerPage
   * 
   * @example <caption>Invoke the <code class="prettyprint">getItemsPerPage</code> method:</caption>
   * var itemsPerPage = $( ".selector" ).ojFilmStrip( "getItemsPerPage" );
   */
  getItemsPerPage: function()
  {
    return this._itemsPerPage;
  },
  
  /**
   * Get the PagingModel created and used by the FilmStrip.  The PagingModel
   * provides information about the FilmStrip's logical pages and a way to
   * programmatically change pages.  
   * @returns {Object} The instance of the PagingModel created and used by the FilmStrip.
   * 
   * @expose 
   * @memberof oj.ojFilmStrip
   * @instance
   * 
   * @example <caption>Invoke the <code class="prettyprint">getPagingModel</code> method:</caption>
   * var pagingModel = $( ".selector" ).ojFilmStrip( "getPagingModel" );
   */
  getPagingModel: function()
  {
    return this._pagingModel;
  },
  
  /**
   * Notifies the component that its subtree has been made visible 
   * programmatically after the component has been created.
   * @memberof oj.ojFilmStrip
   * @instance
   * @protected 
   * @override
   */
  _NotifyShown: function()
  {
    this._super();
    //perform a deferred layout
    if (this._needsSetup)
      this._setup(this._needsSetup[0]);
  },
  
  /**
   * Notifies the component that its subtree has been connected to the document
   * programmatically after the component has been created.
   * @memberof oj.ojFilmStrip
   * @instance
   * @protected 
   * @override
   */
  _NotifyAttached: function()
  {
    this._super();
    //perform a deferred layout
    if (this._needsSetup)
      this._setup(this._needsSetup[0]);
  },

  // isInit is true for init (create and re-init), false for refresh
  /** 
   * Setup the filmStrip.
   * @param {boolean} isInit True if _setup is called from _init(), false
   *        if called from refresh().
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _setup: function(isInit) // Private, not an override (not in base class).  
  {
    //if filmStrip is detached or hidden, we can't layout correctly, so 
    //defer layout until filmStrip is attached or shown
    if (!this._canCalculateSizes())
    {
      //want a true value of isInit to take precedence over a false value
      var oldIsInit = false;
      if (this._needsSetup)
      {
        oldIsInit = this._needsSetup[0];
      }
      this._needsSetup = [isInit || oldIsInit];
      return;
    }
    this._needsSetup = null;
    
    this._bRTL = (this._GetReadingDirection() === "rtl");
    this._bTouchSupported = oj.DomUtils.isTouchSupported();
    var elem = this.element;
    var self = this;
    if (isInit)
    {
      this._itemsPerPage = 0;
      this._handlePageFunc = function(event) {self._handlePage(event);};
      this._pagingModel = new oj.FilmStripPagingModel();
      this._componentSize = 0;
      this._itemSize = -1;
      this._handleTransitionEndFunc = function(event) {self._handleTransitionEnd();};
      this._handleResizeFunc = function(width, height) {self._handleResize();};
      
      if (this._bTouchSupported)
      {
        this._handleTouchStartFunc = function(event) {self._handleTouchStart(event);};
        this._handleTouchMoveFunc = function(event) {self._handleTouchMove(event);};
        this._handleTouchEndFunc = function(event) {self._handleTouchEnd(event);};
        this._addTouchListeners();
      }
      this._handleMouseDownFunc = function(event) {self._handleMouseDown(event);};
      this._handleMouseMoveFunc = function(event) {self._handleMouseMove(event);};
      this._handleMouseUpFunc = function(event) {self._handleMouseUp(event);};
      this._addMouseListeners();
      this._handleKeyDownFunc = function(event) {self._handleKeyDown(event);};
      this._addKeyListeners();
    }
    else
      this._destroyInternal();
    
    //notify the original children of the filmStrip that they were detached from
    //the DOM BEFORE actually detaching so that components can save state
    var originalItems = elem.children();
    for (var i = 0; i < originalItems.length; i++)
      oj.Components.subtreeDetached(originalItems[i]);
    
    //initialize the paging model with the total number of items
    if (isInit)
    {
      var pagingModel = this._pagingModel;
      pagingModel.setTotalSize(originalItems.length);
      //register the page change listener
      pagingModel.on("page", this._handlePageFunc);
    }
    
    //wrap the original child DOM BEFORE adding the resize listener
    this._wrapChildren();
    this._adjustSizes();
    
    //notify the original children of the filmStrip that they were attached
    //again after wrapping them
    for (var i = 0; i < originalItems.length; i++)
      oj.Components.subtreeAttached(originalItems[i]);

    oj.DomUtils.addResizeListener(elem[0], this._handleResizeFunc);
  },

  /** 
   * Destroy the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @override
   * @protected
   */
  _destroy: function() // Override of protected base class method.  
  {
    if (this._bTouchSupported)
    {
      this._removeTouchListeners();
      this._handleTouchStartFunc = null;
      this._handleTouchMoveFunc = null;
      this._handleTouchEndFunc = null;
    }
    this._removeMouseListeners();
    this._handleMouseDownFunc = null;
    this._handleMouseMoveFunc = null;
    this._handleMouseUpFunc = null;
    this._removeKeyListeners();
    this._handleKeyDownFunc = null;
    
    this._destroyInternal();
    this._pagingModel.off("page", this._handlePageFunc);
    this._handlePageFunc = null;
    this._pagingModel = null;
    this._handleResizeFunc = null;
    this._handleTransitionEndFunc = null;
    
    var elem = this.element;
    elem
      .removeClass("oj-filmstrip oj-component " + _OJ_FILMSTRIP_HOVER)
      .removeAttr("tabindex role");
    
    //remove a generated unique id
    elem.removeUniqueId();
    
    this._super();
  },
  
  /** 
   * Destroy the internal structure of the filmStrip that will be recreated
   * during a refresh.
   * @memberof oj.ojMasonryLayout
   * @instance
   * @private
   */
  _destroyInternal: function()
  {
    var elem = this.element;
    oj.DomUtils.removeResizeListener(elem[0], this._handleResizeFunc);
    //reset item size so it will be recalculated at the next layout
    this._itemSize = -1;
    if (this._queuedHandleResize)
    {
      clearTimeout(this._queuedHandleResize);
      this._queuedHandleResize = null;
    }
    
    var originalItems = this._getItems();
    //notify the original children of the filmStrip that they were detached from
    //the DOM BEFORE actually detaching so that components can save state
    for (var i = 0; i < originalItems.length; i++)
      oj.Components.subtreeDetached(originalItems[i]);
    
    this._clearCalculatedSizes();
    var itemContainers = this._getItemContainers();
    //remove logical page containers here instead of in _unwrapChildren because 
    //they're added in _adjustSizes, not in _wrapChildren
    itemContainers.unwrap();
    this._unwrapChildren();
    
    //notify the original children of the filmStrip that they were attached 
    //again after unwrapping them
    for (var i = 0; i < originalItems.length; i++)
      oj.Components.subtreeAttached(originalItems[i]);
  },

  /** 
   * Set an option on the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @override
   * @protected
   */
  _setOption: function(key, value, flags) // Override of protected base class method.
                                   // Method name needn't be quoted since is in externs.js.
  {
    var options = this.options;
    var bRefresh = false;
    var newPageIndex = -1;
    var pagingModel = this._pagingModel;
    var oldPageIndex = pagingModel.getPage();
    switch (key) 
    {
      case "disabled":
        //log warning message when "disabled" option set
        oj.Logger.warn(_WARNING_DISABLED_OPTION);
        break;
      case "orientation":
        //throw error if "orientation" option set to invalid value
        if (value !== _HORIZONTAL && value !== _VERTICAL)
          throw new Error(_ERROR_INVALID_ORIENTATION + value);
        bRefresh = (options.orientation != value);
        break;
      case "maxItemsPerPage":
        bRefresh = (options.maxItemsPerPage != value);
        break;
      case "arrowPlacement":
        //throw error if "arrowPlacement" option set to invalid value
        if (value !== _ADJACENT && value !== _OVERLAY)
          throw new Error(_ERROR_INVALID_NAV_ARROW_PLACEMENT + value);
        bRefresh = (options.arrowPlacement != value);
        break;
      case "arrowVisibility":
        //throw error if "arrowVisibility" option set to invalid value
        if (value !== _VISIBLE && value !== _HIDDEN && 
            value !== _HOVER && value !== _AUTO)
          throw new Error(_ERROR_INVALID_NAV_ARROW_VISIBILITY + value);
        bRefresh = (options.arrowVisibility != value);
        break;
      case _CURRENT_ITEM:
        if (options.currentItem != value)
        {
          newPageIndex = this._findPage(value);
          //throw error if item not found
          if (newPageIndex < 0 || newPageIndex >= pagingModel.getPageCount())
            throw new Error(_ERROR_CURRENT_ITEM_NOT_FOUND + value);
        }
        break;
    }
    
    this._super(key, value, flags);
    
    switch (key)
    {
      case _CURRENT_ITEM:
        if (newPageIndex > -1 && newPageIndex != oldPageIndex)
          pagingModel.setPage(newPageIndex);
        break;
    }
    
    if (bRefresh)
      this._setup(false);
  },
  
  /** 
   * Handle a component resize.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleResize: function()
  {
    //handle the resize if not already handling one
    if (!this._bHandlingResize)
    {
      this._bHandlingResize = true;
      this._adjustSizes(true);
      this._bHandlingResize = false;
    }
    //if already handling a resize, queue another one
    else if (!this._queuedHandleResize)
    {
      var self = this;
      this._queuedHandleResize = setTimeout(function()
        {
          self._queuedHandleResize = null;
          self._handleResize();
        }, 0);
    }
  },
  
  /** 
   * Determine whether the filmStrip is oriented horizontally or vertically.
   * @returns {boolean} True if horizontal, false if vertical.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _isHorizontal: function()
  {
    var options = this.options;
    return (options.orientation !== _VERTICAL);
  },
  
  /** 
   * Get the CSS position attribute to use.
   * @returns {string} CSS position attribute name.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getCssPositionAttr: function()
  {
    var bHorizontal = this._isHorizontal();
    return bHorizontal ? (this._bRTL ? "right" : "left") : "top";
  },
  
  /** 
   * Get the CSS size attribute to use.
   * @returns {string} CSS size attribute name.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getCssSizeAttr: function()
  {
    var bHorizontal = this._isHorizontal();
    return bHorizontal ? "width" : "height";
  },

  /** 
   * Determine whether the filmStrip can calculate sizes (when it is 
   * attached to the page DOM and not hidden).
   * @returns {boolean} True if sizes can be calculated, false if not.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _canCalculateSizes: function() 
  {
    var div = document.createElement("div");
    var style = div.style;
    //need to set absolute position in order to get correct offsetwidth when 
    //flexbox layout is applied; otherwise the offsetwidth becomes 0
    style.position = "absolute";
    style.width = "10px";
    style.height = "10px";
    var elem = this.element[0];
    elem.appendChild(div);
    var bCanCalcSizes = false;
    try
    {
      bCanCalcSizes = div.offsetWidth > 0 && div.offsetHeight > 0;
    }
    catch (e)
    {
      //do nothing
    }
    elem.removeChild(div);
    return bCanCalcSizes;
  },
  
  /** 
   * Wrap the original child items of the filmStrip in internal layout structure.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _wrapChildren: function()
  {
    var elem = this.element;
    var bHorizontal = this._isHorizontal();
    var originalItems = elem.children();
    
    originalItems
      .addClass(_OJ_FILMSTRIP_ITEM)
      .wrap("<div class='" + _OJ_FILMSTRIP_CONTAINER + " " + _OJ_FILMSTRIP_ITEM_CONTAINER + "'></div>"); // @HTMLUpdateOK
    
    //need to initially specify the position on the pagesWrapper so that we can
    //always get the value later
    var cssAttr = this._getCssPositionAttr();
    var pagesWrapper = elem.children()
      .wrapAll("<div class='" + _OJ_FILMSTRIP_CONTAINER + " " + _OJ_FILMSTRIP_PAGES_CONTAINER + "' style='" + cssAttr + ": 0px;'></div>") // @HTMLUpdateOK
      .parent();
    this._pagesWrapper = pagesWrapper;
    
    var options = this.options;
    if (options.arrowVisibility !== _HIDDEN &&
        options.arrowPlacement === _ADJACENT)
    {
      this._contentWrapper = pagesWrapper
        .wrap("<div class='oj-filmstrip-content-container'></div>") // @HTMLUpdateOK
        .parent();
    }
    
    elem.addClass(_OJ_FILMSTRIP_CONTAINER);
    if (!bHorizontal)
      elem.addClass(_OJ_FILMSTRIP_VERTICAL);
    
    if (options.arrowVisibility !== _HIDDEN)
    {
      this._prevButton = this._createPrevNavArrow();
      this._nextButton = this._createNextNavArrow();

      if (this._navArrowsShownOnHover())
        this._setupNavArrowsHoverable();
    }
  },
  
  /** 
   * Unwrap the internal layout structure around the original child items of the 
   * filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _unwrapChildren: function()
  {
    var elem = this.element;
    var originalItems = this._getItems();
    
    //unregister elem as hoverable
    this._tearDownNavArrowsHoverable();
    
    //remove navigation arrow containers
    if (this._prevButton)
    {
      this._UnregisterChildNode(this._prevButton);
      this._prevButton = null;
    }
    if (this._nextButton)
    {
      this._UnregisterChildNode(this._nextButton);
      this._nextButton = null;
    }
    var navArrowContainers = elem.children(_PERIOD + _OJ_FILMSTRIP_ARROW_CONTAINER);
    if (navArrowContainers)
      navArrowContainers.remove();
    
    //the individual page containers that were added in _adjustSizes are not
    //removed here; they are expected to have been removed before calling
    //_unwrapChildren
    originalItems
      .removeClass(_OJ_FILMSTRIP_ITEM) //original children
      .unwrap()                        //remove item containers
      .unwrap();                       //remove pages container
    this._pagesWrapper = null;
    if (this._contentWrapper)
    {
      originalItems.unwrap();          //remove content container
      this._contentWrapper = null;
    }
    
    elem.removeClass(_OJ_FILMSTRIP_CONTAINER + " " + _OJ_FILMSTRIP_VERTICAL);
  },
  
  /** 
   * Setup events to only show the navigation arrows when the mouse hovers over
   * the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _setupNavArrowsHoverable: function()
  {
    //Note: Use oj-filmstrip-hover marker instead of standard oj-hover marker
    //because if we use the standard one, then when it's added to the filmStrip
    //root node, all hoverables inside the filmStrip appear as if the mouse
    //were hovering over them.  
    //Note: keep this logic updated if superclass _hoverable method changes.
    var elem = this.element;
    elem
      .on("mouseenter" + this.navArrowHoverableEventNamespace, function(event) {
        if (!$(event.currentTarget).hasClass( "oj-disabled" ))
          $(event.currentTarget).addClass(_OJ_FILMSTRIP_HOVER);
      })
      .on("mouseleave" + this.navArrowHoverableEventNamespace, function(event) {
        $(event.currentTarget).removeClass(_OJ_FILMSTRIP_HOVER);
      });
  },
  
  /** 
   * Tear down events to only show the navigation arrows when the mouse hovers 
   * over the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _tearDownNavArrowsHoverable: function()
  {
    var elem = this.element;
    elem.off(this.navArrowHoverableEventNamespace);
  },
  
  /** 
   * Determine whether navigation arrows should only be shown on hover.
   * @returns {boolean} True if nav arrows should only be shown on hover, false
   *          otherwise.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _navArrowsShownOnHover: function()
  {
    var options = this.options;
    var arrowVisibility = options.arrowVisibility;
    return  (arrowVisibility === _HOVER || 
             (arrowVisibility === _AUTO && options.arrowPlacement === _OVERLAY));
  },
  
  /** 
   * Determine whether there is a previous logical page.
   * @returns {boolean} True if there is a previous logical page, false otherwise.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _hasPrevPage: function()
  {
    var pagingModel = this._pagingModel;
    return pagingModel.getPage() > 0;
  },
  
  /** 
   * Determine whether there is a next logical page.
   * @returns {boolean} True if there is a next logical page, false otherwise.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _hasNextPage: function()
  {
    var pagingModel = this._pagingModel;
    return pagingModel.getPage() < pagingModel.getPageCount() - 1;
  },
  
  /** 
   * Go to the previous logical page.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _prevPage: function()
  {
    if (this._hasPrevPage())
    {
      var pagingModel = this._pagingModel;
      pagingModel.setPage(pagingModel.getPage() - 1);
    }
  },
  
  /** 
   * Go to the next logical page.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _nextPage: function()
  {
    if (this._hasNextPage())
    {
      var pagingModel = this._pagingModel;
      pagingModel.setPage(pagingModel.getPage() + 1);
    }
  },
  
  /** 
   * Determine whether there is a next logical page.
   * @param {boolean} bShow True to show the navigation arrow, false to hide it.
   * @param {jQuery} jqNavArrow Navigation arrow jQuery object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _displayNavigationArrow: function(bShow, jqNavArrow)
  {
    var options = this.options;
    var navArrowPlacement = options.arrowPlacement;
    if (navArrowPlacement === _ADJACENT)
      jqNavArrow.css("visibility", bShow ? "" : _HIDDEN);
    else
      jqNavArrow.parent().css("display", bShow ? "" : _NONE);
  },
  
  /** 
   * Update whether the navigation arrows are shown.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _updateNavigationArrowsDisplay: function()
  {
    var options = this.options;
    var navArrowVisibility = options.arrowVisibility;
    if (navArrowVisibility !== _HIDDEN)
    {
      var pagingModel = this._pagingModel;
      var pageIndex = pagingModel.getPage();
      var pageCount = pagingModel.getPageCount();
      this._displayNavigationArrow(pageIndex !== 0, this._prevButton);
      this._displayNavigationArrow(pageIndex !== pageCount - 1, this._nextButton);
    }
  },
  
  /** 
   * Create the previous navigation arrow.
   * @returns {jQuery} Navigation arrow jQuery object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _createPrevNavArrow: function()
  {
    var elem = this.element;
    var bHorizontal = this._isHorizontal();
    var locationMarker = bHorizontal ? _OJ_START : _OJ_TOP;
    var container = this._createNavigationArrowContainer(locationMarker);
    
    var options = this.options;
    var navArrowPlacement = options.arrowPlacement;
    //need to append prev button when overlay so that it is in front of the
    //filmstrip items in z-order
    if (navArrowPlacement === _OVERLAY)
      elem.append(container); // @HTMLUpdateOK
    else
      elem.prepend(container); // @HTMLUpdateOK
    
    var label = _escapeHtml(this.getTranslatedString("labelAccArrowPreviousPage"));
    var tooltip = _escapeHtml(this.getTranslatedString("tipArrowPreviousPage"));
    var navArrow = this._createNavigationArrow(container, locationMarker, label, tooltip);
    var self = this;
    navArrow.on("click", function() {self._prevPage();});
    
    return navArrow;
  },
  
  /** 
   * Create the next navigation arrow.
   * @returns {jQuery} Navigation arrow jQuery object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _createNextNavArrow: function()
  {
    var elem = this.element;
    var bHorizontal = this._isHorizontal();
    var locationMarker = bHorizontal ? _OJ_END : _OJ_BOTTOM;
    var container = this._createNavigationArrowContainer(locationMarker);
    elem.append(container); // @HTMLUpdateOK
    
    var label = _escapeHtml(this.getTranslatedString("labelAccArrowNextPage"));
    var tooltip = _escapeHtml(this.getTranslatedString("tipArrowNextPage"));
    var navArrow = this._createNavigationArrow(container, locationMarker, label, tooltip);
    var self = this;
    navArrow.on("click", function() {self._nextPage();});
    
    return navArrow;
  },
  
  /** 
   * Create the container around a navigation arrow.
   * @param {string} locationMarker Marker class for the location of the 
   *        navigation arrow: oj-top, oj-bottom, oj-start, or oj-end.
   * @returns {jQuery} jQuery object for the container around a navigation arrow.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _createNavigationArrowContainer: function(locationMarker)
  {
    var container = $(document.createElement("div"));
    container.addClass(_OJ_FILMSTRIP_ARROW_CONTAINER + " " + locationMarker);
    var options = this.options;
    var navArrowPlacement = options.arrowPlacement;
    if (navArrowPlacement === _OVERLAY)
    {
      container.addClass("oj-filmstrip-arrow-container-overlay");
      
      if (this._navArrowsShownOnHover())
        container.addClass(_OJ_FILMSTRIP_ARROW_TRANSITION);
    }
    return container;
  },
  
  /** 
   * Create a navigation arrow.
   * @param {jQuery} parentElem Parent DOM element of the navigation arrow.
   * @param {string} locationMarker Marker class for the location of the 
   *        navigation arrow: oj-top, oj-bottom, oj-start, or oj-end.
   * @param {string} label Accessible label for the navigation arrow.
   * @param {string} tooltip Tooltip for the navigation arrow.
   * @returns {jQuery} Navigation arrow jQuery object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _createNavigationArrow: function(parentElem, locationMarker, label, tooltip)
  {
    var str = "<div class='" + _OJ_FILMSTRIP_ARROW + " oj-default oj-enabled " + locationMarker + "' role='button' tabindex='-1'";
    str += "><span class='oj-filmstrip-arrow-icon " + locationMarker + " oj-component-icon'></span></div>";
    parentElem.append(str); // @HTMLUpdateOK
    var elem = this.element;
    var elemId = elem.attr("id");
    var arrowElem = parentElem.children(_PERIOD + _OJ_FILMSTRIP_ARROW).eq(0);
    arrowElem.uniqueId();
    var arrowId = arrowElem.attr("id");
    if (label)
      arrowElem.attr("aria-label", label);
    if (tooltip)
      arrowElem.attr("title", tooltip);
    arrowElem.attr("aria-labelledby", arrowId + " " + elemId);
    this._hoverable(arrowElem);
    this._activeable(arrowElem);
    
    var options = this.options;
    var navArrowPlacement = options.arrowPlacement;
    if (navArrowPlacement === _ADJACENT)
    {
      if (this._navArrowsShownOnHover())
        arrowElem.addClass(_OJ_FILMSTRIP_ARROW_TRANSITION);
    }
    
    return arrowElem;
  },
  
  /** 
   * Get the containers around the original child items.
   * @returns {jQuery} The containers around the original child items.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getItemContainers: function()
  {
    var pagesWrapper = this._pagesWrapper;
    var items = pagesWrapper.find(_PERIOD + _OJ_FILMSTRIP_ITEM_CONTAINER);
    return items;
  },
  
  /** 
   * Get the original child items.
   * @returns {jQuery} The original child items.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getItems: function()
  {
    var pagesWrapper = this._pagesWrapper;
    var originalItems = pagesWrapper.find(_PERIOD + _OJ_FILMSTRIP_ITEM);
    return originalItems;
  },
  
  /** 
   * Get the logical page containers.
   * @returns {jQuery} The logical page containers.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getPages: function()
  {
    var pagesWrapper = this._pagesWrapper;
    var pages = pagesWrapper.children(_PERIOD + _OJ_FILMSTRIP_PAGE);
    return pages;
  },
  
  /** 
   * Clear the calculated sizes set on the internal layout DOM structure.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _clearCalculatedSizes: function()
  {
    var pagesWrapper = this._pagesWrapper;
    var pages = this._getPages();
    pages
      .css(_FLEX_BASIS, _EMPTY_STRING)
      .css(_WEBKIT_FLEX_BASIS, _EMPTY_STRING);
    var items = this._getItemContainers();
    items
      .css(_FLEX_BASIS, _EMPTY_STRING)
      .css(_WEBKIT_FLEX_BASIS, _EMPTY_STRING);
    pagesWrapper.css(this._getCssSizeAttr(), _EMPTY_STRING);
  },
  
  /** 
   * Calculate sizes and set them on the internal layout DOM structure.
   * @param {boolean} bNotifyAttach True to notify children when they're 
   *        attached to/detached from the DOM, false to not notify them (when 
   *        the notification will happen outside of this method).
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _adjustSizes: function(bNotifyAttach)
  {
    //clear previously calculated values before recalculating
    this._clearCalculatedSizes();
    
    var options = this.options;
    var bHorizontal = this._isHorizontal();
    var itemsPerPage = options.maxItemsPerPage;
    var bCalcItemsPerPage = (itemsPerPage < 1);
    var elem = this.element;
    
    var items = this._getItemContainers();
    //if we haven't saved the item size yet, do it now
    if (this._itemSize < 0)
    {
      //use the size of the item specified in the options
      var optionItemIndex = this._getItemIndex(options.currentItem);
      var optionItemContainer = $(items[optionItemIndex]);
      
      //unhide the item, in case the app has initially hidden it
      var optionItem = optionItemContainer.children(_PERIOD + _OJ_FILMSTRIP_ITEM);
      optionItem.css(_DISPLAY, _EMPTY_STRING);
      //notify the item that it's being shown
      oj.Components.subtreeShown(optionItem[0]);
      
      this._itemSize = bHorizontal ? optionItemContainer.width() : optionItemContainer.height();
    }
    
    //get component size after itemSize, in case item needed to be unhidden
    var componentSize = bHorizontal ? elem.width() : elem.height();
    if (options.arrowVisibility !== _HIDDEN &&
        options.arrowPlacement === _ADJACENT)
    {
      var arrowContainers = elem.children(_PERIOD + _OJ_FILMSTRIP_ARROW_CONTAINER);
      var firstArrowContainer = arrowContainers.eq(0);
      var arrowSize = bHorizontal ? firstArrowContainer.width() : firstArrowContainer.height();
      componentSize -= 2 * arrowSize;
    }
    this._componentSize = componentSize;
    
    //if there are a fixed number of items per page, but the number specified
    //won't fit, then reduce the number to what will fit
    if (!bCalcItemsPerPage)
    {
      //use min of 1 to prevent browser crash
      var calcFitCount = Math.max(Math.floor(componentSize / this._itemSize), 1);
      if (calcFitCount < itemsPerPage)
        itemsPerPage = calcFitCount;
    }
    
    //if calculating fitCount, use min of 1 to prevent browser crash
    var fitCount = bCalcItemsPerPage ? Math.max(Math.floor(componentSize / this._itemSize), 1) : itemsPerPage;
    var fitItemSize = componentSize / fitCount;
    items
      .css(_FLEX_BASIS, fitItemSize + _PX)
      .css(_WEBKIT_FLEX_BASIS, fitItemSize + _PX);
    
    var newPageCount = Math.ceil(items.length / fitCount);
    //wrap items in logical page containers
    var pages = this._getPages();
    var bCreatePages = false;
    //need to create logical pages if page layout changed, or if we haven't 
    //yet created logical pages
    var pagingModel = this._pagingModel;
    if (pagingModel.getPageCount() != newPageCount ||
        this._itemsPerPage != fitCount ||
        !pages || pages.length < 1)
    {
      bCreatePages = true;
      if (bNotifyAttach)
      {
        //notify the original children of the filmStrip that they were detached from
        //the DOM BEFORE actually detaching so that components can save state
        for (var i = 0; i < items.length; i++)
          oj.Components.subtreeDetached(items[i]);
      }
      
      if (pages && pages.length > 0)
      {
        //remove old logical page containers
        items.unwrap();
      }
      
      //create new logical page containers
      for (var i = 0; i < items.length; i += fitCount)
      {
        var itemsOnPage = items.slice(i, i + fitCount);
        //initially hide the page container
        itemsOnPage.wrapAll("<div class='" + _OJ_FILMSTRIP_CONTAINER + " " + _OJ_FILMSTRIP_PAGE + "' style='" + _DISPLAY + ": " + _NONE + ";' " + _ARIA_HIDDEN + "='true'></div>"); // @HTMLUpdateOK
      }

      if (bNotifyAttach)
      {
        //notify the original children of the filmStrip that they were attached
        //again after wrapping them
        for (var i = 0; i < items.length; i++)
          oj.Components.subtreeAttached(items[i]);
      }
    }
    //always need to update size of each page, even if page count doesn't change
    pages = this._getPages();
    pages
      .css(_FLEX_BASIS, componentSize + _PX)
      .css(_WEBKIT_FLEX_BASIS, componentSize + _PX);
    
    //always need to update the pages container size
    var pagesWrapper = this._pagesWrapper;
    var contentWrapper = this._contentWrapper;
    //only show a single page at a time
    pagesWrapper.css(this._getCssSizeAttr(), componentSize);
    if (contentWrapper)
      contentWrapper.css(this._getCssSizeAttr(), componentSize);
    
    var newPageIndex = 0;
    if (options.currentItem || options.currentItem === 0)
      newPageIndex = this._findPage(options.currentItem, fitCount);
    
    if (pagingModel.getPageCount() != newPageCount || 
        this._itemsPerPage != fitCount ||
        pagingModel.getPage() != newPageIndex)
    {
      pagingModel.setPage(newPageIndex, {"pageSize": fitCount});
    }
    //if the page layout didn't change, but we recreated the logical page
    //containers, then simply go to the current logical page to make sure that
    //pages and items are hidden or shown as appropriate
    else if (bCreatePages)
    {
      var currPage = pagingModel.getPage();
      this._handlePage({"previousPage": currPage, "page": currPage});
    }
  },
  
  /** 
   * Handle a 'page' event from the PagingModel.
   * @param {Object} event Event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handlePage: function(event)
  {
    var pageIndex = event["page"];
    var prevPageIndex = event["previousPage"];
    var pagesWrapper = this._pagesWrapper;
    var options = this.options;
    var pages = this._getPages();
    var pagingModel = this._pagingModel;
    var pageSize = pagingModel.getPageSize();
    var bImmediate = prevPageIndex < 0 || prevPageIndex == pageIndex ||
                     this._itemsPerPage != pageSize;
    //update _itemsPerPage AFTER using it to initialize bImmediate above
    this._itemsPerPage = pageSize;
    
    //get the old page, if there is one
    var oldPage = null;
    if (!bImmediate)
      oldPage = $(pages[prevPageIndex]);
    
    var cssAttr = this._getCssPositionAttr();
    
    //unhide the new page
    var newPage = $(pages[pageIndex]);
    var bPageHidden = newPage.is(_HIDDEN_SELECTOR);
    if (bPageHidden)
      this._unhidePage(newPage);
    //defer the scroll if we're dragging so that we can animate it
    var bDeferScroll = this._bDragInit;
    if (prevPageIndex > -1 && !bImmediate)
    {
      bDeferScroll = true;
      //set size of pages wrapper to show two pages at a time because both of
      //them will temporarily be visible
      pagesWrapper.css(this._getCssSizeAttr(), 2 * this._componentSize);
      //if going to the previous page, initially scroll to show the current page
      //because the previous page will be at the current scroll position
      if (pageIndex < prevPageIndex)
      {
        //only adjust scroll if the page was hidden (in the case of dragging,
        //the page will already be unhidden and scrolled correctly)
        if (bPageHidden)
          pagesWrapper.css(cssAttr, -this._componentSize + _PX);
      }
      
      //set initial transition states on the pages
      if (pageIndex > prevPageIndex)
      {
        oldPage.addClass(_OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_FROM);
        newPage.addClass(_OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_FROM);
      }
      else
      {
        oldPage.addClass(_OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_FROM);
        newPage.addClass(_OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_FROM);
      }
    }
    
    if (bDeferScroll)
    {
      var self = this;
      var bDragInit = this._bDragInit;
      //if bouncing back because a drag scroll didn't cross the threshold,
      //add the transition class before the timeout because the transforms
      //are removed and reapplied in the timeout
      if (bDragInit && prevPageIndex < 0)
      {
        var visiblePages = pages.filter(_VISIBLE_SELECTOR);
        visiblePages.addClass(_OJ_FILMSTRIP_TRANSITION);
      }
      //FIX BUG 21045441: In Safari on Mac OS X, sometimes when changing pages
      //the new page appears hidden until after the transition.  It seems to be
      //a timing issue where the new page has been made visible in the DOM, but
      //Safari hasn't processed that change before the transition starts.  
      //Changing the timeout delay from 0 to 25 seems to help.
      setTimeout(function()
        {
          self._finishHandlePage(pageIndex, prevPageIndex, bImmediate, bDragInit);
        }, 25);
    }
    else
      this._finishHandlePage(pageIndex, prevPageIndex, bImmediate);
  },
  
  /** 
   * Finish handling a 'page' event from the PagingModel.
   * @param {number} pageIndex 0-based page index to go to.
   * @param {number} prevPageIndex 0-based old page index.
   * @param {boolean} bImmediate True to change pages immediately with no 
   *        transition, false to transition them over time.
   * @param {boolean} bDragInit True if we're currently drag scrolling, false
   *        otherwise.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _finishHandlePage: function(pageIndex, prevPageIndex, bImmediate, bDragInit)
  {
    var pagesWrapper = this._pagesWrapper;
    if (!bImmediate)
    {
      this._bPageChangeTransition = true;
      pagesWrapper
        .on("transitionend" + this.eventNamespace + " webkitTransitionEnd" + this.eventNamespace, 
            this._handleTransitionEndFunc);
    }

    if (bImmediate)
      this._handleTransitionEnd();
    else
    {
      var pages = this._getPages();
      //if we're currently drag scrolling, remove the transforms that we used
      //to scroll while we were under the drag threshold
      if (bDragInit)
        _removeTransform(pages);
      
      //changing pages
      if (prevPageIndex > -1)
      {
        //remove initial transition states and set destination states, and
        //transition between them
        var bNext = pageIndex > prevPageIndex;
        var oldPage = $(pages[prevPageIndex]);
        var newPage = $(pages[pageIndex]);
        oldPage.addClass(_OJ_FILMSTRIP_TRANSITION);
        newPage.addClass(_OJ_FILMSTRIP_TRANSITION);
        if (bNext)
        {
          oldPage.removeClass(_OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_FROM);
          newPage.removeClass(_OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_FROM);
          oldPage.addClass(_OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_TO);
          newPage.addClass(_OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_TO);
        }
        else
        {
          oldPage.removeClass(_OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_FROM);
          newPage.removeClass(_OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_FROM);
          oldPage.addClass(_OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_TO);
          newPage.addClass(_OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_TO);
        }
      }
      //bouncing back to current page after drag scrolling under the threshold
      else if (bDragInit)
      {
        var visiblePages = pages.filter(_VISIBLE_SELECTOR);
        _applyTransform(visiblePages, "translate3d(0, 0, 0)");
      }
    }
  },
  
  /** 
   * Handle the end of a page change transition.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleTransitionEnd: function()
  {
    this._bPageChangeTransition = false;
    var pagesWrapper = this._pagesWrapper;
    var cssAttr = this._getCssPositionAttr();
    pagesWrapper
      .off(this.eventNamespace)
      .css(this._getCssSizeAttr(), this._componentSize)
      .css(cssAttr, "0px");
    
    //if the filmStrip contains the focus element, we may need to focus a new
    //element if the old one is on a page that's being hidden
    var focusElem = null;
    if (oj.FocusUtils.containsFocus(pagesWrapper[0]) ||
        (this._nextButton && oj.FocusUtils.containsFocus(this._nextButton)) ||
        (this._prevButton && oj.FocusUtils.containsFocus(this._prevButton)))
      focusElem = document.activeElement;
    
    var pagingModel = this._pagingModel;
    var pageIndex = pagingModel.getPage();
    
    //hide all pages except for current one
    var pages = this._getPages();
    for (var i = 0; i < pages.length; i++)
    {
      if (i != pageIndex)
        this._hidePage($(pages[i]));
    }
    //remove transition classes
    pages
      .removeClass(_OJ_FILMSTRIP_TRANSITION + " " + 
                   _OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_TO + " " + 
                   _OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_TO + " " + 
                   _OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_TO + " " + 
                   _OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_TO);
    //remove transforms left over from drag scrolling
    _removeTransform(pages);
    
    //update display of the navigation arrows
    this._updateNavigationArrowsDisplay();
    
    //if the old focus element is being hidden, transfer focus to something visible
    if (focusElem && $(focusElem).is(_HIDDEN_SELECTOR))
    {
      var elem = this.element;
      //focus an element in the new logical page if possible, otherwise focus
      //the filmStrip itself
      var firstTabStop = oj.FocusUtils.getFirstTabStop(pages[pageIndex]);
      if (firstTabStop)
        oj.FocusUtils.focusElement(firstTabStop);
      else
        oj.FocusUtils.focusElement(elem[0]);
    }
    
    //update currentItem option if it's not on the current page
    var options = this.options;
    var currItemPage = this._findPage(options.currentItem);
    if (currItemPage != pageIndex)
    {
      var newFirstItem = this._getFirstItemOnPage(pageIndex);
      this.option(_CURRENT_ITEM, newFirstItem, {'_context': {writeback: true}});
    }
  },
  
  /** 
   * Get the 0-based index of the specified item.
   * @param {(string|number)} item String id or 0-based index of item.
   * @returns {number} The 0-based index of the specified item, or -1 if not found.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getItemIndex: function(item)
  {
    var itemIndex = -1;
    var items = this._getItems();
    if (typeof item === "number") {
      if (item >= 0 && item < items.length)
        itemIndex = item;
    }
    else if (typeof item === "string") {
      //security test
      if (oj.DomUtils.isValidIdentifier(item))
      {
        for (var i = 0; i < items.length; i++)
        {
          var itemElem = items[i];
          var itemId = itemElem.id;
          if (itemId && itemId.length > 0 && itemId === item)
          {
            itemIndex = i;
            break;
          }
        }
      }
    }
    return itemIndex;
  },
  
  /** 
   * Find the logical page containing the specified item.
   * @param {(string|number)} item String id or 0-based index of item.
   * @param {?number} itemsPerPage The number of items on each logical page.
   * @returns {number} The 0-based index of the logical page containing the item, 
   *          or -1 if not found.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _findPage: function(item, itemsPerPage)
  {
    var itemIndex = this._getItemIndex(item);
    var pageIndex = -1;
    if (itemIndex > -1)
    {
      if (itemsPerPage === undefined)
        itemsPerPage = this._itemsPerPage;
      pageIndex = Math.floor(itemIndex / itemsPerPage);
    }
    return pageIndex;
  },
  
  /** 
   * Get the first item on the specified logical page.
   * @param {number} pageIndex 0-based index of the logical page.
   * @param {?number} pageCount Number of logical pages.
   * @param {?number} itemsPerPage Number of items on each logical page.
   * @returns {(string|number)} The string id or 0-based index of item, with 
   *          preference given to string id.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _getFirstItemOnPage: function(pageIndex, pageCount, itemsPerPage)
  {
    var pagingModel = this._pagingModel;
    if (pageCount === undefined)
      pageCount = pagingModel.getPageCount();
    if (pageIndex >= 0 && pageIndex < pageCount)
    {
      var items = this._getItems();
      if (itemsPerPage === undefined)
        itemsPerPage = this._itemsPerPage;
      var itemIndex = pageIndex * itemsPerPage;
      if (itemIndex < items.length)
      {
        var firstItemOnPage = items[itemIndex];
        var firstId = firstItemOnPage.id;
        return ((firstId && firstId.length > 0) ? firstId : itemIndex);
      }
    }
    return -1;
  },
  
  /** 
   * Hide the logical page.
   * @param {jQuery} page Page to hide.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _hidePage: function(page)
  {
    //notify the page that it's being hidden BEFORE actually hiding it so that
    //components can save state
    oj.Components.subtreeHidden(page[0]);
    
    page
      .css(_DISPLAY, _NONE)
      .attr(_ARIA_HIDDEN, "true");
    
    //hide the items explicitly; unhiding will unhide them explicitly
    var items = page.find(_PERIOD + _OJ_FILMSTRIP_ITEM);
    items.css(_DISPLAY, _NONE);
  },
  
  /** 
   * Unhide the logical page.
   * @param {jQuery} page Page to unhide.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _unhidePage: function(page)
  {
    page
      .css(_DISPLAY, _EMPTY_STRING)
      .removeAttr(_ARIA_HIDDEN);
    
    //unhide the items explicitly because the app may have initially hidden them  
    var items = page.find(_PERIOD + _OJ_FILMSTRIP_ITEM);
    items.css(_DISPLAY, _EMPTY_STRING);
    
    //notify the page that it was shown
    oj.Components.subtreeShown(page[0]);
  },
  
  /** 
   * Add key listeners on the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _addKeyListeners: function()
  {
    var elem = this.element;
    elem.on("keydown" + this.keyEventNamespace, this._handleKeyDownFunc);
  },
  
  /** 
   * Remove key listeners from the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _removeKeyListeners: function()
  {
    var elem = this.element;
    elem.off(this.keyEventNamespace);
  },
  
  /** 
   * Add mouse listeners on the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _addMouseListeners: function()
  {
    var elem = this.element;
    elem
      .on("mousedown" + this.mouseEventNamespace, this._handleMouseDownFunc)
      .on("mousemove" + this.mouseEventNamespace, this._handleMouseMoveFunc)
      .on("mouseup" + this.mouseEventNamespace, this._handleMouseUpFunc);
  },
  
  /** 
   * Remove mouse listeners from the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _removeMouseListeners: function()
  {
    var elem = this.element;
    elem.off(this.mouseEventNamespace);
  },
  
  /** 
   * Add touch listeners on the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _addTouchListeners: function()
  {
    var elem = this.element;
    elem
      .on("touchstart" + this.touchEventNamespace, this._handleTouchStartFunc)
      .on("touchmove" + this.touchEventNamespace, this._handleTouchMoveFunc)
      .on("touchend" + this.touchEventNamespace, this._handleTouchEndFunc)
      .on("touchcancel" + this.touchEventNamespace, this._handleTouchEndFunc);
  },
  
  /** 
   * Remove touch listeners from the filmStrip.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _removeTouchListeners: function()
  {
    var elem = this.element;
    elem.off(this.touchEventNamespace);
  },
  
  /** 
   * Handle a keydown event.
   * @param {Event} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleKeyDown: function(event)
  {
    var pagingModel = this._pagingModel;
    var pageIndex = pagingModel.getPage();
    var newPageIndex = -1;
    switch (event.keyCode)
    {
      case $.ui.keyCode.RIGHT:
        if (this._bRTL)
          newPageIndex = pageIndex - 1;
        else
          newPageIndex = pageIndex + 1;
        break;
      case $.ui.keyCode.LEFT:
        if (this._bRTL)
          newPageIndex = pageIndex + 1;
        else
          newPageIndex = pageIndex - 1;
        break;
      case $.ui.keyCode.DOWN:
        newPageIndex = pageIndex + 1;
        break;
      case $.ui.keyCode.UP:
        newPageIndex = pageIndex - 1;
        break;
      case $.ui.keyCode.HOME:
        newPageIndex = 0;
        break;
      case $.ui.keyCode.END:
        newPageIndex = pagingModel.getPageCount() - 1;
        break;
      default :
        return;
    }
    
    if (newPageIndex > -1 && newPageIndex < pagingModel.getPageCount())
      pagingModel.setPage(newPageIndex);
    
    event.preventDefault();
  },
  
  /** 
   * Handle a mousedown event.
   * @param {Object} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleMouseDown: function(event)
  {
    var originalEvent = event.originalEvent;
    this._dragScrollStart(originalEvent);
  },
  
  /** 
   * Handle a mousemove event.
   * @param {Object} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleMouseMove: function(event)
  {
    var originalEvent = event.originalEvent;
    this._dragScrollMove(event, originalEvent);
  },
  
  /** 
   * Handle a mouseup event.
   * @param {Event} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleMouseUp: function(event)
  {
    this._dragScrollEnd();
  },
  
  /** 
   * Handle a touchstart event.
   * @param {Object} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleTouchStart: function(event)
  {
    var originalEvent = event.originalEvent;
    var eventTouches = originalEvent.touches;
    if (eventTouches.length === 1)
    {
      var firstTouch = eventTouches[0];
      this._dragScrollStart(firstTouch);
    }
  },
  
  /** 
   * Handle a touchmove event.
   * @param {Object} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleTouchMove: function(event)
  {
    var originalEvent = event.originalEvent;
    var eventTouches = originalEvent.touches;
    var firstTouch = eventTouches[0];
    this._dragScrollMove(event, firstTouch);

    //FIX BUG 20952664: if the drag scroll was started, prevent the page
    //from scrolling also
    //FIX BUG 21069733: do this for touchMove events instead of touchStart so 
    //that it doesn't prevent clicks/taps from being detected on nav arrows and
    //item content
    if (this._bTouch || this._scrolledForThisTouch)
      event.preventDefault();
  },
  
  /** 
   * Handle a touchend event.
   * @param {Event} event <code class="prettyprint">jQuery</code> event object.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _handleTouchEnd: function(event)
  {
    this._dragScrollEnd();
  },
  
  /** 
   * Start a drag scroll.
   * @param {Object} coordsObj Object that has pageX and pageY properties.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _dragScrollStart: function(coordsObj)
  {
    var pagingModel = this._pagingModel;
    if (pagingModel.getPageCount() > 1 && !this._bPageChangeTransition)
    {
      this._bTouch = true;
      this._bDragInit = false;
      
      var bHorizontal = this._isHorizontal();
      this._touchStartCoord = bHorizontal ? coordsObj.pageX : coordsObj.pageY;
    }
  },
  
  /** 
   * Initialize a drag scroll.
   * Called after a drag scroll has started if the drag has passed an initial
   * threshold.
   * @param {Object} coordsObj Object that has pageX and pageY properties.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _initDragScroll: function(coordsObj)
  {
    //save off some initial information at the start of a swipe
    var bHorizontal = this._isHorizontal();
    this._touchStartCoord = bHorizontal ? coordsObj.pageX : coordsObj.pageY;
    var cssAttr = this._getCssPositionAttr();
    var pagesWrapper = this._pagesWrapper;
    var pagingModel = this._pagingModel;

    //unhide adjacent pages (unhide both because we don't know which way user 
    //will scroll)
    var pages = this._getPages();
    var pageCountToShow = 1;
    if (pagingModel.getPage() > 0)
    {
      this._unhidePage($(pages[pagingModel.getPage() - 1]));

      //when unhiding previous page, need to adjust current scroll position
      //to continue showing current page
      pagesWrapper.css(cssAttr, -this._componentSize + _PX);

      //increment number of pages we need to show
      pageCountToShow++;
    }
    if (pagingModel.getPage() < pagingModel.getPageCount() - 1)
    {
      this._unhidePage($(pages[pagingModel.getPage() + 1]));

      //increment number of pages we need to show
      pageCountToShow++;
    }

    if (pageCountToShow > 1)
      pagesWrapper.css(this._getCssSizeAttr(), pageCountToShow * this._componentSize);

    this._touchStartScroll = parseInt(pagesWrapper.css(cssAttr), 10);
  },

  /** 
   * Process a move during drag scrolling.
   * @param {Event} event <code class="prettyprint">jQuery</code> event object.
   * @param {Object} coordsObj Object that has pageX and pageY properties.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _dragScrollMove: function(event, coordsObj)
  {
    //do nothing if we haven't started the scroll
    if (!this._bTouch)
      return;
    
    var bHorizontal = this._isHorizontal();
    var touchCoord = bHorizontal ? coordsObj.pageX : coordsObj.pageY;
    var diff = touchCoord - this._touchStartCoord;
    
    //only initialize the drag once we've passed the initial threshold
    if (!this._bDragInit)
    {
      if (Math.abs(diff) > _DRAG_SCROLL_INIT_THRESHOLD)
      {
        this._initDragScroll(coordsObj);
        this._bDragInit = true;
      }
      //return here even if we just initialized the drag so that we'll start
      //processing the drag move on the next event when we have different mouse
      //coords
      return;
    }
    
    //in non-RTL, if swiping left or up, scroll next; otherwise scroll prev
    //in RTL, if swiping right or up, scroll next; otherwise scroll prev
    var bNext = (bHorizontal && this._bRTL) ? (diff > 0) : (diff < 0);
    //determine whether the filmStrip can be scrolled in the direction of the swipe
    var pagingModel = this._pagingModel;
    var pageIndex = pagingModel.getPage();
    var canScrollInSwipeDirection = (bNext && pageIndex < (pagingModel.getPageCount() - 1)) ||
                                    (!bNext && pageIndex > 0);
    //only need to do something if we can scroll in the swipe direction
    if (canScrollInSwipeDirection)
    {
      //only scroll next/prev if the swipe is longer than the threshold; if it's
      //less, then just drag the items with the swipe
      var elem = this.element[0];
      var threshold = Math.min(_DRAG_SCROLL_THRESHOLD * (bHorizontal ? elem.offsetWidth : elem.offsetHeight), _DRAG_SCROLL_MAX_THRESHOLD);
      var cssAttr = this._getCssPositionAttr();
      var pagesWrapper = this._pagesWrapper;
      var pages = this._getPages();

      //if swiping beyond the threshold, scroll to the next/prev page
      if (Math.abs(diff) >= threshold)
      {
        var newPageIndex = bNext ? pageIndex + 1 : pageIndex - 1;
        var newFirstItem = this._getFirstItemOnPage(newPageIndex);
        
        //hide the page that we're not scrolling to
        var pageToHide = bNext ? pageIndex - 1 : pageIndex + 1;
        if (pageIndex > -1 && pageIndex < pagingModel.getPageCount())
          this._hidePage($(pages[pageToHide]));
        
        //when hiding previous page, need to adjust current scroll position
        //to continue showing current page
        if (bNext && pageToHide > -1)
        {
          var currScroll = parseInt(pagesWrapper.css(cssAttr), 10);
          pagesWrapper.css(cssAttr, (currScroll + this._componentSize) + _PX);
        }
        
        //update size to show two pages instead of three
        pagesWrapper.css(this._getCssSizeAttr(), 2 * this._componentSize);

        //don't scroll again for this same swipe
        this._bTouch = false;
        
        pagingModel.setPage(newPageIndex);
      }
      //if swiping under the threshold, just move the conveyor with the swipe
      else
      {
        var scrollVal = (bHorizontal && this._bRTL) ? -diff : diff;
        var transform = bHorizontal ? "translate3d(" + scrollVal + "px, 0, 0)" :
                                      "translate3d(0, " + scrollVal + "px, 0)";
        _applyTransform(pages.filter(_VISIBLE_SELECTOR), transform);
      }

      //set a flag indicating we've scrolled for this touch event
      this._scrolledForThisTouch = true;
    }

    //if we've scrolled for this touch event, consume the event
    //so that the page doesn't also scroll
    if (this._scrolledForThisTouch)
    {
      event.preventDefault();
      event.stopPropagation();
    }
  },

  /** 
   * Process the end of a drag scroll.
   * @memberof oj.ojFilmStrip
   * @instance
   * @private
   */
  _dragScrollEnd: function()
  {
    //if a full page swipe hasn't happened, scroll back to the original position
    if (this._bTouch && this._bDragInit)
    {
      var pagingModel = this._pagingModel;
      var pageIndex = pagingModel.getPage();
      this._handlePage({"previousPage": pageIndex, "page": pageIndex});
    }
    this._bTouch = false;
    this._bDragInit = false;
    //reset the flag indicating if we've scrolled for this touch event
    this._scrolledForThisTouch = false;
  },
  
  /**
   * Return the subcomponent node represented by the documented locator 
   * attribute values.
   * Test authors should target sub elements using the following names:
   * <ul>
   * <li><b>oj-filmstrip-start-arrow</b>: the start navigation
   * arrow of a horizontal FilmStrip</li>
   * <li><b>oj-filmstrip-end-arrow</b>: the end navigation
   * arrow of a horizontal FilmStrip</li>
   * <li><b>oj-filmstrip-top-arrow</b>: the top navigation
   * arrow of a vertical FilmStrip</li>
   * <li><b>oj-filmstrip-bottom-arrow</b>: the bottom navigation
   * arrow of a vertical FilmStrip</li>
   * </ul>
   * @expose
   * @memberof! oj.ojFilmStrip
   * @instance
   * @override
   * @param {Object} locator An Object containing at minimum a subId property 
   *        whose value is a string, documented by the component, that allows 
   *        the component to look up the subcomponent associated with that 
   *        string.  It contains:<p>
   *        component: optional - in the future there may be more than one 
   *        component contained within a page element<p>
   *        subId: the string, documented by the component, that the component 
   *        expects in getNodeBySubId to locate a particular subcomponent
   * @returns {Element|null} the subcomponent located by the subId string passed
   *          in locator, if found.<p>
   */
  getNodeBySubId: function(locator)
  {
    if (locator == null)
    {
      return this.element ? this.element[0] : null;
    }
    
    var subId = locator['subId'];
    if (subId === "oj-filmstrip-start-arrow") {
      return this.widget().find(_PERIOD + _OJ_FILMSTRIP_ARROW + _PERIOD + _OJ_START)[0];
    }
    if (subId === "oj-filmstrip-end-arrow") {
      return this.widget().find(_PERIOD + _OJ_FILMSTRIP_ARROW + _PERIOD + _OJ_END)[0];
    }
    if (subId === "oj-filmstrip-top-arrow") {
      return this.widget().find(_PERIOD + _OJ_FILMSTRIP_ARROW + _PERIOD + _OJ_TOP)[0];
    }
    if (subId === "oj-filmstrip-bottom-arrow") {
      return this.widget().find(_PERIOD + _OJ_FILMSTRIP_ARROW + _PERIOD + _OJ_BOTTOM)[0];
    }
    
    // Non-null locators have to be handled by the component subclasses
    return null;
  }
  
  // start jsdoc fragments /////////////////////////////////////////////////////

  /**
   * <table class="keyboard-table">
   *   <thead>
   *     <tr>
   *       <th>Target</th>
   *       <th>Gesture</th>
   *       <th>Action</th>
   *     </tr>
   *   </thead>
   *   <tbody>
   *     <tr>
   *       <td>FilmStrip</td>
   *       <td><kbd>Swipe</kbd></td>
   *       <td>Transition to an adjacent logical page of child items.</td>
   *     </tr>
   *     <tr>
   *       <td>Navigation Arrow</td>
   *       <td><kbd>Tap</kbd></td>
   *       <td>Transition to an adjacent logical page of child items.</td>
   *     </tr>
   *   </tbody>
   * </table>
   *
   * @ojfragment touchDoc - Used in touch gesture section of classdesc, and standalone gesture doc
   * @memberof oj.ojFilmStrip
   */

  /**
   * <p>FilmStrip itself is a tabstop and supports the following keyboard interactions:
   * 
   * <table class="keyboard-table">
   *   <thead>
   *     <tr>
   *       <th>Target</th>
   *       <th>Key</th>
   *       <th>Action</th>
   *     </tr>
   *   </thead>
   *   <tbody>
   *     <tr>
   *       <td>FilmStrip</td>
   *       <td><kbd>UpArrow or LeftArrow</kbd> (<kbd>RightArrow</kbd> in RTL)</td>
   *       <td>Transition to the previous logical page of child items.</td>
   *     </tr>
   *     <tr>
   *       <td>FilmStrip</td>
   *       <td><kbd>DownArrow or RightArrow</kbd> (<kbd>LeftArrow</kbd> in RTL)</td>
   *       <td>Transition to the next logical page of child items.</td>
   *     </tr>
   *     <tr>
   *       <td>FilmStrip</td>
   *       <td><kbd>Home</kbd></td>
   *       <td>Transition to the first logical page of child items.</td>
   *     </tr>
   *     <tr>
   *       <td>FilmStrip</td>
   *       <td><kbd>End</kbd></td>
   *       <td>Transition to the last logical page of child items.</td>
   *     </tr>
   *     <tr>
   *       <td>Navigation Arrow</td>
   *       <td><kbd>Enter</kbd> or <kbd>Space</kbd></td>
   *       <td>Transition to an adjacent logical page of child items.</td>
   *     </tr>
   *   </tbody>
   * </table>
   *
   * @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
   * @memberof oj.ojFilmStrip
   */
  
  // end jsdoc fragments ///////////////////////////////////////////////////////
  
}); // end of oj.__registerWidget

// start static members and functions //////////////////////////////////////////

//_tmpVar is simply to trick the closure compiler so that it doesn't issue a
//warning about the below real vars declared using a single "var" statement:
//WARNING - declaration of multiple variables with shared type information
var _tmpVar;

var _ADJACENT = "adjacent",
    //aria-hidden DOM attribute
    _ARIA_HIDDEN = "aria-hidden",
    _AUTO = "auto",
    //display CSS attribute
    _DISPLAY = "display",

    //threshold to initialize a drag scroll (pixels)
    _DRAG_SCROLL_INIT_THRESHOLD = 3,
    //drag scroll threshold (percentage of filmStrip size)
    _DRAG_SCROLL_THRESHOLD = .33,
    //drag scroll threshold (max distance, pixels)
    _DRAG_SCROLL_MAX_THRESHOLD = 100,
    
    _EMPTY_STRING = "",
    //throw an error when invalid "currentItem" option set
    _ERROR_CURRENT_ITEM_NOT_FOUND = "JET FilmStrip: Value of 'currentItem' option not found: ",
    //throw an error when "orientation" option set to invalid value
    _ERROR_INVALID_ORIENTATION = "JET FilmStrip: Unsupported value set as 'orientation' option: ",
    //throw an error when "arrowPlacement" option set to invalid value
    _ERROR_INVALID_NAV_ARROW_PLACEMENT = "Unsupported value set as 'arrowPlacement' option: ",
    //throw an error when "arrowVisibility" option set to invalid value
    _ERROR_INVALID_NAV_ARROW_VISIBILITY = "Unsupported value set as 'arrowVisibility' option: ",
    _FLEX_BASIS = "flex-basis",
    _HIDDEN = "hidden",
    //jQuery hidden selector
    _HIDDEN_SELECTOR = ":hidden",
    _HORIZONTAL = "horizontal",
    _HOVER = "hover",
    _CURRENT_ITEM = "currentItem",
    _MS_TRANSFORM = "-ms-transform",
    _NONE = "none",
    
    _OJ_BOTTOM = "oj-bottom",
    _OJ_END = "oj-end",
    _OJ_FILMSTRIP_ARROW = "oj-filmstrip-arrow",
    _OJ_FILMSTRIP_ARROW_CONTAINER = "oj-filmstrip-arrow-container",
    _OJ_FILMSTRIP_ARROW_TRANSITION = "oj-filmstrip-arrow-transition",
    _OJ_FILMSTRIP_CONTAINER = "oj-filmstrip-container",
    _OJ_FILMSTRIP_HOVER = "oj-filmstrip-hover",
    _OJ_FILMSTRIP_ITEM = "oj-filmstrip-item",
    _OJ_FILMSTRIP_ITEM_CONTAINER = "oj-filmstrip-item-container",
    _OJ_FILMSTRIP_PAGE = "oj-filmstrip-page",
    _OJ_FILMSTRIP_PAGES_CONTAINER = "oj-filmstrip-pages-container",
    _OJ_FILMSTRIP_TRANSITION = "oj-filmstrip-transition",
    _OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_FROM = "oj-filmstrip-transition-next-newpage-from",
    _OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_FROM = "oj-filmstrip-transition-next-oldpage-from",
    _OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_FROM = "oj-filmstrip-transition-prev-newpage-from",
    _OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_FROM = "oj-filmstrip-transition-prev-oldpage-from",
    _OJ_FILMSTRIP_TRANSITION_NEXT_NEWPAGE_TO = "oj-filmstrip-transition-next-newpage-to",
    _OJ_FILMSTRIP_TRANSITION_NEXT_OLDPAGE_TO = "oj-filmstrip-transition-next-oldpage-to",
    _OJ_FILMSTRIP_TRANSITION_PREV_NEWPAGE_TO = "oj-filmstrip-transition-prev-newpage-to",
    _OJ_FILMSTRIP_TRANSITION_PREV_OLDPAGE_TO = "oj-filmstrip-transition-prev-oldpage-to",
    _OJ_FILMSTRIP_VERTICAL = "oj-filmstrip-vertical",
    _OJ_START = "oj-start",
    _OJ_TOP = "oj-top",
    
    _OVERLAY = "overlay",
    _PERIOD = ".",
    _PX = "px",
    _TRANSFORM = "transform",
    _VERTICAL = "vertical",
    _VISIBLE = "visible",
    //jQuery visible selector
    _VISIBLE_SELECTOR = ":visible",
    _WEBKIT_FLEX_BASIS = "-webkit-flex-basis",
    _WEBKIT_TRANSFORM = "-webkit-transform",
    
    //log warning message when "disabled" option set
    _WARNING_DISABLED_OPTION = "JET FilmStrip: 'disabled' option not supported",
    
    /**
     * Apply a CSS transform to the given object.
     * @param {jQuery} jqObj Object to apply transform to.
     * @param {string} transform The transform to apply.
     * @private
     */
    _applyTransform = function(jqObj, transform)
    {
      jqObj
        .css(_WEBKIT_TRANSFORM, transform)
        .css(_MS_TRANSFORM, transform)
        .css(_TRANSFORM, transform);
    },
    
    /**
     * Remove a CSS transform from the given object.
     * @param {jQuery} jqObj Object to apply transform to.
     * @private
     */
    _removeTransform = function(jqObj)
    {
      jqObj
        .css(_WEBKIT_TRANSFORM, _EMPTY_STRING)
        .css(_MS_TRANSFORM, _EMPTY_STRING)
        .css(_TRANSFORM, _EMPTY_STRING);
    },

    /**
     * Escape an html fragment/text.
     * @param {string} text Text to escape.
     * @returns {string} Escaped text.
     * @private
     */
    _escapeHtml = function(text)
    {
      //let jQuery escape the text
      var jqDiv = $("<div></div>");
      jqDiv.text(text); // @HTMLUpdateOK
      return jqDiv[0].innerHTML;
    };
    
// end static members and functions ////////////////////////////////////////////

}()); // end of FilmStrip wrapper function