dojo.provide("dojox.image.ThumbnailPicker"); dojo.experimental("dojox.image.ThumbnailPicker"); // // dojox.image.ThumbnailPicker courtesy Shane O Sullivan, licensed under a Dojo CLA // @author Copyright 2007 Shane O Sullivan (shaneosullivan1@gmail.com) // // For a sample usage, see http://www.skynet.ie/~sos/photos.php // // document topics. dojo.require("dojox.fx.scroll"); // is optional, but don't want to dojo[require] it dojo.require("dojo.fx.easing"); dojo.require("dojo.fx"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.declare("dojox.image.ThumbnailPicker", [dijit._Widget, dijit._Templated], { // summary: A scrolling Thumbnail Picker widget // // imageStore: Object // A data store that implements the dojo.data Read API. imageStore: null, // request: Object // A dojo.data Read API Request object. request: null, // size: Number // Width or height in pixels, depending if horizontal or vertical. size: 500, //FIXME: use CSS? // thumbHeight: Number // Default height of a thumbnail image thumbHeight: 75, // FIXME: use CSS? // thumbWidth: Number // Default width of an image thumbWidth: 100, // FIXME: use CSS? // useLoadNotifier: Boolean // Setting useLoadNotifier to true makes a colored DIV appear under each // thumbnail image, which is used to display the loading status of each // image in the data store. useLoadNotifier: false, // useHyperlink: boolean // Setting useHyperlink to true causes a click on a thumbnail to open a link. useHyperlink: false, // hyperlinkTarget: String // If hyperlinkTarget is set to "new", clicking on a thumb will open a new window // If it is set to anything else, clicking a thumbnail will open the url in the // current window. hyperlinkTarget: "new", // isClickable: Boolean // When set to true, the cursor over a thumbnail changes. isClickable: true, // isScrollable: Boolean // When true, uses smoothScroll to move between pages isScrollable: true, // isHorizontal: Boolean // If true, the thumbnails are displayed horizontally. Otherwise they are displayed // vertically isHorizontal: true, //autoLoad: Boolean autoLoad: true, // linkAttr: String // The attribute name for accessing the url from the data store linkAttr: "link", // imageThumbAttr: String // The attribute name for accessing the thumbnail image url from the data store imageThumbAttr: "imageUrlThumb", // imageLargeAttr: String // The attribute name for accessing the large image url from the data store imageLargeAttr: "imageUrl", // pageSize: Number // The number of images to request each time. pageSize: 20, // titleAttr: String // The attribute name for accessing the title from the data store titleAttr: "title", templatePath: dojo.moduleUrl("dojox.image", "resources/ThumbnailPicker.html"), // thumbs: Array // Stores the image nodes for the thumbnails. _thumbs: [], // _thumbIndex: Number // The index of the first thumbnail shown _thumbIndex: 0, // _maxPhotos: Number // The total number of photos in the image store _maxPhotos: 0, // _loadedImages: Object // Stores the indices of images that have been marked as loaded using the // markImageLoaded function. _loadedImages: {}, postCreate: function(){ // summary: Initializes styles and listeners this.widgetid = this.id; this.inherited(arguments); this.pageSize = Number(this.pageSize); this._scrollerSize = this.size - (51 * 2); var sizeProp = this._sizeProperty = this.isHorizontal ? "width" : "height"; // FIXME: do this via css? calculate the correct width for the widget dojo.style(this.outerNode, "textAlign","center"); dojo.style(this.outerNode, sizeProp, this.size+"px"); dojo.style(this.thumbScroller, sizeProp, this._scrollerSize + "px"); //If useHyperlink is true, then listen for a click on a thumbnail, and //open the link if(this.useHyperlink){ dojo.subscribe(this.getClickTopicName(), this, function(packet){ var index = packet.index; var url = this.imageStore.getValue(packet.data,this.linkAttr); //If the data item doesn't contain a URL, do nothing if(!url){return;} if(this.hyperlinkTarget == "new"){ window.open(url); }else{ window.location = url; } }); } if(this.isClickable){ dojo.addClass(this.thumbsNode, "thumbClickable"); } this._totalSize = 0; this.init(); }, init: function(){ // summary: Creates DOM nodes for thumbnail images and initializes their listeners if(this.isInitialized) {return false;} var classExt = this.isHorizontal ? "Horiz" : "Vert"; // FIXME: can we setup a listener around the whole element and determine based on e.target? dojo.addClass(this.navPrev, "prev" + classExt); dojo.addClass(this.navNext, "next" + classExt); dojo.addClass(this.thumbsNode, "thumb"+classExt); dojo.addClass(this.outerNode, "thumb"+classExt); this.navNextImg.setAttribute("src", this._blankGif); this.navPrevImg.setAttribute("src", this._blankGif); this.connect(this.navPrev, "onclick", "_prev"); this.connect(this.navNext, "onclick", "_next"); this.isInitialized = true; if(this.isHorizontal){ this._offsetAttr = "offsetLeft"; this._sizeAttr = "offsetWidth"; this._scrollAttr = "scrollLeft"; }else{ this._offsetAttr = "offsetTop"; this._sizeAttr = "offsetHeight"; this._scrollAttr = "scrollTop"; } this._updateNavControls(); if(this.imageStore && this.request){this._loadNextPage();} return true; }, getClickTopicName: function(){ // summary: Returns the name of the dojo topic that can be // subscribed to in order to receive notifications on // which thumbnail was selected. return (this.widgetId || this.id) + "/select"; // String }, getShowTopicName: function(){ // summary: Returns the name of the dojo topic that can be // subscribed to in order to receive notifications on // which thumbnail is now visible return (this.widgetId || this.id) + "/show"; // String }, setDataStore: function(dataStore, request, /*optional*/paramNames){ // summary: Sets the data store and request objects to read data from. // dataStore: // An implementation of the dojo.data.api.Read API. This accesses the image // data. // request: // An implementation of the dojo.data.api.Request API. This specifies the // query and paging information to be used by the data store // paramNames: // An object defining the names of the item attributes to fetch from the // data store. The four attributes allowed are 'linkAttr', 'imageLargeAttr', // 'imageThumbAttr' and 'titleAttr' this.reset(); this.request = { query: {}, start: request.start || 0, count: request.count || 10, onBegin: dojo.hitch(this, function(total){ this._maxPhotos = total; }) }; if(request.query){ dojo.mixin(this.request.query, request.query);} if(paramNames){ dojo.forEach(["imageThumbAttr", "imageLargeAttr", "linkAttr", "titleAttr"], function(attrName){ if(paramNames[attrName]){ this[attrName] = paramNames[attrName]; } }, this); } this.request.start = 0; this.request.count = this.pageSize; this.imageStore = dataStore; if(!this.init()){this._loadNextPage();} }, reset: function(){ // summary: Resets the widget back to its original state. this._loadedImages = {}; dojo.forEach(this._thumbs, function(img){ if(img){ // dojo.event.browser.clean(img); if(img.parentNode){ img.parentNode.removeChild(img); } } }); this._thumbs = []; this.isInitialized = false; this._noImages = true; }, isVisible: function(index) { // summary: Returns true if the image at the specified index is currently visible. False otherwise. var img = this._thumbs[index]; if(!img){return false;} var pos = this.isHorizontal ? "offsetLeft" : "offsetTop"; var size = this.isHorizontal ? "offsetWidth" : "offsetHeight"; var scrollAttr = this.isHorizontal ? "scrollLeft" : "scrollTop"; var offset = img[pos] - this.thumbsNode[pos]; return (offset >= this.thumbScroller[scrollAttr] && offset + img[size] <= this.thumbScroller[scrollAttr] + this._scrollerSize); }, _next: function() { // summary: Displays the next page of images var pos = this.isHorizontal ? "offsetLeft" : "offsetTop"; var size = this.isHorizontal ? "offsetWidth" : "offsetHeight"; var baseOffset = this.thumbsNode[pos]; var firstThumb = this._thumbs[this._thumbIndex]; var origOffset = firstThumb[pos] - baseOffset; var index = -1, img; for(var i = this._thumbIndex + 1; i < this._thumbs.length; i++){ img = this._thumbs[i]; if(img[pos] - baseOffset + img[size] - origOffset > this._scrollerSize){ this._showThumbs(i); return; } } }, _prev: function(){ // summary: Displays the next page of images if(this.thumbScroller[this.isHorizontal ? "scrollLeft" : "scrollTop"] == 0){return;} var pos = this.isHorizontal ? "offsetLeft" : "offsetTop"; var size = this.isHorizontal ? "offsetWidth" : "offsetHeight"; var firstThumb = this._thumbs[this._thumbIndex]; var origOffset = firstThumb[pos] - this.thumbsNode[pos]; var index = -1, img; for(var i = this._thumbIndex - 1; i > -1; i--) { img = this._thumbs[i]; if(origOffset - img[pos] > this._scrollerSize){ this._showThumbs(i + 1); return; } } this._showThumbs(0); }, _checkLoad: function(img, index){ dojo.publish(this.getShowTopicName(), [{index:index}]); this._updateNavControls(); this._loadingImages = {}; this._thumbIndex = index; //If we have not already requested the data from the store, do so. if(this.thumbsNode.offsetWidth - img.offsetLeft < (this._scrollerSize * 2)){ this._loadNextPage(); } }, _showThumbs: function(index){ // summary: Displays thumbnail images, starting at position 'index' // index: Number // The index of the first thumbnail //FIXME: When is this be called with an invalid index? Do we need this check at all? // if(typeof index != "number"){ index = this._thumbIndex; } index = Math.min(Math.max(index, 0), this._maxPhotos); if(index >= this._maxPhotos){ return; } var img = this._thumbs[index]; if(!img){ return; } var left = img.offsetLeft - this.thumbsNode.offsetLeft; var top = img.offsetTop - this.thumbsNode.offsetTop; var offset = this.isHorizontal ? left : top; if( (offset >= this.thumbScroller[this._scrollAttr]) && (offset + img[this._sizeAttr] <= this.thumbScroller[this._scrollAttr] + this._scrollerSize) ){ // FIXME: WTF is this checking for? return; } if(this.isScrollable){ var target = this.isHorizontal ? {x: left, y: 0} : { x:0, y:top}; dojox.fx.smoothScroll({ target: target, win: this.thumbScroller, duration:300, easing:dojo.fx.easing.easeOut, onEnd: dojo.hitch(this, "_checkLoad", img, index) }).play(10); }else{ if(this.isHorizontal){ this.thumbScroller.scrollLeft = left; }else{ this.thumbScroller.scrollTop = top; } this._checkLoad(img, index); } }, markImageLoaded: function(index){ // summary: Changes a visual cue to show the image is loaded // description: If 'useLoadNotifier' is set to true, then a visual cue is // given to state whether the image is loaded or not. Calling this function // marks an image as loaded. var thumbNotifier = dojo.byId("loadingDiv_"+this.widgetid+"_"+index); if(thumbNotifier){this._setThumbClass(thumbNotifier, "thumbLoaded");} this._loadedImages[index] = true; }, _setThumbClass: function(thumb, className){ // summary: Adds a CSS class to a thumbnail, only if 'autoLoad' is true // thumb: DomNode // The thumbnail DOM node to set the class on // className: String // The CSS class to add to the DOM node. if(!this.autoLoad){ return; } dojo.addClass(thumb, className); }, _loadNextPage: function(){ // summary: Loads the next page of thumbnail images if(this._loadInProgress){return;} this._loadInProgress = true; var start = this.request.start + (this._noImages ? 0 : this.pageSize); var pos = start; while(pos < this._thumbs.length && this._thumbs[pos]){pos ++;} //Define the function to call when the items have been //returned from the data store. var complete = function(items, request){ if(items && items.length){ var itemCounter = 0; var loadNext = dojo.hitch(this, function(){ if(itemCounter >= items.length){ this._loadInProgress = false; return; } var counter = itemCounter++; this._loadImage(items[counter], pos + counter, loadNext); }); loadNext(); //Show or hide the navigation arrows on the thumbnails, //depending on whether or not the widget is at the start, //end, or middle of the list of images. this._updateNavControls(); }else{ this._loadInProgress = false; } }; //Define the function to call if the store reports an error. var error = function(){ this._loadInProgress = false; console.debug("Error getting items"); }; this.request.onComplete = dojo.hitch(this, complete); this.request.onError = dojo.hitch(this, error); //Increment the start parameter. This is the dojo.data API's //version of paging. this.request.start = start; this._noImages = false; //Execute the request for data. this.imageStore.fetch(this.request); }, _loadImage: function(data, index, callback){ var url = this.imageStore.getValue(data,this.imageThumbAttr); var img = document.createElement("img"); var imgContainer = document.createElement("div"); imgContainer.setAttribute("id","img_" + this.widgetid+"_"+index); imgContainer.appendChild(img); img._index = index; img._data = data; this._thumbs[index] = imgContainer; var loadingDiv; if(this.useLoadNotifier){ loadingDiv = document.createElement("div"); loadingDiv.setAttribute("id","loadingDiv_" + this.widgetid+"_"+index); //If this widget was previously told that the main image for this //thumb has been loaded, make the loading indicator transparent. this._setThumbClass(loadingDiv, this._loadedImages[index] ? "thumbLoaded":"thumbNotifier"); imgContainer.appendChild(loadingDiv); } var size = dojo.marginBox(this.thumbsNode); var defaultSize; var sizeParam; if(this.isHorizontal){ defaultSize = this.thumbWidth; sizeParam = 'w'; } else{ defaultSize = this.thumbHeight; sizeParam = 'h'; } size = size[sizeParam]; var sl = this.thumbScroller.scrollLeft, st = this.thumbScroller.scrollTop; dojo.style(this.thumbsNode, this._sizeProperty, (size + defaultSize + 20) + "px"); //Remember the scroll values, as changing the size can alter them this.thumbScroller.scrollLeft = sl; this.thumbScroller.scrollTop = st; this.thumbsNode.appendChild(imgContainer); dojo.connect(img, "onload", this, function(){ var realSize = dojo.marginBox(img)[sizeParam]; this._totalSize += (Number(realSize) + 4); dojo.style(this.thumbsNode, this._sizeProperty, this._totalSize + "px"); if(this.useLoadNotifier){ dojo.style(loadingDiv, "width", (img.width - 4) + "px"); } dojo.style(imgContainer, "width", img.width + "px"); callback(); return false; }); dojo.connect(img, "onclick", this, function(evt){ dojo.publish(this.getClickTopicName(), [{ index: evt.target._index, data: evt.target._data, url: img.getAttribute("src"), largeUrl: this.imageStore.getValue(data,this.imageLargeAttr), title: this.imageStore.getValue(data,this.titleAttr), link: this.imageStore.getValue(data,this.linkAttr) }]); return false; }); dojo.addClass(img, "imageGalleryThumb"); img.setAttribute("src", url); var title = this.imageStore.getValue(data, this.titleAttr); if(title){ img.setAttribute("title",title); } this._updateNavControls(); }, _updateNavControls: function(){ // summary: Updates the navigation controls to hide/show them when at // the first or last images. var cells = []; var change = function(node, add){ var fn = add ? "addClass" : "removeClass"; dojo[fn](node,"enabled"); dojo[fn](node,"thumbClickable"); }; var pos = this.isHorizontal ? "scrollLeft" : "scrollTop"; var size = this.isHorizontal ? "offsetWidth" : "offsetHeight"; change(this.navPrev, (this.thumbScroller[pos] > 0)); var last = this._thumbs[this._thumbs.length - 1]; var addClass = (this.thumbScroller[pos] + this._scrollerSize < this.thumbsNode[size]); change(this.navNext, addClass); } });