/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This module can be used to display multiple items in a horizontal strip that can be scrolled through use
* previous and next buttons. It was written to support the [filmstrip view]{@link module:alfresco/documentlibrary/views/AlfFilmStripView}
* which uses both this module (and the extending module [DocumentCarousel]{@link module:alfresco/documentlibrary/views/layouts/DocumentCarousel}
* to show both the entire contents of a folder and a preview of the currently selected item.
*
* @module alfresco/lists/views/layouts/Carousel
* @extends external:dijit/_WidgetBase
* @mixes external:dojo/_TemplatedMixin
* @mixes external:dojo/_OnDijitClickMixin
* @mixes module:alfresco/core/Core
* @mixes module:alfresco/layout/HeightMixin
* @mixes module:alfresco/core/ResizeMixin
* @mixes module:alfresco/lists/views/layouts/_MultiItemRendererMixin
* @author Dave Draper
*/
define(["dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dijit/_OnDijitClickMixin",
"alfresco/core/Core",
"alfresco/layout/HeightMixin",
"alfresco/core/ResizeMixin",
"alfresco/lists/views/layouts/_MultiItemRendererMixin",
"alfresco/core/topics",
"alfresco/enums/urlTypes",
"dojo/text!./templates/Carousel.html",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/dom-class",
"dojo/dom-construct",
"dojo/dom-style",
"dojo/dom-geometry",
"dojo/on",
"alfresco/html/Image"],
function(declare, _WidgetBase, _TemplatedMixin, _OnDijitClickMixin, AlfCore, HeightMixin, ResizeMixin, _MultiItemRendererMixin,
topics, urlTypes, template, lang, array, domClass, domConstruct, domStyle, domGeom, on, Image) {
return declare([_WidgetBase, _TemplatedMixin, _OnDijitClickMixin, HeightMixin, ResizeMixin, _MultiItemRendererMixin, AlfCore], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/Carousel.properties"}]
* @since 1.0.41
*/
i18nRequirements: [{i18nFile: "./i18n/Carousel.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{cssFile:"./css/Carousel.css"}]
*/
cssRequirements: [{cssFile: "./css/Carousel.css"}],
/**
* The HTML template to use for the widget.
*
* @instance
* @type {String}
*/
templateString: template,
/**
* This keeps track of the current left position (e.g. the setting that controls what items you can see
* within the clipped frame). This value is updated by the
* [onPrevClick]{@link module:alfresco/lists/views/layouts/Carousel#onPrevClick} and
* [onNextClick]{@link module:alfresco/lists/views/layouts/Carousel#onNextClick} functions.
*
* @instance
* @type {number}
* @default
*/
currentLeftPosition: 0,
/**
* This is the default next arrow. It can be overridden using the [nextArrow property]{@link
* module:alfresco/lists/views/Carousel#nextArrow}. It should not be used directly.
*
* @instance
* @type {object}
* @readonly
* @since 1.0.41
*/
defaultNextArrow: {
altText: "next-arrow.alt-text",
src: "alfresco/documentlibrary/views/layouts/css/images/filmstrip-content-nav-next.png",
srcType: urlTypes.REQUIRE_PATH,
width: 27,
height: 100
},
/**
* This is the default previous arrow. It can be overridden using the [prevArrow property]{@link
* module:alfresco/lists/views/Carousel#prevArrow}. It should not be used directly.
*
* @instance
* @type {object}
* @readonly
* @since 1.0.41
*/
defaultPrevArrow: {
altText: "prev-arrow.alt-text",
src: "alfresco/documentlibrary/views/layouts/css/images/filmstrip-content-nav-prev.png",
srcType: urlTypes.REQUIRE_PATH,
width: 27,
height: 100
},
/**
* This keeps track of the first displayed item in the currently visible frame.
*
* @instance
* @type {number}
* @default
*/
firstDisplayedIndex: 0,
/**
* This can be set to a value (in pixels) to fix the height. If this is left as null then
* a suitable height will attempt to be calculated
*
* @instance
* @type {string}
* @default
*/
height: null,
/**
* Sets the width of each item (in pixels)
*
* @instance
* @type {number}
* @default
*/
itemWidth: 100,
/**
* This keeps track of the lasts displayed item in the currently visible frame
*
* @instance
* @type {number}
* @default
*/
lastDisplayedIndex: null,
/**
* Used to indicate more data is required, as we're at the end of the current data
*
* @event loadMoreDataTopic
* @instance
* @type {string}
* @default [topics.SCROLL_NEAR_BOTTOM]{@link module:alfresco/core/topics#SCROLL_NEAR_BOTTOM}
* @since 1.0.32
*/
loadMoreDataTopic: topics.SCROLL_NEAR_BOTTOM,
/**
* Sets the width to allow for the next and previous buttons
*
* @instance
* @type {number}
* @default
*/
navigationMargin: 40,
/**
* <p>This property can be used to customise the arrow used in the carousel. The values within
* this object correspond to the properties of an [Image]{@link module:alfresco/html/Image}
* widget, which should be used as a reference of available properties.</p>
*
* <p><strong>NOTE:</strong> All defaults are as they are in the Image widget, apart from srcType
* which instead defaults to [FULL_PATH]{@link module:alfresco/enums/urlTypes#FULL_PATH}.</p>
*
* @instance
* @type {object}
* @see module:alfresco/html/Image
* @default
* @since 1.0.41
*/
nextArrow: null,
/**
* <p>This property can be used to customise the arrow used in the carousel. The values within
* this object correspond to the properties of an [Image]{@link module:alfresco/html/Image}
* widget, which should be used as a reference of available properties.</p>
*
* <p><strong>NOTE:</strong> All defaults are as they are in the Image widget, apart from srcType
* which instead defaults to [FULL_PATH]{@link module:alfresco/enums/urlTypes#FULL_PATH}.</p>
*
* @instance
* @type {object}
* @see module:alfresco/html/Image
* @default
* @since 1.0.41
*/
prevArrow: null,
/**
* Whether the list that's creating this view has infinite scroll turned on
*
* @instance
* @type {boolean}
* @default
* @since 1.0.32
*/
useInfiniteScroll: false,
/**
* Calls [processWidgets]{@link module:alfresco/core/Core#processWidgets}
*
* @instance postCreate
*/
postCreate: function alfresco_lists_views_layouts_Carousel__postCreate() {
if (this.currentItem)
{
if (this.widgets)
{
this.processWidgets(this.widgets, this.containerNode);
}
}
// Create and place the navigation arrows
this.setupNavigationArrows();
// Subscribe to selection topics...
if (this.itemSelectionTopics)
{
array.forEach(this.itemSelectionTopics, function(topic) {
this.alfSubscribe(topic, lang.hitch(this, this.selectItem));
}, this);
}
// Subscibe to the page widgets ready topic to ensure that sizing occurs...
this.alfSubscribe(topics.PAGE_WIDGETS_READY, lang.hitch(this, this.resize), true);
// Handle resize events...
this.alfSetupResizeSubscriptions(this.resize, this);
},
/**
* This function is called once all widgets have been added onto the page. At this point it can be
* assumed that the widget has been placed into the DOM model and has some dimensions to work with
*
* @instance
*/
resize: function alfresco_lists_views_layouts_Carousel__resize() {
this.calculateSizes();
if (this.itemsNode)
{
domStyle.set(this.itemsNode, "width", this.itemsNodeWidth + "px");
domStyle.set(this.itemsNode, "height", this.height);
}
this.resizeContainer();
// Set the range of displayed items...
this.lastDisplayedIndex = this.firstDisplayedIndex + (this.numberOfItemsShown - 1);
this.renderDisplayedItems();
},
/**
* Resizes the container that holds all the items (some of which may be hidden from view). It
* sets the width by multiplying the number of items by the item width. This specific resizing
* has been abstracted to it's own function so that it can be easily re-used by extending widgets
* that may wish to call it (such as the [DocumentCarousel]{@link module:alfresco/documentlibrary/views/layouts/DocumentCarousel})
*
* @instance
*/
resizeContainer: function alfresco_lists_views_layouts_Carousel__resizeContainer() {
var itemsCount = lang.getObject("currentData.items.length", false, this);
if (this.containerNode && itemsCount !== null)
{
var totalWidth = (itemsCount * this.itemsNodeWidth) + "px";
domStyle.set(this.containerNode, "width", totalWidth);
}
},
/**
* Gets the available dimensions of the DOM node in preparation for resizing the widget components.
* This also works out how many items should be shown within the current viewing frame.
*
* @instance
*/
calculateSizes: function alfresco_lists_views_layouts_Carousel__calculateSizes() {
if (this.domNode)
{
// Get the available width of the items node...
var computedStyle = domStyle.getComputedStyle(this.domNode);
var output = domGeom.getMarginBox(this.domNode, computedStyle);
// The width to use is the node width minus the space reserved for the navigation controls...
var overallWidth = output.w - (2 * this.navigationMargin);
// For now assume that the width of each item will be 100px....
// Divide the itemsNode width by 100 to get the number of items
this.numberOfItemsShown = Math.floor(overallWidth/this.itemWidth);
this.itemsNodeWidth = this.numberOfItemsShown * this.itemWidth;
if (this.fixedHeight)
{
// Use the configured height...
this.height = this.fixedHeight;
}
else
{
this.height = this.calculateHeight(this.domNode) + "px";
}
}
},
/**
* Extends the inherited function to add an additional li element for each item.
*
* @instance
* @param {array} widgets The widgets to create
* @param {element} rootNode The DOM element to add them into.
*/
processWidgets: function alfresco_lists_views_layouts_Carousel__processWidgets(widgets, rootNode) {
var nodeToAdd = domConstruct.create("li", {}, rootNode);
this.inherited(arguments, [widgets, nodeToAdd]);
},
/**
* Handles the user clicking on the previous items navigation control.
*
* @instance
* @param {object} evt The click event
*/
onPrevClick: function alfresco_lists_views_layouts_Carousel__onPrevClick(/*jshint unused:false*/ evt) {
if (this.currentLeftPosition > 0)
{
this.currentLeftPosition -= this.itemsNodeWidth;
// Update the displayed range...
this.firstDisplayedIndex -= this.numberOfItemsShown;
this.lastDisplayedIndex -= this.numberOfItemsShown;
this.renderDisplayedItems();
}
},
/**
* Handles the user clicking on the previous items navigation control.
*
* @instance
* @param {object} evt The click event
*/
onNextClick: function alfresco_lists_views_layouts_Carousel__onNextClick(/*jshint unused:false*/ evt) {
this.currentLeftPosition += this.itemsNodeWidth;
// Update the displayed range...
this.firstDisplayedIndex += this.numberOfItemsShown;
this.lastDisplayedIndex += this.numberOfItemsShown;
this.renderDisplayedItems();
},
/**
* Iterates over the [processed widgets]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#_renderedItemWidgets}
* between the [first]{@link module:alfresco/lists/views/layouts/Carousel#firstDisplayedIndex} and
* [last]{@link module:alfresco/lists/views/layouts/Carousel#lastDisplayedIndex} indices calling
* the render function on each to ensure they display themselves correctly
*
* @instance
* @fires loadMoreDataTopic
*/
renderDisplayedItems: function alfresco_lists_views_layouts_Carousel__renderDisplayedItems() {
for (var i=this.firstDisplayedIndex; i<=this.lastDisplayedIndex; i++)
{
if (this._renderedItemWidgets)
{
var widgets = this._renderedItemWidgets[i];
array.forEach(widgets, lang.hitch(this, this.renderDisplayedItem));
}
}
// Make sure the frame is aligned correctly...
this.currentLeftPosition = (this.firstDisplayedIndex / this.numberOfItemsShown) * this.itemsNodeWidth;
var left = "-" + this.currentLeftPosition + "px";
this.containerNode && domStyle.set(this.containerNode, "left", left);
var itemsCount = lang.getObject("currentData.items.length", false, this);
this.prevNode && domStyle.set(this.prevNode, "visibility", (this.firstDisplayedIndex === 0) ? "hidden": "visible");
this.nextNode && domStyle.set(this.nextNode, "visibility", (this.firstDisplayedIndex <= itemsCount-1 && this.lastDisplayedIndex >= itemsCount-1) ? "hidden": "visible");
// Can only enter this condition if we're an items carousel in infinite
// scroll mode (because the preview carousel doesn't get told this)
if (this.useInfiniteScroll)
{
var hasMoreItems = this.currentData.totalRecords > itemsCount,
numPages = Math.ceil(itemsCount / this.numberOfItemsShown),
lastPageStartIndex = (numPages - 1) * this.numberOfItemsShown,
lastPageEndIndex = lastPageStartIndex + (this.numberOfItemsShown - 1),
onLastPage = (this.lastDisplayedIndex >= lastPageStartIndex) && (this.lastDisplayedIndex <= lastPageEndIndex);
if (hasMoreItems && onLastPage) {
this.alfPublish(this.loadMoreDataTopic);
}
}
},
/**
* Attempts to render a widget that is currently displayed in the viewing frame.
*
* @instance
* @param {object} widget The widget to render
* @param {number} index The index of the widget within the array
*/
renderDisplayedItem: function alfresco_lists_views_layouts_Carousel__renderDisplayedItem(widget, /*jshint unused:false*/ index) {
if (widget && typeof widget.render === "function")
{
widget.render();
}
},
/**
* Handles requests to select an item
*
* @instance
* @param {object} payload
*/
selectItem: function alfresco_lists_views_layouts_Carousel__item(payload) {
if ((payload.index || payload.index === 0) && !isNaN(parseInt(payload.index, 10)))
{
var targetIndex = parseInt(payload.index, 10);
if (targetIndex >= this.firstDisplayedIndex && targetIndex <= this.lastDisplayedIndex)
{
// The requested item is currently displayed, no action necessary...
}
else if (targetIndex > this.lastDisplayedIndex)
{
// Start navigating back to find the item
while(targetIndex > this.lastDisplayedIndex)
{
// this.onNextClick();
this.firstDisplayedIndex += this.numberOfItemsShown;
this.lastDisplayedIndex += this.numberOfItemsShown;
}
this.renderDisplayedItems();
}
else
{
// Start navigating forward to find the item
while(targetIndex < this.firstDisplayedIndex)
{
// this.onPrevClick();
this.firstDisplayedIndex -= this.numberOfItemsShown;
this.lastDisplayedIndex -= this.numberOfItemsShown;
}
this.renderDisplayedItems();
}
}
},
/**
* Configured and create the navigation arrows.
*
* @instance
* @since 1.0.41
*/
setupNavigationArrows: function alfresco_lists_views_layouts_Carousel__setupNavigationArrows() {
// NOTE: Can't use this.createWidget() as the inherited implementation needs a currentItem property
var nextConfig = lang.mixin({
pubSubScope: this.pubSubScope,
parentPubSubScope: this.parentPubSubScope
}, this.nextArrow || this.defaultNextArrow),
prevConfig = lang.mixin({
pubSubScope: this.pubSubScope,
parentPubSubScope: this.parentPubSubScope
}, this.prevArrow || this.defaultPrevArrow),
nextArrow = new Image(nextConfig),
prevArrow = new Image(prevConfig);
nextArrow.placeAt(this.nextNode);
prevArrow.placeAt(this.prevNode);
this.own(on(nextArrow, "click", lang.hitch(this, this.onNextClick)));
this.own(on(prevArrow, "click", lang.hitch(this, this.onPrevClick)));
}
});
});