/** * Lightbox feature for RightJS * http://rightjs.org/ui/lightbox * * Copyright (C) 2009-2010 Nikolay Nemshilov */ var Lightbox = RightJS.Lightbox = (function(document, RightJS) { /** * This module defines the basic widgets constructor * it creates an abstract proxy with the common functionality * which then we reuse and override in the actual widgets * * Copyright (C) 2010 Nikolay Nemshilov */ /** * The filenames to include * * Copyright (C) 2010 Nikolay Nemshilov */ var R = RightJS, $ = RightJS.$, $$ = RightJS.$$, $w = RightJS.$w, $E = RightJS.$E, $ext = RightJS.$ext, Xhr = RightJS.Xhr, Class = RightJS.Class, Object = RightJS.Object, Wrapper = RightJS.Wrapper, Element = RightJS.Element, Browser = RightJS.Browser; // IE6 doesn't support position:fixed so it needs a crunch Browser.IE6 = Browser.OLD && navigator.userAgent.indexOf("MSIE 6") > 0; /** * The widget units constructor * * @param String tag-name or Object methods * @param Object methods * @return Widget wrapper */ function Widget(tag_name, methods) { if (!methods) { methods = tag_name; tag_name = 'DIV'; } /** * An Abstract Widget Unit * * Copyright (C) 2010 Nikolay Nemshilov */ var AbstractWidget = new RightJS.Wrapper(RightJS.Element.Wrappers[tag_name] || RightJS.Element, { /** * The common constructor * * @param Object options * @param String optional tag name * @return void */ initialize: function(key, options) { this.key = key; var args = [{'class': 'rui-' + key}]; // those two have different constructors if (!(this instanceof RightJS.Input || this instanceof RightJS.Form)) { args.unshift(tag_name); } this.$super.apply(this, args); if (RightJS.isString(options)) { options = RightJS.$(options); } // if the options is another element then // try to dynamically rewrap it with our widget if (options instanceof RightJS.Element) { this._ = options._; if ('$listeners' in options) { options.$listeners = options.$listeners; } options = {}; } this.setOptions(options, this); return this; }, // protected /** * Catches the options * * @param Object user-options * @param Element element with contextual options * @return void */ setOptions: function(options, element) { element = element || this; RightJS.Options.setOptions.call(this, RightJS.Object.merge(options, eval("("+( element.get('data-'+ this.key) || '{}' )+")")) ); return this; } }); /** * Creating the actual widget class * */ var Klass = new RightJS.Wrapper(AbstractWidget, methods); // creating the widget related shortcuts RightJS.Observer.createShortcuts(Klass.prototype, Klass.EVENTS || []); return Klass; } /** * A shared module to create textual spinners * * Copyright (C) 2010 Nikolay Nemshilov */ var Spinner = new RightJS.Wrapper(RightJS.Element, { /** * Constructor * * @param Number optional spinner size (4 by default) * @return void */ initialize: function(size) { this.$super('div', {'class': 'rui-spinner'}); this.dots = []; for (var i=0; i < (size || 4); i++) { this.dots.push(new RightJS.Element('div')); } this.dots[0].addClass('glowing'); this.insert(this.dots); RightJS(this.shift).bind(this).periodical(300); }, /** * Shifts the spinner elements * * @return void */ shift: function() { if (this.visible()) { var dot = this.dots.pop(); this.dots.unshift(dot); this.insert(dot, 'top'); } } }); /** * The lightbox widget * * Copyright (C) 2009-2010 Nikolay Nemshilov */ var Lightbox = new Widget({ extend: { version: '2.0.0', EVENTS: $w('show hide load'), Options: { fxName: 'fade', fxDuration: 100, group: null, // a group marker hideOnEsc: true, hideOnOutClick: true, showCloseButton: true, cssRule: "a[data-lightbox]", // all lightbox links css-rule // video links default size mediaWidth: 425, mediaHeight: 350 }, i18n: { Close: 'Close', Prev: 'Previous Image', Next: 'Next Image' }, // the supported image-urls regexp Images: /\.(jpg|jpeg|gif|png|bmp)/, // media content sources Medias: [ [/(http:\/\/.*?youtube\.[a-z]+)\/watch\?v=([^&]+)/, '$1/v/$2', 'swf'], [/(http:\/\/video.google.com)\/videoplay\?docid=([^&]+)/, '$1/googleplayer.swf?docId=$2', 'swf'], [/(http:\/\/vimeo\.[a-z]+)\/([0-9]+).*?/, '$1/moogaloop.swf?clip_id=$2', 'swf'] ] }, /** * basic constructor * * @param Object options override * @param Element optional options holder * @return void */ initialize: function(options, context) { this .$super('lightbox', {}) .setOptions(options, context) .insert([ this.locker = new Locker(this.options), this.dialog = new Dialog(this.options) ]) .on({ close: this._close, next: this._next, prev: this._prev }); }, /** * Extracting the rel="lightboux[groupname]" attributes * * @param Object options * @param Element link with options * @return Dialog this */ setOptions: function(options, context) { this.$super(options, context); if (context) { var rel = context.get('rel'); if (rel && (rel = rel.match(/lightbox\[(.+?)\]/))) { this.options.group = rel[1]; } } return this; }, /** * Sets the popup's title * * @param mixed string or element or somethin' * @return Lighbox self */ setTitle: function(text) { this.dialog.setTitle(text); return this; }, /** * Shows the lightbox * * @param String/Array... content * @return Lightbox this */ show: function(content) { return this._showAnd(function() { this.dialog.show(content, !content); }); }, /** * Hides the lightbox * * @return Lightbox this */ hide: function() { Lightbox.current = null; return this.$super(this.options.fxName, { duration: this.options.fxDuration/3, onFinish: R(function() { this.fire('hide'); this.remove(); }).bind(this) }); }, /** * Loads up the data from url or a link * * @param String address or a link element * @param Object Xhr options * @return Lightbox this */ load: function(link, options) { return this._showAnd(function() { this.dialog.load(link, options); }); }, /** * Resizes the content block to the given size * * @param Hash size * @return Lightbox this */ resize: function(size) { this.dialog.resize(size); return this; }, // protected // handles the 'close' event _close: function(event) { event.stop(); this.hide(); }, // handles the 'prev' event _prev: function(event) { event.stop(); Pager.prev(); }, // handles the 'next' event _next: function(event) { event.stop(); Pager.next(); }, // shows the lightbox element and then calls back _showAnd: function(callback) { if (Lightbox.current !== this) { Lightbox.current = this; // hidding all the hanging around lightboxes $$('div.rui-lightbox').each('remove'); this.insertTo(document.body); this.dialog.show('', true); if (Browser.OLD) { // IE's get screwed by the transparency tricks this.reposition(); Element.prototype.show.call(this); callback.call(this); } else { this.setStyle('display:none'); Element.prototype.show.call(this, this.options.fxName, { duration: this.options.fxDuration/2, onFinish: R(function() { callback.call(this); this.fire('show'); }).bind(this) }); } } else { callback.call(this); } return this; }, // manually repositioning under IE6 browser reposition: function() { if (Browser.IE6) { var win = $(window); this.setStyle({ top: win.scrolls().y + 'px', width: win.size().x + 'px', height: win.size().y + 'px', position: "absolute" }); } } }); /** * The class level interface * * @copyright (C) 2009 Nikolay Nemshilov */ Lightbox.extend({ hide: function() { if (Lightbox.current) { Lightbox.current.hide(); } }, show: function() { return this.inst('show', arguments); }, load: function() { return this.inst('load', arguments); }, // private inst: function(name, args) { var inst = new Lightbox(); return inst[name].apply(inst, args); } }); /** * Lightbox background locker element * * Copyright (C) 2010 Nikolay Nemshilov */ var Locker = new Wrapper(Element, { initialize: function(options) { this.$super('div', {'class': 'rui-lightbox-locker'}); if (options.hideOnOutClick) { this.onClick('fire', 'close'); } } }); /** * The dialog element wrapper * * Copyright (C) 2010 Nikolay Nemshilov */ var Dialog = new Wrapper(Element, { /** * Constructor * * @param Object options * @return void */ initialize: function(options) { var i18n = Lightbox.i18n; this.options = options; this.$super('div', {'class': 'rui-lightbox-dialog'}); // building up the this.insert([ this.title = $E('div', {'class': 'rui-lightbox-title'}), $E('div', {'class': 'rui-lightbox-body'}).insert( $E('div', {'class': 'rui-lightbox-body-inner'}).insert([ this.locker = $E('div', {'class': 'rui-lightbox-body-locker'}).insert(new Spinner(4)), this.scroller = $E('div', {'class': 'rui-lightbox-scroller'}).insert( this.content = $E('div', {'class': 'rui-lightbox-content'}) ) ]) ), $E('div', {'class': 'rui-lightbox-navigation'}).insert([ this.closeButton = $E('div', {'class': 'close', html: '×', title: i18n.Close}).onClick('fire', 'close'), this.prevLink = $E('div', {'class': 'prev', html: '←', title: i18n.Prev}).onClick('fire', 'prev'), this.nextLink = $E('div', {'class': 'next', html: '→', title: i18n.Next}).onClick('fire', 'next') ]) ]); // presetting the navigation state this.prevLink.hide(); this.nextLink.hide(); if (!options.showCloseButton) { this.closeButton.hide(); } }, /** * Sets the dialogue caption * * @param String title * @return Dialog this */ setTitle: function(title) { this.title.update(title||''); }, /** * Nicely resize the dialog box * * @param Object the end size * @param Boolean use fx (false by default) * @return Dialog this */ resize: function(end_size, with_fx) { var win_size = this.parent().size(), cur_size = this.scroller.size(), cur_top = (win_size.y - this.size().y)/2, dlg_diff = this.size().x - cur_size.x; // <- use for IE6 fixes if (end_size) { // getting the actual end-size end_size = this.scroller.setStyle(end_size).size(); this.scroller.setStyle({ width: cur_size.x + 'px', height: cur_size.y + 'px' }); } else { // using the content block size end_size = this.content.size(); } // checking the constraints var threshold = 100; // px if ((end_size.x + threshold) > win_size.x) { end_size.x = win_size.x - threshold; } if ((end_size.y + threshold) > win_size.y) { end_size.y = win_size.y - threshold; } // the actual resize and reposition var end_top = (cur_top * 2 + cur_size.y - end_size.y) / 2; var dialog = this._.style, content = this.scroller._.style; if (RightJS.Fx && with_fx && (end_size.x != cur_size.x || end_size.y != cur_size.y)) { $ext(new RightJS.Fx(this, {duration: this.options.fxDuration, transition: 'Lin'}), { render: function(delta) { content.width = (cur_size.x + (end_size.x - cur_size.x) * delta) + 'px'; content.height = (cur_size.y + (end_size.y - cur_size.y) * delta) + 'px'; dialog.top = (cur_top + (end_top - cur_top) * delta) + 'px'; if (Browser.IE6) { dialog.width = (dlg_diff + cur_size.y + (end_size.y - cur_size.y) * delta) + 'px'; } } }).onFinish(R(this.unlock).bind(this)).start(); } else { // no-fx direct assignment content.width = end_size.x + 'px'; content.height = end_size.y + 'px'; dialog.top = end_top + 'px'; if (Browser.IE6) { dialog.width = (dlg_diff + end_size.x) + 'px'; } if (!this.request) { this.unlock(); } } return this; }, /** * Shows the content * * @param mixed content String/Element/Array and so one * @return Dialog this */ show: function(content, no_fx) { this.content.update(content || ''); this.resize(null, !no_fx); }, /** * Loads up the data from the link * * @param mixed String url address or a link element * @param Object xhr-options * @return void */ load: function(url, options) { if (url instanceof Element) { this.setTitle(url.get('title')); url = url.get('href'); } Pager.show(this, url); this.lock().cancel(); // defined in the loader.js file this.request = new Loader(url, options, R(function(content, no_fx) { this.request = null; this.show(content, no_fx); }).bind(this)); return this.resize(); // the look might be changed for a media-type }, /** * Cancels a currently loading request * * @return Dialog this */ cancel: function() { if (this.request) { this.request.cancel(); } return this; }, /** * Shows the loading lock * * @return Dialog this */ lock: function() { this.locker.setStyle('opacity:1'); return this; }, /** * Hides the loading lock * * @return Dialog this */ unlock: function() { this.locker.morph({opacity: 0}, { duration: this.options.fxDuration * 2/3 }); return this; } }); /** * Xhr/images/medias loading module * * Copyright (C) 2009-2010 Nikolay Nemshilov */ var Loader = new Class({ /** * Constructor * * @param String url address * @param Object Xhr options * @param Function on-finish callback */ initialize: function(url, options, on_finish) { // adjusting the dialog look for different media-types if (this.isImage(url, on_finish)) { Lightbox.current.addClass('rui-lightbox-image'); } else if (this.isMedia(url, on_finish)) { Lightbox.current.addClass('rui-lightbox-media'); } else { this.xhr = new Xhr(url, Object.merge({method: 'get'}, options) ).onComplete(function() { on_finish(this.text); }).send(); } }, /** * Cancels the request * * @return Loader this */ cancel: function() { if (this.xhr) { this.xhr.cancel(); } else if (this.img) { this.img.onload = function() {}; } }, // protected // tries to initialize it as an image loading isImage: function(url, on_finish) { if (url.match(Lightbox.Images)) { var img = this.img = $E('img')._; img.onload = function() { on_finish(img); }; img.src = url; return true; } }, // tries to initialize it as a flash-element isMedia: function(url, on_finish) { var media = R(Lightbox.Medias).map(function(desc) { return url.match(desc[0]) ? this.buildEmbed( url.replace(desc[0], desc[1]), desc[2]) : null; }, this).compact()[0]; if (media) { on_finish(media, true); return true; } }, // builds an embedded media block buildEmbed: function(url, type) { var media_types = { swf: [ 'D27CDB6E-AE6D-11cf-96B8-444553540000', 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0', 'application/x-shockwave-flash' ] }, options = Lightbox.current ? Lightbox.current.options : Lightbox.Options, sizes = ' width="'+ options.mediaWidth + '" height="'+ options.mediaHeight + '"'; return '' + ''+ '' + ''; } }); /** * Processes the link-groups showing things in a single Lightbox * * Copyright (C) 2010 Nikolay Nemshilov */ var Pager = { /** * Checks and shows the pager links on the dialog * * @param Dialog dialog * @param String url-address * @return void */ show: function(dialog, url) { if (dialog.options.group) { this.dialog = dialog; this.links = this.find(dialog.options.group); this.link = this.links.first(function(link) { return link.get('href') === url; }); var index = this.links.indexOf(this.link), size = this.links.length; dialog.prevLink[size && index > 0 ? 'show' : 'hide'](); dialog.nextLink[size && index < size - 1 ? 'show' : 'hide'](); } else { this.dialog = null; } }, /** * Shows the prev link * * @return void */ prev: function() { if (this.dialog && !this.timer) { var id = this.links.indexOf(this.link), link = this.links[id - 1]; if (link) { this.dialog.load(link); this.timeout(); } } }, /** * Shows the next link * * @return void */ next: function() { if (this.dialog && !this.timer) { var id = this.links.indexOf(this.link), link = this.links[id + 1]; if (link) { this.dialog.load(link); this.timeout(); } } }, // private // finding the links list find: function(group) { return $$('a').filter(function(link) { var data = link.get('data-lightbox'); var rel = link.get('rel'); return (data && eval("("+ data + ")").group === group) || (rel && rel.indexOf('lightbox['+ group + ']') > -1); }); }, // having a little nap to prevent ugly quick scrolling timeout: function() { this.timer = R(function() { Pager.timer = null; }).delay(300); } }; /** * document level hooks * * Copyright (C) 2009-2010 Nikolay Nemshilov */ $(document).on({ /** * Catches clicks on the target links * * @param Event click * @return void */ click: function(event) { var target = event.find(Lightbox.Options.cssRule) || event.find('a[rel^=lightbox]'); if (target) { event.stop(); new Lightbox({}, target).load(target); } }, /** * Catches the mousewheel event and tries to scroll * the list of objects on the lightbox * * @param Event mousewheel * @return void */ mousewheel: function(event) { if (Lightbox.current) { event.stop(); Lightbox.current.fire((event._.detail || -event._.wheelDelta) < 0 ? 'prev' : 'next'); } }, /** * Handles the navigation form a keyboard * * @param Event keydown * @return void */ keydown: function(event) { var lightbox = Lightbox.current, name = ({ 27: 'close', // Esc 33: 'prev', // PageUp 37: 'prev', // Left 38: 'prev', // Up 39: 'next', // Right 40: 'next', // Down 34: 'next' // PageDown })[event.keyCode]; if (lightbox && name) { if (name !== 'close' || lightbox.options.hideOnEsc) { event.stop(); lightbox.fire(name); } } } }); $(window).on({ resize: function() { if (Lightbox.current) { Lightbox.current.reposition(); Lightbox.current.dialog.resize(); } }, scroll: function(event) { if (Lightbox.current && Browser.IE6) { Lightbox.current.reposition(); } } }); document.write(""); return Lightbox; })(document, RightJS);