# YUI Container: PhotoBox: Subclassing Panel

# Subclassing Panel to Create PhotoBox

Container classes can be subclassed to create all kinds of rich custom controls. The PhotoBox, which we will build in this tutorial, is an example of how Panel can be subclassed and styled to create a basic popup photo viewer with back and forward navigational controls.

The first step to subclassing the Panel is writing the constructor for the new subclass (PhotoBox, in this case) and specifying its inheritance from the Panel class using YAHOO.extend:

// BEGIN PHOTOBOX SUBCLASS // YAHOO.widget.PhotoBox = function(el, userConfig) { if (arguments.length > 0) { YAHOO.widget.PhotoBox.superclass.constructor.call(this, el, userConfig); } } // Inherit from YAHOO.widget.Panel YAHOO.extend(YAHOO.widget.PhotoBox, YAHOO.widget.Panel);

Next, we will define a few constants for use by the PhotoBox class: "CSS_PHOTOBOX", which defines the CSS class to apply to the Panel, and "NAV_FOOTER_HTML", the HTML that will be used for the footer navigation.

// Define the CSS class for the PhotoBox YAHOO.widget.PhotoBox.CSS_PHOTOBOX = "photobox"; // Define the HTML for the footer navigation YAHOO.widget.PhotoBox.NAV_FOOTER_HTML = "<a id=\"$back.id\" href=/"javascript:void(null)/" class=\"back\"><img src=/"../assets/img/ybox-back.gif/" /></a><a id=\"$next.id\" href=/"javascript:void(null)/" class=\"next\"><img src=/"../assets/img/ybox-next.gif/" /></a>";

Next, the initialization method for the PhotoBox is defined. The first step the initialization must perform is to call the superclass's init method so that the superclasses can initialize first. Then, we fire the beforeInitEvent and add the CSS class to the Panel, and apply the user's configuration properties. The rest of the method configures the footer navigation and fires the initEvent to indicate that the initialization is complete. The full init code is below:

// Initialize the PhotoBox by setting up the footer navigation YAHOO.widget.PhotoBox.prototype.init = function(el, userConfig) { YAHOO.widget.PhotoBox.superclass.init.call(this, el); this.beforeInitEvent.fire(YAHOO.widget.PhotoBox); YAHOO.util.Dom.addClass(this.innerElement, YAHOO.widget.PhotoBox.CSS_PHOTOBOX); if (userConfig) { this.cfg.applyConfig(userConfig, true); } this.setFooter(YAHOO.widget.PhotoBox.NAV_FOOTER_HTML.replace("$back.id",this.id+"_back").replace("$next.id",this.id+"_next")); this.renderEvent.subscribe(function() { var back = document.getElementById(this.id + "_back"); var next = document.getElementById(this.id + "_next"); YAHOO.util.Event.addListener(back, "mousedown", this.back, this, true); YAHOO.util.Event.addListener(next, "mousedown", this.next, this, true); }, this, true); this.initEvent.fire(YAHOO.widget.PhotoBox); };

The PhotoBox has a custom property called photos that accepts an array of object literals containing photo URLs and captions to use. The call to addProperty defines the handler that will be executed when the "photos" property is changed. In this case, that handler is the configPhotos method, which is discussed below. Also of note is the "suppressEvent" property, which prevents configPhotos from being fired until a value is set.

// Set up the PhotoBox's "photos" property for setting up the list of photos YAHOO.widget.PhotoBox.prototype.initDefaultConfig = function() { YAHOO.widget.PhotoBox.superclass.initDefaultConfig.call(this); this.cfg.addProperty("photos", { handler:this.configPhotos, suppressEvent:true }); };

The configPhotos handler, which is fired when the "photos" property is changed, iterates through the list of photo object literals and preloads the image for each photo. At the end of this process, the PhotoBox is automatically set to display the first image in the set, through a call to setImage(0).

// Handler executed when the "photos" property is modified YAHOO.widget.PhotoBox.prototype.configPhotos = function(type, args, obj) { var photos = args[0]; if (photos) { this.images = []; if (! (photos instanceof Array)) { photos = [photos]; } this.currentImage = 0; if (photos.length == 1) { this.footer.style.display = "none"; } for (var p=0;p<photos.length;p++) { var photo = photos[p]; var img = new Image(); img.src = photo.src; img.title = photo.caption; img.id = this.id + "_img"; img.width = 500 this.images[this.images.length] = img; } this.setImage(0); } };

The setImage function uses an index to grab an image from the "images" array, and places it into the body of the PhotoBox. In addition, it sets the photo's caption, and determines whether the next and back navigation buttons should be displayed, based on the current image index. For instance, image 0 would not display a "back" button, and the last image in the set would not display the "next" button.

// Sets the current image displayed in the PhotoBox to the corresponding image in the photo dataset, // and determines whether back and forward arrows should be diplsayed, based on the position in the dataset YAHOO.widget.PhotoBox.prototype.setImage = function(index) { var photos = this.cfg.getProperty("photos"); if (photos) { if (! (photos instanceof Array)) { photos = [photos]; } var back = document.getElementById(this.id + "_back"); var next = document.getElementById(this.id + "_next"); var img = document.getElementById(this.id + "_img"); var title = document.getElementById(this.id + "_title"); this.currentImage = index; var current = this.images[index]; var imgNode = document.createElement("IMG"); imgNode.setAttribute("src",current.src); imgNode.setAttribute("title",current.title); imgNode.setAttribute("width",500); imgNode.setAttribute("id",current.id); img.parentNode.replaceChild((this.browser == "safari"?imgNode:current), img); this.body.style.height = "auto"; title.innerHTML = current.title; if (this.currentImage == 0) { back.style.display = "none"; } else { back.style.display = "block"; } if (this.currentImage == (photos.length-1)) { next.style.display = "none"; } else { next.style.display = "block"; } } };

The next and back methods are used to navigate back and forth through the photos featured in the PhotoBox. They function by evaluating the index of the currently displayed image and adding or subtracting one to that number. Then, a call to setImage actually changes the photo.

// Navigates to the next image YAHOO.widget.PhotoBox.prototype.next = function() { if (typeof this.currentImage == 'undefined') { this.currentImage = 0; } this.setImage(this.currentImage+1); }; // Navigates to the previous image YAHOO.widget.PhotoBox.prototype.back = function() { if (typeof this.currentImage == 'undefined') { this.currentImage = 0; } this.setImage(this.currentImage-1); };

The final step in subclassing Panel is to override the "modal" property to allow for the modality mask that overlays the document to fade in and out in an animated fashion. The customizations employed here detect the opacity of the mask and allow for showMask and hideMask to fade the mask in until it reaches that initial opacity, and out until it's completely transparent.

// Overrides the handler for the "modal" property with special animation-related functionality YAHOO.widget.PhotoBox.prototype.configModal = function(type, args, obj) { var modal = args[0]; if (modal) { this.buildMask(); if (typeof this.maskOpacity == 'undefined') { this.mask.style.visibility = "hidden"; this.mask.style.display = "block"; this.maskOpacity = YAHOO.util.Dom.getStyle(this.mask,"opacity"); this.mask.style.display = "none"; this.mask.style.visibility = "visible"; } if (! YAHOO.util.Config.alreadySubscribed( this.beforeShowEvent, this.showMask, this ) ) { this.beforeShowEvent.subscribe(this.showMask, this, true); } if (! YAHOO.util.Config.alreadySubscribed( this.beforeHideEvent, this.hideMask, this) ) { this.beforeHideEvent.subscribe(this.hideMask, this, true); } if (! YAHOO.util.Config.alreadySubscribed( YAHOO.widget.Overlay.windowResizeEvent, this.sizeMask, this ) ) { YAHOO.widget.Overlay.windowResizeEvent.subscribe(this.sizeMask, this, true); } } else { this.beforeShowEvent.unsubscribe(this.showMask, this); this.beforeHideEvent.unsubscribe(this.hideMask, this); YAHOO.widget.Overlay.windowResizeEvent.unsubscribe(this.sizeMask); } }; // Overrides the showMask function to allow for fade-in animation YAHOO.widget.PhotoBox.prototype.showMask = function() { if (this.cfg.getProperty("modal") && this.mask) { YAHOO.util.Dom.addClass(document.body, "masked"); this.sizeMask(); var o = this.maskOpacity; if (! this.maskAnimIn) { this.maskAnimIn = new YAHOO.util.Anim(this.mask, {opacity: {to:o}}, 0.25) YAHOO.util.Dom.setStyle(this.mask, "opacity", 0); } if (! this.maskAnimOut) { this.maskAnimOut = new YAHOO.util.Anim(this.mask, {opacity: {to:0}}, 0.25) this.maskAnimOut.onComplete.subscribe(function() { this.mask.tabIndex = -1; this.mask.style.display = "none"; this.hideMaskEvent.fire(); YAHOO.util.Dom.removeClass(document.body, "masked"); }, this, true); } this.mask.style.display = "block"; this.maskAnimIn.animate(); this.mask.tabIndex = 0; this.showMaskEvent.fire(); } }; // Overrides the showMask function to allow for fade-out animation YAHOO.widget.PhotoBox.prototype.hideMask = function() { if (this.cfg.getProperty("modal") && this.mask) { this.maskAnimOut.animate(); } };

The last part of the tutorial involves instantiating the PhotoBox once the class has been defined. As you will see in the code below, many of the properties being used are familiar from the Panel API, with the addition of the new "photos" property that allows the photo set to be specified:

YAHOO.example.container.photobox = new YAHOO.widget.PhotoBox("photobox", { effect:{effect:YAHOO.widget.ContainerEffect.FADE,duration:0.45}, fixedcenter:true, constraintoviewport:true, underlay:"none", close:true, visible:false, draggable:false, modal:true, photos:[{src:"http://static.flickr.com/51/129586913_e78683c466.jpg",caption:"Linus"}, {src:"http://static.flickr.com/50/129590195_0642f2d96a.jpg",caption:"Linus 2"}, {src:"http://static.flickr.com/8/12669712_be928a0d97.jpg",caption:"Dobb's Ferry, NY"} ], width:"500px" } ); YAHOO.example.container.photobox.render(); YAHOO.example.container.photobox.show();
Continue to Functional Example >