/**@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 .
*/
/*
* 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 .
* @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.} 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.} 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.} cls
* @return {Element}
*/
_getElement: function (cls) {
return this.popup.getElement(this._class(cls));
},
/**
* @param {string|Array.} cls
* @return {Elements}
*/
_getElements: function (cls) {
return this.popup.getElements(this._class(cls));
},
/**
* @param {string|Array.} 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