/**
* The Image Viewer Base module.
*
* @module aui-image-viewer-base
*/
var CSS_CONTENT = A.getClassName('image', 'viewer', 'base', 'node', 'content'),
CSS_CONTROL = A.getClassName('image', 'viewer', 'base', 'control'),
CSS_CONTROL_LEFT = A.getClassName('image', 'viewer', 'base', 'control', 'left'),
CSS_CONTROL_RIGHT = A.getClassName('image', 'viewer', 'base', 'control', 'right'),
CSS_CURRENT_IMAGE = A.getClassName('image', 'viewer', 'base', 'current', 'image'),
CSS_IMAGE = A.getClassName('image', 'viewer', 'base', 'image'),
CSS_IMAGE_CONTAINER = A.getClassName('image', 'viewer', 'base', 'image', 'container'),
CSS_IMAGE_LIST = A.getClassName('image', 'viewer', 'base', 'image', 'list'),
CSS_IMAGE_LIST_INNER = A.getClassName('image', 'viewer', 'base', 'image', 'list', 'inner'),
CSS_LOADING = A.getClassName('image', 'viewer', 'base', 'loading'),
CSS_LOADING_ICON = A.getClassName('image', 'viewer', 'base', 'loading', 'icon');
/**
* Fired when the current image will be animated in.
*
* @event animate
* @preventable _defAnimateFn
*/
/**
* The base class for Image Viewer.
*
* @class A.ImageViewerBase
* @extends A.Widget
* @uses A.WidgetResponsive
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
A.ImageViewerBase = A.Base.create(
'image-viewer-base',
A.Widget, [
A.WidgetResponsive,
A.WidgetStack
], {
TPL_CONTROL_LEFT: '<a href="#" class="' + CSS_CONTROL + ' ' + CSS_CONTROL_LEFT +
'"><span class="glyphicon glyphicon-chevron-left"></span></a>',
TPL_CONTROL_RIGHT: '<a href="#" class="' + CSS_CONTROL + ' ' + CSS_CONTROL_RIGHT +
'"><span class="glyphicon glyphicon-chevron-right"></span></a>',
TPL_IMAGE: '<img class="' + CSS_IMAGE + '"/>',
TPL_IMAGE_CONTAINER: '<div class="' + CSS_IMAGE_CONTAINER + '">' +
'<span class="glyphicon glyphicon-time ' + CSS_LOADING_ICON + '"></span></div>',
TPL_IMAGE_LIST: '<div class="' + CSS_IMAGE_LIST + '"><div class="' + CSS_IMAGE_LIST_INNER + '"></div></div>',
/**
* Constructor for the `A.ImageViewerBase`. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
this._eventHandles = [];
this.publish({
animate: {
defaultFn: this._defAnimateFn
}
});
if (this.get('useARIA')) {
this.plug(
A.Plugin.Aria,
{
roleName: this.get('role'),
roleNode: this.get('contentBox')
}
);
}
},
/**
* Create the DOM structure for the `A.ImageViewerBase`. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
this.get('boundingBox').unselectable();
this._renderImagesForFirstTime();
this._renderControls();
},
/**
* Bind the events for the `A.ImageViewerBase` UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
this._eventHandles.push(
this.after({
currentIndexChange: this._afterCurrentIndexChange,
preloadAllImagesChange: this._afterPreloadAllImagesChange,
responsive: this._afterResponsive,
showControlsChange: this._afterShowControlsChange,
sourcesChange: this._afterSourcesChange
}),
this.on('responsive', this._onResponsive),
A.after(this._afterUISetVisible, this, '_uiSetVisible')
);
this._bindControls();
},
/**
* Destructor implementation for the `A.ImageViewerBase` class. Lifecycle.
*
* @method destructor
* @protected
*/
destructor: function() {
(new A.EventHandle(this._eventHandles)).detach();
},
/**
* Checks if there is a next element to navigate.
*
* @method hasNext
* @return {Boolean}
*/
hasNext: function() {
return this.get('circular') || (this.get('currentIndex') < (this.get('sources').length - 1));
},
/**
* Checks if there is a previous element to navigate.
*
* @method hasPrev
* @return {Boolean}
*/
hasPrev: function() {
return this.get('circular') || (this.get('currentIndex') > 0);
},
/**
* Loads the next image.
*
* @method next
*/
next: function() {
if (this.hasNext()) {
if (this.get('currentIndex') === (this.get('sources').length - 1)) {
this.set('currentIndex', 0);
}
else {
this.set('currentIndex', (this.get('currentIndex') + 1));
}
}
},
/**
* Loads the previous image.
*
* @method prev
*/
prev: function() {
if (this.hasPrev()) {
if (this.get('currentIndex') === 0) {
this.set('currentIndex', (this.get('sources').length - 1));
}
else {
this.set('currentIndex', (this.get('currentIndex') - 1));
}
}
},
/**
* Fired after the `currentIndex` attribute is set.
*
* @method _afterCurrentIndexChange
* @param {EventFacade} event
* @protected
*/
_afterCurrentIndexChange: function(event) {
this._previousIndex = event.prevVal;
this._showCurrentImage();
},
/**
* Fired after the `preloadAllImages` attribute is changed.
*
* @method _afterPreloadAllImagesChange
* @protected
*/
_afterPreloadAllImagesChange: function() {
this._preloadAll();
},
/**
* Fired after the `responsive` event.
*
* @method _afterResponsive
* @protected
*/
_afterResponsive: function() {
var image = this._getCurrentImage();
if (image) {
image.setStyles({
maxHeight: '100%',
maxWidth: '100%'
});
}
},
/**
* Fired after the `showControls` attribute is changed.
*
* @method _afterShowControlsChange
* @protected
*/
_afterShowControlsChange: function() {
this._syncControlsUI();
},
/**
* Fired after the `sources` attribute is changed.
*
* @method _afterSourcesChange
* @protected
*/
_afterSourcesChange: function() {
this._renderImages();
},
/**
* Fired after the `visible` attribute is set.
*
* @method _afterUISetVisible
* @protected
*/
_afterUISetVisible: function() {
if (this.get('visible')) {
this._showCurrentImage();
}
else {
this._syncControlsUI();
}
},
/**
* Binds the events related to the viewer's controls.
*
* @method _bindControls
* @protected
*/
_bindControls: function() {
this._eventHandles.push(
this.get('boundingBox').delegate('click', this._onClickControl, '.' + CSS_CONTROL, this)
);
},
/**
* Default behavior for animating the current image in the viewer.
*
* @method _defAnimateFn
* @protected
*/
_defAnimateFn: function() {
var image = this._getCurrentImage(),
imageAnim = this.get('imageAnim');
if (imageAnim === false) {
return;
}
if (!this._animation) {
this._animation = new A.Anim(imageAnim);
}
else {
this._animation.stop(true);
this._animation.setAttrs(imageAnim);
}
image.setStyle('opacity', 0);
this._animation.set('node', image);
this._animation.run();
},
/**
* Gets the current image node.
*
* @method _getCurrentImage
* @return {Node}
* @protected
*/
_getCurrentImage: function() {
if (this.get('sources').length) {
return this._getCurrentImageContainer().one('.' + CSS_IMAGE);
}
},
/**
* Returns the container node for the current image.
*
* @method _getCurrentImageContainer
* @protected
*/
_getCurrentImageContainer: function() {
return this._getImageContainerAtIndex(this.get('currentIndex'));
},
/**
* Returns the image container node.
*
* @method _getImageContainer
* @protected
*/
_getImageContainer: function() {
return this.get('contentBox').all('.' + CSS_IMAGE_CONTAINER);
},
/**
* Returns the container node at the requested index.
*
* @method _getImageContainerAtIndex
* @param {Number} index
* @protected
*/
_getImageContainerAtIndex: function(index) {
return this._getImageContainer().item(index);
},
/**
* Fires the necessary events to load the requested image.
*
* @method _loadImage
* @param {Number} index The index of the image to load.
* @protected
*/
_loadImage: function(index) {
this.fire('load' + index);
},
/**
* Fired when one of the viewer's controls is clicked.
*
* @method _onClickControl
* @param {EventFacade} event
* @protected
*/
_onClickControl: function(event) {
event.preventDefault();
if (event.currentTarget.hasClass(CSS_CONTROL_LEFT)) {
this.prev();
}
else if (event.currentTarget.hasClass(CSS_CONTROL_RIGHT)) {
this.next();
}
},
/**
* This should be called when the current image is ready to be
* displayed.
*
* @method _onCurrentImageReady
* @protected
*/
_onCurrentImageReady: function() {
if (this.get('visible')) {
this.updateDimensionsWithNewRatio();
this.fire('animate', {
image: this._getCurrentImage()
});
}
},
/**
* Fired when an image has finished loading.
*
* @method _onImageLoad
* @protected
*/
_onImageLoad: function(image, index) {
image.setData('loaded', true);
image.get('parentNode').removeClass(CSS_LOADING);
if (this.get('visible') && index === this.get('currentIndex')) {
this._onCurrentImageReady();
}
},
/**
* Fired on the `responsive` event.
*
* @method _onResponsive
* @protected
*/
_onResponsive: function() {
var image = this._getCurrentImage();
if (image) {
image.setStyles({
maxHeight: 'none',
maxWidth: 'none'
});
}
},
/**
* Preloads all the images if the `preloadAllImages` attribute is true.
*
* @method _preloadAll
* @protected
*/
_preloadAll: function() {
var sources = this.get('sources');
if (this.get('preloadAllImages')) {
for (var i = 0; i < sources.length; i++) {
this._loadImage(i);
}
}
},
/**
* Renders the viewer's controls.
*
* @method _renderControls
* @protected
*/
_renderControls: function() {
this.get('contentBox').prepend(this.get('controlPrevious'));
this.get('contentBox').append(this.get('controlNext'));
},
/**
* Renders the requested image and registers it to be loaded when used.
*
* @method _renderImage
* @param {Number} index The index of the image to be loaded.
* @param {Node} container The container where the image should be added.
* @protected
*/
_renderImage: function(index, container) {
var group,
image = A.Node.create(this.TPL_IMAGE),
src = this.get('sources')[index];
if (A.Lang.isString(src)) {
container.prepend(image);
this._eventHandles.push(
image.once('load', A.bind(this._onImageLoad, this, image, index))
);
group = new A.ImgLoadGroup();
group.addCustomTrigger('load' + index, this);
group.registerImage({
domId: image.generateID(),
srcUrl: src
});
}
else if (src instanceof A.Node) {
container.prepend(src);
src.setData('loaded', true);
src.addClass(CSS_IMAGE);
src.addClass(CSS_CONTENT);
src.get('parentNode').removeClass(CSS_LOADING);
}
},
/**
* Renders all image containers and returns them in an array.
*
* @method _renderImageContainers
* @return {Array} list of image containers
* @protected
*/
_renderImageContainers: function() {
var aria = this.get('useARIA'),
container,
containers = [],
list = this._renderImageListNode(),
sources = this.get('sources');
for (var i = 0; i < sources.length; i++) {
container = A.Node.create(this.TPL_IMAGE_CONTAINER);
container.addClass(CSS_LOADING);
if (aria) {
this._syncAriaImageContainerUI(container);
}
list.append(container);
containers.push(container);
}
return containers;
},
/**
* Renders the image list node inside the image viewer.
*
* @method _renderImageListNode
* @return {Node} the image list node
* @protected
*/
_renderImageListNode: function() {
var list = A.Node.create(this.TPL_IMAGE_LIST);
this.get('contentBox').setHTML(list);
return list.one('.' + CSS_IMAGE_LIST_INNER);
},
/**
* Renders the images indicated in the `sources` attribute.
*
* @method _renderImages
* @protected
*/
_renderImages: function() {
var containers = this._renderImageContainers();
for (var i = 0; i < containers.length; i++) {
this._renderImage(i, containers[i]);
}
this._preloadAll();
},
/**
* Renders the images indicated in the `sources` attribute for
* the first time. If the images were already present before the widget
* is rendered, we'll mark these images as having already been loaded.
*
* @method _renderImagesForFirstTime
* @protected
*/
_renderImagesForFirstTime: function() {
var container,
images = this.get('contentBox').all('.' + CSS_IMAGE);
this._renderImages();
if (images.size()) {
for (var i = 0; i < this.get('sources').length; i++) {
container = this._getImageContainerAtIndex(i);
container.removeClass(CSS_LOADING);
container.one('.' + CSS_IMAGE).set('loaded', true);
}
}
},
/**
* Shows the current image in the viewer.
*
* @method _showCurrentImage
* @protected
*/
_showCurrentImage: function() {
var currentIndex = this.get('currentIndex'),
image;
if (!this.get('visible') || !this.get('sources').length) {
return;
}
this._updateCurrentImageCSS();
this._loadImage(currentIndex);
if (this.get('preloadNeighborImages')) {
if (this.hasPrev()) {
this._loadImage(currentIndex - 1);
}
if (this.hasNext()) {
this._loadImage(currentIndex + 1);
}
}
image = this._getCurrentImage();
if (image.getData('loaded')) {
this._onCurrentImageReady();
}
this._syncControlsUI();
},
/**
* Sets `currentIndex` attribute.
*
* @method _setCurrentIndex
* @param {Number|String} val
* @protected
*/
_setCurrentIndex: function(val) {
var sourcesLength = this.get('sources').length;
if (val === 'rand') {
return Math.floor(Math.random() * sourcesLength);
}
else {
return Math.max(Math.min(val, (sourcesLength - 1)), 0);
}
},
/**
* Sets `imageAnim` attribute.
*
* @method _setImageAnim
* @param {Object} val
* @protected
*/
_setImageAnim: function(val) {
if (val === false) {
return val;
}
else {
return A.merge({
to: {
opacity: 1
},
duration: 0.5
},
val
);
}
},
/**
* Update the aria attributes for image.
* @method _syncAriaCurrentImageUI
* @protected
*/
_syncAriaCurrentImageUI: function() {
this.aria.setAttributes(
[
{
name: 'hidden',
node: this._getImageContainer(),
value: 'true'
},
{
name: 'hidden',
node: this._getCurrentImageContainer(),
value: 'false'
}
]
);
},
/**
* Update the aria attributes for image container.
* @method _syncAriaCurrentImageUI
* @param {node} container The container for the images
* @protected
*/
_syncAriaImageContainerUI: function(container) {
this.aria.setAttribute('hidden', true, container);
},
/**
* Updates the controls, showing or hiding them as necessary.
*
* @method _syncControlsUI
* @protected
*/
_syncControlsUI: function() {
var hasNext = (this.get('visible') && this.get('showControls') && this.hasNext());
var hasPrev = (this.get('visible') && this.get('showControls') && this.hasPrev());
this.get('controlNext').toggleClass('invisible', !hasNext);
this.get('controlPrevious').toggleClass('invisible', !hasPrev);
},
/**
* Sets the CSS class that indicates the current image.
*
* @method _updateCurrentImageCSS
* @protected
*/
_updateCurrentImageCSS: function() {
if (A.Lang.isNumber(this._previousIndex)) {
this._getImageContainerAtIndex(this._previousIndex).removeClass(CSS_CURRENT_IMAGE);
}
this._getCurrentImageContainer().addClass(CSS_CURRENT_IMAGE);
if (this.get('useARIA')) {
this._syncAriaCurrentImageUI();
}
}
}, {
/**
* Static property used to define the default attribute configuration
* for the `A.ImageViewerBase`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* If the image list will be circular or not.
*
* @attribute circular
* @default false
* @type Boolean
*/
circular: {
value: false,
validator: A.Lang.isBoolean
},
/**
* The node for the control that shows the next image.
*
* @attribute controlNext
* @default null
* @type Node
*/
controlNext: {
validator: A.Lang.isNode,
valueFn: function() {
return A.Node.create(this.TPL_CONTROL_RIGHT);
},
writeOnce: 'initOnly'
},
/**
* The node for the control that shows the previous image.
*
* @attribute controlPrevious
* @default null
* @type Node
*/
controlPrevious: {
validator: A.Lang.isNode,
valueFn: function() {
return A.Node.create(this.TPL_CONTROL_LEFT);
},
writeOnce: 'initOnly'
},
/**
* Index of the current image.
*
* @attribute currentIndex
* @default 0
* @type Number | String
*/
currentIndex: {
setter: '_setCurrentIndex',
value: 0,
validator: function(val) {
return A.Lang.isNumber(val) || A.Lang.isString(val);
}
},
/**
* Configuration attributes passed to the [Anim](Anim.html) class, or
* false if there should be no animation.
*
* @attribute imageAnim
* @default Predefined [Anim](Anim.html) configuration.
* @type Boolean | Object
*/
imageAnim: {
value: {},
setter: '_setImageAnim',
validator: function(val) {
return A.Lang.isObject(val) || val === false;
}
},
/**
* Preloads all images listed in the `sources` attribute.
*
* @attribute preloadAllImages
* @default false
* @type Boolean
*/
preloadAllImages: {
value: false,
validator: A.Lang.isBoolean
},
/**
* Preloads the neighbor image (i.e., the previous and next image
* based on the current load one).
*
* @attribute preloadAllImages
* @default false
* @type Boolean
*/
preloadNeighborImages: {
value: true,
validator: A.Lang.isBoolean
},
/**
* Sets the `aria-role` for carousel.
*
* @attribute role
* @default 'listbox'
* @type String
*/
role: {
validator: A.Lang.isString,
value: 'listbox',
writeOnce: 'initOnly'
},
/**
* Shows the controls.
*
* @attribute showControls
* @default true
* @type Boolean
*/
showControls: {
value: true,
validator: A.Lang.isBoolean
},
/**
* The source links for the images to be shown.
*
* @attribute sources
* @default []
* @type Array
*/
sources: {
value: [],
validator: A.Lang.isArray
},
/**
* Boolean indicating if use of the WAI-ARIA Roles and States
* should be enabled.
*
* @attribute useARIA
* @default true
* @type Boolean
*/
useARIA: {
validator: A.Lang.isBoolean,
value: true,
writeOnce: 'initOnly'
}
},
/**
* Static property provides a string to identify the CSS prefix.
*
* @property CSS_PREFIX
* @type String
* @static
*/
CSS_PREFIX: A.getClassName('image-viewer-base'),
/**
* Object hash, defining how attribute values are to be parsed from
* markup contained in the widget's content box.
*
* @property HTML_PARSER
* @type Object
* @static
*/
HTML_PARSER: {
controlNext: function(srcNode) {
return srcNode.one('.' + CSS_CONTROL_RIGHT);
},
controlPrevious: function(srcNode) {
return srcNode.one('.' + CSS_CONTROL_LEFT);
},
sources: function(srcNode) {
var backgroundImageStyle,
childImg,
img,
images = srcNode.all('.' + CSS_IMAGE + ', .' + CSS_CONTENT),
isImg,
sources = [];
images.each(function() {
isImg = this.test('img'),
childImg = this.one('img'),
backgroundImageStyle = this.getStyle('backgroundImage');
if (this.hasClass(CSS_CONTENT)) {
sources.push(this);
}
else if (isImg || childImg) {
img = isImg ? this : childImg;
sources.push(img.getAttribute('src'));
}
else if (backgroundImageStyle !== 'none') {
sources.push(A.Lang.String.removeAll(backgroundImageStyle.slice(4, -1), '"'));
}
});
return sources;
}
}
}
);