/** * Simple Lightbox * @author Archetyped (http://archetyped.com/tools/simple-lightbox/) * * Inspiration/History * > 2010: Originally based on Lightbox Slideshow v1.1 * > by Justin Barkhuff (http://www.justinbarkhuff.com/lab/lightbox_slideshow/) * > Largely based on Lightbox v2.02 * > by Lokesh Dhakar - http://huddletogether.com/projects/lightbox2/ * > 2011-01-12: Rebuilt as jQuery-compatible codebase * */ (function($) { SLB = { /* Properties */ //Core prefix : 'slb', options : {}, //Activation trigger : null, urls_checked : {}, //Items items : [], item_curr : null, item_init : null, group : null, media : {}, //Content content: { caption_enabled: true, caption_src: true, desc_enabled: true, labels : { closeLink : 'close', loadingMsg : 'loading', nextLink : 'next »', prevLink : '« prev', startSlideshow : 'start slideshow', stopSlideshow : 'stop slideshow', numDisplayPrefix : 'Image', numDisplaySeparator : 'of' } }, //Layout container : document, masks : ['select','object','embed','iframe'], layout: { template: '', parsed: '', placeholders : { slbContent: '', slbLoading: 'loading', slbClose: 'close', navPrev: '« prev', navNext: '» next', navSlideControl: 'Stop', dataCaption: '', dataDescription: '', dataNumber: '' } }, //Slideshow slideshow: { active: null, enabled: true, loop: true, duration: 4, timer: null }, //Animation anim: { active: true, overlay_duration: 0, overlay_opacity: .8, resize_duration: 0 }, /* Initialization */ /** * Initialize lightbox instance * @param object options Instance options */ init: function(options) { //Options this.options = $.extend(true, { //Core prefix: null, //Activation trigger : null, validateLinks : false, //Content captionEnabled: true, captionSrc : true, descEnabled: true, labels : {}, //Layout layout : null, placeholders : {}, //Animation animate : true, overlayDuration : .2, overlayOpacity : .8, resizeSpeed : 400, //Slideshow autoPlay : true, enableSlideshow : true, loop : true, slideTime : 4 }, options); //Stop if no layout is defined if (!this.options.layout || this.options.layout.toString().length == 0) this.end(); /* Setup Properties */ //Prefix if ( $.type(this.options.prefix) == 'string' && this.options.prefix.length > 0 ) { this.prefix = this.options.prefix; } //Activation this.trigger = ( $.isArray(this.options.trigger) ) ? this.options.trigger : [this.prefix]; //Content $.extend(true, this.content, { caption_enabled: !!this.options.captionEnabled, caption_src: !!this.options.captionSrc, desc_enabled: !!this.options.descEnabled, labels: ( $.isPlainObject(this.options.labels) ) ? this.options.labels : {} }); //Layout $.extend(true, this.layout, { template: this.options.layout, placeholders: this.options.placeholders }); //Animation $.extend(this.anim, { overlay_opacity: Math.max(Math.min(this.options.overlayOpacity,1),0) }); if ( this.options.animate ) { $.extend(this.anim, { active: true, overlay_duration: Math.max(this.options.overlayDuration,0), resize_duration: this.options.resizeSpeed }); } else { $.extend(this.anim, { active: false, overlay_duration: 0, resize_duration: 0 }); } //Slideshow $.extend(this.slideshow, { play: !!this.options.autoPlay, active: !!this.options.autoPlay, enabled: !!this.options.enableSlideshow, loop: ( !!this.options.enableSlideshow && !!this.options.loop ), duration: ( $.isNumeric(this.options.slideTime) ) ? parseInt(this.options.slideTime) : 0 }); /* Init Layout */ var t = this; var body = $('body'); //Overlay $('
', { 'id': this.getID('overlay'), 'css': {'display': 'none'} }).appendTo(body) .click(function() {t.end();}); //Viewer var viewer = $('
', { 'id': this.getID('viewer'), 'css': {'display': 'none'} }).appendTo(body) .click(function() {t.end();}); //Build layout from template this.layout.parsed = this.getLayout(); //Insert layout into viewer $(this.layout.parsed).appendTo(viewer); //Set UI this.initUI(); //Add events this.initEvents(); }, /** * Set localized values for UI elements */ initUI: function() { this.get('slbClose').html(this.getLabel('closeLink')); this.get('navNext').html(this.getLabel('nextLink')); this.get('navPrev').html(this.getLabel('prevLink')); this.get('navSlideControl').html( this.getLabel((this.slideshowActive()) ? 'stopSlideshow' : 'startSlideshow') ); }, /** * Add events to various UI elements */ initEvents: function() { var t = this, delay = 500; //Remove all previous handlers this.get('container,details,navPrev,navNext,navSlideControl,slbClose').unbind('click'); //Set event handlers this.get('container,details').click(function(ev) { ev.stopPropagation(); }); var clickP = function() { //Handle double clicks t.get('navPrev').unbind('click').click(false); setTimeout(function() {t.get('navPrev').click(clickP);}, delay); t.navPrev(); return false; }; this.get('navPrev').click(function(){ return clickP(); }); var clickN = function() { //Handle double clicks t.get('navNext').unbind('click').click(false); setTimeout(function() {t.get('navNext').click(clickN);}, delay); t.navNext(); return false; }; this.get('navNext').click(function() { return clickN(); }); this.get('navSlideControl').click(function() { t.slideshowToggle(); return false; }); this.get('slbClose').click(function() { t.end(); return false; }); //Handle links on page this.initLinks(); }, /** * Finds all compatible image links on page * @return void */ initLinks: function() { var ph = '{relattr}', t = this; var sel = [], selBase = 'a[href][rel*="' + ph + '"]:not([rel~="' + this.addPrefix('off') + '"])'; //Click event handler var handler = function() { t.view(this); return false; }; //Build selector for (var x = 0; x < this.trigger.length; x++) { sel.push(selBase.replace(ph, this.trigger[x])); } sel = sel.join(','); //Add event handler to links $(sel, $(this.container)).live('click', handler); }, /* Display */ /** * Display viewer * If item is part of a group, add other items in group * @param node item Link element of item to display */ view: function(item) { item = $(item); this.mask(); this.items = []; this.setGroup(item); var t = this; var groupTemp = {}; this.fileExists(this.itemSource(item), function() { /* File Exists */ /* Handlers */ /** * Add item to group * @param obj el Item to add * @param int idx DOM position index of item * @return int Total number of items in group */ var addItem = function(el, idx) { groupTemp[idx] = el; return groupTemp.length; }; /** * Build final item array & launch viewer */ var proceed = function() { t.item_init = 0; //Sort links by document order var order = [], el; for (var x in groupTemp) { order.push(x); } order.sort(function(a, b) { return (a - b); }); for (x = 0; x < order.length; x++) { el = groupTemp[order[x]]; // Check if link being evaluated is the same as the clicked link if ($(el).get(0) == $(item).get(0)) { t.item_init = x; } t.items.push({'link':t.itemSource($(el)), 'title':t.getCaption(el), 'desc': t.getDescription(el)}); } // Calculate top offset for the viewer and display var vwrTop = $(document).scrollTop() + ($(window).height() / 15); t.get('viewer').css('top', vwrTop + 'px').show(); t.itemLoad(t.item_init); }; /* Display */ //Overlay t.get('overlay') .height($(document).height()) .fadeTo(t.anim.overlay_duration, t.options.overlayOpacity); //Single item (not in group) if ( !t.hasGroup() ) { //Display item t.item_init = 0; addItem(item, t.item_init); proceed(); } else { //Item in group var links = $(t.container).find('a'); //Get other items in group var grpLinks = []; var i, link; for ( i = 0; i < links.length; i++ ) { link = $(links[i]); if ( t.itemSource(link) && t.inGroup(link) ) { //Add links in group grpLinks.push(link); } } //Loop through group links, validate, and add to items array var processed = 0; for ( i = 0; i < grpLinks.length; i++ ) { link = grpLinks[i]; t.fileExists(t.itemSource($(link)), function(args) { /* File exists */ //Add item var el = args.items[args.idx]; addItem(el, args.idx); processed++; //Display valid items after all items parsed if ( processed == args.items.length ) proceed(); }, function(args) { /* File does not exist */ processed++; //Display valid items after all items parsed if (args.idx == args.items.length) proceed(); }, {'idx': i, 'items': grpLinks}); } } }, function() { /* File does not exist */ t.end(); }); }, /** * Close the viewer */ end: function() { this.keyboardDisable(); this.slideshowPause(); this.get('viewer').hide(); this.get('overlay').fadeOut(this.anim.overlay_duration); this.unmask(); }, /** * Resizes viewer to fit image * @param int imgWidth Image width in pixels * @param int imgHeight Image height in pixels */ viewerResize: function(w, h) { var d = this.viewerSize(w, h); //Resize container this.get('container').animate({width: d.width, height: d.height}, this.anim.resize_duration); //Resize overlay this.get('overlay').css('min-width', d.width); this.itemShow(); }, /** * Retrieve or build container size * @param int w Container width to set * @param int h Container height to set * @return obj Container width (w)/height (h) values */ viewerSize: function(w, h) { var style = 'padding'; var hz = ['left', 'right'], vt = ['top', 'bottom']; var ph = 0, pv = 0; var t = this; //Calculate spacing around item var getVal = function(prop) { var unit = 'px'; prop = style + '-' + prop; var ptemp = t.get('content').css( prop ); if ( ptemp.indexOf(unit) == -1 ) { ptemp = 0; } else { ptemp = ptemp.replace(unit, ''); } return ( $.isNumeric(ptemp) ) ? parseFloat(ptemp) : 0; }; //Horizontal for ( var x = 0; x < hz.length; x++) { ph += getVal(hz[x]); } //Vertical for ( x = 0; x < vt.length; x++) { pv += getVal(vt[x]); } var c = { 'width': w + ph, 'height': h + pv }; return c; }, /** * Displays objects that may conflict with the viewer * @param bool show (optional) Whether or not to show objects (Default: TRUE) */ unmask: function (show) { show = ( typeof(show) == 'undefined' ) ? true : !!show; var vis = (show) ? 'visible' : 'hidden'; $(this.masks.join(',')).css('visibility', vis); }, /** * Hides objects that may conflict with the viewer * @uses unmask() to hide objects */ mask: function () { this.unmask(false); }, /* Item */ /** * Retrieve item property * @uses items to retrieve property from item in array * @param int idx Item position index * @param string prop Item property to retrieve * @return mixed Item property (Default: empty string) */ itemProp: function(idx, prop) { return ( idx < this.items.length && prop in this.items[idx] ) ? this.items[idx][prop] : ''; }, /** * Preloads requested item prior to displaying it in viewer * @param int idx Index of item in items property * @uses items to retrieve item at specified index * @uses viewerResize() to resize viewer after item has loaded */ itemLoad: function(idx) { this.item_curr = idx; this.keyboardDisable(); this.slideshowPause(); //Hide elements during transition this.get('slbLoading').show(); this.get('slbContent').hide(); this.get('details').hide(); var preloader = new Image(); var t = this; //Event handler: Display item when loaded $(preloader).bind('load', function() { t.get('slbContent').attr('src', preloader.src); t.viewerResize(preloader.width, preloader.height); //Restart slideshow if active if ( t.slideshowActive() ) t.slideshowStart(); }); //Load image preloader.src = this.itemProp(this.item_curr, 'link'); }, /** * Display image and begin preloading neighbors. */ itemShow: function() { this.get('slbLoading').hide(); var t = this; this.get('slbContent').fadeIn(500, function() { t.contentUpdate(); }); if ( this.hasItems() ) { this.itemPreloadSiblings(); } }, /** * Preloads items surrounding current item */ itemPreloadSiblings: function() { //Prev var idxPrev = ( this.itemFirst() ) ? this.items.length - 1 : this.item_curr - 1; var itemPrev = new Image(); itemPrev.src = this.itemProp(idxPrev, 'link'); //Next var idxNext = ( this.itemLast() ) ? 0 : this.item_curr + 1; if ( idxNext != idxPrev ) { var itemNext = new Image(); itemNext.src = this.itemProp(idxNext, 'link'); } }, /** * Check if there is at least one image to display in the viewer * @return bool TRUE if at least one image is found * @uses items to check for images */ hasItem: function() { return ( this.items.length > 0 ); }, /** * Check if there are multiple images to display in the viewer * @return bool TRUE if there are multiple images * @uses items to determine the number of images */ hasItems: function() { return ( this.items.length > 1 ); }, /** * Check if the current image is the first image in the list * @return bool TRUE if image is first * @uses item_curr to check index of current image */ itemFirst: function() { return ( this.item_curr == 0 ); }, /** * Check if the current image is the last image in the list * @return bool TRUE if image is last * @uses item_curr to check index of current image * @uses items to compare current image to total number of images */ itemLast: function() { return ( this.item_curr == this.items.length - 1 ); }, /** * Retrieve source URI in link * @param obj el * @return string Source file URI */ itemSource: function(el) { var src = $(el).attr('href'); var attr = $(el).attr('rel') || ''; if ( attr.length ) { //Attachment source var mSrc = this.mediaProp(el, 'source'); //Set source using extended properties if ( $.type(mSrc) === 'string' && mSrc.length ) src = mSrc; } return src; }, /** * Check if current link is part of a gallery * @param obj item * @param string gType Gallery type to check for * @return bool Whether link is part of a gallery */ itemGallery: function(item, gType) { var ret = false; var galls = { 'wp': '.gallery-icon', 'ng': '.ngg-gallery-thumbnail' }; if ( typeof gType == 'undefined' || !(gType in galls) ) { gType = 'wp'; } return ( $(item).parent(galls[gType]).length > 0 ) ? true : false ; }, /* Media */ /** * Retrieve ID of media item * @param obj el Link element * @return string|bool Media ID (Default: false - No ID) */ mediaId: function(el) { var h = $(el).attr('href'); if ($.type(h) !== 'string') h = false; return h; }, /** * Retrieve Media properties * @param obj el Link element * @return obj Properties for Media item (Default: Empty) */ mediaProps: function(el) { var props = {}, mId = this.mediaId(el); if (mId && mId in this.media) { props = this.media[mId]; } return props; }, /** * Retrieve single property for media item * @param obj el Image link DOM element * @param string prop Property to retrieve * @return mixed|null Item property (Default: NULL if property does not exist) */ mediaProp: function(el, prop) { var props = this.mediaProps(el); return (prop in props) ? props[prop] : null; }, /* Content */ /** * Retrieve specified label * @param string id Label ID * @param string def (optional) Default value if specified label is invalid * @return string Label text */ getLabel: function(id, def) { if ( typeof def == 'undefined' ) { def = ''; } return ( id in this.content.labels ) ? this.content.labels[id] : def; }, /** * Build caption for displayed item * @param obj item DOM link element * @return string Image caption */ getCaption: function(item) { item = $(item); var caption = ''; if ( this.content.caption_enabled ) { var sels = { 'capt': '.wp-caption-text', 'gIcon': '.gallery-icon' }; var els = { 'link': item, 'origin': item, 'sibs': null, 'img': null }; //WP Caption if ( this.itemGallery(els.link, 'wp') ) { els.origin = $(els.link).parent(); } if ( (els.sibs = $(els.origin).siblings(sels.capt)) && $(els.sibs).length > 0 ) { caption = $.trim($(els.sibs).first().text()); } //Fall back to image properties if ( !caption ) { els.img = $(els.link).find('img').first(); if ( $(els.img).length ) { //Image title / alt caption = $(els.img).attr('title') || $(els.img).attr('alt'); caption = $.trim(caption); } } //Media properties if ( !caption ) { caption = this.mediaProp(els.link, 'title') || ''; caption = $.trim(caption); } //Fall back Link Text if ( !caption ) { var c = ''; if ( ( c = $.trim($(els.link).text()) ) && c.length) { caption = c; } else if (this.options.captionSrc) { //Fall back to Link href caption = $(els.link).attr('href'); var trimChars = ['/', '#', '.']; //Trim invalid characters while ( caption.length && $.inArray(caption.charAt(0), trimChars) != -1 ) caption = caption.substr(1); while ( caption.length && $.inArray(caption.charAt(caption.length - 1), trimChars) != -1 ) caption = caption.substr(0, caption.length - 1); //Strip to base file name var idx = caption.lastIndexOf('/'); if ( -1 != idx ) caption = caption.substr(idx + 1); //Strip extension idx = caption.lastIndexOf('.'); if ( -1 != idx ) { caption = caption.slice(0, idx); } } caption = $.trim(caption); } } return caption; }, /** * Retrieve item description * @param obj item * @return string Item description (Default: empty string) */ getDescription: function(item) { var desc = ''; if ( this.content.desc_enabled ) { //Retrieve description if ( this.itemGallery(item, 'ng') ) { desc = $(item).attr('title'); } else { desc = this.mediaProp(item, 'desc'); } if (!desc) desc = ''; } return desc; }, /** * Display item details */ contentUpdate: function() { //Caption if (this.content.caption_enabled) { this.get('dataCaption').text(this.itemProp(this.item_curr, 'title')); this.get('dataCaption').show(); } else { this.get('dataCaption').hide(); } //Description this.get('dataDescription').html(this.itemProp(this.item_curr, 'desc')); //Handle grouped items if ( this.hasItems() ) { var num_display = this.getLabel('numDisplayPrefix') + ' ' + (this.item_curr + 1) + ' ' + this.getLabel('numDisplaySeparator') + ' ' + this.items.length; this.get('dataNumber') .text(num_display) .show(); } //Resize content area this.get('details').width(this.get('container').width()); this.navUpdate(); var t = this; this.get('details').animate({height: 'show', opacity: 'show'}, 650); }, /* Layout */ /** * Build layout from template * @uses options.layout * @return string Layout markup (HTML) */ getLayout: function() { var l = this.layout.template; //Expand placeholders var ph, phs, phr; for ( ph in this.layout.placeholders ) { phs = '{' + ph + '}'; //Continue to next placeholder if current one is not in layout if (l.indexOf(phs) == -1) continue; phr = new RegExp(phs, "g"); l = l.replace(phr, this.layout.placeholders[ph]); } //Return final layout return l; }, /* Grouping */ /** * Sets group based on current item * @param obj item DOM element to get group from */ setGroup: function(item) { this.group = this.getGroup(item); }, /** * Extract group name from * @param obj item Element to extract group name from * @return string Group name */ getGroup: function(item) { //Return global group property if no item specified if ( typeof item == 'undefined' || 0 == $(item).length ) return this.group; //Get item's group var g = ''; var attr = $(item).attr('rel') || ''; if ( attr != '' ) { var gTmp = '', gSt = '[', gEnd = ']', search = this.addPrefix('group') + gSt, idx, prefix = ' '; //Find group indicator idx = attr.indexOf(search); //Prefix with space to find whole word if ( prefix != search.charAt(0) && idx > 0 ) { search = prefix + search; idx = attr.indexOf(search); } //Continue processing if value is found if ( idx != -1 ) { //Extract group name gTmp = $.trim(attr.substring(idx).replace(search, '')); //Check if group defined if (gTmp.length > 1 && gTmp.indexOf(gEnd) > 0) { //Extract group name g = gTmp.substring(0, gTmp.indexOf(gEnd)); } } } return g; }, /** * Check if item is part of current group * @param obj item Item to check * @return bool TRUE if item is in current group, FALSE otherwise */ inGroup: function(item) { return ( this.hasGroup() && ( this.getGroup(item) == this.getGroup() ) ) ? true : false; }, /** * Check if group is set * @return bool TRUE if group is set, FALSE otherwise */ hasGroup: function() { return ( $.type(this.group) == 'string' && this.group.length > 0 ) ? true : false; }, /* Slideshow */ /** * Checks if slideshow is currently activated * @return bool TRUE if slideshow is active, false otherwise * @uses slideshow.active to check slideshow activation status */ slideshowActive: function() { return this.slideshow.active; }, /** * Start the slideshow */ slideshowStart: function() { this.slideshow.active = true; var t = this; clearInterval(this.slideshow.timer); this.slideshow.timer = setInterval(function() { t.navNext(); t.slideshowPause(); }, this.slideshow.duration * 1000); this.get('navSlideControl').text(this.getLabel('stopSlideshow')); }, /** * Stop the slideshow */ slideshowStop: function() { this.slideshow.active = false; if ( this.slideshow.timer ) { clearInterval(this.slideshow.timer); } this.get('navSlideControl').text(this.getLabel('startSlideshow')); }, /** * Toggles the slideshow status */ slideshowToggle: function() { if ( this.slideshowActive() ) { this.slideshowStop(); } else { this.slideshowStart(); } }, /** * Pauses the slideshow * Stops the slideshow but does not change the slideshow's activation status */ slideshowPause: function() { if ( this.slideshow.timer ) { clearInterval(this.slideshow.timer); } }, /* Navigation */ /** * Display appropriate previous and next hover navigation. */ navUpdate: function() { if ( this.hasItems() ) { this.get('navPrev').show(); this.get('navNext').show(); if ( this.slideshow.enabled ) { this.get('navSlideControl').show(); if ( this.slideshowActive() ) { this.slideshowStart(); } else { this.slideshowStop(); } } else { this.get('navSlideControl').hide(); } } else { // Hide navigation controls when only one image exists this.get('dataNumber').hide(); this.get('navPrev').hide(); this.get('navNext').hide(); this.get('navSlideControl').hide(); } this.keyboardEnable(); }, /** * Show the next image in the list */ navNext : function() { if ( this.hasItems() ) { if ( !this.slideshow.loop && this.itemLast() ) { return this.end(); } if ( this.itemLast() ) { this.navFirst(); } else { this.itemLoad(this.item_curr + 1); } } }, /** * Show the previous image in the list */ navPrev : function() { if ( this.hasItems() ) { if ( !this.slideshow.loop && this.itemFirst() ) { return this.end(); } if ( this.itemFirst() ) { this.navLast(); } else { this.itemLoad(this.item_curr - 1); } } }, /** * Show the first image in the list */ navFirst : function() { if ( this.hasItems() ) { this.itemLoad(0); } }, /** * Show the last image in the list */ navLast : function() { if ( this.hasItems() ) { this.itemLoad(this.items.length - 1); } }, /** * Enable image navigation via the keyboard */ keyboardEnable: function() { var t = this; $(document).keydown(function(e) { t.keyboardControl(e); }); }, /** * Disable image navigation via the keyboard */ keyboardDisable: function() { $(document).unbind('keydown'); }, /** * Handler for keyboard events * @param event e Keyboard event data */ keyboardControl: function(e) { var code = e.which; var key = String.fromCharCode(code).toLowerCase(); if ( code == 27 || key == 'x' ) { //Close this.end(); } else if ( code == 39 || key =='n' ) { //Next this.navNext(); } else if ( code == 37 || key == 'p' ) { //Previous this.navPrev(); } }, /* Helpers */ /** * Generate separator text * @param string sep Separator text * @return string Separator text */ getSep: function(sep) { return ( typeof sep == 'undefined' ) ? '_' : sep; }, /** * Retrieve prefix * @return string Object prefix */ getPrefix: function() { return this.prefix; }, /** * Add prefix to text * @param string txt Text to add prefix to * @param string sep (optional) Separator text * @return string Prefixed text */ addPrefix: function(txt, sep) { return this.getPrefix() + this.getSep(sep) + txt; }, hasPrefix: function(txt) { return ( txt.indexOf(this.addPrefix('')) === 0 ) ? true : false; }, /** * Generate formatted ID for viewer-specific elements * @param string id Base ID of element * @return string Formatted ID */ getID: function(id) { return this.addPrefix(id); }, /** * Generate formatted selector for viewer-specific elements * Compares specified ID to placeholders first, then named elements * Multiple selectors can be included and separated by commas (',') * @param string id Base ID of element * @uses layout.placeholders to compare id to placeholder names * @return string Formatted selector */ getSel: function(id) { //Process multiple selectors var delim = ',', prefix = '#', sel; if (id.toString().indexOf(delim) != -1) { //Split var sels = id.toString().split(delim); //Build selector for (var x = 0; x < sels.length; x++) { sels[x] = this.getSel($.trim(sels[x])); } //Join sel = sels.join(delim); } else { //Single selector if ( id in this.layout.placeholders ) { var ph = $( this.layout.placeholders[id] ); if (!ph.attr('id')) { //Class selector prefix = '.'; } } sel = prefix + this.getID(id); } return sel; }, /** * Retrieve viewer-specific element * @param string id Base ID of element * @uses getSel() to generate formatted selector for element * @return object jQuery object of selected element(s) */ get: function(id) { return $(this.getSel(id)); }, /** * Checks if file exists using AJAX request * @param string url File URL * @param callback success Callback to run if file exists * @param callback failure Callback to run if file does not exist * @param obj args Arguments for callback */ fileExists: function(url, success, failure, args) { //Validate var t = this; if ( !$.isPlainObject(args) ) { args = null; } if ( !$.isFunction(failure) ) { failure = function() { t.end(); }; } if ( !$.isFunction(success) ) { success = failure; } //Immediate success when validation disabled if ( !this.options.validateLinks ) { return success(args); } //Validate link var statusFail = 400; var stateCheck = 4; var proceed = function(res) { if ( res.status < statusFail ) { success(args); } else { failure(args); } }; //Check if URL already processed if ( url in this.urls_checked ) { proceed(this.urls_checked[url]); } else { //Build AJAX request to check new file var req = new XMLHttpRequest(); req.open('HEAD', url, true); req.onreadystatechange = function() { if ( stateCheck == this.readyState ) { t.addUrl(url, this); proceed(this); } }; req.send(); } }, addUrl: function(url, res) { if (!(url in this.urls_checked)) this.urls_checked[url] = res; } }; })(jQuery);