largerBox v3.02 beta 1

- Last modified 24/11/2009

A mootools image lightbox inspired by the apple trailers image viewer



This is the 3rd installment of the image viewer I have come to call 'largerBox' and it now incorporates support for next/previous in set, as well as in-built CSS and image data (through base-64).

Currenty, there is no graceful degradation for when an old browser does not support base64 decoding image data

Report problems and suggestions to christoff@gmail.com or twitter me @D_mitar
// instigating the class over all 'a' link elements with class 'fullImage'
// and custom mouseover events
window.addEvents({
    domready: function() {
        // just a bit here for the overlay effect
        var XL = new Asset.image("http://fragged.org/images/externalLink.gif");

        var large = new largerBox({
            links: $$("a.fullImage"), // all links with class fullImage
            imageEvents: {
                mouseenter: function(el) {
                    var coords = el.getFirst().getCoordinates();
                    // do the overlay popup icon
                    XL.clone().setStyles({
                        position: "absolute",
                        left: coords.right - 13,
                        top: coords.bottom - 14,
                        zIndex: 1000000,
                        background: "#fff",
                        border: "1px solid #000",
                        padding: 1
                    }).addClass("EL").setOpacity(0).inject(large.container).fade(1);
                },
                mouseleave: function() {
                    $$("img.EL").dispose();
                }
            },
            modal: {
                events: {
                    click: function() {
                        large.closeAuto();
                    }
                }
            }
        });
    }
}); // end domready
var StyleWriter = new Class({
    // css classes on the fly, based on by Aaaron Newton's version
    createStyle: function(css, id) {
        try {
            if (document.id(id) && id) return;

            var style = new Element('style', {id: id||'',type:'text/css'}).inject($$('head')[0]);
            if (Browser.Engine.trident)
                style.styleSheet.cssText = css;
            else
                style.set('text', css);

        } catch(e) {C.log("failed:", e);}
    }
});

var largerBox = new Class({
    author: "Dimitar Christoff",
    homepage: "http://fragged.org/",
    version: "3.01b",
    changed: "18/11/2009 13:33:45",

    Implements: [Options,Events,StyleWriter],
    options: {
        links: [],                          // pass on array of anchors that contain links to images.
        imageStyles: {
            border: "1px solid #000",       // border style around image,
            background: "#fff",             // useful to have one for underneath a png or gif with transparency
            zIndex: 1000000000,             // default z-index for the enlarged images
            position: "absolute"            // don't change this.
        },
        imageClass: "largerBox",
        imageBorder: 3,
        imageEvents: {
            mouseenter: $empty(),           // function on mouseover.
            mouseleave: $empty()
        },
        shadows: {                          // enlarged image shadow filters, tweak as needed
            enabled: true,
            gecko: {                        // Firefox
                "-moz-box-shadow": "1px 1px 2px #000"
            },
            webkit: {                       // Chrome and Safari
                "-webkit-box-shadow": "1px 1px 2px #000"
            },
            trident: {                      // IE
                "filter": "progid:DXImageTransform.Microsoft.Shadow(color=#000000,direction=135,strength=2)"
            },
            presto: {                       // Opera 10+
                "box-shadow": "1px 1px 2px #000"
            }
        },
        modal: {                            // options to do with modal div.
            enabled: true,
            background: "#fff",             // default colour for modal tint
            zIndex: 1000000000 - 1,
            opacity: .7,
            events: $empty()                // for example, onclick you may want to close all.
        },
        controls: {
            enabled: true,
            useBase64: true,                // look at embeddedStyles at bottom.
            opacity: .6
        },
        closeButton: "/images/button-close.png"
    },
    cache: [],                              // store all loaded images.
    initialize: function(options) {
        this.setOptions(options);
        this.container = $(this.options.container) || $(document.body); // could change this, i suppose.
        this.elements = this.options.links.length ? this.options.links : [];

        // test for base64 support and halt initialize until it's done
        this.testImage = new Element("img", {
            src: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",
            events: {
                load: function() {
                    this.reInitialize();
                }.bind(this),
                error: function() {
                    this.reInitialize();
                }.bind(this)
            }
        }).inject(this.container);
    },
    reInitialize: function() {
        // initialize, continued after we know of base64 support
        $extend(Browser.Features, {
            base64: this.testImage.width == 1 && this.testImage.height == 1
        });

        if (this.testImage)
            this.testImage.destroy();

        // can we use base64 at all?
        if (this.options.controls.useBase64)
            this.options.controls.useBase64 = (this.options.controls.useBase64 && Browser.Features.base64) ? true : false;

        // kick start it all!
        this.attachAllEvents();

        // add overflow class to css
        this.createStyle(this.options.controls.useBase64 ? this.embeddedStyles[1].join("\n") : this.embeddedStyles[0].join("\n") , "largerBox");
    },
    attachAllEvents: function() {
        // attach all events to this.elements.
        // call this if you have loaded new data into the .elements member to re-init
        this.elements.each(function(el) {
            this.attachEvents(el);
        }.bind(this));
    },
    attachEvents: function(el) {
        // attach events to a single image element.
        el.addEvents({
            mouseenter: function(e) {
                // if we want some hover effect like mini-zoom, shake, etc.
                this.options.imageEvents.mouseenter(el);
            }.bind(this),
            mouseleave: function(e) {
                this.options.imageEvents.mouseleave(el);
            }.bind(this),
            click: function(e) {
                // stop click on href.
                if ($type(e) !== false)
                    new Event(e).stop();

                this.animating = false;
                this.openImage(el);
                this.titleText = el.get("title") || "";
            }.bind(this)
        });
    },
    openImage: function(el) {
        // generic wrapper that kick-starts the opening of an image
        if (this.modal)
            return false;

        if (this.options.modal.enabled)
            this.toggleModal();

        var url = el.get("href"), basename = url.replace(/^.*[\/\\]/g, '');

        // using caching system or import fresh
        if ($type(this.cache[basename]) === "element") {
            this.cache[basename].image.setStyles(this.cache[basename].size);
            this.animateBox(this.cache[basename], el);
        }
        else {
            this.loadImage(url, el); // this will load and then run .animateBox();
        }
    },
    loadImage: function(url, el) {
        new Asset.image(url, {
            onload: function(im) {
                var coords = el.getFirst().getCoordinates(), basename = url.replace(/^.*[\/\\]/g, '');

                this.cache[basename] = {
                    size: {
                        width: im.width,
                        height: im.height
                    },
                    image: im.set({
                        styles: $merge(this.options.imageStyles, coords),
                        events: {
                            click: function() {
                                // this.closeBox(image, coords);

                            }.bind(this)
                        }
                    })
                };

                this.animateBox(this.cache[basename], el);
            }.bind(this)
        });
    },
    animateBox: function(imageObj, link) {
        // gorw the box large!
        if (this.animating)
            return;

        // always works from a cached object, need to preserve .size of original
        // because .width of a static image is set to 0 in IE.
        this.larger = imageObj.image.clone().inject(this.container);
        this.larger.removeEvents().cloneEvents(imageObj.image);

        // find appropriate screen position
        var centerCoords = this.getCenter(imageObj.size.width, imageObj.size.height);

        // save the source coordinates for the close animation
        this.larger.store("coords", link.getFirst().getCoordinates());

        // create an FX instance and store it into image
        this.larger.store("imageEffects", new Fx.Morph(this.larger, {
            transition: Fx.Transitions.Sine.easeOut,
            duration: 300
        }));

        // next/previous, record index of image in array
        this.currentImage = this.elements.indexOf(link);

        // start animation variable
        this.animating = true;

        // run the morph
        this.larger.retrieve("imageEffects").start({
            width: centerCoords.width,
            height: centerCoords.height,
            left: centerCoords.x,
            top: centerCoords.y,
            borderWidth: this.options.imageBorder
        }).chain(function() {
            this.larger.addClass(this.options.imageClass);
            this.animating = false;
            this.addControls(centerCoords);
            this.onComplete();
        }.bind(this));
    },
    onComplete: function() {
        this.fireEvent("complete");

        if ($type(this.options.shadows[Browser.Engine.name]) === "object" && this.options.shadows.enabled === true)
            this.larger.setStyles(this.options.shadows[Browser.Engine.name]);

    },
    addControls: function(coords) {
        // draws the close button, title, next and previous.
        if (!this.animating)
        this.controls = new Element("div", {
            id: "boxClose",
            "class": "cur",
            styles: {
                top: coords.y - 12,
                left: coords.x + coords.width - 12,
                zIndex: this.options.imageStyles.zIndex+2
            },
            title: "click to close",
            events: {
                click: function() {
                    this.closeAuto();
                }.bind(this)
            }
        }).inject(this.container);

        // title and left/ right
        if (this.options.controls.enabled) {
            if (this.titleText.length || this.elements.length) {
                this.titleOverlay = new Element("div", {
                    styles: {
                        width: coords.width - 4,
                        height: 30,
                        padding: 2,
                        background: "#000",
                        position: "absolute",
                        top: coords.y + coords.height - 31,
                        left: coords.x + 3,
                        color: "white",
                        opacity: 0,
                        zIndex: this.options.imageStyles.zIndex+1
                    },
                    html: "
"+this.titleText+"
", events: { mouseleave: function() { this.titleOverlay.fade(.000000009); }.bind(this), mouseenter: function() { this.titleOverlay.fade(this.options.controls.opacity); }.bind(this) } }).inject(this.container).fade(0, this.options.controls.opacity); if (this.elements.length) { this.goLeft = new Element("div", { id: "arrowLeft", "class": "cur", events: { click: function() { this.getPrevious(); }.bind(this) } }).inject(this.titleOverlay, "top"); // if (Browser.Engine.trident) // alert(this.goLeft.getStyle("background-image")); this.goRight = new Element("div", { id: "arrowRight", "class": "cur", events: { click: function() { this.getNext(); }.bind(this) } }).inject(this.titleOverlay); } } } }, closeBox: function() { // shrink image back to coords stored previously if (this.animating || !this.larger) return false; this.animating = true; this.modal.removeEvents(); this.larger.retrieve("imageEffects").setOptions({ duration: 130 }).start($merge({ borderWidth: 0, opacity: .4 }, this.larger.retrieve("coords"))).chain(function() { this.animating = false; }.bind(this)); // flash effect this.larger.fade(0); (function() { if (this.options.modal.enabled) this.toggleModal(); if (this.larger) { this.larger.destroy(); delete this.larger; } this.fireEvent("close"); }).delay(500, this); }, closeAuto: function() { // auto clean up controls and opened images if (this.animating) return false; if (this.controls) this.controls.destroy(); if (this.options.controls.enabled && this.titleOverlay) this.titleOverlay.destroy(); if (this.larger) this.closeBox(this.larger); }, getNext: function() { // find next image if any var next = this.currentImage == this.elements.length - 1 ? 0 : this.currentImage + 1; var closeFunc = function() { this.removeEvent("close", closeFunc); (function() { this.elements[next].fireEvent("click"); }).delay(200, this); }.bind(this); if (this.larger) { this.addEvents({ close: closeFunc }); this.closeAuto(); } }, getPrevious: function() { // find previous image var previous = this.currentImage == 0 ? this.elements.length - 1 : this.currentImage - 1; var closeFunc = function() { this.removeEvent("close", closeFunc); (function() { this.elements[previous].fireEvent("click"); }).delay(200, this); }.bind(this); if (this.larger) { this.addEvents({ close: closeFunc }); this.closeAuto(); } }, getCenter: function(width, height) { // returns a coordinates JSON for positioning of a box in the centre of the browser var winSize = window.getSize(), winScroll = window.getScroll(), windowHeight = window.getScrollHeight()-1, coords = { y: Math.round(winScroll.y + (winSize.y / 2) - (height / 2)), x: Math.round(winSize.x / 2 - width / 2), width: width, height: height }; if (coords.y + height + 20 > windowHeight) coords.y = windowHeight - 20 - height; if (coords.y <= 0) coords.y = 20; return coords; }, toggleModal: function() { // modal view for the whole screen if (this.modal) { this.container.removeClass("overflowHidden"); if (Browser.Engine.trident) $(document.html).removeClass("overflowHidden"); this.modal.destroy(); delete this.modal; return false; } this.container.addClass("overflowHidden"); if (Browser.Engine.trident) $(document.html).addClass("overflowHidden"); this.modal = new Element("div", { styles: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", background: this.options.modal.background, zIndex: this.options.modal.zIndex }, opacity: this.options.modal.opacity, events: this.options.modal.events }).inject(this.container); return this.modal; // can chain into it. }, embeddedStyles: [ [ // first set is w/o base64 support, css with real files. ".overflowHidden { overflow: hidden; }", "#arrowLeft {float:left;width:34px;height:100%;background:url(/images/arrow-left.gif) no-repeat center center;}", "#arrowRight {float:right;width:34px;height:100%;background:url(/images/arrow-right.gif) no-repeat center center;}", "#boxClose {position:absolute;width:32px;height:32px;background:url(/images/button-close.png) no-repeat center center}" ], [ // css for base64 support ".overflowHidden { overflow: hidden; }", "#arrowLeft {float:left;width:34px;height:100%;background:url(data:image/gif;base64,R0lGODlhIwAhALMAAAAAADQ0NENDQ25ubouLi6ioqLa2ttPT0/Dw8P///wAAAAAAAAAAAAAAAAAAAAAAACwAAAAAIwAhAAAIpQABCBxIsCCAAAYTKlw4cECCAQwjMnSY4KHEiwQpVrSIUaLGjRw7Kvy4EYEAkSNBljyJ0iDJiiZbulQJk6XMhjQTxLyJk+ZOngBe6rTJU+jPojmTKqXZc6nTpAKFPp0qsMDUqyoHWsWKleBWrk8LfgV5AKjYnGXNakWrdi3NtG3HbjTQtmrOAnUByK2It+7eBH3j5iSQVy5cv3PzegWsuCDExmYDAgA7) no-repeat center center;}", "#arrowRight {float:right;width:34px;height:100%;background:url(data:image/gif;base64,R0lGODlhIwAhALMAAAAAADQ0NENDQ25ubouLi6ioqLa2ttPT0/Dw8P///wAAAAAAAAAAAAAAAAAAAAAAACwAAAAAIwAhAAAIpwABCBxIsODAAAYTKlw4cECCAQwjMnSY4KHEiwQpVrSIUaLGjRw7LhSAAORGiCJHljQZMqVBkixbuiQIkyXKmQVrmryJc6BOkDx7Avh5UijNlTsLxlzK1CbBplCb3oxKNSbKqlg3FhCYFetWrl2jfjV6IOZYoWVZnu2Z1uRanG1Bvp1pwKxRgQXs3s2r9i4Avm79EtDrN26CuULrVkRsNC/ju0H99gwIADs%3D) no-repeat center center;}", "#boxClose {position:absolute;width:32px;height:32px;background:url(data:image/png;base64,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) no-repeat center center}" ] ] }); // end largerBox var C = { // console wrapper debug: true, // global debug on|off quietDismiss: true, // may want to just drop, or alert instead log: function() { if (!C.debug) return false; if (typeof console == 'object' && typeof console.log != "undefined") try { console.log.apply(this, arguments); // safari's console.log can't accept scope... } catch(e) { // so we loop instead. for (var i = 0, l = arguments.length; i < l; i++) console.log(arguments[i]); } else if (!C.quietDismiss) { var result = ""; for (var i = 0, l = arguments.length; i < l; i++) result += arguments[i] + " ("+typeof arguments[i]+") "; alert(result); } } }; // end console wrapper.