/**
* @fileOverview Requirejs module containing the antie.widgets.HorizontalCarousel class.
* @preserve Copyright (c) 2013-present British Broadcasting Corporation. All rights reserved.
* @license See https://github.com/fmtvp/tal/blob/master/LICENSE for full licence
*/
define('antie/widgets/horizontalcarousel',
[
'antie/widgets/horizontallist',
'antie/widgets/list',
'antie/events/keyevent',
'antie/events/beforeselecteditemchangeevent'
],
function (HorizontalList, List, KeyEvent, BeforeSelectedItemChangeEvent) {
'use strict';
/**
* The HorizontalCarousel widget extends the HorizontalList widget to modify the animation behaviour to render a carousel rather than a list.
* @name antie.widgets.HorizontalCarousel
* @class
* @extends antie.widgets.HorizontalList
* @requires antie.widgets.List
* @requires antie.events.KeyEvent
* @param {String} [id] The unique ID of the widget. If excluded, a temporary internal ID will be used (but not included in any output).
* @param {antie.Formatter} [itemFormatter] A formatter class used on each data item to generate the list item child widgets.
* @param {antie.DataSource|Array} [dataSource] An array of data to be used to generate the list items, or an asynchronous data source.
* @deprecated This class is deprecated in favour of of antie.widgets.carousel
*/
var HorizontalCarousel = HorizontalList.extend(/** @lends antie.widgets.HorizontalCarousel.prototype */ {
/**
* @constructor
* @ignore
*/
init: function init (id, itemFormatter, dataSource, overrideAnimation, activeWidgetAlignment) {
this._prefixClones = 0;
this._wrapMode = HorizontalCarousel.WRAP_MODE_VISUAL;
this._viewportMode = HorizontalCarousel.VIEWPORT_MODE_NONE;
this._viewportSize = 0;
this._activateThenScroll = false;
this._scrollHandle = null;
this._keepHidden = false;
this._multiWidthItems = false;
this._overrideAnimation = overrideAnimation;
this._activeWidgetAlignment = activeWidgetAlignment || HorizontalCarousel.ALIGNMENT_CENTER;
this._activeWidgetAnimationFPS = 25;
this._activeWidgetAnimationDuration = 840;
this._activeWidgetAnimationEasing = 'easeFromTo';
this._nodeOffset = 0;
this._childWidgetsInDocument = [];
this._paddingItemsCreated = false;
init.base.call(this, id, itemFormatter, dataSource);
this.addClass('horizontalcarousel');
var self = this;
this.addEventListener('databound', function (evt) {
if (evt.target !== self) {
return;
}
// Delaying this because our mask might not have a size yet. Shouldn't be a problem for dynamic data
// source, only for static carousels (such as the menu). It's been found to need to differ on
// devices.
var config = self.getCurrentApplication().getDevice().getConfig();
var delay = 100;
if (config.widgets && config.widgets.horizontalcarousel && config.widgets.horizontalcarousel.bindDelay) {
delay = config.widgets.horizontalcarousel.bindDelay;
}
setTimeout(function () {
self._onDataBound(evt);
}, delay);
});
},
/**
* Renders the widget and any child widgets to device-specific output.
* @param {antie.devices.Device} device The device to render to.
* @returns A device-specific object that represents the widget as displayed on the device (in a browser, a DOMElement);
*/
render: function render (device) {
// keep the element hidden until data is bound and items created
if (!this._maskElement) {
this._maskElement = device.createContainer(this.id + '_mask', ['horizontallistmask', 'notscrolling']);
} else {
device.clearElement(this._maskElement);
this._childWidgetsInDocument = [];
}
if (this._viewportMode !== HorizontalCarousel.VIEWPORT_MODE_DOM) {
device.appendChildElement(this._maskElement, render.base.call(this, device));
} else {
if (!this._dataBound && this._dataSource && this._itemFormatter) {
this._createDataBoundItems(device);
}
if (!this.outputElement) {
if (this._renderMode === List.RENDER_MODE_LIST) {
this.outputElement = device.createList(this.id, this.getClasses());
} else {
this.outputElement = device.createContainer(this.id, this.getClasses());
}
}
device.appendChildElement(this._maskElement, this.outputElement);
}
// Don't hide if we're never going to databind (or it'll never be shown);
if (this._dataSource) {
device.hideElement({
el: this._maskElement,
skipAnim: true
});
} else {
var self = this;
var config = device.getConfig();
var delay = 100;
if (config.widgets && config.widgets.horizontalcarousel && config.widgets.horizontalcarousel.bindDelay) {
delay = config.widgets.horizontalcarousel.bindDelay;
}
setTimeout(function () {
self._onDataBound();
}, delay);
}
return this._maskElement;
},
refreshViewport: function refreshViewport () {
var _centerWidget = this._activeChildWidget || this._childWidgetOrder[0];
if (!_centerWidget) {
return;
}
var device = this.getCurrentApplication().getDevice();
var i, index, elpos, elsize;
if (this._viewportMode === HorizontalCarousel.VIEWPORT_MODE_DOM) {
this.setAutoRenderChildren(true);
if (!_centerWidget.outputElement) {
_centerWidget.outputElement = _centerWidget.render(device);
}
// iterate through the widgets currently in the document
// removing any that are no-longer in or near the viewport
for (i = 0; i < this._childWidgetsInDocument.length; i++) {
index = i + this._nodeOffset;
if (index < this._selectedIndex - this._viewportSize || index > this._selectedIndex + this._viewportSize) {
if (this._childWidgetsInDocument[i].outputElement) {
device.removeElement(this._childWidgetsInDocument[i].outputElement);
}
}
}
// find the elements that are in the view port and add them
// to the document (and keep a record of them)
this._childWidgetsInDocument = [];
var start = ((start = this._selectedIndex - this._viewportSize) < 0) ? 0 : start;
for (i = start; (i <= this._selectedIndex + this._viewportSize) && (i < this._childWidgetOrder.length); i++) {
index = i - start + this._prefixClones;
this._childWidgetOrder[i].addClass('inviewport');
if (!this._childWidgetOrder[i].outputElement) {
this._childWidgetOrder[i].outputElement = this._childWidgetOrder[i].render(device);
}
if (!device.getElementParent(this._childWidgetOrder[i].outputElement)) {
device.insertChildElementAt(this.outputElement, this._childWidgetOrder[i].outputElement, index);
}
this._childWidgetsInDocument.push(this._childWidgetOrder[i]);
}
this._nodeOffset = this._selectedIndex - this._viewportSize;
if (this._nodeOffset < 0) {
this._nodeOffset = 0;
}
// reposition the carousel over the active item
elpos = device.getElementOffset(_centerWidget.outputElement);
elsize = device.getElementSize(_centerWidget.outputElement);
this._alignToElement(_centerWidget.outputElement, true);
//device.scrollElementToCenter(this._maskElement, elpos.left + (elsize.width / 2), null, true);
this.setAutoRenderChildren(false);
} else if ((this._viewportMode === HorizontalCarousel.VIEWPORT_MODE_CLASSES) && this.outputElement && _centerWidget.outputElement) {
device = this.getCurrentApplication().getDevice();
elpos = device.getElementOffset(_centerWidget.outputElement);
elsize = device.getElementSize(_centerWidget.outputElement);
var maskSize = device.getElementSize(this._maskElement);
var nodes = device.getChildElementsByTagName(this.outputElement,
this._renderMode === List.RENDER_MODE_LIST ? 'li' : 'div'
);
var viewportLeft = (elpos.left + (elsize.width / 2)) - (maskSize.width / 2);
var viewportRight = (elpos.left + (elsize.width / 2)) + (maskSize.width / 2);
var nearViewportLeft = (elpos.left + (elsize.width / 2)) - (maskSize.width * 1.5);
var nearViewportRight = (elpos.left + (elsize.width / 2)) + (maskSize.width * 1.5);
var w, node;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
if (!node.cloneOfWidget) {
w = this._childWidgetOrder[i - this._prefixClones];
if (w) {
w.removeClass('inviewport');
w.removeClass('nearviewport');
}
}
}
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
var nodepos = device.getElementOffset(node);
var nodesize = device.getElementSize(node);
w = node.cloneOfWidget || this._childWidgetOrder[i - this._prefixClones];
if (!w) {
continue;
}
if (((nodepos.left + nodesize.width) >= viewportLeft) && (nodepos.left < viewportRight)) {
// work out which elements are on screen and given them a 'inviewport' class
if (node.cloneOfWidget) {
device.removeClassFromElement(node, 'nearviewport');
device.addClassToElement(node, 'inviewport');
}
w.removeClass('nearviewport');
w.addClass('inviewport');
} else if (((nodepos.left + nodesize.width) >= nearViewportLeft) && (nodepos.left < nearViewportRight)) {
// work out which elements are near the screen, and give them a 'nearviewport' class
if (node.cloneOfWidget) {
device.removeClassFromElement(node, 'inviewport');
device.addClassToElement(node, 'nearviewport');
}
w.removeClass('inviewport');
w.addClass('nearviewport');
} else if (node.cloneOfWidget) {
device.removeClassFromElement(node, 'inviewport');
device.removeClassFromElement(node, 'nearviewport');
}
}
}
},
/**
* turns animation on/off
* @param {Boolean} [reposition] Set to <code>true</code> if you want the carousel to animate
*/
setAnimationOverride: function setAnimationOverride (animationOn) {
this._overrideAnimation = !animationOn;
return this._overrideAnimation;
},
/**
* Attempt to set focus to the given child widget.
* @param {antie.widgets.Widget} widget The child widget to set focus to.
* @param {Boolean} [reposition] Set to <code>true</code> if you want to scroll the carousel to the new item.
* @returns Boolean true if the child widget was focusable, otherwise boolean false.
*/
setActiveChildWidget: function setActiveChildWidget (widget, reposition) {
var moved = setActiveChildWidget.base.call(this, widget);
if (this._activeChildWidget && this.outputElement && reposition) {
if (this._viewportMode !== HorizontalCarousel.VIEWPORT_MODE_DOM) {
this._alignToElement(this._activeChildWidget.outputElement, true);
}
this.refreshViewport();
}
return moved;
},
/**
* Attempts to set focus to the child widget at the given index.
* @see #setActiveChildWidget
* @param {Integer} index Index of the child widget to set focus to.
* @returns Boolean true if the child widget was focusable, otherwise boolean false.
*/
setActiveChildIndex: function setActiveChildIndex (index, reposition) {
if (index < 0 || index >= this._childWidgetOrder.length) {
throw new Error("HorizontalCarousel::setActiveChildIndex Index out of bounds. " + this.id + " contains " + this._childWidgetOrder.length + " children, but an index of " + index + " was specified.");
}
return this.setActiveChildWidget(this._childWidgetOrder[index], reposition);
},
setDataSource: function setDataSource (data) {
this._prefixClones = 0;
setDataSource.base.call(this, data);
},
rebindDataSource: function rebindDataSource () {
var device = this.getCurrentApplication().getDevice();
var config = device.getConfig();
var animate = !config.widgets || !config.widgets.horizontalcarousel || (config.widgets.horizontalcarousel.fade !== false);
var self = this;
var func = rebindDataSource.base;
device.hideElement({
el: this._maskElement,
skipAnim: !animate,
onComplete: function onComplete () {
func.call(self);
}
});
},
/**
* Handle key events to scroll the carousel.
* @private
*/
_onKeyDown: function _onKeyDown (evt) {
// This event handler is already bound (int HorizontalList), we override it to add wrapping logic
// Block all movement if the carousel is scrolling
if (this._scrollHandle && (
evt.keyCode === KeyEvent.VK_LEFT ||
evt.keyCode === KeyEvent.VK_RIGHT ||
evt.keyCode === KeyEvent.VK_UP ||
evt.keyCode === KeyEvent.VK_DOWN
)) {
evt.stopPropagation();
return;
}
switch (evt.keyCode) {
case KeyEvent.VK_LEFT:
if (this.selectPreviousChildWidget()) {
evt.stopPropagation();
}
break;
case KeyEvent.VK_RIGHT:
if (this.selectNextChildWidget()) {
evt.stopPropagation();
}
break;
}
},
/**
* DataBound event handler. Clone carousel items to allow infinite scrolling.
* @private
*/
_onDataBound: function _onDataBound (/*evt*/) {
var application = this.getCurrentApplication();
if (!application) {
// application has been destroyed, abort
return;
}
var device = application.getDevice();
if (this._childWidgetOrder.length > 0) {
// How this implements wrap-around infinite scrolling:
// * Prepend a copy of the last page of items
// * Append a copy of the first page of items
// * Scroll through items normally
// * When moving left and hitting index -1 flip scrollLeft to end item
// * When moving right and hitting out-of-bounds index, flip scrollLeft to start item
//
// Adding clones means higher memory usage and more to scroll, but this approach works
// with any number of items and allows for smooth transitions between start and end.
var maskSize = device.getElementSize(this._maskElement);
var copyWidth = 0;
var i = 0;
var prefixClones = 0;
this._nodeOffset = 0;
this._childWidgetsInDocument = [];
var w, clone;
if (this._viewportMode === HorizontalCarousel.VIEWPORT_MODE_NONE) {
for (w = 0; w < this._childWidgetOrder.length; w++) {
this._childWidgetOrder[w].addClass('inviewport');
}
}
if (this._wrapMode !== HorizontalCarousel.WRAP_MODE_VISUAL) {
if (this._paddingItemsCreated) {
var paddingFunction = (this._renderMode === List.RENDER_MODE_LIST) ?
device.createListItem :
device.createContainer;
var leftPadding = paddingFunction.call(device, this.id + 'PaddingLeft', ['viewportPadding', 'viewportPaddingLeft']);
device.setElementSize(leftPadding, {width: maskSize.width});
device.prependChildElement(this.outputElement, leftPadding);
var rightPadding = paddingFunction.call(device, this.id + 'PaddingRight', ['viewportPadding', 'viewportPaddingRight']);
device.setElementSize(rightPadding, {width: maskSize.width});
device.appendChildElement(this.outputElement, rightPadding);
prefixClones = 1;
}
this._paddingItemsCreated = true;
} else {
// TODO: there's an optimisation we could do here, but it may not work with all carousels
// TODO: especially those that have different sized elements.
// TODO:
// TODO: For carousels with items that all have the same width we can use 'maskSize.width / 2'
var requiredWidth = this._multiWidthItems ? maskSize.width : Math.ceil(maskSize.width / 2);
while (copyWidth < requiredWidth) {
w = this._childWidgetOrder[i];
clone = device.cloneElement(w.outputElement, true, "clone", "_clone");
clone.cloneOfWidget = w;
if (w.hasClass('active')) {
device.removeClassFromElement(clone, 'active', true);
}
if (w.hasClass('focus')) {
device.removeClassFromElement(clone, 'focus', true);
device.removeClassFromElement(clone, 'buttonFocussed', true);
}
device.appendChildElement(this.outputElement, clone);
w = device.getElementSize(w.outputElement).width;
if (i === 0 && this._childWidgetOrder.length !== 1) {
requiredWidth += w;
}
copyWidth += w;
i++;
if (i === this._childWidgetOrder.length) {
i = 0;
}
}
copyWidth = 0;
i = this._childWidgetOrder.length - 1;
while (copyWidth < requiredWidth) {
w = this._childWidgetOrder[i];
clone = device.cloneElement(w.outputElement, true, "clone", "_clone");
clone.cloneOfWidget = w;
if (w.hasClass('active')) {
device.removeClassFromElement(clone, 'active', true);
}
if (w.hasClass('focus')) {
device.removeClassFromElement(clone, 'focus', true);
}
device.prependChildElement(this.outputElement, clone);
copyWidth += device.getElementSize(w.outputElement).width;
i--;
prefixClones++;
if (i === -1) {
i = this._childWidgetOrder.length - 1;
}
}
}
this._prefixClones = prefixClones;
// TODO: we shouldn't really do this here - we can't assume the preselected item is at index 0
// TODO: or at least support moving to the correct item when the selected index is change
// TODO: from an external class
if (this._activeChildWidget && (this._viewportMode !== HorizontalCarousel.VIEWPORT_MODE_DOM)) {
this._alignToElement(this._activeChildWidget.outputElement, true);
}
this.refreshViewport();
}
// everything is now in place, show the carousel
this.show({});
},
/**
* Shows the carousel.
* @param {Object} options Details of the element to be shown, with optional parameters.
* @param {Boolean} [options.skipAnim] By default the showing of the element will be animated (faded in). Pass <code>true</code> here to prevent animation.
* @param {Function} [options.onComplete] Callback function to be called when the element has been shown.
* @param {Number} [options.fps=25] Frames per second for fade animation.
* @param {Number} [options.duration=840] Duration of fade animation, in milliseconds (ms).
* @param {String} [options.easing=linear] Easing style for fade animation.
* @returns Boolean true if animation was called, otherwise false
*/
show: function show (options) {
var application = this.getCurrentApplication();
if (!application) {
return;
}
var device = application.getDevice();
if (!this._keepHidden) {
var config = device.getConfig();
var animate = !config.widgets || !config.widgets.horizontalcarousel || (config.widgets.horizontalcarousel.fade !== false);
options.el = this._maskElement;
options.skipAnim = !animate;
return device.showElement(options);
}
return false;
},
/**
* Set whether to support wrapping within the carousel.
* @param {Integer} wrapMode Pass <code>HorizontalCarousel.WRAP_MODE_NONE</code> for no wrapping.
* Pass <code>HorizontalCarousel.WRAP_MODE_NAVIGATION_ONLY</code> to allow navigation to wrap.
* Pass <code>HorizontalCarousel.WRAP_MODE_VISUAL</code> to visually wrap the carousel (includes navigation).
*/
setWrapMode: function setWrapMode (wrapMode) {
if (this._viewportMode === HorizontalCarousel.VIEWPORT_MODE_DOM) {
if (wrapMode === HorizontalCarousel.WRAP_MODE_VISUAL) {
throw new Error('HorizontalCarousel::setWrapMode - VIEWPORT_MODE_DOM not supported for WRAP_MODE_VISUAL');
}
}
this._wrapMode = wrapMode;
},
/**
* Set method used to control which carousel items are in this rendered DOM
* @param {Integer} viewportMode One of <code>HorizontalCarousel.VIEWPORT_MODE_NONE</code>,
* <code>HorizontalCarousel.VIEWPORT_MODE_DOM</code> or
* <code>HorizontalCarousel.VIEWPORT_MODE_CLASSES</code>.
* @param {Integer} size Number of items in the viewport.
*/
setViewportMode: function setViewportMode (viewportMode, size) {
if (this._wrapMode === HorizontalCarousel.WRAP_MODE_VISUAL) {
if (viewportMode === HorizontalCarousel.VIEWPORT_MODE_DOM) {
throw new Error('HorizontalCarousel::setViewportMode - VIEWPORT_MODE_DOM not supported for WRAP_MODE_VISUAL');
}
}
if (viewportMode === HorizontalCarousel.VIEWPORT_MODE_DOM) {
if (!size) {
throw new Error('HorizontalCarousel::setViewportMode - You must specify a viewport size when using VIEWPORT_MODE_DOM');
}
this.setAutoRenderChildren(false);
} else {
this.setAutoRenderChildren(true);
}
this._viewportMode = viewportMode;
this._viewportSize = size;
},
/**
* Set the alignment of the active item.
* @param {Integer} align One of <code>HorizontalCarousel.ALIGNMENT_CENTER</code> (default),
* <code>HorizontalCarousel.ALIGNMENT_LEFT</code> or
* <code>HorizontalCarousel.ALIGNMENT_RIGHT</code>.
*/
setAlignment: function setAlignment (align) {
this._activeWidgetAlignment = align;
},
/**
* Get the current alignment of the active item.
* @returns {Integer} One of <code>HorizontalCarousel.ALIGNMENT_CENTER</code>,
* <code>HorizontalCarousel.ALIGNMENT_LEFT</code> or
* <code>HorizontalCarousel.ALIGNMENT_RIGHT</code>.
*/
getAlignment: function getAlignment () {
return this._activeWidgetAlignment;
},
/**
* Set the alignment offsetof the active item.
* @param {Integer} offset
*/
setAlignmentOffset: function setAlignmentOffset (offset) {
this._activeWidgetAlignmentOffset = offset;
},
/**
* Get the current alignment offest of the active item.
* @returns {Integer}
*/
getAlignmentOffset: function getAlignmentOffset () {
return this._activeWidgetAlignmentOffset;
},
/**
* Set the frames per second of the active widget selection animation.
* @param {Integer} fps
*/
setWidgetAnimationFPS: function setWidgetAnimationFPS (fps) {
this._activeWidgetAnimationFPS = fps;
},
/**
* Get the frames per second of the active widget selection animation.
* @returns {Integer}
*/
getWidgetAnimationFPS: function getWidgetAnimationFPS () {
return this._activeWidgetAnimationFPS;
},
/**
* Set the duration of the active widget selection animation.
* @param {Integer} duration
*/
setWidgetAnimationDuration: function setWidgetAnimationDuration (duration) {
this._activeWidgetAnimationDuration = duration;
},
/**
* Get the duration of the active widget selection animation.
* @returns {Integer}
*/
getWidgetAnimationDuration: function getWidgetAnimationDuration () {
return this._activeWidgetAnimationDuration;
},
/**
* Set the easing style of the active widget selection animation.
* @param {String} easing
* Acceptable values are:
* bounce
* bouncePast
* easeFrom
* easeTo
* easeFromTo
* easeInCirc
* easeOutCirc
* easeInOutCirc
* easeInCubic
* easeOutCubic
* easeInOutCubic
* easeInQuad
* easeOutQuad
* easeInOutQuad
* easeInQuart
* easeOutQuart
* easeInOutQuart
* easeInQuint
* easeOutQuint
* easeInOutQuint
* easeInSine
* easeOutSine
* easeInOutSine
* easeInExpo
* easeOutExpo
* easeInOutExpo
* easeOutBounce
* easeInBack
* easeOutBack
* easeInOutBack
* elastic
* swingFrom
* swingTo
* swingFromTo
*
*/
setWidgetAnimationEasing: function setWidgetAnimationEasing (easing) {
this._activeWidgetAnimationEasing = easing;
},
/**
* Get the current alignment of the active item.
* @returns {String}
*
*/
getWidgetAnimationEasing: function getWidgetAnimationEasing () {
return this._activeWidgetAnimationEasing;
},
/**
* Set whether the carousel contains items of differing widths. When all items are the
* same width, we can enabled additional optimisations
* @param {Boolean} multiWidthItems Pass <code>true</code> if the carousel contains items of differing widths.
*/
setHasMultiWidthItems: function setHasMultiWidthItems (multiWidthItems) {
this._multiWidthItems = multiWidthItems;
},
/**
* Set whether to activate the next item then scroll. By default, the carousel will
* be scrolled, then the new item activated once the scrolling has finished.
* Note: If set to true, you must make sure your styling of activated/focussed items
* do not behave strangely where the carousel wraps.
* @param {Boolean} wrap Pass <code>true</code> to activate then scroll. Pass <code>false</code>
* to scroll then activate (default).
*/
setActivateThenScroll: function setActivateThenScroll (activateThenScroll) {
this._activateThenScroll = activateThenScroll;
},
setKeepHidden: function setKeepHidden (keepHidden) {
this._keepHidden = keepHidden;
},
/**
* Returns this index of the currently selected child widget.
*/
getSelectedChildWidgetIndex: function getSelectedChildWidgetIndex () {
return this._selectedIndex;
},
/**
* Moves the selection to the previous focusable child widget.
*/
selectPreviousChildWidget: function selectPreviousChildWidget () {
return this._moveChildWidgetSelection(HorizontalCarousel.SELECTION_DIRECTION_LEFT);
},
/**
* Selects the next widget in the carousel.
*/
selectNextChildWidget: function selectNextChildWidget () {
return this._moveChildWidgetSelection(HorizontalCarousel.SELECTION_DIRECTION_RIGHT);
},
/**
* Finds a selectable widget in the specified direction and moves
* the focus to it.
*/
_moveChildWidgetSelection: function _moveChildWidgetSelection (direction) {
var device = this.getCurrentApplication().getDevice();
if (this._scrollHandle) {
device.stopAnimation(this._scrollHandle);
}
var _newIndex = this._selectedIndex;
var _nodeIndex = this._selectedIndex + this._prefixClones;
var _oldSelectedWidget = this._activeChildWidget;
var _newSelectedWidget = null;
var _centerElement = null;
var _wrapped = false;
do {
if (direction === HorizontalCarousel.SELECTION_DIRECTION_LEFT) {
_nodeIndex--;
if (_newIndex > 0) {
_newIndex--;
} else if (this._wrapMode && this._childWidgetOrder.length > 3) { /* Only wrap when more than 3 items */
_newIndex = this._childWidgetOrder.length - 1;
_wrapped = true;
} else {
break;
}
} else if (direction === HorizontalCarousel.SELECTION_DIRECTION_RIGHT) {
_nodeIndex++;
if (_newIndex < this._childWidgetOrder.length - 1) {
_newIndex++;
} else if (this._wrapMode && this._childWidgetOrder.length > 3) { /* Only wrap when more than 3 items */
_newIndex = 0;
_wrapped = true;
} else {
break;
}
}
var _widget = this._childWidgetOrder[_newIndex];
if (_widget.isFocusable()) {
_newSelectedWidget = _widget;
break;
}
} while (true);
// Centre on a cloned carousel item if we're wrapping to the other end.
// Otherwise, just go to the new selected item.
if (_wrapped && this._wrapMode === HorizontalCarousel.WRAP_MODE_VISUAL) {
_centerElement = this._getWrappedElement(direction, _oldSelectedWidget.outputElement);
}
else if (_newSelectedWidget) {
_centerElement = _newSelectedWidget.outputElement;
}
if (_newSelectedWidget && _centerElement) {
var self = this;
this.bubbleEvent(new BeforeSelectedItemChangeEvent(this, _newSelectedWidget, _newIndex));
var scrollDone = function () {
if (!self._activateThenScroll) {
self.setActiveChildWidget(_newSelectedWidget);
self._selectedIndex = _newIndex;
}
// If we've just moved to the fake item off the end of the wrapped carousel,
// snap to the real item at the opposite end when the animation completes.
if (_wrapped && self._wrapMode === HorizontalCarousel.WRAP_MODE_VISUAL) {
self._alignToElement(self._activeChildWidget.outputElement, true);
}
// Allow the carousel to move again.
self.refreshViewport();
self._scrollHandle = null;
};
if (this._activateThenScroll) {
this.setActiveChildWidget(_newSelectedWidget);
this._selectedIndex = _newIndex;
}
// If the offset is zero it means the element is not in the DOM, i.e. the other end of the carousel, scroll to 1 pixel
// otherwise in CSS3 the scrollDone event will never be called
var elpos = device.getElementOffset(_centerElement);
if (elpos.left === 0) {
elpos.left = 1;
}
var config = device.getConfig();
var animate = !config.widgets || !config.widgets.horizontalcarousel || (config.widgets.horizontalcarousel.animate !== false);
this._scrollHandle = this._alignToElement(_centerElement, this._isAnimationOverridden(animate), scrollDone);
return true;
} else {
return false;
}
},
_getWrappedElement: function _getWrappedElement (direction, element) {
// Return the next/previous widget in the carousel - used to grab dummy widgets
// used in the visual wrapping mode.
do {
element = (direction === HorizontalCarousel.SELECTION_DIRECTION_RIGHT ? element.nextSibling : element.previousSibling);
} while (element && element.nodeType !== 1);
return element;
},
_isAnimationOverridden: function _isAnimationOverridden (animate) {
return this._overrideAnimation || !animate;
},
_alignToElement: function _alignToElement (el, skipAnimation, onAnimationCompleteHandler) {
var device = this.getCurrentApplication().getDevice();
var widgetpos = device.getElementOffset(el);
var widgetsize = device.getElementSize(el);
var masksize = device.getElementSize(this._maskElement);
var offset = this._activeWidgetAlignmentOffset || 0;
var newLeftPosition;
switch (this._activeWidgetAlignment) {
case HorizontalCarousel.ALIGNMENT_CENTER:
newLeftPosition = widgetpos.left - (masksize.width - widgetsize.width) / 2 + offset;
break;
case HorizontalCarousel.ALIGNMENT_LEFT:
newLeftPosition = widgetpos.left + offset;
break;
case HorizontalCarousel.ALIGNMENT_RIGHT:
newLeftPosition = widgetpos.left - (masksize.width - widgetsize.width) - offset;
break;
}
return device.scrollElementTo({
el: this._maskElement,
to: {
left: newLeftPosition
},
fps: this.getWidgetAnimationFPS(),
duration: this.getWidgetAnimationDuration(),
easing: this.getWidgetAnimationEasing(),
skipAnim: skipAnimation,
onComplete: onAnimationCompleteHandler
});
}
});
HorizontalCarousel.ALIGNMENT_CENTER = 0;
HorizontalCarousel.ALIGNMENT_LEFT = 1;
HorizontalCarousel.ALIGNMENT_RIGHT = 2;
HorizontalCarousel.SELECTION_DIRECTION_RIGHT = 'right';
HorizontalCarousel.SELECTION_DIRECTION_LEFT = 'left';
HorizontalCarousel.WRAP_MODE_NONE = 0;
HorizontalCarousel.WRAP_MODE_NAVIGATION_ONLY = 1;
HorizontalCarousel.WRAP_MODE_VISUAL = 2;
HorizontalCarousel.VIEWPORT_MODE_NONE = 0;
HorizontalCarousel.VIEWPORT_MODE_CLASSES = 1;
HorizontalCarousel.VIEWPORT_MODE_DOM = 2;
return HorizontalCarousel;
}
);