source: Dev/branches/rest-dojo-ui/client/dojox/image/ThumbnailPicker.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 17.0 KB
Line 
1dojo.provide("dojox.image.ThumbnailPicker");
2dojo.experimental("dojox.image.ThumbnailPicker");
3//
4// dojox.image.ThumbnailPicker courtesy Shane O Sullivan, licensed under a Dojo CLA
5//
6// For a sample usage, see http://www.skynet.ie/~sos/photos.php
7//
8//      document topics.
9
10dojo.require("dojox.fx.scroll"); // is optional, but don't want to dojo[require] it
11dojo.require("dojo.fx.easing");
12
13dojo.require("dojo.fx");
14dojo.require("dijit._Widget");
15dojo.require("dijit._Templated");
16
17// FIXME: use CSS for size, thumbHeight, and thumbWidth
18
19dojo.declare("dojox.image.ThumbnailPicker",
20        [dijit._Widget, dijit._Templated],
21        {
22        // summary: A scrolling Thumbnail Picker widget
23        //
24        // imageStore: Object
25        // A data store that implements the dojo.data Read API.
26        imageStore: null,
27
28        // request: Object
29        // A dojo.data Read API Request object.
30        request: null,
31
32        // size: Number
33        // Width or height in pixels, depending if horizontal or vertical.
34        size: 500,
35
36        // thumbHeight: Number
37        // Default height of a thumbnail image
38        thumbHeight: 75,
39
40        // thumbWidth: Number
41        // Default width of an image
42        thumbWidth: 100,
43
44        // useLoadNotifier: Boolean
45        // Setting useLoadNotifier to true makes a colored DIV appear under each
46        // thumbnail image, which is used to display the loading status of each
47        // image in the data store.
48        useLoadNotifier: false,
49
50        // useHyperlink: boolean
51        // Setting useHyperlink to true causes a click on a thumbnail to open a link.
52        useHyperlink: false,
53
54        // hyperlinkTarget: String
55        // If hyperlinkTarget is set to "new", clicking on a thumb will open a new window
56        // If it is set to anything else, clicking a thumbnail will open the url in the
57        // current window.
58        hyperlinkTarget: "new",
59
60        // isClickable: Boolean
61        // When set to true, the cursor over a thumbnail changes.
62        isClickable: true,
63
64        // isScrollable: Boolean
65        // When true, uses smoothScroll to move between pages
66        isScrollable: true,
67
68        // isHorizontal: Boolean
69        // If true, the thumbnails are displayed horizontally. Otherwise they are displayed
70        // vertically
71        isHorizontal: true,
72
73        //autoLoad: Boolean
74        autoLoad: true,
75
76        // linkAttr: String
77        // The attribute name for accessing the url from the data store
78        linkAttr: "link",
79       
80        // imageThumbAttr: String
81        // The attribute name for accessing the thumbnail image url from the data store
82        imageThumbAttr: "imageUrlThumb",
83       
84        // imageLargeAttr: String
85        // The attribute name for accessing the large image url from the data store
86        imageLargeAttr: "imageUrl",
87       
88        // pageSize: Number
89        //      The number of images to request each time.
90        pageSize: 20,
91       
92        // titleAttr: String
93        // The attribute name for accessing the title from the data store
94        titleAttr: "title",
95       
96        templateString: dojo.cache("dojox.image", "resources/ThumbnailPicker.html"),
97       
98        // thumbs: Array
99        // Stores the image nodes for the thumbnails.
100        _thumbs: [],
101       
102        // _thumbIndex: Number
103        // The index of the first thumbnail shown
104        _thumbIndex: 0,
105       
106        // _maxPhotos: Number
107        // The total number of photos in the image store
108        _maxPhotos: 0,
109       
110        // _loadedImages: Object
111        // Stores the indices of images that have been marked as loaded using the
112        // markImageLoaded function.
113        _loadedImages: {},
114
115        postCreate: function(){
116                // summary:
117                //              Initializes styles and listeners
118                this.widgetid = this.id;
119                this.inherited(arguments);
120                this.pageSize = Number(this.pageSize);
121
122                this._scrollerSize = this.size - (51 * 2);
123               
124                var sizeProp = this._sizeProperty = this.isHorizontal ? "width" : "height";
125       
126                // FIXME: do this via css? calculate the correct width for the widget
127                dojo.style(this.outerNode, "textAlign","center");
128                dojo.style(this.outerNode, sizeProp, this.size+"px");
129       
130                dojo.style(this.thumbScroller, sizeProp, this._scrollerSize + "px");
131       
132                //If useHyperlink is true, then listen for a click on a thumbnail, and
133                //open the link
134                if(this.useHyperlink){
135                        dojo.subscribe(this.getClickTopicName(), this, function(packet){
136                                var index = packet.index;
137                                var url = this.imageStore.getValue(packet.data,this.linkAttr);
138                               
139                                //If the data item doesn't contain a URL, do nothing
140                                if(!url){return;}
141                               
142                                if(this.hyperlinkTarget == "new"){
143                                        window.open(url);
144                                }else{
145                                        window.location = url;
146                                }
147                        });
148                }
149       
150                if(this.isClickable){
151                        dojo.addClass(this.thumbsNode, "thumbClickable");
152                }
153                this._totalSize = 0;
154                this.init();
155        },
156
157        init: function(){
158                // summary:
159                //              Creates DOM nodes for thumbnail images and initializes their listeners
160                if(this.isInitialized) {return false;}
161       
162                var classExt = this.isHorizontal ? "Horiz" : "Vert";
163       
164                // FIXME: can we setup a listener around the whole element and determine based on e.target?
165                dojo.addClass(this.navPrev, "prev" + classExt);
166                dojo.addClass(this.navNext, "next" + classExt);
167                dojo.addClass(this.thumbsNode, "thumb"+classExt);
168                dojo.addClass(this.outerNode, "thumb"+classExt);
169       
170                dojo.attr(this.navNextImg, "src", this._blankGif);
171                dojo.attr(this.navPrevImg, "src", this._blankGif);
172               
173                this.connect(this.navPrev, "onclick", "_prev");
174                this.connect(this.navNext, "onclick", "_next");
175                this.isInitialized = true;
176               
177                if(this.isHorizontal){
178                        this._offsetAttr = "offsetLeft";
179                        this._sizeAttr = "offsetWidth";
180                        this._scrollAttr = "scrollLeft";
181                }else{
182                        this._offsetAttr = "offsetTop";
183                        this._sizeAttr = "offsetHeight";
184                        this._scrollAttr = "scrollTop";
185                }
186       
187                this._updateNavControls();
188                if(this.imageStore && this.request){this._loadNextPage();}
189                return true;
190        },
191
192        getClickTopicName: function(){
193                // summary:
194                //              Returns the name of the dojo topic that can be
195                //              subscribed to in order to receive notifications on
196                //              which thumbnail was selected.
197                return (this.widgetId || this.id) + "/select"; // String
198        },
199
200        getShowTopicName: function(){
201                // summary:
202                //              Returns the name of the dojo topic that can be
203                //              subscribed to in order to receive notifications on
204                //              which thumbnail is now visible
205                return (this.widgetId || this.id) + "/show"; // String
206        },
207
208        setDataStore: function(dataStore, request, /*optional*/paramNames){
209                // summary:
210                //              Sets the data store and request objects to read data from.
211                // dataStore:
212                //              An implementation of the dojo.data.api.Read API. This accesses the image
213                //              data.
214                // request:
215                //              An implementation of the dojo.data.api.Request API. This specifies the
216                //              query and paging information to be used by the data store
217                // paramNames:
218                //              An object defining the names of the item attributes to fetch from the
219                //              data store.  The four attributes allowed are 'linkAttr', 'imageLargeAttr',
220                //              'imageThumbAttr' and 'titleAttr'
221                this.reset();
222       
223                this.request = {
224                        query: {},
225                        start: request.start || 0,
226                        count: request.count || 10,
227                        onBegin: dojo.hitch(this, function(total){
228                                this._maxPhotos = total;
229                        })
230                };
231       
232                if(request.query){ dojo.mixin(this.request.query, request.query);}
233       
234                if(paramNames){
235                        dojo.forEach(["imageThumbAttr", "imageLargeAttr", "linkAttr", "titleAttr"], function(attrName){
236                                if(paramNames[attrName]){ this[attrName] = paramNames[attrName]; }
237                        }, this);
238                }
239               
240                this.request.start = 0;
241                this.request.count = this.pageSize;
242                this.imageStore = dataStore;
243                this._loadInProgress = false;
244                if(!this.init()){this._loadNextPage();}
245        },
246
247        reset: function(){
248                // summary:
249                //              Resets the widget back to its original state.
250                this._loadedImages = {};
251                dojo.forEach(this._thumbs, function(img){
252                        if(img && img.parentNode){
253                                dojo.destroy(img);
254                        }
255                });
256       
257                this._thumbs = [];
258                this.isInitialized = false;
259                this._noImages = true;
260        },
261       
262        isVisible: function(index) {
263                // summary:
264                //              Returns true if the image at the specified index is currently visible. False otherwise.
265                var img = this._thumbs[index];
266                if(!img){return false;}
267                var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
268                var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
269                var scrollAttr = this.isHorizontal ? "scrollLeft" : "scrollTop";
270                var offset = img[pos] - this.thumbsNode[pos];
271                return (offset >= this.thumbScroller[scrollAttr]
272                        && offset + img[size] <= this.thumbScroller[scrollAttr] + this._scrollerSize);
273        },
274       
275        resize: function(dim){
276                var sizeParam = this.isHorizontal ? "w": "h";
277
278                var total = 0;
279
280                if(this._thumbs.length > 0 && dojo.marginBox(this._thumbs[0]).w == 0){
281                        // Skip the resize if the widget is not visible
282                        return;
283                }
284
285                // Calculate the complete size of the thumbnails
286                dojo.forEach(this._thumbs, dojo.hitch(this, function(imgContainer){
287                        var mb = dojo.marginBox(imgContainer.firstChild);
288                        var size = mb[sizeParam];
289                        total += (Number(size) + 10);
290                       
291                        if(this.useLoadNotifier && mb.w > 0){
292                                dojo.style(imgContainer.lastChild, "width", (mb.w - 4) + "px");
293                        }
294                        dojo.style(imgContainer, "width", mb.w + "px");
295                }));
296
297                dojo.style(this.thumbsNode, this._sizeProperty, total + "px");
298                this._updateNavControls();
299        },
300       
301        _next: function() {
302                // summary:
303                //              Displays the next page of images
304                var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
305                var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
306                var baseOffset = this.thumbsNode[pos];
307                var firstThumb = this._thumbs[this._thumbIndex];
308                var origOffset = firstThumb[pos] - baseOffset;
309       
310                var index = -1, img;
311       
312                for(var i = this._thumbIndex + 1; i < this._thumbs.length; i++){
313                        img = this._thumbs[i];
314                        if(img[pos] - baseOffset + img[size] - origOffset > this._scrollerSize){
315                                this._showThumbs(i);
316                                return;
317                        }
318                }
319        },
320
321        _prev: function(){
322                // summary:
323                //              Displays the next page of images
324                if(this.thumbScroller[this.isHorizontal ? "scrollLeft" : "scrollTop"] == 0){return;}
325                var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
326                var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
327       
328                var firstThumb = this._thumbs[this._thumbIndex];
329                var origOffset = firstThumb[pos] - this.thumbsNode[pos];
330       
331                var index = -1, img;
332       
333                for(var i = this._thumbIndex - 1; i > -1; i--) {
334                        img = this._thumbs[i];
335                        if(origOffset - img[pos] > this._scrollerSize){
336                                this._showThumbs(i + 1);
337                                return;
338                        }
339                }
340                this._showThumbs(0);
341        },
342
343        _checkLoad: function(img, index){
344                // summary:
345                //              Checks if an image is loaded.
346                dojo.publish(this.getShowTopicName(), [{index:index}]);
347                this._updateNavControls();
348                this._loadingImages = {};
349       
350                this._thumbIndex = index;
351       
352                //If we have not already requested the data from the store, do so.
353                if(this.thumbsNode.offsetWidth - img.offsetLeft < (this._scrollerSize * 2)){
354                        this._loadNextPage();
355                }
356        },
357
358        _showThumbs: function(index){
359                // summary:
360                //              Displays thumbnail images, starting at position 'index'
361                // index: Number
362                //              The index of the first thumbnail
363
364//FIXME: When is this be called with an invalid index?  Do we need this check at all?
365//              if(typeof index != "number"){ index = this._thumbIndex; }
366                index = Math.min(Math.max(index, 0), this._maxPhotos);
367               
368                if(index >= this._maxPhotos){ return; }
369               
370                var img = this._thumbs[index];
371                if(!img){ return; }
372               
373                var left = img.offsetLeft - this.thumbsNode.offsetLeft;
374                var top = img.offsetTop - this.thumbsNode.offsetTop;
375                var offset = this.isHorizontal ? left : top;
376                               
377                if(     (offset >= this.thumbScroller[this._scrollAttr]) &&
378                        (offset + img[this._sizeAttr] <= this.thumbScroller[this._scrollAttr] + this._scrollerSize)
379                ){
380                        // FIXME: WTF is this checking for?
381                        return;
382                }
383               
384               
385                if(this.isScrollable){
386                        var target = this.isHorizontal ? {x: left, y: 0} : { x:0, y:top};
387                        dojox.fx.smoothScroll({
388                                target: target,
389                                win: this.thumbScroller,
390                                duration:300,
391                                easing:dojo.fx.easing.easeOut,
392                                onEnd: dojo.hitch(this, "_checkLoad", img, index)
393                        }).play(10);
394                }else{
395                        if(this.isHorizontal){
396                                this.thumbScroller.scrollLeft = left;
397                        }else{
398                                this.thumbScroller.scrollTop = top;
399                        }
400                        this._checkLoad(img, index);
401                }
402        },
403       
404        markImageLoaded: function(index){
405                // summary:
406                //              Changes a visual cue to show the image is loaded
407                // description:
408                //              If 'useLoadNotifier' is set to true, then a visual cue is
409                //              given to state whether the image is loaded or not.      Calling this function
410                //              marks an image as loaded.
411                var thumbNotifier = dojo.byId("loadingDiv_"+this.widgetid+"_"+index);
412                if(thumbNotifier){this._setThumbClass(thumbNotifier, "thumbLoaded");}
413                this._loadedImages[index] = true;
414        },
415
416        _setThumbClass: function(thumb, className){
417                // summary:
418                //              Adds a CSS class to a thumbnail, only if 'autoLoad' is true
419                // thumb: DomNode
420                //              The thumbnail DOM node to set the class on
421                // className: String
422                //              The CSS class to add to the DOM node.
423                if(!this.autoLoad){ return; }
424                dojo.addClass(thumb, className);
425        },
426                                                 
427        _loadNextPage: function(){
428                // summary:
429                //              Loads the next page of thumbnail images
430                if(this._loadInProgress){return;}
431                this._loadInProgress = true;
432                var start = this.request.start + (this._noImages ? 0 : this.pageSize);
433               
434                var pos = start;
435                while(pos < this._thumbs.length && this._thumbs[pos]){pos ++;}
436               
437                var store = this.imageStore;
438               
439                //Define the function to call when the items have been
440                //returned from the data store.
441                var complete = function(items, request){
442                        if(store != this.imageStore){
443                                // If the store has been changed, ignore this callback.
444                                return;
445                        }
446                        if(items && items.length){
447                                var itemCounter = 0;
448                                var loadNext = dojo.hitch(this, function(){
449                                        if(itemCounter >= items.length){
450                                                this._loadInProgress = false;
451                                                return;
452                                        }
453                                        var counter = itemCounter++;
454
455                                        this._loadImage(items[counter], pos + counter, loadNext);
456                                });
457                                loadNext();
458
459                                //Show or hide the navigation arrows on the thumbnails,
460                                //depending on whether or not the widget is at the start,
461                                //end, or middle of the list of images.
462                                this._updateNavControls();
463                        }else{
464                                this._loadInProgress = false;
465                        }
466                };
467       
468                //Define the function to call if the store reports an error.
469                var error = function(){
470                        this._loadInProgress = false;
471                        console.log("Error getting items");
472                };
473
474                this.request.onComplete = dojo.hitch(this, complete);
475                this.request.onError = dojo.hitch(this, error);
476       
477                //Increment the start parameter. This is the dojo.data API's
478                //version of paging.
479                this.request.start = start;
480                this._noImages = false;
481               
482                //Execute the request for data.
483                this.imageStore.fetch(this.request);
484       
485        },
486
487        _loadImage: function(data, index, callback){
488                // summary:
489                //              Loads an image.
490
491                var store = this.imageStore;
492                var url = store.getValue(data,this.imageThumbAttr);
493               
494                var imgContainer = dojo.create("div", {
495                        id: "img_" + this.widgetid + "_" + index
496                });
497                var img = dojo.create("img", {}, imgContainer);
498                img._index = index;
499                img._data = data;
500       
501                this._thumbs[index] = imgContainer;
502                var loadingDiv;
503                if(this.useLoadNotifier){
504                        loadingDiv = dojo.create("div", {
505                                id: "loadingDiv_" + this.widgetid+"_" + index
506                        }, imgContainer);
507       
508                        //If this widget was previously told that the main image for this
509                        //thumb has been loaded, make the loading indicator transparent.
510                        this._setThumbClass(loadingDiv,
511                                this._loadedImages[index] ? "thumbLoaded":"thumbNotifier");
512                }
513                var size = dojo.marginBox(this.thumbsNode);
514                var defaultSize;
515                var sizeParam;
516                if(this.isHorizontal){
517                        defaultSize = this.thumbWidth;
518                        sizeParam = 'w';
519                } else{
520                        defaultSize = this.thumbHeight;
521                        sizeParam = 'h';
522                }
523                size = size[sizeParam];
524                var sl = this.thumbScroller.scrollLeft, st = this.thumbScroller.scrollTop;
525
526                dojo.style(this.thumbsNode, this._sizeProperty, (size + defaultSize + 20) + "px");
527
528                //Remember the scroll values, as changing the size can alter them
529                this.thumbScroller.scrollLeft = sl;
530                this.thumbScroller.scrollTop = st;
531                this.thumbsNode.appendChild(imgContainer);
532       
533                dojo.connect(img, "onload", this, dojo.hitch(this, function(){
534                        if(store != this.imageStore){
535                                // If the store has changed, ignore this load event
536                                return false;
537                        }
538                        this.resize();
539                                               
540                        // Have to use a timeout here to prevent a call stack that gets
541                        // so deep that IE throws stack overflow errors
542                        setTimeout(callback, 0);
543                        return false;
544                }));
545       
546                dojo.connect(img, "onclick", this, function(evt){
547                        dojo.publish(this.getClickTopicName(),  [{
548                                index: evt.target._index,
549                                data: evt.target._data,
550                                url: img.getAttribute("src"),
551                                largeUrl: this.imageStore.getValue(data,this.imageLargeAttr),
552                                title: this.imageStore.getValue(data,this.titleAttr),
553                                link: this.imageStore.getValue(data,this.linkAttr)
554                        }]);
555                        return false;
556                });
557                dojo.addClass(img, "imageGalleryThumb");
558                img.setAttribute("src", url);
559                var title = this.imageStore.getValue(data, this.titleAttr);
560                if(title){ img.setAttribute("title",title); }
561                this._updateNavControls();
562       
563        },
564
565        _updateNavControls: function(){
566                // summary:
567                //              Updates the navigation controls to hide/show them when at
568                //              the first or last images.
569                var cells = [];
570                var change = function(node, add){
571                        var fn = add ? "addClass" : "removeClass";
572                        dojo[fn](node,"enabled");
573                        dojo[fn](node,"thumbClickable");
574                };
575               
576                var pos = this.isHorizontal ? "scrollLeft" : "scrollTop";
577                var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
578                change(this.navPrev, (this.thumbScroller[pos] > 0));
579               
580                var last = this._thumbs[this._thumbs.length - 1];
581                var addClass = (this.thumbScroller[pos] + this._scrollerSize < this.thumbsNode[size]);
582                change(this.navNext, addClass);
583        }
584});
Note: See TracBrowser for help on using the repository browser.