Lightbox.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  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.Lightbox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.image.Lightbox"] = true;
  8. dojo.provide("dojox.image.Lightbox");
  9. dojo.experimental("dojox.image.Lightbox");
  10. dojo.require("dojo.window");
  11. dojo.require("dijit.Dialog");
  12. dojo.require("dojox.fx._base");
  13. dojo.declare("dojox.image.Lightbox",
  14. dijit._Widget, {
  15. // summary:
  16. // A dojo-based Lightbox implementation.
  17. //
  18. // description:
  19. // An Elegant, keyboard accessible, markup and store capable Lightbox widget to show images
  20. // in a modal dialog-esque format. Can show individual images as Modal dialog, or can group
  21. // images with multiple entry points, all using a single "master" Dialog for visualization
  22. //
  23. // key controls:
  24. // ESC - close
  25. // Down Arrow / Rt Arrow / N - Next Image
  26. // Up Arrow / Lf Arrow / P - Previous Image
  27. //
  28. // example:
  29. // | <a href="image1.jpg" dojoType="dojox.image.Lightbox">show lightbox</a>
  30. //
  31. // example:
  32. // | <a href="image2.jpg" dojoType="dojox.image.Lightbox" group="one">show group lightbox</a>
  33. // | <a href="image3.jpg" dojoType="dojox.image.Lightbox" group="one">show group lightbox</a>
  34. //
  35. // example:
  36. // | not implemented fully yet, though works with basic datastore access. need to manually call
  37. // | widget._attachedDialog.addImage(item,"fromStore") for each item in a store result set.
  38. // | <div dojoType="dojox.image.Lightbox" group="fromStore" store="storeName"></div>
  39. //
  40. // group: String
  41. // Grouping images in a page with similar tags will provide a 'slideshow' like grouping of images
  42. group: "",
  43. // title: String
  44. // A string of text to be shown in the Lightbox beneath the image (empty if using a store)
  45. title: "",
  46. // href; String
  47. // Link to image to use for this Lightbox node (empty if using a store).
  48. href: "",
  49. // duration: Integer
  50. // Generic time in MS to adjust the feel of widget. could possibly add various
  51. // durations for the various actions (dialog fadein, sizeing, img fadein ...)
  52. duration: 500,
  53. // modal: Boolean
  54. // If true, this Dialog instance will be truly modal and prevent closing until
  55. // explicitly told to by calling hide() or clicking the (x) - Defaults to false
  56. // to preserve previous behaviors. (aka: enable click-to-click on the underlay)
  57. modal: false,
  58. // _allowPassthru: Boolean
  59. // Privately set this to disable/enable natural link of anchor tags
  60. _allowPassthru: false,
  61. // _attachedDialg: dojox.image._LightboxDialog
  62. // The pointer to the global lightbox dialog for this widget
  63. _attachedDialog: null, // try to share a single underlay per page?
  64. startup: function(){
  65. this.inherited(arguments);
  66. // setup an attachment to the masterDialog (or create the masterDialog)
  67. var tmp = dijit.byId('dojoxLightboxDialog');
  68. if(tmp){
  69. this._attachedDialog = tmp;
  70. }else{
  71. // this is the first instance to start, so we make the masterDialog
  72. this._attachedDialog = new dojox.image.LightboxDialog({ id: "dojoxLightboxDialog" });
  73. this._attachedDialog.startup();
  74. }
  75. if(!this.store){
  76. // FIXME: full store support lacking, have to manually call this._attachedDialog.addImage(imgage,group) as it stands
  77. this._addSelf();
  78. this.connect(this.domNode, "onclick", "_handleClick");
  79. }
  80. },
  81. _addSelf: function(){
  82. // summary: Add this instance to the master LightBoxDialog
  83. this._attachedDialog.addImage({
  84. href: this.href,
  85. title: this.title
  86. }, this.group || null);
  87. },
  88. _handleClick: function(/* Event */e){
  89. // summary: Handle the click on the link
  90. if(!this._allowPassthru){ e.preventDefault(); }
  91. else{ return; }
  92. this.show();
  93. },
  94. show: function(){
  95. // summary: Show the Lightbox with this instance as the starting point
  96. this._attachedDialog.show(this);
  97. },
  98. hide: function(){
  99. // summary: Hide the Lightbox currently showing
  100. this._attachedDialog.hide();
  101. },
  102. // FIXME: switch to .attr, deprecate eventually.
  103. disable: function(){
  104. // summary: Disables event clobbering and dialog, and follows natural link
  105. this._allowPassthru = true;
  106. },
  107. enable: function(){
  108. // summary: Enables the dialog (prevents default link)
  109. this._allowPassthru = false;
  110. },
  111. onClick: function(){
  112. // summary:
  113. // Stub fired when the image in the lightbox is clicked.
  114. },
  115. destroy: function(){
  116. this._attachedDialog.removeImage(this);
  117. this.inherited(arguments);
  118. }
  119. });
  120. dojo.declare("dojox.image.LightboxDialog",
  121. dijit.Dialog, {
  122. // summary:
  123. // The "dialog" shared between any Lightbox instances on the page, publically available
  124. // for programatic manipulation.
  125. //
  126. // description:
  127. //
  128. // A widget that intercepts anchor links (typically around images)
  129. // and displays a modal Dialog. this is the actual Dialog, which you can
  130. // create and populate manually, though should use simple Lightbox's
  131. // unless you need the direct access.
  132. //
  133. // There should only be one of these on a page, so all dojox.image.Lightbox's will us it
  134. // (the first instance of a Lightbox to be show()'n will create me If i do not exist)
  135. //
  136. // example:
  137. // | // show a single image from a url
  138. // | var url = "http://dojotoolkit.org/logo.png";
  139. // | var dialog = new dojox.image.LightboxDialog().startup();
  140. // | dialog.show({ href: url, title:"My Remote Image"});
  141. //
  142. // title: String
  143. // The current title, read from object passed to show()
  144. title: "",
  145. // FIXME: implement titleTemplate
  146. // inGroup: Array
  147. // Array of objects. this is populated by from the JSON object _groups, and
  148. // should not be populate manually. it is a placeholder for the currently
  149. // showing group of images in this master dialog
  150. inGroup: null,
  151. // imgUrl: String
  152. // The src="" attribute of our imageNode (can be null at statup)
  153. imgUrl: dijit._Widget.prototype._blankGif,
  154. // errorMessage: String
  155. // The text to display when an unreachable image is linked
  156. errorMessage: "Image not found.",
  157. // adjust: Boolean
  158. // If true, ensure the image always stays within the viewport
  159. // more difficult than necessary to disable, but enabled by default
  160. // seems sane in most use cases.
  161. adjust: true,
  162. // modal: Boolean
  163. // If true, this Dialog instance will be truly modal and prevent closing until
  164. // explicitly told to by calling hide() or clicking the (x) - Defaults to false
  165. // to preserve previous behaviors. (aka: enable click-to-click on the underlay)
  166. modal: false,
  167. /*=====
  168. // _groups: Object
  169. // an object of arrays, each array (of objects) being a unique 'group'
  170. _groups: { XnoGroupX: [] },
  171. =====*/
  172. // errorImg: Url
  173. // Path to the image used when a 404 is encountered
  174. errorImg: dojo.moduleUrl("dojox.image","resources/images/warning.png"),
  175. templateString: dojo.cache("dojox.image", "resources/Lightbox.html", "<div class=\"dojoxLightbox\" dojoAttachPoint=\"containerNode\">\n\t<div style=\"position:relative\">\n\t\t<div dojoAttachPoint=\"imageContainer\" class=\"dojoxLightboxContainer\" dojoAttachEvent=\"onclick: _onImageClick\">\n\t\t\t<img dojoAttachPoint=\"imgNode\" src=\"${imgUrl}\" class=\"dojoxLightboxImage\" alt=\"${title}\">\n\t\t\t<div class=\"dojoxLightboxFooter\" dojoAttachPoint=\"titleNode\">\n\t\t\t\t<div class=\"dijitInline LightboxClose\" dojoAttachPoint=\"closeButtonNode\"></div>\n\t\t\t\t<div class=\"dijitInline LightboxNext\" dojoAttachPoint=\"nextButtonNode\"></div>\t\n\t\t\t\t<div class=\"dijitInline LightboxPrev\" dojoAttachPoint=\"prevButtonNode\"></div>\n\t\t\t\t<div class=\"dojoxLightboxText\" dojoAttachPoint=\"titleTextNode\"><span dojoAttachPoint=\"textNode\">${title}</span><span dojoAttachPoint=\"groupCount\" class=\"dojoxLightboxGroupText\"></span></div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n"),
  176. constructor: function(args){
  177. this._groups = this._groups || (args && args._groups) || { XnoGroupX:[] };
  178. },
  179. startup: function(){
  180. // summary: Add some extra event handlers, and startup our superclass.
  181. //
  182. // returns: dijit._Widget
  183. // Perhaps the only `dijit._Widget` that returns itself to allow
  184. // 'chaining' or var referencing with .startup()
  185. this.inherited(arguments);
  186. this._animConnects = [];
  187. this.connect(this.nextButtonNode, "onclick", "_nextImage");
  188. this.connect(this.prevButtonNode, "onclick", "_prevImage");
  189. this.connect(this.closeButtonNode, "onclick", "hide");
  190. this._makeAnims();
  191. this._vp = dojo.window.getBox();
  192. return this;
  193. },
  194. show: function(/* Object */groupData){
  195. // summary: Show the Master Dialog. Starts the chain of events to show
  196. // an image in the dialog, including showing the dialog if it is
  197. // not already visible
  198. //
  199. // groupData: Object
  200. // needs href and title attributes. the values for this image.
  201. //
  202. //
  203. var _t = this; // size
  204. this._lastGroup = groupData;
  205. // we only need to call dijit.Dialog.show() if we're not already open.
  206. if(!_t.open){
  207. _t.inherited(arguments);
  208. _t._modalconnects.push(
  209. dojo.connect(dojo.global, "onscroll", this, "_position"),
  210. dojo.connect(dojo.global, "onresize", this, "_position"),
  211. dojo.connect(dojo.body(), "onkeypress", this, "_handleKey")
  212. );
  213. if(!groupData.modal){
  214. _t._modalconnects.push(
  215. dojo.connect(dijit._underlay.domNode, "onclick", this, "onCancel")
  216. );
  217. }
  218. }
  219. if(this._wasStyled){
  220. // ugly fix for IE being stupid. place the new image relative to the old
  221. // image to allow for overriden templates to adjust the location of the
  222. // titlebar. DOM will remain "unchanged" between views.
  223. var tmpImg = dojo.create("img", null, _t.imgNode, "after");
  224. dojo.destroy(_t.imgNode);
  225. _t.imgNode = tmpImg;
  226. _t._makeAnims();
  227. _t._wasStyled = false;
  228. }
  229. dojo.style(_t.imgNode,"opacity","0");
  230. dojo.style(_t.titleNode,"opacity","0");
  231. var src = groupData.href;
  232. if((groupData.group && groupData !== "XnoGroupX") || _t.inGroup){
  233. if(!_t.inGroup){
  234. _t.inGroup = _t._groups[(groupData.group)];
  235. // determine where we were or are in the show
  236. dojo.forEach(_t.inGroup, function(g, i){
  237. if(g.href == groupData.href){
  238. _t._index = i;
  239. //return false;
  240. }
  241. //return true;
  242. });
  243. }
  244. if(!_t._index){
  245. _t._index = 0;
  246. var sr = _t.inGroup[_t._index];
  247. src = (sr && sr.href) || _t.errorImg;
  248. }
  249. // FIXME: implement titleTemplate
  250. _t.groupCount.innerHTML = " (" + (_t._index + 1) + " of " + Math.max(1, _t.inGroup.length) + ")";
  251. _t.prevButtonNode.style.visibility = "visible";
  252. _t.nextButtonNode.style.visibility = "visible";
  253. }else{
  254. // single images don't have buttons, or counters:
  255. _t.groupCount.innerHTML = "";
  256. _t.prevButtonNode.style.visibility = "hidden";
  257. _t.nextButtonNode.style.visibility = "hidden";
  258. }
  259. if(!groupData.leaveTitle){
  260. _t.textNode.innerHTML = groupData.title;
  261. }
  262. _t._ready(src);
  263. },
  264. _ready: function(src){
  265. // summary: A function to trigger all 'real' showing of some src
  266. var _t = this;
  267. // listen for 404's:
  268. _t._imgError = dojo.connect(_t.imgNode, "error", _t, function(){
  269. dojo.disconnect(_t._imgError);
  270. // trigger the above onload with a new src:
  271. _t.imgNode.src = _t.errorImg;
  272. _t.textNode.innerHTML = _t.errorMessage;
  273. });
  274. // connect to the onload of the image
  275. _t._imgConnect = dojo.connect(_t.imgNode, "load", _t, function(e){
  276. _t.resizeTo({
  277. w: _t.imgNode.width,
  278. h: _t.imgNode.height,
  279. duration:_t.duration
  280. });
  281. // cleanup
  282. dojo.disconnect(_t._imgConnect);
  283. if(_t._imgError){
  284. dojo.disconnect(_t._imgError);
  285. }
  286. });
  287. _t.imgNode.src = src;
  288. },
  289. _nextImage: function(){
  290. // summary: Load next image in group
  291. if(!this.inGroup){ return; }
  292. if(this._index + 1 < this.inGroup.length){
  293. this._index++;
  294. }else{
  295. this._index = 0;
  296. }
  297. this._loadImage();
  298. },
  299. _prevImage: function(){
  300. // summary: Load previous image in group
  301. if(this.inGroup){
  302. if(this._index == 0){
  303. this._index = this.inGroup.length - 1;
  304. }else{
  305. this._index--;
  306. }
  307. this._loadImage();
  308. }
  309. },
  310. _loadImage: function(){
  311. // summary: Do the prep work before we can show another image
  312. this._loadingAnim.play(1);
  313. },
  314. _prepNodes: function(){
  315. // summary: A localized hook to accompany _loadImage
  316. this._imageReady = false;
  317. if(this.inGroup && this.inGroup[this._index]){
  318. this.show({
  319. href: this.inGroup[this._index].href,
  320. title: this.inGroup[this._index].title
  321. });
  322. }else{
  323. this.show({
  324. title: this.errorMessage,
  325. href: this.errorImg
  326. });
  327. }
  328. },
  329. _calcTitleSize: function(){
  330. var sizes = dojo.map(dojo.query("> *", this.titleNode).position(), function(s){ return s.h; });
  331. return { h: Math.max.apply(Math, sizes) };
  332. },
  333. resizeTo: function(/* Object */size, forceTitle){
  334. // summary: Resize our dialog container, and fire _showImage
  335. var adjustSize = dojo.boxModel == "border-box" ?
  336. dojo._getBorderExtents(this.domNode).w : 0,
  337. titleSize = forceTitle || this._calcTitleSize()
  338. ;
  339. this._lastTitleSize = titleSize;
  340. if(this.adjust &&
  341. (size.h + titleSize.h + adjustSize + 80 > this._vp.h ||
  342. size.w + adjustSize + 60 > this._vp.w
  343. )
  344. ){
  345. this._lastSize = size;
  346. size = this._scaleToFit(size);
  347. }
  348. this._currentSize = size;
  349. var _sizeAnim = dojox.fx.sizeTo({
  350. node: this.containerNode,
  351. duration: size.duration||this.duration,
  352. width: size.w + adjustSize,
  353. height: size.h + titleSize.h + adjustSize
  354. });
  355. this.connect(_sizeAnim, "onEnd", "_showImage");
  356. _sizeAnim.play(15);
  357. },
  358. _scaleToFit: function(/* Object */size){
  359. // summary: resize an image to fit within the bounds of the viewport
  360. // size: Object
  361. // The 'size' object passed around for this image
  362. var ns = {}, // New size
  363. nvp = {
  364. w: this._vp.w - 80,
  365. h: this._vp.h - 60 - this._lastTitleSize.h
  366. }; // New viewport
  367. // Calculate aspect ratio
  368. var viewportAspect = nvp.w / nvp.h,
  369. imageAspect = size.w / size.h;
  370. // Calculate new image size
  371. if(imageAspect >= viewportAspect){
  372. ns.h = nvp.w / imageAspect;
  373. ns.w = nvp.w;
  374. }else{
  375. ns.w = imageAspect * nvp.h;
  376. ns.h = nvp.h;
  377. }
  378. // we actually have to style this image, it's too big
  379. this._wasStyled = true;
  380. this._setImageSize(ns);
  381. ns.duration = size.duration;
  382. return ns; // Object
  383. },
  384. _setImageSize: function(size){
  385. // summary: Reset the image size to some actual size.
  386. var s = this.imgNode;
  387. s.height = size.h;
  388. s.width = size.w;
  389. },
  390. // clobber inherited function, it is useless.
  391. _size: function(){},
  392. _position: function(/* Event */e){
  393. // summary: we want to know the viewport size any time it changes
  394. this._vp = dojo.window.getBox();
  395. this.inherited(arguments);
  396. // determine if we need to scale up or down, if at all.
  397. if(e && e.type == "resize"){
  398. if(this._wasStyled){
  399. this._setImageSize(this._lastSize);
  400. this.resizeTo(this._lastSize);
  401. }else{
  402. if(this.imgNode.height + 80 > this._vp.h || this.imgNode.width + 60 > this._vp.h){
  403. this.resizeTo({
  404. w: this.imgNode.width, h: this.imgNode.height
  405. });
  406. }
  407. }
  408. }
  409. },
  410. _showImage: function(){
  411. // summary: Fade in the image, and fire showNav
  412. this._showImageAnim.play(1);
  413. },
  414. _showNav: function(){
  415. // summary: Fade in the footer, and setup our connections.
  416. var titleSizeNow = dojo.marginBox(this.titleNode);
  417. if(titleSizeNow.h > this._lastTitleSize.h){
  418. this.resizeTo(this._wasStyled ? this._lastSize : this._currentSize, titleSizeNow);
  419. }else{
  420. this._showNavAnim.play(1);
  421. }
  422. },
  423. hide: function(){
  424. // summary: Hide the Master Lightbox
  425. dojo.fadeOut({
  426. node: this.titleNode,
  427. duration: 200,
  428. // #5112 - if you _don't_ change the .src, safari will
  429. // _never_ fire onload for this image
  430. onEnd: dojo.hitch(this, function(){
  431. this.imgNode.src = this._blankGif;
  432. })
  433. }).play(5);
  434. this.inherited(arguments);
  435. this.inGroup = null;
  436. this._index = null;
  437. },
  438. addImage: function(child, group){
  439. // summary: Add an image to this Master Lightbox
  440. //
  441. // child: Object
  442. // The image information to add.
  443. // href: String - link to image (required)
  444. // title: String - title to display
  445. //
  446. // group: String?
  447. // attach to group of similar tag or null for individual image instance
  448. var g = group;
  449. if(!child.href){ return; }
  450. if(g){
  451. if(!this._groups[g]){
  452. this._groups[g] = [];
  453. }
  454. this._groups[g].push(child);
  455. }else{ this._groups["XnoGroupX"].push(child); }
  456. },
  457. removeImage: function(/* Widget */child){
  458. // summary: Remove an image instance from this LightboxDialog.
  459. // child: Object
  460. // A reference to the Lightbox child that was added (or an object literal)
  461. // only the .href member is compared for uniqueness. The object may contain
  462. // a .group member as well.
  463. var g = child.group || "XnoGroupX";
  464. dojo.every(this._groups[g], function(item, i, ar){
  465. if(item.href == child.href){
  466. ar.splice(i, 1);
  467. return false;
  468. }
  469. return true;
  470. });
  471. },
  472. removeGroup: function(group){
  473. // summary: Remove all images in a passed group
  474. if(this._groups[group]){ this._groups[group] = []; }
  475. },
  476. _handleKey: function(/* Event */e){
  477. // summary: Handle keyboard navigation internally
  478. if(!this.open){ return; }
  479. var dk = dojo.keys;
  480. switch(e.charOrCode){
  481. case dk.ESCAPE:
  482. this.hide();
  483. break;
  484. case dk.DOWN_ARROW:
  485. case dk.RIGHT_ARROW:
  486. case 78: // key "n"
  487. this._nextImage();
  488. break;
  489. case dk.UP_ARROW:
  490. case dk.LEFT_ARROW:
  491. case 80: // key "p"
  492. this._prevImage();
  493. break;
  494. }
  495. },
  496. _makeAnims: function(){
  497. // summary: make and cleanup animation and animation connections
  498. dojo.forEach(this._animConnects, dojo.disconnect);
  499. this._animConnects = [];
  500. this._showImageAnim = dojo.fadeIn({
  501. node: this.imgNode,
  502. duration: this.duration
  503. });
  504. this._animConnects.push(dojo.connect(this._showImageAnim, "onEnd", this, "_showNav"));
  505. this._loadingAnim = dojo.fx.combine([
  506. dojo.fadeOut({ node:this.imgNode, duration:175 }),
  507. dojo.fadeOut({ node:this.titleNode, duration:175 })
  508. ]);
  509. this._animConnects.push(dojo.connect(this._loadingAnim, "onEnd", this, "_prepNodes"));
  510. this._showNavAnim = dojo.fadeIn({ node: this.titleNode, duration:225 });
  511. },
  512. onClick: function(groupData){
  513. // summary: a stub function, called with the currently displayed image as the only argument
  514. },
  515. _onImageClick: function(e){
  516. if(e && e.target == this.imgNode){
  517. this.onClick(this._lastGroup);
  518. // also fire the onclick for the Lightbox widget which triggered, if you
  519. // aren't working directly with the LBDialog
  520. if(this._lastGroup.declaredClass){
  521. this._lastGroup.onClick(this._lastGroup);
  522. }
  523. }
  524. }
  525. });
  526. }