dialog.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /**
  2. * Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
  3. * Available via the MIT license.
  4. * see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
  5. */
  6. /**
  7. * The dialog module enables the display of message boxes, custom modal dialogs and other overlays or slide-out UI abstractions. Dialogs are constructed by the composition system which interacts with a user defined dialog context. The dialog module enforced the activator lifecycle.
  8. * @module dialog
  9. * @requires system
  10. * @requires app
  11. * @requires composition
  12. * @requires activator
  13. * @requires viewEngine
  14. * @requires jquery
  15. * @requires knockout
  16. */
  17. define(['durandal/system', 'durandal/app', 'durandal/composition', 'durandal/activator', 'durandal/viewEngine', 'jquery', 'knockout'], function (system, app, composition, activator, viewEngine, $, ko) {
  18. var contexts = {},
  19. dialogCount = ko.observable(0),
  20. dialog;
  21. /**
  22. * Models a message box's message, title and options.
  23. * @class MessageBox
  24. */
  25. var MessageBox = function (message, title, options, autoclose, settings) {
  26. this.message = message;
  27. this.title = title || MessageBox.defaultTitle;
  28. this.options = options || MessageBox.defaultOptions;
  29. this.autoclose = autoclose || false;
  30. this.settings = $.extend({}, MessageBox.defaultSettings, settings);
  31. };
  32. /**
  33. * Selects an option and closes the message box, returning the selected option through the dialog system's promise.
  34. * @method selectOption
  35. * @param {string} dialogResult The result to select.
  36. */
  37. MessageBox.prototype.selectOption = function (dialogResult) {
  38. dialog.close(this, dialogResult);
  39. };
  40. /**
  41. * Provides the view to the composition system.
  42. * @method getView
  43. * @return {DOMElement} The view of the message box.
  44. */
  45. MessageBox.prototype.getView = function () {
  46. return viewEngine.processMarkup(MessageBox.defaultViewMarkup);
  47. };
  48. /**
  49. * Configures a custom view to use when displaying message boxes.
  50. * @method setViewUrl
  51. * @param {string} viewUrl The view url relative to the base url which the view locator will use to find the message box's view.
  52. * @static
  53. */
  54. MessageBox.setViewUrl = function (viewUrl) {
  55. delete MessageBox.prototype.getView;
  56. MessageBox.prototype.viewUrl = viewUrl;
  57. };
  58. /**
  59. * The title to be used for the message box if one is not provided.
  60. * @property {string} defaultTitle
  61. * @default Application
  62. * @static
  63. */
  64. MessageBox.defaultTitle = app.title || 'Application';
  65. /**
  66. * The options to display in the message box if none are specified.
  67. * @property {string[]} defaultOptions
  68. * @default ['Ok']
  69. * @static
  70. */
  71. MessageBox.defaultOptions = ['Ok'];
  72. MessageBox.defaultSettings = { buttonClass: "btn btn-default", primaryButtonClass: "btn-primary autofocus", secondaryButtonClass: "", "class": "modal-content messageBox", style: null };
  73. /**
  74. * Sets the classes and styles used throughout the message box markup.
  75. * @method setDefaults
  76. * @param {object} settings A settings object containing the following optional properties: buttonClass, primaryButtonClass, secondaryButtonClass, class, style.
  77. */
  78. MessageBox.setDefaults = function (settings) {
  79. $.extend(MessageBox.defaultSettings, settings);
  80. };
  81. MessageBox.prototype.getButtonClass = function ($index) {
  82. var c = "";
  83. if (this.settings) {
  84. if (this.settings.buttonClass) {
  85. c = this.settings.buttonClass;
  86. }
  87. if ($index() === 0 && this.settings.primaryButtonClass) {
  88. if (c.length > 0) {
  89. c += " ";
  90. }
  91. c += this.settings.primaryButtonClass;
  92. }
  93. if ($index() > 0 && this.settings.secondaryButtonClass) {
  94. if (c.length > 0) {
  95. c += " ";
  96. }
  97. c += this.settings.secondaryButtonClass;
  98. }
  99. }
  100. return c;
  101. };
  102. MessageBox.prototype.getClass = function () {
  103. if (this.settings) {
  104. return this.settings["class"];
  105. }
  106. return "messageBox";
  107. };
  108. MessageBox.prototype.getStyle = function () {
  109. if (this.settings) {
  110. return this.settings.style;
  111. }
  112. return null;
  113. };
  114. MessageBox.prototype.getButtonText = function (stringOrObject) {
  115. var t = $.type(stringOrObject);
  116. if (t === "string") {
  117. return stringOrObject;
  118. }
  119. else if (t === "object") {
  120. if ($.type(stringOrObject.text) === "string") {
  121. return stringOrObject.text;
  122. } else {
  123. system.error('The object for a MessageBox button does not have a text property that is a string.');
  124. return null;
  125. }
  126. }
  127. system.error('Object for a MessageBox button is not a string or object but ' + t + '.');
  128. return null;
  129. };
  130. MessageBox.prototype.getButtonValue = function (stringOrObject) {
  131. var t = $.type(stringOrObject);
  132. if (t === "string") {
  133. return stringOrObject;
  134. }
  135. else if (t === "object") {
  136. if ($.type(stringOrObject.text) === "undefined") {
  137. system.error('The object for a MessageBox button does not have a value property defined.');
  138. return null;
  139. } else {
  140. return stringOrObject.value;
  141. }
  142. }
  143. system.error('Object for a MessageBox button is not a string or object but ' + t + '.');
  144. return null;
  145. };
  146. /**
  147. * The markup for the message box's view.
  148. * @property {string} defaultViewMarkup
  149. * @static
  150. */
  151. MessageBox.defaultViewMarkup = [
  152. '<div data-view="plugins/messageBox" data-bind="css: getClass(), style: getStyle()">',
  153. '<div class="modal-header">',
  154. '<h3 data-bind="html: title"></h3>',
  155. '</div>',
  156. '<div class="modal-body">',
  157. '<p class="message" data-bind="html: message"></p>',
  158. '</div>',
  159. '<div class="modal-footer">',
  160. '<!-- ko foreach: options -->',
  161. '<button data-bind="click: function () { $parent.selectOption($parent.getButtonValue($data)); }, text: $parent.getButtonText($data), css: $parent.getButtonClass($index)"></button>',
  162. '<!-- /ko -->',
  163. '<div style="clear:both;"></div>',
  164. '</div>',
  165. '</div>'
  166. ].join('\n');
  167. function ensureDialogInstance(objOrModuleId) {
  168. return system.defer(function (dfd) {
  169. if (system.isString(objOrModuleId)) {
  170. system.acquire(objOrModuleId).then(function (module) {
  171. dfd.resolve(system.resolveObject(module));
  172. }).fail(function (err) {
  173. system.error('Failed to load dialog module (' + objOrModuleId + '). Details: ' + err.message);
  174. });
  175. } else {
  176. dfd.resolve(objOrModuleId);
  177. }
  178. }).promise();
  179. }
  180. /**
  181. * @class DialogModule
  182. * @static
  183. */
  184. dialog = {
  185. /**
  186. * The constructor function used to create message boxes.
  187. * @property {MessageBox} MessageBox
  188. */
  189. MessageBox: MessageBox,
  190. /**
  191. * The css zIndex that the last dialog was displayed at.
  192. * @property {number} currentZIndex
  193. */
  194. currentZIndex: 1050,
  195. /**
  196. * Gets the next css zIndex at which a dialog should be displayed.
  197. * @method getNextZIndex
  198. * @return {number} The next usable zIndex.
  199. */
  200. getNextZIndex: function () {
  201. return ++this.currentZIndex;
  202. },
  203. /**
  204. * Determines whether or not there are any dialogs open.
  205. * @method isOpen
  206. * @return {boolean} True if a dialog is open. false otherwise.
  207. */
  208. isOpen: ko.computed(function() {
  209. return dialogCount() > 0;
  210. }),
  211. /**
  212. * Gets the dialog context by name or returns the default context if no name is specified.
  213. * @method getContext
  214. * @param {string} [name] The name of the context to retrieve.
  215. * @return {DialogContext} True context.
  216. */
  217. getContext: function (name) {
  218. return contexts[name || 'default'];
  219. },
  220. /**
  221. * Adds (or replaces) a dialog context.
  222. * @method addContext
  223. * @param {string} name The name of the context to add.
  224. * @param {DialogContext} dialogContext The context to add.
  225. */
  226. addContext: function (name, dialogContext) {
  227. dialogContext.name = name;
  228. contexts[name] = dialogContext;
  229. var helperName = 'show' + name.substr(0, 1).toUpperCase() + name.substr(1);
  230. this[helperName] = function (obj, activationData) {
  231. return this.show(obj, activationData, name);
  232. };
  233. },
  234. createCompositionSettings: function (obj, dialogContext) {
  235. var settings = {
  236. model: obj,
  237. activate: false,
  238. transition: false
  239. };
  240. if (dialogContext.binding) {
  241. settings.binding = dialogContext.binding;
  242. }
  243. if (dialogContext.attached) {
  244. settings.attached = dialogContext.attached;
  245. }
  246. if (dialogContext.compositionComplete) {
  247. settings.compositionComplete = dialogContext.compositionComplete;
  248. }
  249. return settings;
  250. },
  251. /**
  252. * Gets the dialog model that is associated with the specified object.
  253. * @method getDialog
  254. * @param {object} obj The object for whom to retrieve the dialog.
  255. * @return {Dialog} The dialog model.
  256. */
  257. getDialog: function (obj) {
  258. if (obj) {
  259. return obj.__dialog__;
  260. }
  261. return undefined;
  262. },
  263. /**
  264. * Closes the dialog associated with the specified object.
  265. * @method close
  266. * @param {object} obj The object whose dialog should be closed.
  267. * @param {object} results* The results to return back to the dialog caller after closing.
  268. */
  269. close: function (obj) {
  270. var theDialog = this.getDialog(obj);
  271. if (theDialog) {
  272. var rest = Array.prototype.slice.call(arguments, 1);
  273. theDialog.close.apply(theDialog, rest);
  274. }
  275. },
  276. /**
  277. * Shows a dialog.
  278. * @method show
  279. * @param {object|string} obj The object (or moduleId) to display as a dialog.
  280. * @param {object} [activationData] The data that should be passed to the object upon activation.
  281. * @param {string} [context] The name of the dialog context to use. Uses the default context if none is specified.
  282. * @return {Promise} A promise that resolves when the dialog is closed and returns any data passed at the time of closing.
  283. */
  284. show: function (obj, activationData, context) {
  285. var that = this;
  286. var dialogContext = contexts[context || 'default'];
  287. return system.defer(function (dfd) {
  288. ensureDialogInstance(obj).then(function (instance) {
  289. var dialogActivator = activator.create();
  290. dialogActivator.activateItem(instance, activationData).then(function (success) {
  291. if (success) {
  292. var theDialog = instance.__dialog__ = {
  293. owner: instance,
  294. context: dialogContext,
  295. activator: dialogActivator,
  296. close: function () {
  297. var args = arguments;
  298. dialogActivator.deactivateItem(instance, true).then(function (closeSuccess) {
  299. if (closeSuccess) {
  300. dialogCount(dialogCount() - 1);
  301. dialogContext.removeHost(theDialog);
  302. delete instance.__dialog__;
  303. if (args.length === 0) {
  304. dfd.resolve();
  305. } else if (args.length === 1) {
  306. dfd.resolve(args[0]);
  307. } else {
  308. dfd.resolve.apply(dfd, args);
  309. }
  310. }
  311. });
  312. }
  313. };
  314. theDialog.settings = that.createCompositionSettings(instance, dialogContext);
  315. dialogContext.addHost(theDialog);
  316. dialogCount(dialogCount() + 1);
  317. composition.compose(theDialog.host, theDialog.settings);
  318. } else {
  319. dfd.resolve(false);
  320. }
  321. });
  322. });
  323. }).promise();
  324. },
  325. /**
  326. * Shows a message box.
  327. * @method showMessage
  328. * @param {string} message The message to display in the dialog.
  329. * @param {string} [title] The title message.
  330. * @param {string[]} [options] The options to provide to the user.
  331. * @param {boolean} [autoclose] Automatically close the the message box when clicking outside?
  332. * @param {Object} [settings] Custom settings for this instance of the messsage box, used to change classes and styles.
  333. * @return {Promise} A promise that resolves when the message box is closed and returns the selected option.
  334. */
  335. showMessage: function (message, title, options, autoclose, settings) {
  336. if (system.isString(this.MessageBox)) {
  337. return dialog.show(this.MessageBox, [
  338. message,
  339. title || MessageBox.defaultTitle,
  340. options || MessageBox.defaultOptions,
  341. autoclose || false,
  342. settings || {}
  343. ]);
  344. }
  345. return dialog.show(new this.MessageBox(message, title, options, autoclose, settings));
  346. },
  347. /**
  348. * Installs this module into Durandal; called by the framework. Adds `app.showDialog` and `app.showMessage` convenience methods.
  349. * @method install
  350. * @param {object} [config] Add a `messageBox` property to supply a custom message box constructor. Add a `messageBoxView` property to supply custom view markup for the built-in message box. You can also use messageBoxViewUrl to specify the view url.
  351. */
  352. install: function (config) {
  353. app.showDialog = function (obj, activationData, context) {
  354. return dialog.show(obj, activationData, context);
  355. };
  356. app.closeDialog = function () {
  357. return dialog.close.apply(dialog, arguments);
  358. };
  359. app.showMessage = function (message, title, options, autoclose, settings) {
  360. return dialog.showMessage(message, title, options, autoclose, settings);
  361. };
  362. if (config.messageBox) {
  363. dialog.MessageBox = config.messageBox;
  364. }
  365. if (config.messageBoxView) {
  366. dialog.MessageBox.prototype.getView = function () {
  367. return viewEngine.processMarkup(config.messageBoxView);
  368. };
  369. }
  370. if (config.messageBoxViewUrl) {
  371. dialog.MessageBox.setViewUrl(config.messageBoxViewUrl);
  372. }
  373. }
  374. };
  375. /**
  376. * @class DialogContext
  377. */
  378. dialog.addContext('default', {
  379. blockoutOpacity: 0.2,
  380. removeDelay: 200,
  381. /**
  382. * In this function, you are expected to add a DOM element to the tree which will serve as the "host" for the modal's composed view. You must add a property called host to the modalWindow object which references the dom element. It is this host which is passed to the composition module.
  383. * @method addHost
  384. * @param {Dialog} theDialog The dialog model.
  385. */
  386. addHost: function (theDialog) {
  387. var body = $('body');
  388. var blockout = $('<div class="modalBlockout"></div>')
  389. .css({ 'z-index': dialog.getNextZIndex(), 'opacity': this.blockoutOpacity })
  390. .appendTo(body);
  391. var host = $('<div class="modalHost"></div>')
  392. .css({ 'z-index': dialog.getNextZIndex() })
  393. .appendTo(body);
  394. theDialog.host = host.get(0);
  395. theDialog.blockout = blockout.get(0);
  396. if (!dialog.isOpen()) {
  397. theDialog.oldBodyMarginRight = body.css("margin-right");
  398. theDialog.oldInlineMarginRight = body.get(0).style.marginRight;
  399. var html = $("html");
  400. var oldBodyOuterWidth = body.outerWidth(true);
  401. var oldScrollTop = html.scrollTop();
  402. $("html").css("overflow-y", "hidden");
  403. var newBodyOuterWidth = $("body").outerWidth(true);
  404. body.css("margin-right", (newBodyOuterWidth - oldBodyOuterWidth + parseInt(theDialog.oldBodyMarginRight, 10)) + "px");
  405. html.scrollTop(oldScrollTop); // necessary for Firefox
  406. }
  407. },
  408. /**
  409. * This function is expected to remove any DOM machinery associated with the specified dialog and do any other necessary cleanup.
  410. * @method removeHost
  411. * @param {Dialog} theDialog The dialog model.
  412. */
  413. removeHost: function (theDialog) {
  414. $(theDialog.host).css('opacity', 0);
  415. $(theDialog.blockout).css('opacity', 0);
  416. setTimeout(function () {
  417. ko.removeNode(theDialog.host);
  418. ko.removeNode(theDialog.blockout);
  419. }, this.removeDelay);
  420. if (!dialog.isOpen()) {
  421. var html = $("html");
  422. var oldScrollTop = html.scrollTop(); // necessary for Firefox.
  423. html.css("overflow-y", "").scrollTop(oldScrollTop);
  424. if (theDialog.oldInlineMarginRight) {
  425. $("body").css("margin-right", theDialog.oldBodyMarginRight);
  426. } else {
  427. $("body").css("margin-right", '');
  428. }
  429. }
  430. },
  431. attached: function (view) {
  432. //To prevent flickering in IE8, we set visibility to hidden first, and later restore it
  433. $(view).css("visibility", "hidden");
  434. },
  435. /**
  436. * This function is called after the modal is fully composed into the DOM, allowing your implementation to do any final modifications, such as positioning or animation. You can obtain the original dialog object by using `getDialog` on context.model.
  437. * @method compositionComplete
  438. * @param {DOMElement} child The dialog view.
  439. * @param {DOMElement} parent The parent view.
  440. * @param {object} context The composition context.
  441. */
  442. compositionComplete: function (child, parent, context) {
  443. var theDialog = dialog.getDialog(context.model);
  444. var $child = $(child);
  445. var loadables = $child.find("img").filter(function () {
  446. //Remove images with known width and height
  447. var $this = $(this);
  448. return !(this.style.width && this.style.height) && !($this.attr("width") && $this.attr("height"));
  449. });
  450. $child.data("predefinedWidth", $child.get(0).style.width);
  451. var setDialogPosition = function (childView, objDialog) {
  452. //Setting a short timeout is need in IE8, otherwise we could do this straight away
  453. setTimeout(function () {
  454. var $childView = $(childView);
  455. objDialog.context.reposition(childView);
  456. $(objDialog.host).css('opacity', 1);
  457. $childView.css("visibility", "visible");
  458. $childView.find('.autofocus').first().focus();
  459. }, 1);
  460. };
  461. setDialogPosition(child, theDialog);
  462. loadables.load(function () {
  463. setDialogPosition(child, theDialog);
  464. });
  465. if ($child.hasClass('autoclose') || context.model.autoclose) {
  466. $(theDialog.blockout).click(function () {
  467. theDialog.close();
  468. });
  469. }
  470. },
  471. /**
  472. * This function is called to reposition the model view.
  473. * @method reposition
  474. * @param {DOMElement} view The dialog view.
  475. */
  476. reposition: function (view) {
  477. var $view = $(view),
  478. $window = $(window);
  479. //We will clear and then set width for dialogs without width set
  480. if (!$view.data("predefinedWidth")) {
  481. $view.css({ width: '' }); //Reset width
  482. }
  483. var width = $view.outerWidth(false),
  484. height = $view.outerHeight(false),
  485. windowHeight = $window.height() - 10, //leave at least 10 pixels free
  486. windowWidth = $window.width() - 10, //leave at least 10 pixels free
  487. constrainedHeight = Math.min(height, windowHeight),
  488. constrainedWidth = Math.min(width, windowWidth);
  489. $view.css({
  490. 'margin-top': (-constrainedHeight / 2).toString() + 'px',
  491. 'margin-left': (-constrainedWidth / 2).toString() + 'px'
  492. });
  493. if (height > windowHeight) {
  494. $view.css("overflow-y", "auto").outerHeight(windowHeight);
  495. } else {
  496. $view.css({
  497. "overflow-y": "",
  498. "height": ""
  499. });
  500. }
  501. if (width > windowWidth) {
  502. $view.css("overflow-x", "auto").outerWidth(windowWidth);
  503. } else {
  504. $view.css("overflow-x", "");
  505. if (!$view.data("predefinedWidth")) {
  506. //Ensure the correct width after margin-left has been set
  507. $view.outerWidth(constrainedWidth);
  508. } else {
  509. $view.css("width", $view.data("predefinedWidth"));
  510. }
  511. }
  512. }
  513. });
  514. return dialog;
  515. });