ThumbnailPicker.js 18 KB

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