Skip to content

Instantly share code, notes, and snippets.

@tivac
Created May 20, 2010 16:33
Show Gist options
  • Select an option

  • Save tivac/407770 to your computer and use it in GitHub Desktop.

Select an option

Save tivac/407770 to your computer and use it in GitHub Desktop.
/**
* Inspired by the original Lightbox, this is a port to YUI.
* See Lokesh Dhakar's original at http://www.huddletogether.com/projects/lightbox2/.
* Currently supports everything that module supports with plans to integrate
* additional functionality (i.e. non-images, slideshow mode, etc.) coming soon.
*
* @module gallery-lightbox
*/
var L = Y.Lang,
Node = Y.Node,
PX = "px",
CLICK = "click",
ANIM = "anim",
ACTIVE_IMAGE = "activeImage",
IMAGE_ARRAY = "imageArray",
OVERLAY_OPACITY = "overlayOpacity",
OVERLAY_DURATION = "overlayDuration",
LIGHTBOX = "lightbox",
OVERLAY = "overlay",
PREV_LINK = "prevLink",
NEXT_LINK = "nextLink",
HOVER_NAV = "hoverNav",
// global lightbox instance
lightboxInstance = null;
/**** BEGIN EXTENDING THE NODE CLASS ****/
// Add a few helper methods to the Node class that hopefully will be added
// in a future release of the Node class. They simplify showing/hiding a given node
// by manipulating its "display" style.
Y.mix(
Node.prototype, {
/**
* Display a node.
*
* @method show
* @chainable
*/
show: function () {
this.setStyle("display", "");
return this;
},
/**
* Hide a node.
*
* @method hide
* @chainable
*/
hide: function () {
this.setStyle("display", "none");
return this;
},
/**
* Check is a node is being shown. Specifically not called "visible"
* so as not to confuse it with the visibility property.
*
* @method displayed
* @return boolean
*/
displayed: function() {
return this.getStyle("display") != "none";
},
/**
* Toggle the display of an element.
*
* @method toggle
* @chainable
*/
toggle: function() {
this[this.displayed() ? "hide" : "show"]();
return this;
}
}
);
/**** END EXTENDING THE NODE CLASS ****/
/**
* The Lightbox class provides the functionality for displaying
* images in a panel above an overlay. It automatically binds to
* all anchor tags determined by the "selector" attribute. It supports
* grouping images together to produce a slideshow like effect.
*
* @class Lightbox
* @constructor
* @extends Base
* @uses Node
* @uses Anim
*
* @param config {Object} object with configuration property name/value pairs
*/
var LB = function (config) {
LB.superclass.constructor.apply(this, arguments);
};
/**
* The identity of the widget.
*
* @property Lightbox.NAME
* @type String
* @static
*/
LB.NAME = LIGHTBOX;
/**
* Static property used to define the default attribute configuration of
* the Widget.
*
* @property Lightbox.ATTRS
* @type Object
* @protected
* @static
*/
LB.ATTRS = {
/**
* The selector to determine which anchors should be bound to the Lightbox
* instance. If an anchor element is bound to Lightbox, it's content will
* be displayed in a modal panel rather than on a separate page.
*
* @attribute selector
* @type String
* @default "a[rel^=lightbox]"
*/
selector: {
value: "a[rel^=lightbox]",
validator: L.isString
},
/**
* The width of the border surrounding the displayed content. This is used during
* resize operations.
*
* @attribute borderWidth
* @type Number
* @default 10
*/
borderWidth: {
value: 10,
validator: L.isNumber
},
/**
* The amount of time (in seconds) for the overlay to take to appear when the
* Lightbox is displayed.
*
* @attribute overlayDuration
* @type Number
* @default 0.2
*/
overlayDuration: {
value: 0.2,
validator: L.isNumber
},
/**
* The opacity of the overlay element once it is displayed. This value is used
* during animation so that the overlay appears to be eased in.
*
* @attribute overlayOpacity
* @type Number
* @default 0.8
*/
overlayOpacity: {
value: 0.8,
validator: L.isNumber
},
/**
* The amount of time (in seconds) each reisze animation should take. This is used
* specifically during Lightbox height and width resize transformations.
*
* @attribute resizeDuration
* @type Number
* @default 0.5
*/
resizeDuration: {
value: 0.5,
validator: L.isNumber
},
/**
* Whether or the Lighbox module should use animation when displaying, changing images,
* and hiding. If set to false, the values of attributes that control animation settings
* are ignored.
*
* @attribute anim
* @type boolean
* @default !L.isUndefined(Y.Anim)
*/
anim: {
value: !L.isUndefined(Y.Anim),
validator: L.isBoolean
},
/**
* A managed array of images that Lightbox can currently cycle through. The size of this array
* is defined by the number of images in a particular image group. This array determines
* whether or not there are next and previous options. It's initialized when an image
* is clicked on.
*
* @attribute imageArray
* @type Array
*/
imageArray: {
validator: L.isArray
},
/**
* The index of the currently displayed image in the "imageArray."
*
* @attribute activeImage
* @type Number
*/
activeImage: {
validator: L.isNumber
},
/**
* Set of strings to be used when displaying content. These can be customized
* (i.e. for internationalization) if necessary.
*
* @attribute strings
* @type Object
*/
strings: {
value : {
labelImage: "Image",
labelOf: "of"
}
}
};
Y.extend(LB, Y.Base, {
/**
* Construction logic executed during Lightbox instantiation. This
* builds and inserts the markup necessary for the Lightbox to function
* as well as binds all of the elements to the necessary events to make
* the Lightbox functional.
*
* @method initializer
* @param config (Object) set of configuration name/value pairs
* @protected
*/
initializer: function (config) {
// Code inserts html at the bottom of the page that looks similar to this:
//
// <div id="overlay"></div>
// <div id="lightbox">
// <div id="outerImageContainer">
// <div id="imageContainer">
// <img id="lightboxImage">
// <div style="" id="hoverNav">
// <a href="#" id="prevLink"></a>
// <a href="#" id="nextLink"></a>
// </div>
// <div id="loading"></div>
// </div>
// </div>
// <div id="imageDataContainer">
// <div id="imageData">
// <div id="imageDetails">
// <span id="caption"></span>
// <span id="originalLink">
// <a href="#">Original Link</a>
// </span>
// <span id="numberDisplay"></span>
// </div>
// <div id="bottomNav">
// <a href="#" id="bottomNavClose"></a>
// </div>
// </div>
// </div>
// </div>
var objBody = Y.one(document.body),
create = Node.create;
objBody.append(create('<div id="overlay"></div>'));
objBody.append(create('<div id="lightbox"></div>')
.append(create('<div id="outerImageContainer"></div>')
.append(create('<div id="imageContainer"></div>')
.append(create('<img id="lightboxImage" />'))
.append(create('<div id="hoverNav"></div>')
.append(create('<a id="prevLink" href="#"></a>'))
.append(create('<a id="nextLink" href="#"></a>'))
)
.append(create('<div id="loading"></div>'))
)
)
.append(create('<div id="imageDataContainer"></div>')
.append(create('<div id="imageData"></div>')
.append(create('<div id="imageDetails"></div>')
.append(create('<span id="caption"></span>'))
.append(create('<span id="originalLink"></span>')
.append(create('<a href="#" target="_blank">Original Image</a>'))
)
.append(create('<span id="numberDisplay"></span>'))
)
.append(create('<div id="bottomNav"></div>')
.append(create('<a id="bottomNavClose" href="#"></a>'))
)
)
)
);
this._bindStartListener();
Y.one("#overlay").hide().on(CLICK, function () { this.end(); }, this);
Y.one("#lightbox").hide().on(CLICK, function (evt) {
if (evt.currentTarget.get("id") === LIGHTBOX) {
this.end();
}
}, this);
var size = (this.get(ANIM) ? 250 : 1) + PX;
Y.one("#outerImageContainer").setStyles({ width: size, height: size });
Y.one("#prevLink").on(CLICK, function (evt) { evt.halt(); this._changeImage(this.get(ACTIVE_IMAGE) - 1); }, this);
Y.one("#nextLink").on(CLICK, function (evt) { evt.halt(); this._changeImage(this.get(ACTIVE_IMAGE) + 1); }, this);
Y.one("#bottomNavClose").on(CLICK, function (evt) { evt.halt(); this.end(); }, this);
L.later(0, this, function () {
var ids = "overlay lightbox outerImageContainer imageContainer lightboxImage hoverNav prevLink nextLink loading " +
"imageDataContainer imageData imageDetails caption numberDisplay bottomNav bottomNavClose originalLink";
Y.Array.each(ids.split(" "), function (element, index, array) {
this.addAttr(element, { value: Y.one("#" + element) });
}, this);
});
},
/**
* Display overlay and Lightbox. If image is part of a set, it
* adds those images to an array so that a user can navigate between them.
*
* @method start
* @param selectedLink { Y.Node } the node whose content should be displayed
* @protected
*/
start: function (selectedLink) {
Y.all("select, object, embed").each(function() {
this.setStyle("visibility", "hidden");
});
// Stretch overlap to fill page and fade in
var overlay = this.get(OVERLAY).setStyles({ height: Y.DOM.docHeight() + PX, width: Y.DOM.docWidth() + PX }).show();
if (this.get(ANIM)) {
var anim = new Y.Anim({
node: overlay,
from: { opacity: 0 },
to: { opacity: this.get(OVERLAY_OPACITY) },
duration: this.get(OVERLAY_DURATION)
});
anim.run();
} else {
overlay.setStyle("opacity", this.get(OVERLAY_OPACITY));
}
var imageArray = [],
imageNum = 0,
gallery;
if (selectedLink.get("rel") === LIGHTBOX) {
// If image is NOT part of a set, add single image to imageArray
imageArray.push([selectedLink.get("href"), selectedLink.get("title")]);
} else {
//work around for http://yuilibrary.com/projects/yui3/ticket/2528799
gallery = selectedLink.get("rel").match(/\[(.*)\]/)[1];
// If image is part of a set...
//Y.all(selectedLink.get("tagName") + '[href][rel="' + selectedLink.get("rel") + '"]').each(function () {
Y.all(selectedLink.get("tagName") + "[href][rel*=" + gallery + "]").each(function () {
imageArray.push([this.get("href"), this.get("title")]);
});
while (imageArray[imageNum][0] !== selectedLink.get("href")) { imageNum++; }
}
this.set(IMAGE_ARRAY, imageArray);
var lightboxTop = Y.DOM.docScrollY() + (Y.DOM.winHeight() / 10),
lightboxLeft = Y.DOM.docScrollX();
this.get(LIGHTBOX).setStyles({ display: "", top: lightboxTop + PX, left: lightboxLeft + PX });
this._changeImage(imageNum);
},
/**
* Hide the overlay and Lightbox and unbind any event listeners.
*
* @method end
* @protected
*/
end: function () {
this._disableKeyboardNav();
this.get(LIGHTBOX).hide();
var overlay = this.get(OVERLAY);
if (this.get(ANIM)) {
var anim = new Y.Anim({
node: overlay,
from: { opacity: this.get(OVERLAY_OPACITY) },
to: { opacity: 0 },
duration: this.get(OVERLAY_DURATION)
});
anim.on("end", function () { overlay.hide(); });
anim.run();
} else {
overlay.setStyles({ opacity: 0 }).hide();
}
Y.all("select, object, embed").each(function() {
this.setStyle("visibility", "visible");
});
},
/**
* Helper method responsible for binding listener to the page to process
* lightbox anchors and images.
*
* @method _bindStartListener
* @private
*/
_bindStartListener: function () {
Y.delegate(CLICK, Y.bind(function (evt) {
evt.halt();
this.start(evt.currentTarget);
}, this), document.body, this.get("selector"));
},
/**
* Display the selected index by first showing a loading screen, preloading it
* and displaying it once it has been loaded.
*
* @method _changeImage
* @param imageNum { Number } the index of the image to be displayed
* @private
*/
_changeImage: function (imageNum) {
this.set(ACTIVE_IMAGE, imageNum);
// Hide elements during transition
if (this.get(ANIM)) {
this.get("loading").show();
}
this.get("lightboxImage").hide();
this.get(HOVER_NAV).hide();
this.get(PREV_LINK).hide();
this.get(NEXT_LINK).hide();
// Hack: Opera9 doesn't support something in scriptaculous opacity and appear fx
// TODO: Do I need this since we are using YUI? Is this a scriptaculous/Opera
// bug, or just Opera bug?
this.get("imageDataContainer").setStyle("opacity", 0.0001);
this.get("numberDisplay").hide();
var imagePreloader = new Image();
// Once image is preloaded, resize image container
imagePreloader.onload = Y.bind(function () {
var img = this.get("lightboxImage"),
winHeight = img.get('winHeight'),
winWidth = img.get('winWidth'),
width = (imagePreloader.width < (winWidth - 100)) ? imagePreloader.width : false,
height = (imagePreloader.height < (winHeight - 100)) ? imagePreloader.height : false,
scale;
//if img was too big in either direction
if(!width || !height) {
scale = Math.min((winWidth / imagePreloader.width), (winHeight / imagePreloader.height));
width = Math.floor(scale * imagePreloader.width);
height = Math.floor(scale * imagePreloader.height);
}
img.set("src", this.get(IMAGE_ARRAY)[imageNum][0]);
img.set("width", width);
img.set("height", height);
this._resizeImageContainer(width, height);
}, this);
imagePreloader.src = this.get(IMAGE_ARRAY)[imageNum][0];
},
/**
* Resize the image container so it is large enough to display the entire image.
* Once this is complete it will delegate to another method to actually display the image.
*
* @method _resizeImageContainer
* @param imgWidth { Number } image width
* @param imgWidth { Number } image height
* @private
*/
_resizeImageContainer: function (imgWidth, imgHeight) {
// Get current width and height
var outerImageContainer = this.get("outerImageContainer"),
widthCurrent = outerImageContainer.get("offsetWidth"),
heightCurrent = outerImageContainer.get("offsetHeight"),
// Get new width and height
widthNew = imgWidth + this.get("borderWidth") * 2,
heightNew = imgHeight + this.get("borderWidth") * 2,
// calculate size difference between new and old image
wDiff = widthCurrent - widthNew,
hDiff = heightCurrent - heightNew,
afterResize = Y.bind(function () {
this.get(PREV_LINK).setStyles({ height: imgHeight + PX });
this.get(NEXT_LINK).setStyles({ height: imgHeight + PX });
this.get("imageDataContainer").setStyles({ width: widthNew + PX });
this._showImage();
}, this);
if (wDiff !== 0 || hDiff !== 0) {
if (this.get(ANIM)) {
var resizeDuration = this.get("resizeDuration"),
anim = new Y.Anim({
node: outerImageContainer,
from: { width: widthCurrent + PX },
to: { width: widthNew + PX },
duration: resizeDuration
}),
onEnd = function () {
anim.getEvent("end").detach(onEnd);
this.setAttrs({
from: { height: heightCurrent + PX },
to: { height: heightNew + PX },
duration: resizeDuration
});
this.on("end", afterResize);
this.run();
};
anim.on("end", onEnd);
anim.run();
} else {
outerImageContainer.setStyles({ width: widthNew + PX, height: heightNew + PX});
L.later(0, this, afterResize);
}
} else {
// If new and old image are the same size, and no scaling is necessary,
// do a quick pause to prevent image flicker.
L.later(100, this, afterResize);
}
},
/**
* Display the currently loaded image and then try to preload any neighboring images.
*
* @method _showImage
* @private
*/
_showImage: function () {
this.get("loading").hide();
var lightBoxImage = this.get("lightboxImage");
if (this.get(ANIM)) {
var startOpacity = lightBoxImage.getStyle("display") === "none" ? 0 : lightBoxImage.getStyle("opacity") || 0,
anim = new Y.Anim({
node: lightBoxImage,
from: { opacity: startOpacity },
to: { opacity: 1 }
});
anim.on("end", this._updateDetails, this);
lightBoxImage.setStyle("opacity", startOpacity).show();
anim.run();
} else {
lightBoxImage.setStyle("opacity", 1).show();
this._updateDetails();
}
this._preloadNeighborImages();
},
/**
* Use the title of the image as a caption and display information
* about the current image and it's location in an image set (if applicable).
*
* @method _updateDetails
* @private
*/
_updateDetails: function () {
var imageArray = this.get(IMAGE_ARRAY),
activeImage = this.get(ACTIVE_IMAGE),
caption = imageArray[activeImage][1],
resized = /-\d+x\d+/, a;
// If caption is not null
if (caption !== "") {
this.get("caption").setContent(caption).show();
}
// If image is part of a set display "Image x of x"
if (imageArray.length > 1) {
this.get("numberDisplay").setContent(this.get("strings.labelImage") + " " + (activeImage + 1) + " " + this.get("strings.labelOf") + " " + imageArray.length).show();
}
if(resized.test(imageArray[activeImage][0])) {
this.get("originalLink").get('firstChild').setAttribute("href", imageArray[activeImage][0].replace(resized, ""))
.removeClass("hide");
} else {
this.get("originalLink").get('firstChild').addClass("hide");
}
var imageDataContainer = this.get("imageDataContainer");
if (this.get(ANIM)) {
var startOpacity = imageDataContainer.getStyle("display") === "none" ? 0 : imageDataContainer.getStyle("opacity") || 0,
anim = new Y.Anim({
node: imageDataContainer,
from: { opacity: startOpacity },
to: { opacity: 1 },
duration: this.get("resizeDuration")
});
anim.on("end", function () {
// Update overlay size and update nav
this.get(OVERLAY).setStyle("height", Y.DOM.docHeight() + PX);
this._updateNav();
}, this);
imageDataContainer.setStyle("opacity", startOpacity).show();
anim.run();
} else {
imageDataContainer.setStyle("opacity", 1).show();
this.get(OVERLAY).setStyle("height", Y.DOM.docHeight() + PX);
this._updateNav();
}
},
/**
* Update the navigation elements to display forward and/or backward
* links if they're appropriate.
*
* @method _updateNav
* @private
*/
_updateNav: function () {
var activeImage = this.get(ACTIVE_IMAGE);
this.get(HOVER_NAV).show();
// If not first image in set, display previous image button
if (activeImage > 0) {
this.get(PREV_LINK).show();
}
// If not first image in set, display previous image button
if (activeImage < (this.get(IMAGE_ARRAY).length - 1)) {
this.get(NEXT_LINK).show();
}
this._enableKeyboardNav();
},
/**
* Enable keyboard shortcuts for closing Lightbox or switching images.
*
* @method _enableKeyboardNav
* @private
*/
_enableKeyboardNav: function () {
Y.get(document.body).on("keydown", this._keyboardAction, this);
},
/**
* Disable keyboard shortcuts for closing Lightbox or switching images.
*
* @method _disableKeyboardNav
* @private
*/
_disableKeyboardNav: function () {
Y.get(document.body).unsubscribe("keydown", this._keyboardAction);
},
/**
* Handle key strokes to allow for users to close Lightbox or switch images.
*
* @method _keyboardAction
* @private
*/
_keyboardAction: function (evt) {
var keyCode = evt.keyCode,
escapeKey = 27,
key = String.fromCharCode(keyCode).toLowerCase();
if (key.match(/x|o|c/) || (keyCode === escapeKey)) { // close lightbox
this.end();
} else if ((key === 'p') || (keyCode === 37)) { // Display the previous image
if (this.get(ACTIVE_IMAGE) !== 0) {
this._disableKeyboardNav();
this._changeImage(this.get(ACTIVE_IMAGE) - 1);
}
} else if ((key === 'n') || (keyCode === 39)) { // Display the next image
if (this.get(ACTIVE_IMAGE) !== (this.get(IMAGE_ARRAY).length - 1)) {
this._disableKeyboardNav();
this._changeImage(this.get(ACTIVE_IMAGE) + 1);
}
}
},
/**
* Preload images that are adjacent to the current image, if they exist,
* to reduce waiting time.
*
* @method _preloadNeighborImages
* @private
*/
_preloadNeighborImages: function () {
var activeImage = this.get(ACTIVE_IMAGE),
imageArray = this.get(IMAGE_ARRAY),
preloadNextImage, preloadPrevImage;
if (imageArray.length > activeImage + 1) {
preloadNextImage = new Image();
preloadNextImage.src = imageArray[activeImage + 1][0];
}
if (activeImage > 0) {
preloadPrevImage = new Image();
preloadPrevImage.src = imageArray[activeImage - 1][0];
}
}
});
Y.Lightbox = {
/**
* This method returns the single, global LightBox instance. Upon creation,
* the Lightbox instance attaches itself to the page and is ready to be used.
*
* @method init
* @return { Lightbox } global instance
* @static
*/
init: function(config) {
if (lightboxInstance === null) {
lightboxInstance = new LB(config);
}
return lightboxInstance;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment