index.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. // nb. This is for IE10 and lower _only_.
  2. var supportCustomEvent = window.CustomEvent;
  3. if (!supportCustomEvent || typeof supportCustomEvent === 'object') {
  4. supportCustomEvent = function CustomEvent(event, x) {
  5. x = x || {};
  6. var ev = document.createEvent('CustomEvent');
  7. ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
  8. return ev;
  9. };
  10. supportCustomEvent.prototype = window.Event.prototype;
  11. }
  12. /**
  13. * Dispatches the passed event to both an "on<type>" handler as well as via the
  14. * normal dispatch operation. Does not bubble.
  15. *
  16. * @param {!EventTarget} target
  17. * @param {!Event} event
  18. * @return {boolean}
  19. */
  20. function safeDispatchEvent(target, event) {
  21. var check = 'on' + event.type.toLowerCase();
  22. if (typeof target[check] === 'function') {
  23. target[check](event);
  24. }
  25. return target.dispatchEvent(event);
  26. }
  27. /**
  28. * @param {Element} el to check for stacking context
  29. * @return {boolean} whether this el or its parents creates a stacking context
  30. */
  31. function createsStackingContext(el) {
  32. while (el && el !== document.body) {
  33. var s = window.getComputedStyle(el);
  34. var invalid = function(k, ok) {
  35. return !(s[k] === undefined || s[k] === ok);
  36. };
  37. if (s.opacity < 1 ||
  38. invalid('zIndex', 'auto') ||
  39. invalid('transform', 'none') ||
  40. invalid('mixBlendMode', 'normal') ||
  41. invalid('filter', 'none') ||
  42. invalid('perspective', 'none') ||
  43. s['isolation'] === 'isolate' ||
  44. s.position === 'fixed' ||
  45. s.webkitOverflowScrolling === 'touch') {
  46. return true;
  47. }
  48. el = el.parentElement;
  49. }
  50. return false;
  51. }
  52. /**
  53. * Finds the nearest <dialog> from the passed element.
  54. *
  55. * @param {Element} el to search from
  56. * @return {HTMLDialogElement} dialog found
  57. */
  58. function findNearestDialog(el) {
  59. while (el) {
  60. if (el.localName === 'dialog') {
  61. return /** @type {HTMLDialogElement} */ (el);
  62. }
  63. if (el.parentElement) {
  64. el = el.parentElement;
  65. } else if (el.parentNode) {
  66. el = el.parentNode.host;
  67. } else {
  68. el = null;
  69. }
  70. }
  71. return null;
  72. }
  73. /**
  74. * Blur the specified element, as long as it's not the HTML body element.
  75. * This works around an IE9/10 bug - blurring the body causes Windows to
  76. * blur the whole application.
  77. *
  78. * @param {Element} el to blur
  79. */
  80. function safeBlur(el) {
  81. // Find the actual focused element when the active element is inside a shadow root
  82. while (el && el.shadowRoot && el.shadowRoot.activeElement) {
  83. el = el.shadowRoot.activeElement;
  84. }
  85. if (el && el.blur && el !== document.body) {
  86. el.blur();
  87. }
  88. }
  89. /**
  90. * @param {!NodeList} nodeList to search
  91. * @param {Node} node to find
  92. * @return {boolean} whether node is inside nodeList
  93. */
  94. function inNodeList(nodeList, node) {
  95. for (var i = 0; i < nodeList.length; ++i) {
  96. if (nodeList[i] === node) {
  97. return true;
  98. }
  99. }
  100. return false;
  101. }
  102. /**
  103. * @param {HTMLFormElement} el to check
  104. * @return {boolean} whether this form has method="dialog"
  105. */
  106. function isFormMethodDialog(el) {
  107. if (!el || !el.hasAttribute('method')) {
  108. return false;
  109. }
  110. return el.getAttribute('method').toLowerCase() === 'dialog';
  111. }
  112. /**
  113. * @param {!DocumentFragment|!Element} hostElement
  114. * @return {?Element}
  115. */
  116. function findFocusableElementWithin(hostElement) {
  117. // Note that this is 'any focusable area'. This list is probably not exhaustive, but the
  118. // alternative involves stepping through and trying to focus everything.
  119. var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
  120. var query = opts.map(function(el) {
  121. return el + ':not([disabled])';
  122. });
  123. // TODO(samthor): tabindex values that are not numeric are not focusable.
  124. query.push('[tabindex]:not([disabled]):not([tabindex=""])'); // tabindex != "", not disabled
  125. var target = hostElement.querySelector(query.join(', '));
  126. if (!target && 'attachShadow' in Element.prototype) {
  127. // If we haven't found a focusable target, see if the host element contains an element
  128. // which has a shadowRoot.
  129. // Recursively search for the first focusable item in shadow roots.
  130. var elems = hostElement.querySelectorAll('*');
  131. for (var i = 0; i < elems.length; i++) {
  132. if (elems[i].tagName && elems[i].shadowRoot) {
  133. target = findFocusableElementWithin(elems[i].shadowRoot);
  134. if (target) {
  135. break;
  136. }
  137. }
  138. }
  139. }
  140. return target;
  141. }
  142. /**
  143. * Determines if an element is attached to the DOM.
  144. * @param {Element} element to check
  145. * @return {boolean} whether the element is in DOM
  146. */
  147. function isConnected(element) {
  148. return element.isConnected || document.body.contains(element);
  149. }
  150. /**
  151. * @param {!Event} event
  152. * @return {?Element}
  153. */
  154. function findFormSubmitter(event) {
  155. if (event.submitter) {
  156. return event.submitter;
  157. }
  158. var form = event.target;
  159. if (!(form instanceof HTMLFormElement)) {
  160. return null;
  161. }
  162. var submitter = dialogPolyfill.formSubmitter;
  163. if (!submitter) {
  164. var target = event.target;
  165. var root = ('getRootNode' in target && target.getRootNode() || document);
  166. submitter = root.activeElement;
  167. }
  168. if (!submitter || submitter.form !== form) {
  169. return null;
  170. }
  171. return submitter;
  172. }
  173. /**
  174. * @param {!Event} event
  175. */
  176. function maybeHandleSubmit(event) {
  177. if (event.defaultPrevented) {
  178. return;
  179. }
  180. var form = /** @type {!HTMLFormElement} */ (event.target);
  181. // We'd have a value if we clicked on an imagemap.
  182. var value = dialogPolyfill.imagemapUseValue;
  183. var submitter = findFormSubmitter(event);
  184. if (value === null && submitter) {
  185. value = submitter.value;
  186. }
  187. // There should always be a dialog as this handler is added specifically on them, but check just
  188. // in case.
  189. var dialog = findNearestDialog(form);
  190. if (!dialog) {
  191. return;
  192. }
  193. // Prefer formmethod on the button.
  194. var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
  195. if (formmethod !== 'dialog') {
  196. return;
  197. }
  198. event.preventDefault();
  199. if (value != null) {
  200. // nb. we explicitly check against null/undefined
  201. dialog.close(value);
  202. } else {
  203. dialog.close();
  204. }
  205. }
  206. /**
  207. * @param {!HTMLDialogElement} dialog to upgrade
  208. * @constructor
  209. */
  210. function dialogPolyfillInfo(dialog) {
  211. this.dialog_ = dialog;
  212. this.replacedStyleTop_ = false;
  213. this.openAsModal_ = false;
  214. // Set a11y role. Browsers that support dialog implicitly know this already.
  215. if (!dialog.hasAttribute('role')) {
  216. dialog.setAttribute('role', 'dialog');
  217. }
  218. dialog.show = this.show.bind(this);
  219. dialog.showModal = this.showModal.bind(this);
  220. dialog.close = this.close.bind(this);
  221. dialog.addEventListener('submit', maybeHandleSubmit, false);
  222. if (!('returnValue' in dialog)) {
  223. dialog.returnValue = '';
  224. }
  225. if ('MutationObserver' in window) {
  226. var mo = new MutationObserver(this.maybeHideModal.bind(this));
  227. mo.observe(dialog, {attributes: true, attributeFilter: ['open']});
  228. } else {
  229. // IE10 and below support. Note that DOMNodeRemoved etc fire _before_ removal. They also
  230. // seem to fire even if the element was removed as part of a parent removal. Use the removed
  231. // events to force downgrade (useful if removed/immediately added).
  232. var removed = false;
  233. var cb = function() {
  234. removed ? this.downgradeModal() : this.maybeHideModal();
  235. removed = false;
  236. }.bind(this);
  237. var timeout;
  238. var delayModel = function(ev) {
  239. if (ev.target !== dialog) { return; } // not for a child element
  240. var cand = 'DOMNodeRemoved';
  241. removed |= (ev.type.substr(0, cand.length) === cand);
  242. window.clearTimeout(timeout);
  243. timeout = window.setTimeout(cb, 0);
  244. };
  245. ['DOMAttrModified', 'DOMNodeRemoved', 'DOMNodeRemovedFromDocument'].forEach(function(name) {
  246. dialog.addEventListener(name, delayModel);
  247. });
  248. }
  249. // Note that the DOM is observed inside DialogManager while any dialog
  250. // is being displayed as a modal, to catch modal removal from the DOM.
  251. Object.defineProperty(dialog, 'open', {
  252. set: this.setOpen.bind(this),
  253. get: dialog.hasAttribute.bind(dialog, 'open')
  254. });
  255. this.backdrop_ = document.createElement('div');
  256. this.backdrop_.className = 'backdrop';
  257. this.backdrop_.addEventListener('mouseup' , this.backdropMouseEvent_.bind(this));
  258. this.backdrop_.addEventListener('mousedown', this.backdropMouseEvent_.bind(this));
  259. this.backdrop_.addEventListener('click' , this.backdropMouseEvent_.bind(this));
  260. }
  261. dialogPolyfillInfo.prototype = /** @type {HTMLDialogElement.prototype} */ ({
  262. get dialog() {
  263. return this.dialog_;
  264. },
  265. /**
  266. * Maybe remove this dialog from the modal top layer. This is called when
  267. * a modal dialog may no longer be tenable, e.g., when the dialog is no
  268. * longer open or is no longer part of the DOM.
  269. */
  270. maybeHideModal: function() {
  271. if (this.dialog_.hasAttribute('open') && isConnected(this.dialog_)) { return; }
  272. this.downgradeModal();
  273. },
  274. /**
  275. * Remove this dialog from the modal top layer, leaving it as a non-modal.
  276. */
  277. downgradeModal: function() {
  278. if (!this.openAsModal_) { return; }
  279. this.openAsModal_ = false;
  280. this.dialog_.style.zIndex = '';
  281. // This won't match the native <dialog> exactly because if the user set top on a centered
  282. // polyfill dialog, that top gets thrown away when the dialog is closed. Not sure it's
  283. // possible to polyfill this perfectly.
  284. if (this.replacedStyleTop_) {
  285. this.dialog_.style.top = '';
  286. this.replacedStyleTop_ = false;
  287. }
  288. // Clear the backdrop and remove from the manager.
  289. this.backdrop_.parentNode && this.backdrop_.parentNode.removeChild(this.backdrop_);
  290. dialogPolyfill.dm.removeDialog(this);
  291. },
  292. /**
  293. * @param {boolean} value whether to open or close this dialog
  294. */
  295. setOpen: function(value) {
  296. if (value) {
  297. this.dialog_.hasAttribute('open') || this.dialog_.setAttribute('open', '');
  298. } else {
  299. this.dialog_.removeAttribute('open');
  300. this.maybeHideModal(); // nb. redundant with MutationObserver
  301. }
  302. },
  303. /**
  304. * Handles mouse events ('mouseup', 'mousedown', 'click') on the fake .backdrop element, redirecting them as if
  305. * they were on the dialog itself.
  306. *
  307. * @param {!Event} e to redirect
  308. */
  309. backdropMouseEvent_: function(e) {
  310. if (!this.dialog_.hasAttribute('tabindex')) {
  311. // Clicking on the backdrop should move the implicit cursor, even if dialog cannot be
  312. // focused. Create a fake thing to focus on. If the backdrop was _before_ the dialog, this
  313. // would not be needed - clicks would move the implicit cursor there.
  314. var fake = document.createElement('div');
  315. this.dialog_.insertBefore(fake, this.dialog_.firstChild);
  316. fake.tabIndex = -1;
  317. fake.focus();
  318. this.dialog_.removeChild(fake);
  319. } else {
  320. this.dialog_.focus();
  321. }
  322. var redirectedEvent = document.createEvent('MouseEvents');
  323. redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
  324. e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
  325. e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
  326. this.dialog_.dispatchEvent(redirectedEvent);
  327. e.stopPropagation();
  328. },
  329. /**
  330. * Focuses on the first focusable element within the dialog. This will always blur the current
  331. * focus, even if nothing within the dialog is found.
  332. */
  333. focus_: function() {
  334. // Find element with `autofocus` attribute, or fall back to the first form/tabindex control.
  335. var target = this.dialog_.querySelector('[autofocus]:not([disabled])');
  336. if (!target && this.dialog_.tabIndex >= 0) {
  337. target = this.dialog_;
  338. }
  339. if (!target) {
  340. target = findFocusableElementWithin(this.dialog_);
  341. }
  342. safeBlur(document.activeElement);
  343. target && target.focus();
  344. },
  345. /**
  346. * Sets the zIndex for the backdrop and dialog.
  347. *
  348. * @param {number} dialogZ
  349. * @param {number} backdropZ
  350. */
  351. updateZIndex: function(dialogZ, backdropZ) {
  352. if (dialogZ < backdropZ) {
  353. throw new Error('dialogZ should never be < backdropZ');
  354. }
  355. this.dialog_.style.zIndex = dialogZ;
  356. this.backdrop_.style.zIndex = backdropZ;
  357. },
  358. /**
  359. * Shows the dialog. If the dialog is already open, this does nothing.
  360. */
  361. show: function() {
  362. if (!this.dialog_.open) {
  363. this.setOpen(true);
  364. this.focus_();
  365. }
  366. },
  367. /**
  368. * Show this dialog modally.
  369. */
  370. showModal: function() {
  371. if (this.dialog_.hasAttribute('open')) {
  372. throw new Error('Failed to execute \'showModal\' on dialog: The element is already open, and therefore cannot be opened modally.');
  373. }
  374. if (!isConnected(this.dialog_)) {
  375. throw new Error('Failed to execute \'showModal\' on dialog: The element is not in a Document.');
  376. }
  377. if (!dialogPolyfill.dm.pushDialog(this)) {
  378. throw new Error('Failed to execute \'showModal\' on dialog: There are too many open modal dialogs.');
  379. }
  380. if (createsStackingContext(this.dialog_.parentElement)) {
  381. console.warn('A dialog is being shown inside a stacking context. ' +
  382. 'This may cause it to be unusable. For more information, see this link: ' +
  383. 'https://github.com/GoogleChrome/dialog-polyfill/#stacking-context');
  384. }
  385. this.setOpen(true);
  386. this.openAsModal_ = true;
  387. // Optionally center vertically, relative to the current viewport.
  388. if (dialogPolyfill.needsCentering(this.dialog_)) {
  389. dialogPolyfill.reposition(this.dialog_);
  390. this.replacedStyleTop_ = true;
  391. } else {
  392. this.replacedStyleTop_ = false;
  393. }
  394. // Insert backdrop.
  395. this.dialog_.parentNode.insertBefore(this.backdrop_, this.dialog_.nextSibling);
  396. // Focus on whatever inside the dialog.
  397. this.focus_();
  398. },
  399. /**
  400. * Closes this HTMLDialogElement. This is optional vs clearing the open
  401. * attribute, however this fires a 'close' event.
  402. *
  403. * @param {string=} opt_returnValue to use as the returnValue
  404. */
  405. close: function(opt_returnValue) {
  406. if (!this.dialog_.hasAttribute('open')) {
  407. throw new Error('Failed to execute \'close\' on dialog: The element does not have an \'open\' attribute, and therefore cannot be closed.');
  408. }
  409. this.setOpen(false);
  410. // Leave returnValue untouched in case it was set directly on the element
  411. if (opt_returnValue !== undefined) {
  412. this.dialog_.returnValue = opt_returnValue;
  413. }
  414. // Triggering "close" event for any attached listeners on the <dialog>.
  415. var closeEvent = new supportCustomEvent('close', {
  416. bubbles: false,
  417. cancelable: false
  418. });
  419. safeDispatchEvent(this.dialog_, closeEvent);
  420. }
  421. });
  422. var dialogPolyfill = {};
  423. dialogPolyfill.reposition = function(element) {
  424. var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  425. var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
  426. element.style.top = Math.max(scrollTop, topValue) + 'px';
  427. };
  428. dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
  429. for (var i = 0; i < document.styleSheets.length; ++i) {
  430. var styleSheet = document.styleSheets[i];
  431. var cssRules = null;
  432. // Some browsers throw on cssRules.
  433. try {
  434. cssRules = styleSheet.cssRules;
  435. } catch (e) {}
  436. if (!cssRules) { continue; }
  437. for (var j = 0; j < cssRules.length; ++j) {
  438. var rule = cssRules[j];
  439. var selectedNodes = null;
  440. // Ignore errors on invalid selector texts.
  441. try {
  442. selectedNodes = document.querySelectorAll(rule.selectorText);
  443. } catch(e) {}
  444. if (!selectedNodes || !inNodeList(selectedNodes, element)) {
  445. continue;
  446. }
  447. var cssTop = rule.style.getPropertyValue('top');
  448. var cssBottom = rule.style.getPropertyValue('bottom');
  449. if ((cssTop && cssTop !== 'auto') || (cssBottom && cssBottom !== 'auto')) {
  450. return true;
  451. }
  452. }
  453. }
  454. return false;
  455. };
  456. dialogPolyfill.needsCentering = function(dialog) {
  457. var computedStyle = window.getComputedStyle(dialog);
  458. if (computedStyle.position !== 'absolute') {
  459. return false;
  460. }
  461. // We must determine whether the top/bottom specified value is non-auto. In
  462. // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
  463. // Firefox returns the used value. So we do this crazy thing instead: check
  464. // the inline style and then go through CSS rules.
  465. if ((dialog.style.top !== 'auto' && dialog.style.top !== '') ||
  466. (dialog.style.bottom !== 'auto' && dialog.style.bottom !== '')) {
  467. return false;
  468. }
  469. return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
  470. };
  471. /**
  472. * @param {!Element} element to force upgrade
  473. */
  474. dialogPolyfill.forceRegisterDialog = function(element) {
  475. if (window.HTMLDialogElement || element.showModal) {
  476. console.warn('This browser already supports <dialog>, the polyfill ' +
  477. 'may not work correctly', element);
  478. }
  479. if (element.localName !== 'dialog') {
  480. throw new Error('Failed to register dialog: The element is not a dialog.');
  481. }
  482. new dialogPolyfillInfo(/** @type {!HTMLDialogElement} */ (element));
  483. };
  484. /**
  485. * @param {!Element} element to upgrade, if necessary
  486. */
  487. dialogPolyfill.registerDialog = function(element) {
  488. if (!element.showModal) {
  489. dialogPolyfill.forceRegisterDialog(element);
  490. }
  491. };
  492. /**
  493. * @constructor
  494. */
  495. dialogPolyfill.DialogManager = function() {
  496. /** @type {!Array<!dialogPolyfillInfo>} */
  497. this.pendingDialogStack = [];
  498. var checkDOM = this.checkDOM_.bind(this);
  499. // The overlay is used to simulate how a modal dialog blocks the document.
  500. // The blocking dialog is positioned on top of the overlay, and the rest of
  501. // the dialogs on the pending dialog stack are positioned below it. In the
  502. // actual implementation, the modal dialog stacking is controlled by the
  503. // top layer, where z-index has no effect.
  504. this.overlay = document.createElement('div');
  505. this.overlay.className = '_dialog_overlay';
  506. this.overlay.addEventListener('click', function(e) {
  507. this.forwardTab_ = undefined;
  508. e.stopPropagation();
  509. checkDOM([]); // sanity-check DOM
  510. }.bind(this));
  511. this.handleKey_ = this.handleKey_.bind(this);
  512. this.handleFocus_ = this.handleFocus_.bind(this);
  513. this.zIndexLow_ = 100000;
  514. this.zIndexHigh_ = 100000 + 150;
  515. this.forwardTab_ = undefined;
  516. if ('MutationObserver' in window) {
  517. this.mo_ = new MutationObserver(function(records) {
  518. var removed = [];
  519. records.forEach(function(rec) {
  520. for (var i = 0, c; c = rec.removedNodes[i]; ++i) {
  521. if (!(c instanceof Element)) {
  522. continue;
  523. } else if (c.localName === 'dialog') {
  524. removed.push(c);
  525. }
  526. removed = removed.concat(c.querySelectorAll('dialog'));
  527. }
  528. });
  529. removed.length && checkDOM(removed);
  530. });
  531. }
  532. };
  533. /**
  534. * Called on the first modal dialog being shown. Adds the overlay and related
  535. * handlers.
  536. */
  537. dialogPolyfill.DialogManager.prototype.blockDocument = function() {
  538. document.documentElement.addEventListener('focus', this.handleFocus_, true);
  539. document.addEventListener('keydown', this.handleKey_);
  540. this.mo_ && this.mo_.observe(document, {childList: true, subtree: true});
  541. };
  542. /**
  543. * Called on the first modal dialog being removed, i.e., when no more modal
  544. * dialogs are visible.
  545. */
  546. dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
  547. document.documentElement.removeEventListener('focus', this.handleFocus_, true);
  548. document.removeEventListener('keydown', this.handleKey_);
  549. this.mo_ && this.mo_.disconnect();
  550. };
  551. /**
  552. * Updates the stacking of all known dialogs.
  553. */
  554. dialogPolyfill.DialogManager.prototype.updateStacking = function() {
  555. var zIndex = this.zIndexHigh_;
  556. for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
  557. dpi.updateZIndex(--zIndex, --zIndex);
  558. if (i === 0) {
  559. this.overlay.style.zIndex = --zIndex;
  560. }
  561. }
  562. // Make the overlay a sibling of the dialog itself.
  563. var last = this.pendingDialogStack[0];
  564. if (last) {
  565. var p = last.dialog.parentNode || document.body;
  566. p.appendChild(this.overlay);
  567. } else if (this.overlay.parentNode) {
  568. this.overlay.parentNode.removeChild(this.overlay);
  569. }
  570. };
  571. /**
  572. * @param {Element} candidate to check if contained or is the top-most modal dialog
  573. * @return {boolean} whether candidate is contained in top dialog
  574. */
  575. dialogPolyfill.DialogManager.prototype.containedByTopDialog_ = function(candidate) {
  576. while (candidate = findNearestDialog(candidate)) {
  577. for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
  578. if (dpi.dialog === candidate) {
  579. return i === 0; // only valid if top-most
  580. }
  581. }
  582. candidate = candidate.parentElement;
  583. }
  584. return false;
  585. };
  586. dialogPolyfill.DialogManager.prototype.handleFocus_ = function(event) {
  587. var target = event.composedPath ? event.composedPath()[0] : event.target;
  588. if (this.containedByTopDialog_(target)) { return; }
  589. if (document.activeElement === document.documentElement) { return; }
  590. event.preventDefault();
  591. event.stopPropagation();
  592. safeBlur(/** @type {Element} */ (target));
  593. if (this.forwardTab_ === undefined) { return; } // move focus only from a tab key
  594. var dpi = this.pendingDialogStack[0];
  595. var dialog = dpi.dialog;
  596. var position = dialog.compareDocumentPosition(target);
  597. if (position & Node.DOCUMENT_POSITION_PRECEDING) {
  598. if (this.forwardTab_) {
  599. // forward
  600. dpi.focus_();
  601. } else if (target !== document.documentElement) {
  602. // backwards if we're not already focused on <html>
  603. document.documentElement.focus();
  604. }
  605. } else {
  606. // TODO: Focus after the dialog, is ignored.
  607. }
  608. return false;
  609. };
  610. dialogPolyfill.DialogManager.prototype.handleKey_ = function(event) {
  611. this.forwardTab_ = undefined;
  612. if (event.keyCode === 27) {
  613. event.preventDefault();
  614. event.stopPropagation();
  615. var cancelEvent = new supportCustomEvent('cancel', {
  616. bubbles: false,
  617. cancelable: true
  618. });
  619. var dpi = this.pendingDialogStack[0];
  620. if (dpi && safeDispatchEvent(dpi.dialog, cancelEvent)) {
  621. dpi.dialog.close();
  622. }
  623. } else if (event.keyCode === 9) {
  624. this.forwardTab_ = !event.shiftKey;
  625. }
  626. };
  627. /**
  628. * Finds and downgrades any known modal dialogs that are no longer displayed. Dialogs that are
  629. * removed and immediately readded don't stay modal, they become normal.
  630. *
  631. * @param {!Array<!HTMLDialogElement>} removed that have definitely been removed
  632. */
  633. dialogPolyfill.DialogManager.prototype.checkDOM_ = function(removed) {
  634. // This operates on a clone because it may cause it to change. Each change also calls
  635. // updateStacking, which only actually needs to happen once. But who removes many modal dialogs
  636. // at a time?!
  637. var clone = this.pendingDialogStack.slice();
  638. clone.forEach(function(dpi) {
  639. if (removed.indexOf(dpi.dialog) !== -1) {
  640. dpi.downgradeModal();
  641. } else {
  642. dpi.maybeHideModal();
  643. }
  644. });
  645. };
  646. /**
  647. * @param {!dialogPolyfillInfo} dpi
  648. * @return {boolean} whether the dialog was allowed
  649. */
  650. dialogPolyfill.DialogManager.prototype.pushDialog = function(dpi) {
  651. var allowed = (this.zIndexHigh_ - this.zIndexLow_) / 2 - 1;
  652. if (this.pendingDialogStack.length >= allowed) {
  653. return false;
  654. }
  655. if (this.pendingDialogStack.unshift(dpi) === 1) {
  656. this.blockDocument();
  657. }
  658. this.updateStacking();
  659. return true;
  660. };
  661. /**
  662. * @param {!dialogPolyfillInfo} dpi
  663. */
  664. dialogPolyfill.DialogManager.prototype.removeDialog = function(dpi) {
  665. var index = this.pendingDialogStack.indexOf(dpi);
  666. if (index === -1) { return; }
  667. this.pendingDialogStack.splice(index, 1);
  668. if (this.pendingDialogStack.length === 0) {
  669. this.unblockDocument();
  670. }
  671. this.updateStacking();
  672. };
  673. dialogPolyfill.dm = new dialogPolyfill.DialogManager();
  674. dialogPolyfill.formSubmitter = null;
  675. dialogPolyfill.imagemapUseValue = null;
  676. /**
  677. * Installs global handlers, such as click listers and native method overrides. These are needed
  678. * even if a no dialog is registered, as they deal with <form method="dialog">.
  679. */
  680. if (window.HTMLDialogElement === undefined) {
  681. /**
  682. * If HTMLFormElement translates method="DIALOG" into 'get', then replace the descriptor with
  683. * one that returns the correct value.
  684. */
  685. var testForm = document.createElement('form');
  686. testForm.setAttribute('method', 'dialog');
  687. if (testForm.method !== 'dialog') {
  688. var methodDescriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'method');
  689. if (methodDescriptor) {
  690. // nb. Some older iOS and older PhantomJS fail to return the descriptor. Don't do anything
  691. // and don't bother to update the element.
  692. var realGet = methodDescriptor.get;
  693. methodDescriptor.get = function() {
  694. if (isFormMethodDialog(this)) {
  695. return 'dialog';
  696. }
  697. return realGet.call(this);
  698. };
  699. var realSet = methodDescriptor.set;
  700. /** @this {HTMLElement} */
  701. methodDescriptor.set = function(v) {
  702. if (typeof v === 'string' && v.toLowerCase() === 'dialog') {
  703. return this.setAttribute('method', v);
  704. }
  705. return realSet.call(this, v);
  706. };
  707. Object.defineProperty(HTMLFormElement.prototype, 'method', methodDescriptor);
  708. }
  709. }
  710. /**
  711. * Global 'click' handler, to capture the <input type="submit"> or <button> element which has
  712. * submitted a <form method="dialog">. Needed as Safari and others don't report this inside
  713. * document.activeElement.
  714. */
  715. document.addEventListener('click', function(ev) {
  716. dialogPolyfill.formSubmitter = null;
  717. dialogPolyfill.imagemapUseValue = null;
  718. if (ev.defaultPrevented) { return; } // e.g. a submit which prevents default submission
  719. var target = /** @type {Element} */ (ev.target);
  720. if ('composedPath' in ev) {
  721. var path = ev.composedPath();
  722. target = path.shift() || target;
  723. }
  724. if (!target || !isFormMethodDialog(target.form)) { return; }
  725. var valid = (target.type === 'submit' && ['button', 'input'].indexOf(target.localName) > -1);
  726. if (!valid) {
  727. if (!(target.localName === 'input' && target.type === 'image')) { return; }
  728. // this is a <input type="image">, which can submit forms
  729. dialogPolyfill.imagemapUseValue = ev.offsetX + ',' + ev.offsetY;
  730. }
  731. var dialog = findNearestDialog(target);
  732. if (!dialog) { return; }
  733. dialogPolyfill.formSubmitter = target;
  734. }, false);
  735. /**
  736. * Global 'submit' handler. This handles submits of `method="dialog"` which are invalid, i.e.,
  737. * outside a dialog. They get prevented.
  738. */
  739. document.addEventListener('submit', function(ev) {
  740. var form = ev.target;
  741. var dialog = findNearestDialog(form);
  742. if (dialog) {
  743. return; // ignore, handle there
  744. }
  745. var submitter = findFormSubmitter(ev);
  746. var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
  747. if (formmethod === 'dialog') {
  748. ev.preventDefault();
  749. }
  750. });
  751. /**
  752. * Replace the native HTMLFormElement.submit() method, as it won't fire the
  753. * submit event and give us a chance to respond.
  754. */
  755. var nativeFormSubmit = HTMLFormElement.prototype.submit;
  756. var replacementFormSubmit = function () {
  757. if (!isFormMethodDialog(this)) {
  758. return nativeFormSubmit.call(this);
  759. }
  760. var dialog = findNearestDialog(this);
  761. dialog && dialog.close();
  762. };
  763. HTMLFormElement.prototype.submit = replacementFormSubmit;
  764. }
  765. export default dialogPolyfill;