/**
* The ImageViewer Utility
*
* @module aui-image-viewer
*/
var CSS_CAPTION = A.getClassName('image', 'viewer', 'caption'),
CSS_CLOSE = A.getClassName('image', 'viewer', 'close'),
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_FOOTER_BUTTONS = A.getClassName('image', 'viewer', 'footer', 'buttons'),
CSS_FOOTER_CONTENT = A.getClassName('image', 'viewer', 'footer', 'content'),
CSS_INFO = A.getClassName('image', 'viewer', 'info'),
CSS_MASK = A.getClassName('image', 'viewer', 'mask'),
CSS_THUMBNAILS = A.getClassName('image', 'viewer', 'thumbnails');
/**
* A class for `A.ImageViewer`, providing:
*
* - Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)
* - Displays an image in a Overlay
* - Keyboard navigation support
*
* Check the [live demo](http://alloyui.com/examples/image-viewer/).
*
* @class A.ImageViewer
* @extends A.ImageViewerBase
* @uses A.ImageViewerSlideshow, A.WidgetCssClass, WidgetStdMod, A.WidgetToggle,
* WidgetPosition, WidgetStack, WidgetPositionAlign, WidgetPositionConstrain,
* WidgetModality
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
A.ImageViewer = A.Base.create(
'image-viewer',
A.ImageViewerBase, [
A.ImageViewerSlideshow,
A.WidgetCssClass,
A.WidgetStdMod,
A.WidgetToggle,
A.WidgetPosition,
A.WidgetStack,
A.WidgetPositionAlign,
A.WidgetPositionConstrain,
A.WidgetModality
], {
TPL_CAPTION: '<h4 class="' + CSS_CAPTION + '"></h4>',
TPL_CLOSE: '<button class="close ' + CSS_CONTROL + ' ' + CSS_CLOSE + '" type="button">' +
'<span class="glyphicon glyphicon-remove"></span></button>',
TPL_CONTROL_LEFT: '<a href="#" class="carousel-control left ' +
CSS_CONTROL + ' ' + CSS_CONTROL_LEFT +
'"><span class="glyphicon glyphicon-chevron-left"></span></a>',
TPL_CONTROL_RIGHT: '<a href="#" class="carousel-control right ' +
CSS_CONTROL + ' ' + CSS_CONTROL_RIGHT +
'"><span class="glyphicon glyphicon-chevron-right"></span></a>',
TPL_FOOTER_BUTTONS: '<span class="' + CSS_FOOTER_BUTTONS + '"></span>',
TPL_FOOTER_CONTENT: '<div class="' + CSS_FOOTER_CONTENT + '"></div>',
TPL_INFO: '<h5 class="' + CSS_INFO + '"></h5>',
TPL_PLAYER: '<span><span class="glyphicon glyphicon-play"></span></span>',
TPL_THUMBNAILS: '<div class="' + CSS_THUMBNAILS + '"></div>',
/**
* Create the DOM structure for the `A.ImageViewer`. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
A.ImageViewer.superclass.renderUI.apply(this, arguments);
this._renderFooter();
},
/**
* Bind the events for the `A.ImageViewer` UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
A.ImageViewer.superclass.bindUI.apply(this, arguments);
this._attachLinkEvent();
this._eventHandles.push(
this.after({
linksChange: this._afterLinksChange,
thumbnailsConfigChange: this._afterThumbnailsConfigChange
}),
this._closeEl.after('click', A.bind(this._afterCloseClicked, this)),
A.getDoc().on('keydown', A.bind(this._onKeydown, this)),
A.after(this._afterFillHeight, this, 'fillHeight')
);
this._bindThumbnails();
},
/**
* Destructor lifecycle implementation for the `A.ImageViewer` class.
*
* @method destructor
* @protected
*/
destructor: function() {
this._detachLinkEvent();
this.get('controlPrevious').remove(true);
this.get('controlNext').remove(true);
this._closeEl.remove(true);
this.get('maskNode').removeClass(CSS_MASK);
if (this._thumbnailsWidget) {
this._thumbnailsWidget.destroy();
}
},
/**
* Gets the `Node` reference to the `index` element from the
* [links](A.ImageViewer.html#attr_links).
*
* @method getLink
* @param index
* @return {Node}
*/
getLink: function(index) {
return this.get('links').item(index);
},
/**
* Fired after the close button is clicked.
*
* @method _afterCloseClicked
* @protected
*/
_afterCloseClicked: function() {
this.hide();
},
/**
* Fired after `fillHeight` function is called. Updates the line-height
* of the fillHeight node to be equal to its height, so the images can
* be vertically centered.
*
* @method _afterFillHeight
* @protected
*/
_afterFillHeight: function() {
var fillHeightNode = this.getStdModNode(this.get('fillHeight'));
fillHeightNode.setStyle('lineHeight', fillHeightNode.getStyle('height'));
},
/**
* Fired after the `link` attribute is changed. It will render the
* referenced images and attach click events to those links.
*
* @method _attachLinkEvents
* @protected
*/
_afterLinksChange: function() {
this._attachLinkEvent();
},
/**
* Fired after the `responsive` event.
*
* @method _afterResponsive
* @protected
*/
_afterResponsive: function() {
A.ImageViewer.superclass._afterResponsive.apply(this, arguments);
this.footerNode.setStyle('width', '');
this.bodyNode.setStyles({
height: '',
lineHeight: ''
});
this._fillHeight();
this._setAlignCenter(true);
},
/**
* Fired after the `thumbnailsConfig` attribute changes.
*
* @method _afterThumbnailsConfigChange
* @param {EventFacade} event
* @protected
*/
_afterThumbnailsConfigChange: function(event) {
if (event.newVal === false) {
this._thumbnailsEl.hide();
}
else if (event.prevVal === false) {
if (this._thumbnailsWidget) {
this._thumbnailsEl.show();
}
else {
this._renderThumbnailsWidget();
this._bindThumbnails();
}
}
else {
this._thumbnailsWidget.setAttrs(this.get('thumbnailsConfig'));
}
},
/**
* Fired after the current thumbnail index changes.
*
* @method _afterThumbnailsIndexChange
* @protected
*/
_afterThumbnailsIndexChange: function() {
this.set('currentIndex', this._thumbnailsWidget.get('currentIndex'));
},
/**
* Shows the `A.ImageViewer` UI.
*
* @method _afterUISetVisible
* @protected
*/
_afterUISetVisible: function() {
A.ImageViewer.superclass._afterUISetVisible.apply(this, arguments);
if (this.get('visible')) {
this._fillHeight();
if (this._thumbnailsWidget) {
this._thumbnailsWidget.updateDimensions();
}
this._closeEl.show();
if (this.get('modal')) {
this.get('maskNode').addClass(CSS_MASK);
}
}
else {
this._closeEl.hide();
this.get('maskNode').removeClass(CSS_MASK);
}
},
/**
* Attaches the click event for the image links.
*
* @method _attachLinkEvent
* @protected
*/
_attachLinkEvent: function() {
this._detachLinkEvent();
this._linkEvent = this.get('links').on(
'click',
A.bind(this._onClickLinks, this)
);
},
/**
* Binds the events related to the viewer's controls.
*
* @method _bindControls
* @protected
*/
_bindControls: function() {
this._eventHandles.push(
this.get('controlPrevious').after('click', A.bind(this._onClickControl, this)),
this.get('controlNext').after('click', A.bind(this._onClickControl, this))
);
},
/**
* Binds the events related to the thumbnails.
*
* @method _bindThumbnails
* @protected
*/
_bindThumbnails: function() {
if (this._thumbnailsWidget) {
this._eventHandles.push(
this._thumbnailsWidget.after('currentIndexChange', A.bind(this._afterThumbnailsIndexChange, this))
);
}
},
/**
* Detaches the click event for the image links, if one was attached
* earlier.
*
* @method _detachLinkEvent
* @protected
*/
_detachLinkEvent: function() {
if (this._linkEvent) {
this._linkEvent.detach();
this._linkEvent = null;
}
},
/**
* Gets the [info](A.ImageViewer.html#attr_info) template.
*
* @method _getInfoTemplate
* @param {String} v template
* @return {String} Parsed string.
*/
_getInfoTemplate: function(val) {
return A.Lang.sub(val, {
current: this.get('currentIndex') + 1,
total: this.get('links').size()
});
},
/**
* Gets `thumbnailsConfig` attribute.
*
* @method _getThumbnailsConfig
* @param {Boolean | Object} val
* @protected
*/
_getThumbnailsConfig: function(val) {
if (val === false) {
return val;
}
return A.merge({
height: 70,
showControls: false,
sources: this._getThumbnailImageSources(),
width: '100%'
}, val);
},
/**
* Returns an array with the sources of the images to be used as the
* viewer's thumbnails. They should be the images included inside the
* elements given as the `links` attributes or, if there isn't one,
* the respective item in the `sources` attribute.
*
* @method _getThumbnailImageSources
* @protected
*/
_getThumbnailImageSources: function() {
var image,
index,
links = this.get('links'),
sources = this.get('sources'),
thumbnailSources = [];
for (index = 0; index < links.size(); index++) {
image = links.item(index).one('img');
if (image) {
thumbnailSources.push(image.getAttribute('src'));
}
else {
thumbnailSources.push(sources[index]);
}
}
return thumbnailSources;
},
/**
* Fired when the nodes in the `links` attribute are clicked.
*
* @method _onClickLinks
* @param {EventFacade} event
* @protected
*/
_onClickLinks: function(event) {
event.preventDefault();
this.show();
this.set(
'currentIndex',
this.get('links').indexOf(event.currentTarget)
);
},
/**
* Fired when the user presses a key on the keyboard.
*
* @method _onKeydown
* @param {EventFacade} event
* @protected
*/
_onKeydown: function(event) {
if (!this.get('visible')) {
return false;
}
if (event.isKey('LEFT')) {
this.prev();
}
else if (event.isKey('RIGHT')) {
this.next();
}
else if (event.isKey('ESC')) {
this.hide();
}
},
/**
* Fired on the `responsive` event.
*
* @method _onResponsive
* @protected
*/
_onResponsive: function() {
A.ImageViewer.superclass._onResponsive.apply(this, arguments);
this.footerNode.setStyle('width', 0);
},
/**
* Renders the viewer controsl (next/prev/close).
*
* @method _renderControls
* @protected
*/
_renderControls: function() {
var body = A.one('body');
body.append(this.get('controlPrevious'));
body.append(this.get('controlNext'));
this._closeEl = A.Node.create(this.TPL_CLOSE);
body.append(this._closeEl);
},
/**
* Renders the footer of the viewer.
*
* @method _renderFooter
* @protected
*/
_renderFooter: function() {
var container = A.Node.create(this.TPL_FOOTER_CONTENT);
this.setStdModContent('footer', container);
this._captionEl = A.Node.create(this.TPL_CAPTION);
this._captionEl.selectable();
container.append(this._captionEl);
this._infoEl = A.Node.create(this.TPL_INFO);
this._infoEl.selectable();
container.append(this._infoEl);
container.append(A.Node.create(this.TPL_FOOTER_BUTTONS));
this._thumbnailsEl = A.Node.create(this.TPL_THUMBNAILS);
container.append(this._thumbnailsEl);
this._renderThumbnailsWidget();
},
/**
* Overrides this method from ImageViewerBase so that the image list will
* be rendered inside the body node, instead of directly in the content box.
*
* @method _renderImageListNode
* @protected
*/
_renderImageListNode: function() {
var list = A.Node.create(this.TPL_IMAGE_LIST);
this.setStdModContent('body', list);
return list.one('.image-viewer-base-image-list-inner');
},
/**
* Renders the player button.
*
* @method _renderPlayer
* @protected
*/
_renderPlayer: function() {
if (this.get('showPlayer') && !this._player) {
this._player = A.Node.create(this.TPL_PLAYER);
this.get('contentBox').one('.' + CSS_FOOTER_BUTTONS).append(this._player);
}
},
/**
* Renders the thumbnails widget.
*
* @method _renderThumbnailsWidget
* @protected
*/
_renderThumbnailsWidget: function() {
var thumbnailsConfig = this.get('thumbnailsConfig');
if (thumbnailsConfig === false) {
this._thumbnailsEl.hide();
}
else {
this._thumbnailsEl.show();
this._thumbnailsWidget = new A.ImageViewerMultiple(thumbnailsConfig)
.render(this._thumbnailsEl);
}
},
/**
* Sets `links` attribute.
*
* @method _setLinks
* @param {String | NodeList} val
* @protected
*/
_setLinks: function(val) {
var links,
sources = [];
if (val instanceof A.NodeList) {
links = val;
}
else if (A.Lang.isString(val)) {
links = A.all(val);
}
else {
links = new A.NodeList([val]);
}
if (links.size() > 0) {
links.each(function() {
sources.push(this.getAttribute('href'));
});
this.set('sources', sources);
}
return links;
},
/**
* Loads the image specified at `currentIndex` and shows it.
*
* @method _showCurrentImage
* @protected
*/
_showCurrentImage: function() {
A.ImageViewer.superclass._showCurrentImage.apply(this, arguments);
this._syncCaptionUI();
this._syncInfoUI();
this._syncThumbnails();
},
/**
* Updates the caption.
*
* @method _syncCaptionUI
* @protected
*/
_syncCaptionUI: function() {
var caption = this.get('caption'),
link;
if (this.get('captionFromTitle')) {
link = this.get('links').item(this.get('currentIndex'));
caption = link.attr('title') ? link.attr('title') : caption;
}
this._captionEl.set('text', caption);
},
/**
* Updates the info node.
*
* @method _syncInfoUI
* @protected
*/
_syncInfoUI: function() {
this._infoEl.setHTML(this.get('infoTemplate'));
},
/**
* Updates the thumbnails node.
*
* @method _syncThumbnails
* @protected
*/
_syncThumbnails: function() {
if (this._thumbnailsWidget) {
this._thumbnailsWidget.set('currentIndex', this.get('currentIndex'));
}
}
}, {
/**
* Static property used to define the default attribute configuration
* for the `A.ImageViewer`.
*
* @property ATTRS
* @type {Object}
* @static
*/
ATTRS: {
/**
* The caption of the displayed image.
*
* @attribute caption
* @default 'blank'
* @type {String}
*/
caption: {
value: 'blank',
validator: A.Lang.isString
},
/**
* If `true` the [caption](A.ImageViewer.html#attr_caption) will be
* pulled from the title DOM attribute.
*
* @attribute captionFromTitle
* @default true
* @type {Boolean}
*/
captionFromTitle: {
value: true,
validator: A.Lang.isBoolean
},
/**
* If `true` the Overlay with the image will be positioned on the
* center of the viewport.
*
* @attribute centered
* @default true
* @type {Boolean}
*/
centered: {
value: true
},
/**
* The height of the image viewer.
*
* @attribute height
* @default '100%'
* @type {String | Number}
*/
height: {
value: '100%'
},
/**
* String template used to display the information.
*
* @attribute infoTemplate
* @default 'Image {current} of {total}'
* @type {String}
*/
infoTemplate: {
getter: '_getInfoTemplate',
value: 'Image {current} of {total}',
validator: A.Lang.isString
},
/**
* Selector or NodeList containing the links where the
* `A.ImageViewer` extracts the information to generate the
* thumbnails.
*
* @attribute links
* @type {String | NodeList}
*/
links: {
lazyAdd: false,
setter: '_setLinks'
},
/**
* Displays a mask behind the viewer. Set to `false` to disable.
*
* @attribute modal
* @default true
* @type {Boolean}
*/
modal: {
value: true
},
/**
* Configuration options for the thumbnails widget (ImageViewerMultiple).
*
* @attribute thumbnailsConfig
* @default {}
* @type {Boolean | Object}
*/
thumbnailsConfig: {
getter: '_getThumbnailsConfig',
value: {}
},
/**
* Determines if the `A.ImageViewer` should be visible or not.
*
* @attribute visible
* @default false
* @type {Boolean}
*/
visible: {
value: false
},
/**
* The width of the image viewer.
*
* @attribute width
* @default '100%'
* @type {String | Number}
*/
width: {
value: '100%'
}
},
/**
* Static property provides a string to identify the CSS prefix.
*
* @property CSS_PREFIX
* @type {String}
* @static
*/
CSS_PREFIX: A.getClassName('image-viewer')
}
);