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