Преглед изворни кода

Add polyfill

customisations
alemart пре 2 месеци
родитељ
комит
dcd59ffa5e
3 измењених фајлова са 1000 додато и 76 уклоњено
  1. 97
    76
      docs_overrides/index.html
  2. 866
    0
      docs_overrides/js/dialog-polyfill.js
  3. 37
    0
      docs_overrides/style/dialog-polyfill.css

+ 97
- 76
docs_overrides/index.html Прегледај датотеку

1
 {% extends "main.html" %}
1
 {% extends "main.html" %}
2
 
2
 
3
-{% block announce %}{% endblock %}
4
-{% block header %}{% endblock %}
5
-{% block tabs %}{% endblock %}
6
-{% block site_nav %}{% endblock %}
7
-{% block footer %}{% endblock %}
3
+{% block libs %}
4
+{{ super() }}
5
+<script src="js/dialog-polyfill.js"></script>
6
+{% endblock %}
8
 
7
 
9
-{% block content %}
8
+{% block scripts %}
9
+{{ super() }}
10
+<script>
11
+(function() {
12
+
13
+window.addEventListener('load', function() {
14
+  setupDialogPolyfill();
15
+  setupDialog();
16
+  setupReveal();
17
+});
18
+
19
+function setupDialogPolyfill()
20
+{
21
+  if(typeof HTMLDialogElement === 'undefined')
22
+    return;
23
+
24
+  document.querySelectorAll('dialog').forEach(function(dialog) {
25
+    dialogPolyfill.registerDialog(dialog);
26
+    dialog.classList.add('fixed');
27
+  });
28
+}
29
+
30
+function setupDialog()
31
+{
32
+  if(location.search.indexOf('checkout_session_id') < 0)
33
+    return;
34
+
35
+  try {
36
+    const searchParams = new URLSearchParams(location.search);
37
+    const sessionId = searchParams.get('checkout_session_id') || '';
38
+    const next = searchParams.get('next') || location.href;
39
+
40
+    const url = new URL(next);
41
+    url.searchParams.append('checkout_session_id', sessionId);
42
+
43
+    const button = document.getElementById('download-dialog-download');
44
+    const redirect = function() { location.href = url.href; };
45
+    button.onclick = redirect;
46
+    setTimeout(redirect, 3000);
47
+
48
+    const dialog = document.getElementById('download-dialog');
49
+    dialog.showModal();
50
+  }
51
+  catch(e) {
52
+    console.error(e);
53
+    alert(e.message);
54
+  }
55
+}
56
+
57
+function setupReveal()
58
+{
59
+  if(typeof IntersectionObserver === 'undefined')
60
+    return;
61
+
62
+  const observer = new IntersectionObserver(function(entries) {
63
+    for(const entry of entries) {
64
+      if(entry.intersectionRatio >= 0.125)
65
+        entry.target.classList.remove('unrevealed');
66
+      /*else if(entry.intersectionRatio == 0.0)
67
+        entry.target.classList.add('unrevealed');*/
68
+    }
69
+  }, { threshold: [0, 0.125] });
70
+
71
+  document.querySelectorAll('.reveal').forEach(function(reveal) {
72
+    reveal.classList.add('unrevealed');
73
+    observer.observe(reveal);
74
+  });
75
+}
76
+
77
+})();
78
+</script>
79
+{% endblock %}
80
+
81
+{% block styles %}
82
+{{ super() }}
83
+<link rel="stylesheet" href="style/dialog-polyfill.css">
10
 <style>
84
 <style>
11
 :root {
85
 :root {
12
   --max-width: 42rem;
86
   --max-width: 42rem;
424
   #download article footer > * { margin: 0; }
498
   #download article footer > * { margin: 0; }
425
 }
499
 }
426
 </style>
500
 </style>
427
-<script>
428
-(function() {
429
-
430
-window.addEventListener('load', function() {
431
-  setupDialog();
432
-  setupReveal();
433
-});
434
-
435
-function setupDialog()
436
-{
437
-  if(location.search.indexOf('checkout_session_id') < 0)
438
-    return;
439
-
440
-  try {
441
-    const searchParams = new URLSearchParams(location.search);
442
-    const sessionId = searchParams.get('checkout_session_id') || '';
443
-    const next = searchParams.get('next') || location.href;
444
-
445
-    const url = new URL(next);
446
-    url.searchParams.append('checkout_session_id', sessionId);
447
-
448
-    const button = document.getElementById('download-dialog-download');
449
-    const redirect = function() { location.href = url.href; };
450
-    button.onclick = redirect;
451
-    setTimeout(redirect, 3000);
452
-
453
-    const dialog = document.getElementById('download-dialog');
454
-    dialog.showModal();
455
-  }
456
-  catch(e) {
457
-    console.error(e);
458
-    alert(e.message);
459
-  }
460
-}
461
-
462
-function setupReveal()
463
-{
464
-  if(typeof IntersectionObserver === 'undefined')
465
-    return;
466
-
467
-  const observer = new IntersectionObserver(function(entries) {
468
-    for(const entry of entries) {
469
-      if(entry.intersectionRatio >= 0.125)
470
-        entry.target.classList.remove('unrevealed');
471
-      /*else if(entry.intersectionRatio == 0.0)
472
-        entry.target.classList.add('unrevealed');*/
473
-    }
474
-  }, { threshold: [0, 0.125] });
475
-
476
-  document.querySelectorAll('.reveal').forEach(function(reveal) {
477
-    reveal.classList.add('unrevealed');
478
-    observer.observe(reveal);
479
-  });
480
-}
481
-
482
-})();
483
-</script>
484
-
485
-
486
-
501
+{% endblock %}
487
 
502
 
503
+{% block content %}
488
 <header id="title">
504
 <header id="title">
489
   <h1>encantar.js</h1>
505
   <h1>encantar.js</h1>
490
   <h2>High performance Web AR framework &mdash; no app required!</h2>
506
   <h2>High performance Web AR framework &mdash; no app required!</h2>
590
 
606
 
591
 
607
 
592
 
608
 
609
+<footer class="md-footer">
610
+  <div class="md-copyright">
611
+    encantar.js: GPU-accelerated Augmented Reality framework for the web. Copyright &copy; 2022 &ndash; present Alexandre Martins
612
+  </div>
613
+</footer>
614
+
615
+
616
+
617
+
593
 <dialog id="download-dialog">
618
 <dialog id="download-dialog">
594
   <form method="dialog">
619
   <form method="dialog">
595
     <h1>Thank you for your interest in encantar.js</h1>
620
     <h1>Thank you for your interest in encantar.js</h1>
634
   </form>
659
   </form>
635
   <button id="donation-dialog-close" onclick="document.getElementById('donation-dialog').close()">&times;</button>
660
   <button id="donation-dialog-close" onclick="document.getElementById('donation-dialog').close()">&times;</button>
636
 </dialog>
661
 </dialog>
637
-
638
-
639
-
640
-
641
-<footer class="md-footer">
642
-  <div class="md-copyright">
643
-    encantar.js: GPU-accelerated Augmented Reality framework for the web. Copyright &copy; 2022 &ndash; present Alexandre Martins
644
-  </div>
645
-</footer>
646
-
647
 {% endblock %}
662
 {% endblock %}
663
+
664
+{% block announce %}{% endblock %}
665
+{% block header %}{% endblock %}
666
+{% block tabs %}{% endblock %}
667
+{% block site_nav %}{% endblock %}
668
+{% block footer %}{% endblock %}

+ 866
- 0
docs_overrides/js/dialog-polyfill.js Прегледај датотеку

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

+ 37
- 0
docs_overrides/style/dialog-polyfill.css Прегледај датотеку

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
+}

Loading…
Откажи
Сачувај