Include polyfill css

3e465d155cb544d5d1d246a032bc0eafd84a9b0e

QuantumEntangledAndy <sheepchaan@gmail.com>

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