/**@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