Files
conservatorio-tomadini/media/com_phocagallery/js/boxplus/boxplus.js
2024-12-17 17:34:10 +01:00

1785 lines
52 KiB
JavaScript

/**@license boxplus: a versatile lightweight pop-up window engine for MooTools
* @author Levente Hunyadi
* @version 0.9.3
* @remarks Copyright (C) 2009-2011 Levente Hunyadi
* @remarks Licensed under GNU/GPLv3, see http://www.gnu.org/licenses/gpl-3.0.html
* @see http://hunyadi.info.hu/projects/boxplus
**/
/*
* boxplus: a versatile lightweight pop-up window engine for MooTools
* Copyright 2009-2011 Levente Hunyadi
*
* boxplus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* boxplus 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with boxplus. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Requires MooTools Core 1.2 or later.
* Picasa support requires Request.JSONP from MooTools 1.2 More or later.
*
* Annotated for use with Google Closure Compiler's advanced optimization
* method when supplemented with a MooTools extern file.
*
* Search for "EDIT OPTIONS" to find out where to modify default settings.
*/
;
(function ($) {
Object.append(Element['NativeEvents'], {
'popstate': 2,
'dragstart': 2 // listen to browser-native drag-and-drop events
});
/**
* Converts a query string into an object.
* @param {string} querystring
* @return {!Object}
*/
function fromQueryString(querystring) {
var data = {};
if (querystring.length > 1) {
querystring.substr(1).split('&').each(function (keyvalue) {
var index = keyvalue.indexOf('=');
var key = index >= 0 ? keyvalue.substr(0,index) : keyvalue;
var value = index >= 0 ? keyvalue.substr(index+1) : '';
data[unescape(key)] = unescape(value);
});
}
return data;
}
/**
* Identifier of boxplus container element.
* @type {string}
* @const
*/
var BOXPLUS_ID = 'boxplus';
/**
* @type {string}
* @const
*/
var BOXPLUS_HIDDEN = 'boxplus-hidden';
/**
* @type {string}
* @const
*/
var BOXPLUS_DISABLED = 'boxplus-disabled';
/**
* @type {string}
* @const
*/
var BOXPLUS_UNAVAILABLE = 'boxplus-unavailable';
/**
* Time between successive scroll animations [ms].
* @type {number}
* @const
*/
var BOXPLUS_SCROLL_INTERVAL = 10;
/**
* Key under which the cloaked href attribute of the anchor should be stored in the mootools Elements storage.
* @type {string}
* @const
*/
var BOXPLUS_HREF = 'boxplus-href';
/**
* Cloaks an anchor href attribute by moving it to the Elements Storage.
* @param {Element} anchor
*/
function cloak(anchor) {
if (!anchor.retrieve(BOXPLUS_HREF)) { // prevent double-obfuscating an anchor
anchor.store(BOXPLUS_HREF, anchor.get('href'));
anchor.set('href', 'javascript:void(0);');
}
}
/**
* Uncloaks an anchor by settings its href attribute based on the value in the Elements Storage.
* @param {Element} anchor
*/
function uncloak(anchor) {
var href = anchor.retrieve(BOXPLUS_HREF);
if (href) {
anchor.set('href', href);
anchor.eliminate(BOXPLUS_HREF);
}
}
/**
* Represents the boxplus dialog.
* Events fired are 'close', 'previous', 'next', 'first', 'last', 'start', 'stop' and 'change'.
*/
var boxplusDialog = new Class({
'Implements': [Events, Options],
// --- EDIT OPTIONS BELOW TO MODIFY DEFAULTS --- //
// --- SEE FURTHER BELOW FOR OTHER OPTIONS --- //
/**
* boxplus dialog options.
* Normally, these would be configured via a boxplus gallery and not directly.
*/
'options': {
/**
* Pop-up window theme. If set, stylesheets that have a "title" attribute starting with
* "boxplus" but with a different ending than specified will be disabled. For instance,
* the value "darksquare" will enable the stylesheet "boxplus-darksquare" but disable
* "boxplus-darkrounded" and "boxplus-lightsquare".
* @type {boolean|string}
*/
'theme': false,
/**
* Whether navigation controls are displayed.
* @type {boolean}
*/
'navigation': true,
/**
* Whether to center pop-up windows smaller than browser window size.
* @type {boolean}
*/
'autocenter': true,
/**
* Whether to reduce images that would otherwise exceed screen dimensions to fit
* the browser window when they are displayed.
* @type {boolean}
*/
'autofit': true,
/**
* Duration of animation sequences. Expects a value in milliseconds, or one of 'short' or 'long'.
* @type {string|number}
*/
'duration': 'short',
/**
* Easing equation to use for the transition effect.
* The easing equation determines the speed at which the animation progresses
* at different stages of an animation. Examples values include 'sine', 'linear' and
* 'bounce'. For a complete list of supported values see the MooTools framework object
* Fx.Transitions <http://mootools.net/docs/core/Fx/Fx.Transitions>.
* @type {string}
*/
'transition': 'sine',
/**
* Client-side image protection feature.
* This feature suppresses the browser "contextmenu" and "dragstart" events so that
* a user cannot easily extract the image with conventional methods. Needless to say,
* such measures are completely ineffective against advanced users who can always
* extract the image from the browser cache or use developer page inspection tools
* like Firebug.
* @type {boolean}
*/
'protection': true,
/**
* Scroll speed [px/s].
* @type {number}
*/
'scrollspeed': 200,
/**
* Acceleration factor, multiplier of scroll speed in fast scroll mode.
* @type {number}
*/
'scrollfactor': 5,
/**
* Default width if no width is specified or can be derived.
* @type {number}
*/
'width': 800,
/**
* Default height if no height is specified or can be derived.
* @type {number}
*/
'height': 600
},
// --- END OF DEFAULT OPTIONS --- //
// Properties assigned on initialization:
// container,
// shadedbackground,
// popup,
// viewer,
// viewerimage,
// viewerframe,
// viewervideo,
// viewerobject,
// viewercontent
// thumbs
/**
* Download URL associated with the current item.
* @type {string}
*/
_url: '',
/**
* Actual dimensions of the current item.
*/
_imagedims: null,
/**
* Timer used in animating the progress indicator.
*/
_progresstimer: null,
/**
* Timer used in scrolling the quick-access navigation bar.
*/
_scrolltimer: null,
/**
* Current speed of the quick-access navigation bar.
* @type {number}
*/
_scrollspeed: 0,
/**
* Injects the pop-up window HTML code into the document.
*/
'initialize': function (options) {
this['setOptions'](options); // protect "setOptions" from being renamed during minification
var self = this;
self.decelerateScroll();
/**
* Creates a boxplus pop-up window element.
* @param {string|Array.<string>} cls A class name or array of class names to apply to the element.
* @param {!Object=} attrs An object of attributes to apply to the element.
* @param {...Element} children Child elements to inject into the element.
* @return {Element}
*/
function _create(cls, attrs, children) {
var elem = new Element('div', {
'class': typeof(cls) == 'string' ? 'boxplus-' + cls : cls.map(function (classname) { return 'boxplus-' + classname; }).join(' ')
});
if (attrs) {
elem.set(attrs);
}
for (var i = 2; i < arguments.length; i++) {
elem.adopt(arguments[i]);
}
return elem;
}
/**
* @return {Element}
*/
function _message(cls) {
return new Element('span', {
'class': 'boxplus-' + cls
});
}
/**
* Binds a callback function to an event.
* @param {function()} callback The callback function to subscribe for the event.
* @param {string=} eventtype The name of the event.
*/
function _bind(cls, callback, eventtype) {
if (!eventtype) {
eventtype = 'click';
}
self._getElements(cls).addEvent(eventtype, callback.bind(self));
}
// navigation controls in the quick-access navigation bar
var thumbselem = _create('thumbs', {},
new Element('ul'),
_create('rewind'),
_create('forward')
);
// title and text for caption
var captionelem = _create('caption', {},
_create('title'),
_create('text')
);
// control buttons outside the image area
var controlselem = _create('controls', {},
_create('prev'),
_create('next'),
_create('start'),
_create(['stop','unavailable']),
_create('close'),
_create('download'),
_create('metadata')
);
/**
* @type {string}
* @const
*/
var HIDDEN = 'hidden';
self.container = _create([], {id: BOXPLUS_ID},
self.shadedbackground = _create(['background',HIDDEN]),
self.popup = _create(['dialog',HIDDEN], {},
_create('progress'),
_create('sideways', {},
thumbselem.clone(),
controlselem.clone(),
captionelem.clone()
),
_create('title'),
_create('main', {},
self.centerpanel = _create('center', {},
self.viewer = _create(['viewer',HIDDEN], {},
/** @type {HTMLImageElement} */
self.viewerimage = new Element('img'),
_create('prev'),
_create('next'),
_create('resizer', {},
_create('enlarge').addEvent('click', function () { self.magnify(); }),
_create(['shrink','unavailable']).addEvent('click', function () { self.magnify(); })
),
self.thumbs = thumbselem.clone(),
/** @type {HTMLIFrameElement} */
self.viewerframe = new Element('iframe', {
'frameborder': 0
}),
/** @type {HTMLVideoElement} */
self.viewervideo = new Element('video', {
'autoplay': true,
'controls': true
}),
/** @type {HTMLObjectElement} */
self.viewerobject = _create('object'),
/** @type {HTMLDivElement} */
self.viewercontent = _create('content'),
_create('progress')
)
),
_create('bottom', {},
thumbselem.clone(),
controlselem.clone(),
captionelem.clone()
)
),
_create('lt'),
_create('t'),
_create('rt'),
_create('l'),
_create(['m',HIDDEN]),
_create('r'),
_create('lb'),
_create('b'),
_create('rb')
),
self.popupclone = _create(['dialog',HIDDEN], {},
self.sidewaysclone = _create('sideways', {},
thumbselem.clone(),
captionelem.clone(),
controlselem.clone()
),
_create('title'),
_create('main', {},
self.centerclone = _create('center', {},
self.viewerclone = _create(['viewer',HIDDEN], {},
self.viewercontentclone = new Element('div')
)
),
self.bottomclone = _create('bottom', {},
thumbselem.clone(),
captionelem.clone(),
controlselem.clone()
)
)
),
_message('unknown-type'),
_message('not-found')
).inject(document.body);
// close window when user clicks outside window area (but not on mobile devices)
if (self.container.getStyle('background-repeat') == 'repeat') { // test for CSS @media handheld
self.shadedbackground.addEvent('click', function () { self.close(); });
}
/**
* Fired when the user right-clicks or starts to drag an item in the viewer to open the context menu or copy an image.
* @param {Event} event An event object.
*/
self._onProhibitedUIAction = function (event) {
return !self.options['protection'] || !self.viewer.getElements('*').contains(event.target);
};
/**
* Fired when the user presses a key while the lightweight pop-up window is shown.
* @param {Event} event An event object.
*/
self._onKeyDown = function (event) {
if (!['input','textarea'].contains($(event.target).get('tag'))) { // let form elements handle their own input
var keyindex = [37,39,36,35,13,27].indexOf(event.code); // keys are [left arrow, right arrow, home, end, ENTER, ESC]
if (keyindex >= (self['options']['navigation'] ? 0 : 4)) { // ignore navigation keys if navigation buttons are disabled
[self.previous,self.next,self.first,self.last,self.magnify,self.close][keyindex].bind(self)(); // call function with proper context for "this"
return false; // cancel event propagation
}
}
};
/**
* Fired when the user resizes the browser window while the lightweight pop-up window is shown.
*/
var resizeTimer;
self._onResize = function () {
window.clearTimeout(resizeTimer);
if (!self.resizing) {
resizeTimer = window.setTimeout(function () {
self.resize.bind(self)();
}, 10);
}
};
_bind('prev', self.previous);
_bind('next', self.next);
_bind('start', self.start);
_bind('stop', self.stop);
_bind('close', self.close);
_bind('download', self.download);
_bind('metadata', self.toggleMetadata);
_bind('rewind', self.startRewind, 'mouseover');
_bind('rewind', self.stopScroll, 'mouseout');
_bind('rewind', self.accelerateScroll, 'mousedown')
_bind('rewind', self.decelerateScroll, 'mouseup')
_bind('forward', self.startForward, 'mouseover');
_bind('forward', self.stopScroll, 'mouseout');
_bind('forward', self.accelerateScroll, 'mousedown')
_bind('forward', self.decelerateScroll, 'mouseup')
self.setEmpty();
},
/**
* @param {string|Array.<string>} cls
* @return {string}
*/
_class: function (cls) {
return Array.from(cls).map(function (selector) {
return selector.replace(/\b([\w-]+)/g, '.boxplus-$1');
}).join(', ');
},
/**
* @param {string|Array.<string>} cls
* @return {Element}
*/
_getElement: function (cls) {
return this.popup.getElement(this._class(cls));
},
/**
* @param {string|Array.<string>} cls
* @return {Elements}
*/
_getElements: function (cls) {
return this.popup.getElements(this._class(cls));
},
/**
* @param {string|Array.<string>} cls
* @return {Elements}
*/
_getClonedElements: function (cls) {
return this.popupclone.getElements(this._class(cls));
},
_toggle: function (cls, clstoggle, state) {
this._getElements(cls)[state ? 'addClass' : 'removeClass'](clstoggle);
},
_toggleCloned: function (cls, clstoggle, state) {
this._getClonedElements(cls)[state ? 'addClass' : 'removeClass'](clstoggle);
},
setAvailable: function (cls, state) {
this._toggle(cls, BOXPLUS_UNAVAILABLE, !state);
},
setAllAvailable: function (cls, state) {
this._toggle(cls, BOXPLUS_UNAVAILABLE, !state);
this._toggleCloned(cls, BOXPLUS_UNAVAILABLE, !state);
},
setEnabled: function (cls, state) {
this._toggle(cls, BOXPLUS_DISABLED, !state);
},
setAllEnabled: function (cls, state) {
this._toggle(cls, BOXPLUS_DISABLED, !state);
this._toggleCloned(cls, BOXPLUS_DISABLED, !state);
},
setVisible: function (cls, state) {
this._toggle(cls, BOXPLUS_HIDDEN, !state);
},
setAllVisible: function (cls, state) {
this._toggle(cls, BOXPLUS_HIDDEN, !state);
this._toggleCloned(cls, BOXPLUS_HIDDEN, !state);
},
_bindEvents: function (events, state) {
var self = this;
for (var name in events) {
window[state ? 'addEvent' : 'removeEvent'](name, events[name]);
}
},
_fireEvent: function (event, arg) {
this['fireEvent'](event, arg);
},
getMessage: function (msg) {
return this.container.getElement('.boxplus-' + msg).get('html');
},
/**
* Shows the lightweight pop-up window.
* @param {!Object} options
*/
show: function (options) {
var self = this;
self['setOptions'](options); // prevent minification of "setOptions"
// enable associated theme (if any) and disable other themes that might be linked to the page
var theme = self['options']['theme'];
if (theme) {
// disable unused themes and enable selected theme
$$('link[rel=stylesheet][title^=boxplus]').set('disabled', true).filter('[title="boxplus-' + theme + '"]').set('disabled', false);
}
// toggle navigation buttons
self.setEnabled(['prev','next','start','stop'], self['options']['navigation']);
// show visuals
self.setVisible('bottom', false); // will be shown when resizing terminates
self.setVisible('sideways', false); // will be shown when resizing terminates
self.center(self.popup);
$$([self.shadedbackground, self.popup]).removeClass(BOXPLUS_HIDDEN);
self.shadedbackground.fade('hide').fade('in');
// register events
self._bindEvents({
'contextmenu': self._onProhibitedUIAction,
'dragstart': self._onProhibitedUIAction,
'keydown': self._onKeyDown,
'resize': self._onResize
}, true);
},
/**
* Hides the lightweight pop-up window.
* Fired when the user clicks the close button, clicks outside the pop-up window or presses the ESC key.
*/
hide: function () {
var self = this;
// unregister events
self._bindEvents({
'contextmenu': self._onProhibitedUIAction,
'dragstart': self._onProhibitedUIAction,
'keydown': self._onKeyDown,
'resize': self._onResize
}, false);
// hide visuals
self.shadedbackground.fade('out');
$$([self.shadedbackground, self.popup]).addClass(BOXPLUS_HIDDEN);
},
/**
* Closes the pop-up window.
*/
close: function () {
var self = this;
self.setEmpty();
self.resize(function () {
self._fireEvent('close');
});
},
/**
* Navigates to the first image/content.
* Fired when the user clicks the navigate to first control or presses the HOME key.
*/
first: function () {
this._fireEvent('first');
},
/**
* Navigates to the previous image/content.
* Fired when the user clicks the navigate to previous control or presses the left arrow key.
*/
previous: function () {
this._fireEvent('previous');
},
/**
* Navigates to the next image/content.
* Fired when the user clicks the navigate to next control or presses the right arrow key.
*/
next: function () {
this._fireEvent('next');
},
/**
* Navigates to the last image/content.
* Fired when the user clicks the navigate to last control or presses the END key.
*/
last: function () {
this._fireEvent('last');
},
/**
* Start the slideshow timer.
* Fired when the user clicks the play control.
*/
start: function () {
this._fireEvent('start');
},
/**
* Stop the slideshow timer.
* Fired when the user clicks the stop control.
*/
stop: function () {
this._fireEvent('stop');
},
magnify: function () {
var self = this;
self._getElements('shrink').toggleClass(BOXPLUS_UNAVAILABLE);
self._getElements('enlarge').toggleClass(BOXPLUS_UNAVAILABLE);
self._setPositioning();
self.resize();
},
startRewind: function () {
this.startScroll(-1);
},
startForward: function () {
this.startScroll(1);
},
/**
* Starts scrolling the thumbs navigation bar either forward or backward.
* @param {number} dir -1 to scroll backward, 1 to scroll forward, 0 to initialize controls (no scrolling)
*/
startScroll: function (dir) {
var self = this;
var target = self._getElements('thumbs').getElement('ul'); // thumbs navigation bars in either panel
var bar = self.thumbs.getElement('ul'); // thumbs navigation bar in main panel
// current left offset of thumbs navigation bar w.r.t. left edge of viewer
var x = bar.getStyle('left').toInt(); // 0 > x > minpos
x = isNaN(x) ? 0 : x;
// maximum negative value permitted as left offset w.r.t. left edge of viewer
var minpos = self.viewer.getSize().x - bar.getSize().x;
// set initial values for current state of forward and rewind scroll buttons
var forward_current;
var rewind_current;
// set initial visibility of forward and rewind buttons
self.setVisible('forward', true);
self.setVisible('rewind', true);
// assign scroll function, avoid complex operations
var func = function () {
var forward_next = true;
var rewind_next = true;
x -= dir * self._scrollspeed;
if (x <= minpos) {
x = minpos;
forward_next = false;
}
if (x >= 0) {
x = 0;
rewind_next = false;
}
// update visibility of forward and rewind scroll buttons only if their visibility status has changed
forward_current === forward_next || self.setVisible('forward', forward_current = forward_next);
rewind_current === rewind_next || self.setVisible('rewind', rewind_current = rewind_next);
target.setStyle('left', x + 'px');
};
// invoke scroll function to force initial visibility
func();
// start scrolling only if it would advance thumbs navigation bar in either direction
if (dir) {
self._scrolltimer = window.setInterval(func, BOXPLUS_SCROLL_INTERVAL);
}
},
stopScroll: function () {
this.decelerateScroll();
if (this._scrolltimer) {
window.clearInterval(this._scrolltimer);
this._scrolltimer = null;
}
},
accelerateScroll: function () {
this._scrollspeed = this['options']['scrollspeed'] * BOXPLUS_SCROLL_INTERVAL * this['options']['scrollfactor'] / 1000;
},
decelerateScroll: function () {
this._scrollspeed = this['options']['scrollspeed'] * BOXPLUS_SCROLL_INTERVAL / 1000;
},
download: function () {
window.location.href = this._url;
},
/**
* @param {boolean} state
*/
showMetadata: function (state) {
var self = this;
state = !state; // invert state (show controls when metadata is NOT displayed)
self.setVisible('resizer', state);
self.setVisible('thumbs', state);
self.setVisible('viewer prev', state);
self.setVisible('viewer next', state);
var elems = $$([self.viewerimage, self.viewervideo, self.viewerobject, self.viewerframe]);
if (state) {
elems.removeClass(BOXPLUS_HIDDEN);
self.viewercontent.addClass(BOXPLUS_HIDDEN);
} else {
elems.addClass(BOXPLUS_HIDDEN);
self.viewercontent.removeClass(BOXPLUS_HIDDEN);
}
},
/**
* Shows or hides image metainformation.
* Fired when the user clicks the metadata icon.
*/
toggleMetadata: function () {
this.showMetadata(!this.isMetadata());
},
/**
* @return {boolean}
*/
isMetadata: function () {
return !this.viewercontent.hasClass(BOXPLUS_HIDDEN);
},
/**
* Sets an image (with metadata) to be shown in the pop-up window.
* @param {HTMLImageElement} image An image element.
* @param {string=} url
* @param {string|Element|HTMLElement=} metadata
*/
setImage: function (image, url, metadata) {
var self = this;
self.setEmpty();
if (image) {
// store image dimensions for future use to be able to restore image to original size
self._imagedims = {
width: image.width,
height: image.height
};
// set image
self.viewerimage.set('src', image.src).set(self._imagedims).removeClass(BOXPLUS_UNAVAILABLE);
// set download availability
self._url = url;
self.setAvailable('download', url);
// set metadata availability
if (metadata) {
switch (typeof(metadata)) {
case 'string':
self.viewercontent.set('html', metadata); break;
default:
metadata = $(metadata); // make Element methods available on object
if (metadata) {
self.viewercontent.adopt(metadata.clone());
}
}
self.viewercontent.removeClass(BOXPLUS_UNAVAILABLE);
}
self.setAvailable('metadata', metadata);
}
},
/**
* @param {Element} elem
*/
setContent: function (elem) {
var self = this;
self.setEmpty();
self.setVisible('resizer', false);
self.setVisible('thumbs', false);
self.setVisible('viewer prev', false);
self.setVisible('viewer next', false);
self.viewercontent.adopt(elem.clone()).removeClass(BOXPLUS_UNAVAILABLE).removeClass(BOXPLUS_HIDDEN);
self._imagedims = {
width: self.options.width,
height: self.options.height
};
},
/**
* Set dimensions data based on explicitly set values.
* @param {HTMLAnchorElement} anchor An HTML anchor element.
*/
setDimensions: function (anchor) {
var dims = fromQueryString(anchor.search);
dims = {
width: dims.width ? dims.width.toInt() : this.options.width,
height: dims.height ? dims.height.toInt() : this.options.height
};
this._imagedims = dims;
},
/**
* @param {HTMLAnchorElement} anchor An HTML anchor element.
*/
setObject: function (anchor) {
var self = this;
self.setEmpty();
// fetch dimension data
self.setDimensions(anchor);
var dims = self._imagedims;
var href = anchor.href;
var path = anchor.pathname;
if (/\.(ogg|webM)$/i.test(path)) { // supported by HTML5-native <video> tag
self.viewervideo.set(dims).set('src', href).setStyles(dims).removeClass(BOXPLUS_UNAVAILABLE);
} else {
self.viewerobject.setStyles(dims).removeClass(BOXPLUS_UNAVAILABLE);
if (/\.(pdf|mov)$/i.test(path)) {
var classid;
var type;
var attrs = {
src: href
};
if (/\.pdf$/i.test(path)) {
classid = 'CA8A9780-280D-11CF-A24D-444553540000';
type = 'application/pdf';
} else if (/\.mov$/i.test(path)) {
classid = '02BF25D5-8C17-4B23-BC80-D3488ABDDC6B';
type = 'video/quicktime';
} else if (/\.mpe?g/i.test(path)) {
classid = '22d6f312-b0f6-11d0-94ab-0080c74c7e95';
type = 'video/mpeg';
}
/**
* Converts an object into a name="value" HTML attribute list.
*/
function _getAsAttributeList(attrs) {
var s = '';
for (var name in attrs) {
s += ' ' + name + '="' + attrs[name] + '"';
}
return s;
}
/**
* Converts an object into a list of HTML <param /> elements.
*/
function _getAsParameterList(attrs) {
var s = '';
for (var name in attrs) {
s += '<param name="' + name + '" value="' + attrs[name] + '" />';
}
return s;
}
// build custom HTML string of nested <object> elements with the specified dimensions and attributes
self.viewerobject.set('html',
'<object' + _getAsAttributeList(Object.merge({
'classid': 'clsid:' + classid
}, dims)) + '>' +
_getAsParameterList(attrs) +
'<!--[if lt IE 9]><!--><object' + _getAsAttributeList(Object.merge({
'type': type,
'data': href
}, dims)) + '>' + dialog.getMessage('unknown-type').replace('%s', type) + '</object><!--<![endif]-->' +
'</object>'
);
} else { // /\.swf$/i.test(path)
// classid = 'D27CDB6E-AE6D-11cf-96B8-444553540000';
new Swiff(href, Object.merge({
'container': self.viewerobject,
'params': {
'allowFullScreen': true
}
}, dims));
}
}
},
/**
* @param {HTMLAnchorElement} anchor An HTML anchor element.
*/
setFrame: function (anchor) {
var self = this;
self.setEmpty();
self.setDimensions(anchor);
self.viewerframe.set('src', anchor.href).setStyles(self._imagedims).removeClass(BOXPLUS_UNAVAILABLE);
},
/**
* Clears all content from the pop-up window.
*/
setEmpty: function () {
var self = this;
// reset content
$$([self.viewercontent, self.viewerimage, self.viewervideo, self.viewerobject, self.viewerframe]).addClass(BOXPLUS_UNAVAILABLE);
self.viewercontent.empty();
self.viewerimage.erase('src');
!self.viewervideo.pause || self.viewervideo.pause();
self.viewervideo.erase('src');
self.viewerobject.empty();
self.viewerframe.set('src', 'about:blank').erase('src');
// reset pop-up window size
self.viewer.setStyles(self._imagedims = {
width: 150,
height: 150
});
// clear download URL
self._url = '';
self.setAvailable('download', false);
self.setAvailable('metadata', false)
// clear title and description text
self.setCaption();
},
/**
* Sets title and description text.
* @param {string} title
* @param {string} text
*/
setCaption: function (title, text) {
var self = this;
self._getElements('title').set('html', title);
self._getElements('text').set('html', text);
self._getClonedElements('title').set('html', title);
self._getClonedElements('text').set('html', text);
self.setAllAvailable('title', title);
self.setAllAvailable('text', text);
self.setAllAvailable('caption', title || text);
},
/**
* @param {string} pos
*/
setCaptionPosition: function (pos) {
this.setAllEnabled('bottom', false);
this.setAllEnabled('sideways', false);
switch (pos) {
case 'bottom':
this.setAllEnabled('bottom', true); break;
case 'sideways':
this.setAllEnabled('sideways', true); break;
}
},
/**
* Toggles quick-access thumbnail navigation bars inside/outside image viewport.
* @param {string} pos 'inside' or 'outside'
*/
setThumbPosition: function (pos) {
this.setAllEnabled('thumbs', false);
switch (pos) {
case 'inside':
this.setAllEnabled('viewer thumbs', true);
break;
case 'outside':
this.setAllEnabled('bottom thumbs', true);
this.setAllEnabled('sideways thumbs', true);
break;
}
},
/**
* Adds thumbnails to the quick-access thumbnail navigation bars.
* @param {Elements} images The thumbnail images to use.
*/
addThumbs: function (images) {
var self = this;
self._getElements('thumbs').each(function (item) {
images.each(function (image) {
new Element('li').adopt(image.clone().addEvent('click', function () {
var item = $(this).getParent();
self._fireEvent('change', item.getParent().getChildren().indexOf(item));
})).inject(item.getElement('ul'));
});
});
self.setAvailable('thumbs', images.length > 1);
},
/**
* Removes all thumbnails from the quick-access thumbnail navigation bars.
*/
clearThumbs: function () {
this._getElements('thumbs').each(function (item) {
item.getElement('ul').empty();
});
},
isShrunk: function () {
var self = this;
// resizing videos impacts performance and HTML <object> does not always support dynamic resizing
return self.viewervideo.get('src') || self.viewerobject.getChildren().length ? false : self._getElement('shrink').hasClass(BOXPLUS_UNAVAILABLE);
},
/**
* @param {Element} obj
*/
getCenter: function (obj) {
var winsize = window.getSize();
var objsize = obj.getSize();
var x = (0).max((winsize['x'] - objsize['x']) / 2); // function max avoids dialog extending beyond left or top edge where browser does not let user scroll
var y = (0).max((winsize['y'] - objsize['y']) / 2);
if (this.popup.getStyle('position') != 'fixed') {
var scroll = window.getScroll();
x += scroll['x'];
y += scroll['y'];
}
return {
'x': x,
'y': y
};
},
center: function (obj) {
obj.setPosition(this.getCenter(obj));
},
recenter: function () {
var self = this;
self.center(self.popupclone);
self.popup.set('morph', {
duration: self.options.duration,
link: 'cancel'
}).morph(self.popupclone.getStyles(['left','top']));
},
/**
* Sets absolute or fixed positioning on the pop-up window.
*/
_setPositioning: function() {
var self = this;
var iscentered = self['options']['autocenter'] && self.isShrunk();
var dst = iscentered ? 'fixed' : 'absolute';
var src = self.popup.getStyle('position');
var position = self.popup.getPosition();
var x = position['x'];
var y = position['y'];
if (src != dst) {
var scroll = window.getScroll();
if (iscentered) { // fixed positioned target, absolute positioned at the moment
x -= scroll['x']; // absolute positioning takes into account window scroll
y -= scroll['y'];
} else { // absolute positioned target, fixed positioned at the moment
x += scroll['x']; // fixed positioning does not take into account window scroll
y += scroll['y'];
}
}
self.popup.setStyle('position', dst);
self.popup.setPosition({
'x': x,
'y': y
});
},
/**
* Resizes the pop-up window dialog.
* @param {function()=} callback A function to invoke when the animated sizing completes.
*/
resize: function (callback) {
var self = this;
self.resizing = true;
// hide bottom and sideways caption area temporarily while resizing
self.setVisible('bottom', false);
self.setVisible('sideways', false);
// hide viewer area
self.setVisible('viewer', false);
// whether only HTML content is shown (no image, video or flash)
var contentonly = !self.viewerimage.get('src') && !self.viewervideo.get('src') && !self.viewerobject.getChildren().length && !self.viewerframe.get('src');
// show metadata only if available
self.showMetadata(contentonly); // hide metadata (show image, video or flash instead)
// show quick-access thumbnail navigation bar only if available
self.setVisible('thumbs', self._getElement('thumbs').getChildren().length > 1);
// calculate pop-up window size based on internal image copy
var w = self._imagedims.width;
var h = self._imagedims.height;
/**
* Updates the size of the viewer and returns the new dimensions of the dialog.
*/
function _dialogresize(w, h) {
self.viewerclone.setStyles({
width: w,
height: h
});
return self.popupclone.getSize();
}
self.viewerclone.setStyle('margin-right', self.sidewaysclone.getSize().x);
// calculate size for large images that need shrinking
var winsize = window.getSize();
var dlgsize;
if (contentonly) {
dlgsize = _dialogresize(w, 'auto');
self.viewercontentclone.empty();
var children = self.viewercontent.getChildren();
if (children.length) {
self.viewercontentclone.adopt(children); // temporarily give children to other clone parent
}
h = self.viewerclone.getSize().y;
dlgsize = _dialogresize(w, h);
dlgsize = _dialogresize(Math.min(winsize.x, dlgsize.x) - (dlgsize.x-w), Math.min(winsize.y, dlgsize.y) - (dlgsize.y-h));
if (children.length) {
self.viewercontent.adopt(children); // take children back from clone parent
}
} else {
dlgsize = _dialogresize(w, h);
var needShrunk = self['options']['autofit'] // autofit is enabled
&& ((winsize.x < dlgsize.x) || (winsize.y < dlgsize.y)); // does not fit browser window dimensions
if (needShrunk && self.isShrunk()) { // needs shrinking and is in shrunk mode
var ratio;
if ((ratio = winsize.x / dlgsize.x) < 1.0) {
w *= ratio;
h *= ratio;
dlgsize = _dialogresize(w, h);
}
while ((ratio = winsize.y / dlgsize.y) < 1.0) {
w *= ratio;
h *= ratio;
dlgsize = _dialogresize(w, h);
}
}
self.setAvailable('resizer', needShrunk);
}
// set positioning
self._setPositioning();
// calculate pop-up window center position
self.center(self.popupclone);
function _dialogdimensions() {
return self.popupclone.getStyles(['left','top','width','height']);
}
// hide bottom and sideways panel
self.setAllAvailable('bottom', false);
self.setAllAvailable('sideways', false);
// set center panel to occupy the entire content of the pop-up window
self.centerpanel.setStyle('height', '100%');
// morph pop-up window to appropriate position and size
var dims = _dialogdimensions(); // reduced height
var params = {
'duration': self['options']['duration'],
'link': 'cancel',
'transition': self['options']['transition']
};
var morph = new Fx.Morph(self.popup, Object.merge(params, {
'onComplete': function () {
// clear forced height of center panel, the pop-up window dimensions should allow for bottom and sideways panel
self.centerpanel.setStyle('height', 'auto');
// set dimensions of viewer when first resizing phase completes
var viewerdims = self.viewerclone.getStyles(['width','height']);
self.viewerframe.setStyles(viewerdims); // inline frame size should reflect viewer size
self.viewer.setStyles(viewerdims);
// scroll inner content window into view
self.viewercontent.scrollTo(0,0);
// show viewer content
self.setVisible('viewer', true);
// reset thumbnail quick-access navigation bar
self.startScroll(0);
// invoke callback if defined
callback && callback();
new Fx.Morph(self.popup, Object.merge(params, {
'onComplete': function () {
self.setAvailable('bottom', true);
self.setAvailable('sideways', true);
// show bottom and sideways caption area temporarily hidden
self.setVisible('bottom', true);
self.setVisible('sideways', true);
// set resizing complete
self.resizing = false;
// re-center dialog if page has been scrolled during resizing
self.recenter();
}
})).start(_dialogdimensions());
}
}));
morph.start(dims);
self.bottomclone.removeClass(BOXPLUS_UNAVAILABLE);
self.sidewaysclone.removeClass(BOXPLUS_UNAVAILABLE);
},
/**
* Enables or disables progress indicators.
* A progress indicator is a PNG image with alpha transparency
* @param {boolean} state
*/
toggleProgress: function (state) {
var indicators = this._getElements('progress');
if (this._progresstimer) {
window.clearInterval(this._progresstimer);
}
if (indicators.length && state) {
/** @type {string} */
var backpos = indicators[0].getStyle('background-position'); // extract first integer from a string like "-64px 0px"
var offset = backpos ? backpos.toInt() : 0;
this._progresstimer = window.setInterval(function () {
indicators.setStyle('background-position', (offset = (offset - 32) % 384) + 'px 0'); // 384px = 12 states * 32px width
}, 150);
} else {
this._progresstimer = null;
}
this.setVisible('progress', state);
}
});
var dialog;
window.addEvent('domready', function () {
dialog = new boxplusDialog(); // inject pop-up HTML to document
});
/**
* A boxplus gallery.
* Fires the following events:
* 1. change: fired when new content is being shown
* 2. open: fired when the pop-up window opens
* 3. close: fired when the pop-up window closes
* @constructor
* @param {Element|Elements} el
* @param {!Object=} options
*/
var boxplus = new Class({
'Implements': [Events,Options],
/**
* The index of the currently shown item. Read only.
* @type {number}
*/
current: -1,
/**
* @type {Elements}
*/
_anchors: $$([]),
_timer: null,
// --- EDIT FUNCTIONS BELOW TO MODIFY DEFAULT TITLE, DESCRIPTION, DOWNLOAD URL AND METADATA --- //
/**
* Title text that belongs an anchor.
* @param {Element} anchor A mootools Element object representing the anchor.
* @return {?string} Raw HTML data as a string.
*/
_getTitle: function (anchor) {
var image = anchor.getElement('img');
return anchor.retrieve('title') || (image ? image.getProperty('alt') : '');
},
/**
* Description text that belongs to an anchor.
* @param {Element} anchor A mootools Element object representing the anchor.
* @return {string} Raw HTML data as a string.
*/
_getText: function (anchor) {
return anchor.retrieve('summary') || anchor.getProperty('title');
},
/**
* Download URL associated with an anchor.
* @param {Element} anchor A mootools Element object representing the anchor.
* @return {string} A URL.
*/
_getDownloadUrl: function (anchor) {
return anchor.retrieve('download');
},
/**
* Metadata associated with an anchor.
* @param {Element} anchor A mootools Element object representing the anchor.
* @return {string|Element} Raw HTML data as a string, or an Element object.
*/
_getMetadata: function (anchor) { },
// --- EDIT OPTIONS BELOW TO MODIFY DEFAULTS --- //
// --- SEE FURTHER ABOVE FOR OTHER OPTIONS --- //
/**
* boxplus gallery options.
*/
'options': {
/*
onChange: function () {}, // triggered when the current item shown is about to be changed
onClose: function () {}, // triggered when the pop-up window has been closed
*/
/**
* Time spent viewing an image when slideshow mode is active, or 0 to disable slideshow mode.
* @type {number}
*/
'slideshow': 0,
/**
* Whether to start a slideshow when the dialog opens.
* @type {boolean}
*/
'autostart': false,
/**
* Whether the image/content sequence loops such that the first image/content follows the last.
* @type {boolean}
*/
'loop': false,
/**
* Placement of captions. Takes the value 'bottom' (below the image), 'sideways' (next to the image) or 'none'.
*/
'captions': 'bottom',
/**
* Placement of thumbnail navigation bar. Takes the value 'inside' (over the image), 'outside' (in the caption area) or 'none'.
*/
'thumbs': 'inside',
/**
* Whether to cloak anchor URLs. Cloaked anchors will not reveal the target image URL
* when the user positions the mouse cursor over the image.
* Cloaking URLs also prevents other javascript code from reading the anchor "href" attribute.
*/
'cloak': false
},
// Other options include (see their default definition above):
// getTitle
// getText
// getDownloadUrl
// getMetadata
// --- END OF DEFAULT OPTIONS --- //
/**
* @param {Element} elem
* @param {!Object} options
*/
'initialize': function (elem, options) {
var self = this;
// find anchors that belong to a gallery.
var anchors; // a collection of anchors that form the gallery
var tag = elem.get('tag');
if (elem.each) { // is a collection
if (tag.every(function (item) { return item == 'a'; })) {
anchors = elem;
self.current = 0;
} else {
return;
}
} else if (tag == 'a') { // is a single anchor item
boxplus['extenders'].each(function (fn) {
anchors = fn.bind(self)(elem);
});
if (!anchors) { // anchor not recognized by any of the extenders
// extend with related anchors (if any) whose rel attribute starts with "boxplus-" or "boxplus["
var rel = elem.get('rel');
anchors = $$(rel && rel.test(/^boxplus\b(?!$)/) ? 'a[rel="' + rel + '"]' : elem);
self.current = anchors.indexOf(elem);
}
} else if (/^[udo]l$/.test(tag)) { // is a single list item
anchors = $$(elem.getChildren('li,dt').map(function (item) {
return item.getElement('a');
}).filter(function (item) {
return item; // filter null values
}));
self.current = anchors.length > 0 ? 0 : -1;
} else {
self.initialize(elem.getElement('ul,ol,dl'), options);
return;
}
// interpret settings
self['setOptions'](options);
if (options) {
self._getTitle = [options['getTitle'], self._getTitle].pick();
self._getText = [options['getText'], self._getText].pick();
self._getDownloadUrl = [options['getDownloadUrl'], self._getDownloadUrl].pick();
self._getMetadata = [options['getMetadata'], self._getMetadata].pick();
}
// click event bindings
anchors.addEvent('click', function () { // event "click" opens the gallery showing image of selected anchor
self.show['delay'](1, self, this); // this = image clicked, delay required for seamless history support (event processing function must exit before function "show" is invoked)
return false;
});
// cloak anchor href attributes
self['options']['cloak'] && anchors.each(cloak);
// set anchors used in extracting image properties
self._anchors = anchors;
},
/**
* Shows the gallery in a pop-up window.
* @param {Element} anchor An anchor (HTML <a>) that points to an image/content, or a list (HTML <ul> or <ol>) with each item containing an anchor.
*/
'show': function (anchor) {
var self = this;
if (anchor) {
self.current = (0).max(self._anchors.get('href').indexOf(anchor.get('href')));
}
dialog.hide(); // hide other dialog if visible
// show progress indicator
dialog.toggleProgress(true);
// toggle captions below/next to image viewport based on settings
dialog.setCaptionPosition(self['options']['captions']);
// toggle thumbnail bars inside/outside image viewport based on settings
dialog.setThumbPosition(self['options']['thumbs']);
// add thumbnails
dialog.addThumbs($$(self._anchors.map(function (anchor) {
var image = [anchor.retrieve('thumb'), anchor.getElement('img')].pick(); // use thumbnail associated with anchor (via element storage) or find an image child
if (image) {
// thumbnail image source, inspecting all candidate attributes
var attrthumb = image.get('data-thumb');
return new Element('img', {
'src': attrthumb ? attrthumb : image.get('src')
});
} else {
return null;
}
})));
// associate events with callback functions
var events = ['previous','next','first','last','start','stop','change','close'];
var callbacks = events.map(function (event) {
return self[event].bind(self);
});
self.dialogevents = callbacks.associate(events);
// add history support
self._updateHistory(anchor['href']);
window.addEvent('popstate', self.close['bind'](self));
// subscribe to dialog box events
dialog.addEvents(self.dialogevents);
dialog.show(self['options']);
// toggle slideshow start button
var allowSlideshow = self['options']['slideshow'] && self._anchors.length > 1;
dialog.setEnabled('start', allowSlideshow);
// change to image
self._replace(self.current);
if (allowSlideshow && self['options']['autostart']) {
self.start();
}
// fire onShow event
self._fireEvent('open');
},
'close': function () {
dialog.setEmpty();
dialog.removeEvents(this.dialogevents);
dialog.hide();
dialog.clearThumbs();
// unwind history stack
while (window.history.state == 'boxplus') { // boxplus uses the special history state string "boxplus"
window.history.go(-1);
}
// fire onClose event
this._fireEvent('close');
},
'previous': function () {
this._change(this.current - 1);
},
'next': function () {
this._change(this.current + 1);
},
'first': function () {
this._change(0);
},
'last': function () {
this._change(this._anchors.length - 1);
},
/**
* @param {number} index
*/
'change': function (index) {
this._change(index);
},
'start': function () {
var self = this;
if (!self._timer) {
self._timer = setTimeout(self.next.bind(self), self['options']['slideshow']);
}
dialog.setAvailable('start', false);
dialog.setAvailable('stop', true);
},
'stop': function () {
var self = this;
if (self._timer) {
clearTimeout(self._timer);
self._timer = null;
}
dialog.setAvailable('stop', false);
dialog.setAvailable('start', true);
},
/**
* @param {number} index
*/
_change: function (index) {
var self = this;
if (index != self.current && (self['options']['loop'] || index >= 0 && index < self._anchors.length)) {
self._replace(index);
self._fireEvent('change');
}
},
_fireEvent: function (event, arg) {
this['fireEvent'](event, arg);
},
/**
* Updates the latest history entry, either injecting a new entry or updating an existing one.
* The history entry is updated only if the latest entry has also been injected by this class.
* @param {string} href The new URL for the topmost history entry.
*/
_updateHistory: function (href) {
// check if the latest history entry has been injected by boxplus (which uses the special history state string "boxplus")
var hist = window.history;
var name = hist.state == 'boxplus' ? 'replaceState' : 'pushState';
// check for history support and execute function
var fn = hist[name];
if (fn) {
try {
fn('boxplus', '', href);
} catch (err) {
// catch security vulnerability errors (e.g. site URL domain does not match image URL domain)
}
}
},
/**
* @param {number} index
*/
_replace: function (index) {
var self = this;
var noSlideshow = !self._timer;
self.stop(); // stop slideshow animation
dialog.setEmpty(); // clear current image
var count = self._anchors.length;
var last = index >= count - 1;
dialog.setVisible('viewer', false); // hide viewer
dialog.setAvailable('start', !last);
var loop = self['options']['loop'];
dialog.setAvailable('prev', loop && count > 1 || index > 0);
dialog.setAvailable('next', loop && count > 1 || !last);
function _show() {
dialog.resize(function () {
dialog.toggleProgress(false);
noSlideshow || last || self.start(); // continue slideshow if possible
});
}
function _showCaption(anchor) {
dialog.setCaption(self._getTitle(anchor), self._getText(anchor));
}
function _showImage(anchor, image) {
dialog.setImage(image, self._getDownloadUrl(anchor), self._getMetadata(anchor));
_showCaption(anchor);
_show();
}
function _showContent(elem) {
dialog.setContent(elem);
_show();
}
if (count > 0) {
self.current = (index + count) % count; // avoid mod operator with negative numbers
var anchor = self._anchors[self.current];
// un-cloak URLs to be able to extract anchor parameters
uncloak(anchor);
// update history
self._updateHistory(anchor['href']);
// extract anchor parameters
var href = anchor.get('href'); // <a> href (as it occurs in source)
var url = anchor.href; // <a> href (resolved)
var path = anchor.pathname;
if (/^#/.test(href)) { // content in the same document
var elem = $(href.substr(1));
switch (elem.get('tag')) {
case 'img':
_showImage(anchor, elem);
break;
default:
_showContent(elem);
}
} else if (/\.(txt|html?)$/i.test(path) && anchor['host'] == window['location']['host']) { // use AJAX only for requests to the same domain
new Request.HTML({
'url': url,
'onSuccess': function (html) {
_showContent($$(html));
},
'onFailure': function (xhr) {
alert(dialog.getMessage('not-found'));
dialog.hide();
}
}).get();
} else if (/\.(gif|jpe?g|png)$/i.test(path)) { // preload image
$(new Image).addEvent('load', function () { // triggered when the image has been preloaded
_showImage(anchor, this); // the keyword 'this' refers to the image
}).set('src', url);
} else if (/\.(pdf|mov|mpe?g|ogg|swf|webM|wmv)$/i.test(path) || /(viddler|vimeo|youtube)\.com$/.test(anchor.hostname)) {
dialog.setObject(anchor);
_showCaption(anchor);
_show();
} else if (anchor.protocol != 'javascript:') {
dialog.setFrame(anchor);
_showCaption(anchor);
_show();
}
// re-cloak URLs if needed
self['options']['cloak'] && cloak(anchor);
}
}
});
// create constructor and add static methods
Object.append(window['boxplus'] = boxplus, {
/**
* List of custom URL parser functions.
*/
'extenders': [
/**
* Picasa extender.
* Remove this function if you do not need Picasa support.
*/
function (anchor) {
var self = this;
var match; // results of a regular expression match
if (Request.JSONP && (match = anchor.href.match(/https?:\/\/picasaweb.google.com\/data\/feed\/(?:api|base)\/user\/([^\/?#]+)\/albumid\/([^\/?#]+)/))) {
new Request.JSONP({
'url': 'http://picasaweb.google.com/data/feed/api/user/' + match[1] + '/albumid/' + match[2],
'data': {
'alt': 'json',
'imgmax': 800, // maximum image size (uncropped)
'kind': 'photo',
'thumbsize': '64u' // maximum thumbnail image size (uncropped or cropped)
},
'onComplete': function (data) {
self._anchors = $$([]);
data['feed']['entry'].each(function (entry) {
var thumb = entry['media$group']['media$thumbnail'][0];
self._anchors.include(
new Element('a', {
'href': entry['content']['src'],
'title': entry['summary']['$t']
}).adopt(
new Element('img', {
'width': thumb['width'],
'height': thumb['height'],
'alt': entry['title']['$t'],
'src': thumb['url']
})
)
);
});
self.current = 0;
}
}).send();
return $$(anchor);
}
}
],
/**
* Auto-discovery service.
* Remove this function if you do not need automatic boxplus binding.
* @param {boolean} strict
* @param {!Object} options
*/
'autodiscover': function (strict, options) {
window.addEvent('domready', function () {
// links part of a gallery
var groups = [];
$$('a[rel^=boxplus]:not([rel=boxplus])').each(function (item) { // leave out individual items
groups.include(item.get('rel'));
});
groups.each(function (group) {
new boxplus($$('a[rel="'+ group +'"]'), options);
});
// individual links with rel attribute set
$$('a[rel=boxplus]').each(function (item) {
new boxplus(item, options);
});
if (!strict) {
// individual links to images or flash not part of a gallery
$$('a[href]:not([rel^=boxplus])').filter(function (item) {
return /\.(gif|jpe?g|png|swf)$/i.test(item.pathname) && !/_(blank|parent|self|top)/.test(item.get('target'));
}).each(function (item) {
new boxplus(item, options);
});
}
});
}
});
})(document.id);