Source: js/app_modal_dialog.js

  1. /* global BaseUI */
  2. 'use strict';
  3. (function(exports) {
  4. var _ = navigator.mozL10n.get;
  5. var _id = 0;
  6. /**
  7. * The ModalDialog of the AppWindow.
  8. *
  9. * Including **alert**, **prompt**, **confirm**, and
  10. * **single select** dialogs.
  11. *
  12. * @class AppModalDialog
  13. * @param {AppWindow} app The app window instance
  14. * where this dialog should popup.
  15. * @extends BaseUI
  16. */
  17. var AppModalDialog = function AppModalDialog(app) {
  18. this.app = app;
  19. this.containerElement = app.element;
  20. this.events = [];
  21. // One to one mapping.
  22. this.instanceID = _id++;
  23. this._injected = false;
  24. this._visible = false;
  25. app.element.addEventListener('mozbrowsershowmodalprompt', this);
  26. return this;
  27. };
  28. AppModalDialog.prototype = Object.create(BaseUI.prototype);
  29. AppModalDialog.prototype.CLASS_NAME = 'AppModalDialog';
  30. AppModalDialog.prototype.ELEMENT_PREFIX = 'modal-dialog-';
  31. AppModalDialog.prototype.customID = function amd_customID() {
  32. if (this.app) {
  33. return '[' + this.app.origin + ']';
  34. } else {
  35. return '';
  36. }
  37. };
  38. AppModalDialog.prototype.handleEvent = function amd_handleEvent(evt) {
  39. this.app.debug('handling ' + evt.type);
  40. evt.preventDefault();
  41. evt.stopPropagation();
  42. this.events.push(evt);
  43. this.menuHeight = 0;
  44. if (!this._injected) {
  45. this.render();
  46. }
  47. this.show();
  48. this._injected = true;
  49. };
  50. AppModalDialog.prototype.isVisible = function amd_isVisible() {
  51. return this._visible;
  52. };
  53. AppModalDialog.prototype._fetchElements = function amd__fetchElements() {
  54. this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
  55. this.elements = {};
  56. var toCamelCase = function toCamelCase(str) {
  57. return str.replace(/\-(.)/g, function replacer(str, p1) {
  58. return p1.toUpperCase();
  59. });
  60. };
  61. this.elementClasses = ['alert', 'alert-ok', 'alert-message',
  62. 'prompt', 'prompt-ok', 'prompt-cancel', 'prompt-input', 'prompt-message',
  63. 'confirm', 'confirm-ok', 'confirm-cancel', 'confirm-message',
  64. 'select-one', 'select-one-cancel', 'select-one-menu', 'select-one-title',
  65. 'alert-title', 'confirm-title', 'prompt-title',
  66. 'custom-prompt', 'custom-prompt-message', 'custom-prompt-buttons',
  67. 'custom-prompt-checkbox'];
  68. // Loop and add element with camel style name to Modal Dialog attribute.
  69. this.elementClasses.forEach(function createElementRef(name) {
  70. this.elements[toCamelCase(name)] =
  71. this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
  72. }, this);
  73. this.elements.menu = this.element.querySelector('menu');
  74. };
  75. AppModalDialog.prototype._registerEvents = function amd__registerEvents() {
  76. var elements = this.elements;
  77. for (var id in elements) {
  78. var tagName = elements[id].tagName.toLowerCase();
  79. if (tagName == 'button' || tagName == 'ul') {
  80. if (elements[id].classList.contains('confirm')) {
  81. elements[id].addEventListener('click',
  82. this.confirmHandler.bind(this));
  83. } else if (elements[id].classList.contains('cancel')) {
  84. elements[id].addEventListener('click', this.cancelHandler.bind(this));
  85. }
  86. }
  87. }
  88. };
  89. AppModalDialog.prototype.view = function amd_view() {
  90. var id = this.CLASS_NAME + this.instanceID;
  91. return `<div class="modal-dialog" id="${id}">
  92. <form class="modal-dialog-alert generic-dialog"
  93. role="dialog" tabindex="-1">
  94. <div class="modal-dialog-message-container inner">
  95. <h3 class="modal-dialog-alert-title"></h3>
  96. <p>
  97. <span class="modal-dialog-alert-message"></span>
  98. </p>
  99. </div>
  100. <menu>
  101. <button class="modal-dialog-alert-ok confirm affirmative"
  102. data-l10n-id="ok"></button>
  103. </menu>
  104. </form>
  105. <form class="modal-dialog-confirm generic-dialog"
  106. role="dialog" tabindex="-1">
  107. <div class="modal-dialog-message-container inner">
  108. <h3 class="modal-dialog-confirm-title"></h3>
  109. <p>
  110. <span class="modal-dialog-confirm-message"></span>
  111. </p>
  112. </div>
  113. <menu data-items="2">
  114. <button class="modal-dialog-confirm-cancel cancel"
  115. data-l10n-id="cancel"></button>
  116. <button class="modal-dialog-confirm-ok confirm affirmative"
  117. data-l10n-id="ok"></button>
  118. </menu>
  119. </form>
  120. <form class="modal-dialog-prompt generic-dialog"
  121. role="dialog" tabindex="-1">
  122. <div class="modal-dialog-message-container inner">
  123. <h3 class="modal-dialog-prompt-title"></h3>
  124. <p>
  125. <span class="modal-dialog-prompt-message"></span>
  126. <input class="modal-dialog-prompt-input" />
  127. </p>
  128. </div>
  129. <menu data-items="2">
  130. <button class="modal-dialog-prompt-cancel cancel"
  131. data-l10n-id="cancel"></button>
  132. <button class="modal-dialog-prompt-ok confirm affirmative"
  133. data-l10n-id="ok"></button>
  134. </menu>
  135. </form>
  136. <form class="modal-dialog-select-one generic-dialog"
  137. role="dialog" tabindex="-1">
  138. <div class="modal-dialog-message-container inner">
  139. <h3 class="modal-dialog-select-one-title"></h3>
  140. <ul class="modal-dialog-select-one-menu"></ul>
  141. </div>
  142. <menu>
  143. <button class="modal-dialog-select-one-cancel cancel"
  144. data-l10n-id="cancel"></button>
  145. </menu>
  146. </form>
  147. <form class="modal-dialog-custom-prompt generic-dialog"
  148. role="dialog" tabindex="-1">
  149. <div class="modal-dialog-message-container inner">
  150. <h3 class="modal-dialog-custom-prompt-title"></h3>
  151. <p class="modal-dialog-custom-prompt-message"></p>
  152. <label class="pack-checkbox">
  153. <input class="modal-dialog-custom-prompt-checkbox"
  154. type="checkbox"/>
  155. <span></span>
  156. </label>
  157. </div>
  158. <menu class="modal-dialog-custom-prompt-buttons"></menu>
  159. </form>
  160. </div>`;
  161. };
  162. AppModalDialog.prototype.processNextEvent = function amd_processNextEvent() {
  163. this.events.splice(0, 1);
  164. if (this.events.length) {
  165. this.show();
  166. } else {
  167. this.hide();
  168. }
  169. };
  170. AppModalDialog.prototype.kill = function amd_kill() {
  171. this._visible = false;
  172. this.containerElement.removeChild(this.element);
  173. };
  174. // Show relative dialog and set message/input value well
  175. AppModalDialog.prototype.show = function amd_show() {
  176. if (!this.events.length) {
  177. return;
  178. }
  179. this._visible = true;
  180. var evt = this.events[0];
  181. var message = evt.detail.message || '';
  182. var title = this._getTitle(evt.detail.title);
  183. var elements = this.elements;
  184. var type = evt.detail.promptType || evt.detail.type;
  185. switch (type) {
  186. case 'alert':
  187. elements.alertTitle.textContent = title;
  188. elements.alertMessage.textContent = message;
  189. elements.alert.classList.add('visible');
  190. elements.alertOk.textContent = evt.yesText ? evt.yesText : _('ok');
  191. elements.alert.focus();
  192. this.updateMaxHeight();
  193. break;
  194. case 'prompt':
  195. elements.prompt.classList.add('visible');
  196. elements.promptInput.value = evt.detail.initialValue;
  197. elements.promptTitle.textContent = title;
  198. elements.promptMessage.textContent = message;
  199. elements.promptOk.textContent = evt.yesText ? evt.yesText : _('ok');
  200. elements.promptCancel.textContent = evt.noText ?
  201. evt.noText : _('cancel');
  202. elements.prompt.focus();
  203. break;
  204. case 'confirm':
  205. elements.confirm.classList.add('visible');
  206. elements.confirmTitle.textContent = title;
  207. elements.confirmMessage.textContent = message;
  208. elements.confirmOk.textContent = evt.yesText ? evt.yesText : _('ok');
  209. elements.confirmCancel.textContent = evt.noText ?
  210. evt.noText : _('cancel');
  211. elements.confirm.focus();
  212. break;
  213. case 'selectone':
  214. this.buildSelectOneDialog(message);
  215. elements.selectOne.classList.add('visible');
  216. elements.selectOne.focus();
  217. break;
  218. case 'custom-prompt':
  219. var customPrompt = evt.detail;
  220. elements.customPrompt.classList.add('visible');
  221. elements.customPromptMessage.textContent = customPrompt.message;
  222. // Display custom list of buttons
  223. elements.customPromptButtons.innerHTML = '';
  224. elements.customPromptButtons.setAttribute('data-items',
  225. customPrompt.buttons.length);
  226. var domElement = null;
  227. for (var i = customPrompt.buttons.length - 1; i >= 0; i--) {
  228. var button = customPrompt.buttons[i];
  229. domElement = document.createElement('button');
  230. domElement.dataset.buttonIndex = i;
  231. if (button.messageType === 'builtin') {
  232. domElement.setAttribute('data-l10n-id', button.message);
  233. } else if (button.messageType === 'custom') {
  234. // For custom button, we assume that the text is already translated
  235. domElement.textContent = button.message;
  236. } else {
  237. console.error('Unexpected button type : ' + button.messageType);
  238. continue;
  239. }
  240. domElement.addEventListener('click', this.confirmHandler.bind(this));
  241. elements.customPromptButtons.appendChild(domElement);
  242. }
  243. domElement.classList.add('affirmative');
  244. // Eventualy display a checkbox:
  245. var checkbox = elements.customPromptCheckbox;
  246. if (customPrompt.showCheckbox) {
  247. if (customPrompt.checkboxCheckedByDefault) {
  248. checkbox.setAttribute('checked', 'true');
  249. } else {
  250. checkbox.removeAttribute('checked');
  251. }
  252. // We assume that checkbox custom message is already translated
  253. checkbox.nextElementSibling.textContent =
  254. customPrompt.checkboxMessage;
  255. } else {
  256. checkbox.parentNode.classList.add('hidden');
  257. }
  258. elements.customPrompt.focus();
  259. break;
  260. }
  261. this.app.browser.element.setAttribute('aria-hidden', true);
  262. this.element.classList.add('visible');
  263. };
  264. AppModalDialog.prototype.hide = function amd_hide() {
  265. this._visible = false;
  266. this.element.blur();
  267. this.app.browser.element.removeAttribute('aria-hidden');
  268. this.element.classList.remove('visible');
  269. if (this.app) {
  270. this.app.focus();
  271. }
  272. if (!this.events.length) {
  273. return;
  274. }
  275. var evt = this.events[0];
  276. var type = evt.detail.promptType || evt.detail.type;
  277. if (type === 'prompt') {
  278. this.elements.promptInput.blur();
  279. }
  280. this.elements[type].classList.remove('visible');
  281. };
  282. // When user clicks OK button on alert/confirm/prompt
  283. AppModalDialog.prototype.confirmHandler =
  284. function amd_confirmHandler(clickEvt) {
  285. if (!this.events.length) {
  286. return;
  287. }
  288. clickEvt.preventDefault();
  289. var elements = this.elements;
  290. var evt = this.events[0];
  291. var type = evt.detail.promptType || evt.detail.type;
  292. switch (type) {
  293. case 'alert':
  294. elements.alert.classList.remove('visible');
  295. break;
  296. case 'prompt':
  297. evt.detail.returnValue = elements.promptInput.value;
  298. elements.prompt.classList.remove('visible');
  299. break;
  300. case 'confirm':
  301. evt.detail.returnValue = true;
  302. elements.confirm.classList.remove('visible');
  303. break;
  304. case 'custom-prompt':
  305. var returnValue = {
  306. selectedButton: clickEvt.target.dataset.buttonIndex
  307. };
  308. if (evt.detail.showCheckbox) {
  309. returnValue.checked = elements.customPromptCheckbox.checked;
  310. }
  311. evt.detail.returnValue = returnValue;
  312. elements.customPrompt.classList.remove('visible');
  313. break;
  314. }
  315. if (evt.detail.unblock) {
  316. evt.detail.unblock();
  317. }
  318. this.processNextEvent();
  319. };
  320. // When user clicks cancel button on confirm/prompt or
  321. // when the user try to escape the dialog with the escape key
  322. AppModalDialog.prototype.cancelHandler =
  323. function amd_cancelHandler(clickEvt) {
  324. if (!this.events.length) {
  325. return;
  326. }
  327. clickEvt.preventDefault();
  328. var evt = this.events[0];
  329. var elements = this.elements;
  330. var type = evt.detail.promptType || evt.detail.type;
  331. switch (type) {
  332. case 'alert':
  333. elements.alert.classList.remove('visible');
  334. break;
  335. case 'prompt':
  336. /* return null when click cancel */
  337. evt.detail.returnValue = null;
  338. elements.prompt.classList.remove('visible');
  339. break;
  340. case 'confirm':
  341. /* return false when click cancel */
  342. evt.detail.returnValue = false;
  343. elements.confirm.classList.remove('visible');
  344. break;
  345. case 'selectone':
  346. /* return null when click cancel */
  347. evt.detail.returnValue = null;
  348. elements.selectOne.classList.remove('visible');
  349. break;
  350. }
  351. if (evt.detail.unblock) {
  352. evt.detail.unblock();
  353. }
  354. this.processNextEvent();
  355. };
  356. // When user selects an option on selectone dialog
  357. AppModalDialog.prototype.selectOneHandler =
  358. function amd_selectOneHandler(target) {
  359. if (!this.events.length) {
  360. return;
  361. }
  362. var elements = this.elements;
  363. var evt = this.events[0];
  364. evt.detail.returnValue = target.id;
  365. elements.selectOne.classList.remove('visible');
  366. if (evt.detail.unblock) {
  367. evt.detail.unblock();
  368. }
  369. this.processNextEvent();
  370. };
  371. AppModalDialog.prototype.updateMaxHeight = function() {
  372. // Setting maxHeight for being able to scroll long
  373. // texts: formHeight - menuHeight - titleHeight - margin
  374. // We should fix this in a common way for all the dialogs
  375. // in the building blocks layer: Bug 1096902
  376. this.menuHeight = this.menuHeight || this.elements.menu.offsetHeight;
  377. var messageHeight = this.element.offsetHeight - this.menuHeight;
  378. messageHeight -= this.elements.alertTitle.offsetHeight;
  379. var margin = window.getComputedStyle(this.elements.alertTitle).marginBottom;
  380. var messageContainer = this.elements.alert.querySelector('.inner p');
  381. var calc = 'calc(' + messageHeight + 'px - ' + margin + ')';
  382. messageContainer.style.maxHeight = calc;
  383. };
  384. AppModalDialog.prototype._getTitle =
  385. function amd__getTitle(title) {
  386. //
  387. // XXX Bug 982006, subsystems like uriloader still report errors with
  388. // titles which are important to the user for context in diagnosing
  389. // issues.
  390. //
  391. // However, we will ignore all titles containing a URL using the app
  392. // protocol. These types of titles simply indicate that the active
  393. // application is prompting and are more confusing to the user than
  394. // useful. Instead we will return the application name if there is one
  395. // or an empty string.
  396. //
  397. if (!title ||
  398. title.includes('app://')) {
  399. return this.app.name || '';
  400. }
  401. return title;
  402. };
  403. exports.AppModalDialog = AppModalDialog;
  404. }(window));