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