Dialog.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  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["dijit.Dialog"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.Dialog"] = true;
  8. dojo.provide("dijit.Dialog");
  9. dojo.require("dojo.dnd.move");
  10. dojo.require("dojo.dnd.TimedMoveable");
  11. dojo.require("dojo.fx");
  12. dojo.require("dojo.window");
  13. dojo.require("dijit._Widget");
  14. dojo.require("dijit._Templated");
  15. dojo.require("dijit._CssStateMixin");
  16. dojo.require("dijit.form._FormMixin");
  17. dojo.require("dijit._DialogMixin");
  18. dojo.require("dijit.DialogUnderlay");
  19. dojo.require("dijit.layout.ContentPane");
  20. dojo.requireLocalization("dijit", "common", null, "ROOT,ar,az,bg,ca,cs,da,de,el,es,fi,fr,he,hr,hu,it,ja,kk,ko,nb,nl,pl,pt,pt-pt,ro,ru,sk,sl,sv,th,tr,zh,zh-tw");
  21. dojo.require("dijit.TooltipDialog");
  22. // dijit/TooltipDialog required for back-compat. TODO: remove in 2.0
  23. /*=====
  24. dijit._underlay = function(kwArgs){
  25. // summary:
  26. // A shared instance of a `dijit.DialogUnderlay`
  27. //
  28. // description:
  29. // A shared instance of a `dijit.DialogUnderlay` created and
  30. // used by `dijit.Dialog`, though never created until some Dialog
  31. // or subclass thereof is shown.
  32. };
  33. =====*/
  34. dojo.declare(
  35. "dijit._DialogBase",
  36. [dijit._Templated, dijit.form._FormMixin, dijit._DialogMixin, dijit._CssStateMixin],
  37. {
  38. // summary:
  39. // A modal dialog Widget
  40. //
  41. // description:
  42. // Pops up a modal dialog window, blocking access to the screen
  43. // and also graying out the screen Dialog is extended from
  44. // ContentPane so it supports all the same parameters (href, etc.)
  45. //
  46. // example:
  47. // | <div dojoType="dijit.Dialog" href="test.html"></div>
  48. //
  49. // example:
  50. // | var foo = new dijit.Dialog({ title: "test dialog", content: "test content" };
  51. // | dojo.body().appendChild(foo.domNode);
  52. // | foo.startup();
  53. templateString: dojo.cache("dijit", "templates/Dialog.html", "<div class=\"dijitDialog\" role=\"dialog\" aria-labelledby=\"${id}_title\">\n\t<div dojoAttachPoint=\"titleBar\" class=\"dijitDialogTitleBar\">\n\t<span dojoAttachPoint=\"titleNode\" class=\"dijitDialogTitle\" id=\"${id}_title\"></span>\n\t<span dojoAttachPoint=\"closeButtonNode\" class=\"dijitDialogCloseIcon\" dojoAttachEvent=\"ondijitclick: onCancel\" title=\"${buttonCancel}\" role=\"button\" tabIndex=\"-1\">\n\t\t<span dojoAttachPoint=\"closeText\" class=\"closeText\" title=\"${buttonCancel}\">x</span>\n\t</span>\n\t</div>\n\t\t<div dojoAttachPoint=\"containerNode\" class=\"dijitDialogPaneContent\"></div>\n</div>\n"),
  54. baseClass: "dijitDialog",
  55. cssStateNodes: {
  56. closeButtonNode: "dijitDialogCloseIcon"
  57. },
  58. attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, {
  59. title: [
  60. { node: "titleNode", type: "innerHTML" },
  61. { node: "titleBar", type: "attribute" }
  62. ],
  63. "aria-describedby":""
  64. }),
  65. // open: [readonly] Boolean
  66. // True if Dialog is currently displayed on screen.
  67. open: false,
  68. // duration: Integer
  69. // The time in milliseconds it takes the dialog to fade in and out
  70. duration: dijit.defaultDuration,
  71. // refocus: Boolean
  72. // A Toggle to modify the default focus behavior of a Dialog, which
  73. // is to re-focus the element which had focus before being opened.
  74. // False will disable refocusing. Default: true
  75. refocus: true,
  76. // autofocus: Boolean
  77. // A Toggle to modify the default focus behavior of a Dialog, which
  78. // is to focus on the first dialog element after opening the dialog.
  79. // False will disable autofocusing. Default: true
  80. autofocus: true,
  81. // _firstFocusItem: [private readonly] DomNode
  82. // The pointer to the first focusable node in the dialog.
  83. // Set by `dijit._DialogMixin._getFocusItems`.
  84. _firstFocusItem: null,
  85. // _lastFocusItem: [private readonly] DomNode
  86. // The pointer to which node has focus prior to our dialog.
  87. // Set by `dijit._DialogMixin._getFocusItems`.
  88. _lastFocusItem: null,
  89. // doLayout: [protected] Boolean
  90. // Don't change this parameter from the default value.
  91. // This ContentPane parameter doesn't make sense for Dialog, since Dialog
  92. // is never a child of a layout container, nor can you specify the size of
  93. // Dialog in order to control the size of an inner widget.
  94. doLayout: false,
  95. // draggable: Boolean
  96. // Toggles the moveable aspect of the Dialog. If true, Dialog
  97. // can be dragged by it's title. If false it will remain centered
  98. // in the viewport.
  99. draggable: true,
  100. //aria-describedby: String
  101. // Allows the user to add an aria-describedby attribute onto the dialog. The value should
  102. // be the id of the container element of text that describes the dialog purpose (usually
  103. // the first text in the dialog).
  104. // <div dojoType="dijit.Dialog" aria-describedby="intro" .....>
  105. // <div id="intro">Introductory text</div>
  106. // <div>rest of dialog contents</div>
  107. // </div>
  108. "aria-describedby":"",
  109. postMixInProperties: function(){
  110. var _nlsResources = dojo.i18n.getLocalization("dijit", "common");
  111. dojo.mixin(this, _nlsResources);
  112. this.inherited(arguments);
  113. },
  114. postCreate: function(){
  115. dojo.style(this.domNode, {
  116. display: "none",
  117. position:"absolute"
  118. });
  119. dojo.body().appendChild(this.domNode);
  120. this.inherited(arguments);
  121. this.connect(this, "onExecute", "hide");
  122. this.connect(this, "onCancel", "hide");
  123. this._modalconnects = [];
  124. },
  125. onLoad: function(){
  126. // summary:
  127. // Called when data has been loaded from an href.
  128. // Unlike most other callbacks, this function can be connected to (via `dojo.connect`)
  129. // but should *not* be overridden.
  130. // tags:
  131. // callback
  132. // when href is specified we need to reposition the dialog after the data is loaded
  133. // and find the focusable elements
  134. this._position();
  135. if(this.autofocus && dijit._DialogLevelManager.isTop(this)){
  136. this._getFocusItems(this.domNode);
  137. dijit.focus(this._firstFocusItem);
  138. }
  139. this.inherited(arguments);
  140. },
  141. _endDrag: function(){
  142. // summary:
  143. // Called after dragging the Dialog. Saves the position of the dialog in the viewport
  144. // and also adjust position to be fully within the viewport, so user doesn't lose access to handle
  145. // tags:
  146. // private
  147. var nodePosition = dojo.position(this.domNode),
  148. viewport = dojo.window.getBox();
  149. nodePosition.y = Math.min(Math.max(nodePosition.y, 0), (viewport.h - nodePosition.h));
  150. nodePosition.x = Math.min(Math.max(nodePosition.x, 0), (viewport.w - nodePosition.w));
  151. this._relativePosition = nodePosition;
  152. this._position();
  153. },
  154. _setup: function(){
  155. // summary:
  156. // Stuff we need to do before showing the Dialog for the first
  157. // time (but we defer it until right beforehand, for
  158. // performance reasons).
  159. // tags:
  160. // private
  161. var node = this.domNode;
  162. if(this.titleBar && this.draggable){
  163. this._moveable = (dojo.isIE == 6) ?
  164. new dojo.dnd.TimedMoveable(node, { handle: this.titleBar }) : // prevent overload, see #5285
  165. new dojo.dnd.Moveable(node, { handle: this.titleBar, timeout: 0 });
  166. this.connect(this._moveable, "onMoveStop", "_endDrag");
  167. }else{
  168. dojo.addClass(node,"dijitDialogFixed");
  169. }
  170. this.underlayAttrs = {
  171. dialogId: this.id,
  172. "class": dojo.map(this["class"].split(/\s/), function(s){ return s+"_underlay"; }).join(" ")
  173. };
  174. },
  175. _size: function(){
  176. // summary:
  177. // If necessary, shrink dialog contents so dialog fits in viewport
  178. // tags:
  179. // private
  180. this._checkIfSingleChild();
  181. // If we resized the dialog contents earlier, reset them back to original size, so
  182. // that if the user later increases the viewport size, the dialog can display w/out a scrollbar.
  183. // Need to do this before the dojo.marginBox(this.domNode) call below.
  184. if(this._singleChild){
  185. if(this._singleChildOriginalStyle){
  186. this._singleChild.domNode.style.cssText = this._singleChildOriginalStyle;
  187. }
  188. delete this._singleChildOriginalStyle;
  189. }else{
  190. dojo.style(this.containerNode, {
  191. width:"auto",
  192. height:"auto"
  193. });
  194. }
  195. var mb = dojo._getMarginSize(this.domNode);
  196. var viewport = dojo.window.getBox();
  197. if(mb.w >= viewport.w || mb.h >= viewport.h){
  198. // Reduce size of dialog contents so that dialog fits in viewport
  199. var w = Math.min(mb.w, Math.floor(viewport.w * 0.75)),
  200. h = Math.min(mb.h, Math.floor(viewport.h * 0.75));
  201. if(this._singleChild && this._singleChild.resize){
  202. this._singleChildOriginalStyle = this._singleChild.domNode.style.cssText;
  203. this._singleChild.resize({w: w, h: h});
  204. }else{
  205. dojo.style(this.containerNode, {
  206. width: w + "px",
  207. height: h + "px",
  208. overflow: "auto",
  209. position: "relative" // workaround IE bug moving scrollbar or dragging dialog
  210. });
  211. }
  212. }else{
  213. if(this._singleChild && this._singleChild.resize){
  214. this._singleChild.resize();
  215. }
  216. }
  217. },
  218. _position: function(){
  219. // summary:
  220. // Position modal dialog in the viewport. If no relative offset
  221. // in the viewport has been determined (by dragging, for instance),
  222. // center the node. Otherwise, use the Dialog's stored relative offset,
  223. // and position the node to top: left: values based on the viewport.
  224. // tags:
  225. // private
  226. if(!dojo.hasClass(dojo.body(), "dojoMove")){ // don't do anything if called during auto-scroll
  227. var node = this.domNode,
  228. viewport = dojo.window.getBox(),
  229. p = this._relativePosition,
  230. bb = p ? null : dojo._getBorderBox(node),
  231. l = Math.floor(viewport.l + (p ? p.x : (viewport.w - bb.w) / 2)),
  232. t = Math.floor(viewport.t + (p ? p.y : (viewport.h - bb.h) / 2))
  233. ;
  234. dojo.style(node,{
  235. left: l + "px",
  236. top: t + "px"
  237. });
  238. }
  239. },
  240. _onKey: function(/*Event*/ evt){
  241. // summary:
  242. // Handles the keyboard events for accessibility reasons
  243. // tags:
  244. // private
  245. if(evt.charOrCode){
  246. var dk = dojo.keys;
  247. var node = evt.target;
  248. if(evt.charOrCode === dk.TAB){
  249. this._getFocusItems(this.domNode);
  250. }
  251. var singleFocusItem = (this._firstFocusItem == this._lastFocusItem);
  252. // see if we are shift-tabbing from first focusable item on dialog
  253. if(node == this._firstFocusItem && evt.shiftKey && evt.charOrCode === dk.TAB){
  254. if(!singleFocusItem){
  255. dijit.focus(this._lastFocusItem); // send focus to last item in dialog
  256. }
  257. dojo.stopEvent(evt);
  258. }else if(node == this._lastFocusItem && evt.charOrCode === dk.TAB && !evt.shiftKey){
  259. if(!singleFocusItem){
  260. dijit.focus(this._firstFocusItem); // send focus to first item in dialog
  261. }
  262. dojo.stopEvent(evt);
  263. }else{
  264. // see if the key is for the dialog
  265. while(node){
  266. if(node == this.domNode || dojo.hasClass(node, "dijitPopup")){
  267. if(evt.charOrCode == dk.ESCAPE){
  268. this.onCancel();
  269. }else{
  270. return; // just let it go
  271. }
  272. }
  273. node = node.parentNode;
  274. }
  275. // this key is for the disabled document window
  276. if(evt.charOrCode !== dk.TAB){ // allow tabbing into the dialog for a11y
  277. dojo.stopEvent(evt);
  278. // opera won't tab to a div
  279. }else if(!dojo.isOpera){
  280. try{
  281. this._firstFocusItem.focus();
  282. }catch(e){ /*squelch*/ }
  283. }
  284. }
  285. }
  286. },
  287. show: function(){
  288. // summary:
  289. // Display the dialog
  290. // returns: dojo.Deferred
  291. // Deferred object that resolves when the display animation is complete
  292. if(this.open){ return; }
  293. if(!this._started){
  294. this.startup();
  295. }
  296. // first time we show the dialog, there's some initialization stuff to do
  297. if(!this._alreadyInitialized){
  298. this._setup();
  299. this._alreadyInitialized=true;
  300. }
  301. if(this._fadeOutDeferred){
  302. // There's a hide() operation in progress, so cancel it, but still call DialogLevelManager.hide()
  303. // as though the hide() completed, in preparation for the DialogLevelManager.show() call below.
  304. this._fadeOutDeferred.cancel();
  305. dijit._DialogLevelManager.hide(this);
  306. }
  307. this._modalconnects.push(dojo.connect(window, "onscroll", this, "layout"));
  308. this._modalconnects.push(dojo.connect(window, "onresize", this, function(){
  309. // IE gives spurious resize events and can actually get stuck
  310. // in an infinite loop if we don't ignore them
  311. var viewport = dojo.window.getBox();
  312. if(!this._oldViewport ||
  313. viewport.h != this._oldViewport.h ||
  314. viewport.w != this._oldViewport.w){
  315. this.layout();
  316. this._oldViewport = viewport;
  317. }
  318. }));
  319. this._modalconnects.push(dojo.connect(this.domNode, "onkeypress", this, "_onKey"));
  320. dojo.style(this.domNode, {
  321. opacity:0,
  322. display:""
  323. });
  324. this._set("open", true);
  325. this._onShow(); // lazy load trigger
  326. this._size();
  327. this._position();
  328. // fade-in Animation object, setup below
  329. var fadeIn;
  330. this._fadeInDeferred = new dojo.Deferred(dojo.hitch(this, function(){
  331. fadeIn.stop();
  332. delete this._fadeInDeferred;
  333. }));
  334. fadeIn = dojo.fadeIn({
  335. node: this.domNode,
  336. duration: this.duration,
  337. beforeBegin: dojo.hitch(this, function(){
  338. dijit._DialogLevelManager.show(this, this.underlayAttrs);
  339. }),
  340. onEnd: dojo.hitch(this, function(){
  341. if(this.autofocus && dijit._DialogLevelManager.isTop(this)){
  342. // find focusable items each time dialog is shown since if dialog contains a widget the
  343. // first focusable items can change
  344. this._getFocusItems(this.domNode);
  345. dijit.focus(this._firstFocusItem);
  346. }
  347. this._fadeInDeferred.callback(true);
  348. delete this._fadeInDeferred;
  349. })
  350. }).play();
  351. return this._fadeInDeferred;
  352. },
  353. hide: function(){
  354. // summary:
  355. // Hide the dialog
  356. // returns: dojo.Deferred
  357. // Deferred object that resolves when the hide animation is complete
  358. // If we haven't been initialized yet then we aren't showing and we can just return.
  359. // Likewise if we are already hidden, or are currently fading out.
  360. if(!this._alreadyInitialized || !this.open){
  361. return;
  362. }
  363. if(this._fadeInDeferred){
  364. this._fadeInDeferred.cancel();
  365. }
  366. // fade-in Animation object, setup below
  367. var fadeOut;
  368. this._fadeOutDeferred = new dojo.Deferred(dojo.hitch(this, function(){
  369. fadeOut.stop();
  370. delete this._fadeOutDeferred;
  371. }));
  372. fadeOut = dojo.fadeOut({
  373. node: this.domNode,
  374. duration: this.duration,
  375. onEnd: dojo.hitch(this, function(){
  376. this.domNode.style.display = "none";
  377. dijit._DialogLevelManager.hide(this);
  378. this.onHide();
  379. this._fadeOutDeferred.callback(true);
  380. delete this._fadeOutDeferred;
  381. })
  382. }).play();
  383. if(this._scrollConnected){
  384. this._scrollConnected = false;
  385. }
  386. dojo.forEach(this._modalconnects, dojo.disconnect);
  387. this._modalconnects = [];
  388. if(this._relativePosition){
  389. delete this._relativePosition;
  390. }
  391. this._set("open", false);
  392. return this._fadeOutDeferred;
  393. },
  394. layout: function(){
  395. // summary:
  396. // Position the Dialog and the underlay
  397. // tags:
  398. // private
  399. if(this.domNode.style.display != "none"){
  400. if(dijit._underlay){ // avoid race condition during show()
  401. dijit._underlay.layout();
  402. }
  403. this._position();
  404. }
  405. },
  406. destroy: function(){
  407. if(this._fadeInDeferred){
  408. this._fadeInDeferred.cancel();
  409. }
  410. if(this._fadeOutDeferred){
  411. this._fadeOutDeferred.cancel();
  412. }
  413. if(this._moveable){
  414. this._moveable.destroy();
  415. }
  416. dojo.forEach(this._modalconnects, dojo.disconnect);
  417. dijit._DialogLevelManager.hide(this);
  418. this.inherited(arguments);
  419. }
  420. }
  421. );
  422. dojo.declare(
  423. "dijit.Dialog",
  424. [dijit.layout.ContentPane, dijit._DialogBase],
  425. {}
  426. );
  427. dijit._DialogLevelManager = {
  428. // summary:
  429. // Controls the various active "levels" on the page, starting with the
  430. // stuff initially visible on the page (at z-index 0), and then having an entry for
  431. // each Dialog shown.
  432. show: function(/*dijit._Widget*/ dialog, /*Object*/ underlayAttrs){
  433. // summary:
  434. // Call right before fade-in animation for new dialog.
  435. // Saves current focus, displays/adjusts underlay for new dialog,
  436. // and sets the z-index of the dialog itself.
  437. //
  438. // New dialog will be displayed on top of all currently displayed dialogs.
  439. //
  440. // Caller is responsible for setting focus in new dialog after the fade-in
  441. // animation completes.
  442. var ds = dijit._dialogStack;
  443. // Save current focus
  444. ds[ds.length-1].focus = dijit.getFocus(dialog);
  445. // Display the underlay, or if already displayed then adjust for this new dialog
  446. var underlay = dijit._underlay;
  447. if(!underlay || underlay._destroyed){
  448. underlay = dijit._underlay = new dijit.DialogUnderlay(underlayAttrs);
  449. }else{
  450. underlay.set(dialog.underlayAttrs);
  451. }
  452. // Set z-index a bit above previous dialog
  453. var zIndex = ds[ds.length-1].dialog ? ds[ds.length-1].zIndex + 2 : 950;
  454. if(ds.length == 1){ // first dialog
  455. underlay.show();
  456. }
  457. dojo.style(dijit._underlay.domNode, 'zIndex', zIndex - 1);
  458. // Dialog
  459. dojo.style(dialog.domNode, 'zIndex', zIndex);
  460. ds.push({dialog: dialog, underlayAttrs: underlayAttrs, zIndex: zIndex});
  461. },
  462. hide: function(/*dijit._Widget*/ dialog){
  463. // summary:
  464. // Called when the specified dialog is hidden/destroyed, after the fade-out
  465. // animation ends, in order to reset page focus, fix the underlay, etc.
  466. // If the specified dialog isn't open then does nothing.
  467. //
  468. // Caller is responsible for either setting display:none on the dialog domNode,
  469. // or calling dijit.popup.hide(), or removing it from the page DOM.
  470. var ds = dijit._dialogStack;
  471. if(ds[ds.length-1].dialog == dialog){
  472. // Removing the top (or only) dialog in the stack, return focus
  473. // to previous dialog
  474. ds.pop();
  475. var pd = ds[ds.length-1]; // the new active dialog (or the base page itself)
  476. // Adjust underlay
  477. if(ds.length == 1){
  478. // Returning to original page.
  479. // Hide the underlay, unless the underlay widget has already been destroyed
  480. // because we are being called during page unload (when all widgets are destroyed)
  481. if(!dijit._underlay._destroyed){
  482. dijit._underlay.hide();
  483. }
  484. }else{
  485. // Popping back to previous dialog, adjust underlay
  486. dojo.style(dijit._underlay.domNode, 'zIndex', pd.zIndex - 1);
  487. dijit._underlay.set(pd.underlayAttrs);
  488. }
  489. // Adjust focus
  490. if(dialog.refocus){
  491. // If we are returning control to a previous dialog but for some reason
  492. // that dialog didn't have a focused field, set focus to first focusable item.
  493. // This situation could happen if two dialogs appeared at nearly the same time,
  494. // since a dialog doesn't set it's focus until the fade-in is finished.
  495. var focus = pd.focus;
  496. if(!focus || (pd.dialog && !dojo.isDescendant(focus.node, pd.dialog.domNode))){
  497. pd.dialog._getFocusItems(pd.dialog.domNode);
  498. focus = pd.dialog._firstFocusItem;
  499. }
  500. try{
  501. dijit.focus(focus);
  502. }catch(e){
  503. /* focus() will fail if user opened the dialog by clicking a non-focusable element */
  504. }
  505. }
  506. }else{
  507. // Removing a dialog out of order (#9944, #10705).
  508. // Don't need to mess with underlay or z-index or anything.
  509. var idx = dojo.indexOf(dojo.map(ds, function(elem){return elem.dialog}), dialog);
  510. if(idx != -1){
  511. ds.splice(idx, 1);
  512. }
  513. }
  514. },
  515. isTop: function(/*dijit._Widget*/ dialog){
  516. // summary:
  517. // Returns true if specified Dialog is the top in the task
  518. var ds = dijit._dialogStack;
  519. return ds[ds.length-1].dialog == dialog;
  520. }
  521. };
  522. // Stack representing the various active "levels" on the page, starting with the
  523. // stuff initially visible on the page (at z-index 0), and then having an entry for
  524. // each Dialog shown.
  525. // Each element in stack has form {
  526. // dialog: dialogWidget,
  527. // focus: returnFromGetFocus(),
  528. // underlayAttrs: attributes to set on underlay (when this widget is active)
  529. // }
  530. dijit._dialogStack = [
  531. {dialog: null, focus: null, underlayAttrs: null} // entry for stuff at z-index: 0
  532. ];
  533. }