From 7429e58f8a738e294460e676d0e0373b87c864e6 Mon Sep 17 00:00:00 2001 From: Matt Falkenhagen Date: Fri, 26 Jul 2013 10:29:52 +0900 Subject: [PATCH] Initial commit --- LICENSE | 27 +++ README.md | 42 +++- dialog-polyfill.css | 16 ++ dialog-polyfill.js | 185 +++++++++++++++++ tests/backdrop.html | 43 ++++ tests/dialog-centering.html | 46 +++++ tests/dialog-recentering.html | 65 ++++++ tests/fancy-modal-dialog.html | 272 +++++++++++++++++++++++++ tests/modal-dialog-stacking.html | 89 ++++++++ tests/modal-dialog.html | 45 ++++ tests/resources/close_dialog.png | Bin 0 -> 140 bytes tests/resources/close_dialog_hover.png | Bin 0 -> 215 bytes tests/respect-positioned-dialog.html | 59 ++++++ 13 files changed, 886 insertions(+), 3 deletions(-) create mode 100644 LICENSE create mode 100644 dialog-polyfill.css create mode 100644 dialog-polyfill.js create mode 100644 tests/backdrop.html create mode 100644 tests/dialog-centering.html create mode 100644 tests/dialog-recentering.html create mode 100644 tests/fancy-modal-dialog.html create mode 100644 tests/modal-dialog-stacking.html create mode 100644 tests/modal-dialog.html create mode 100644 tests/resources/close_dialog.png create mode 100644 tests/resources/close_dialog_hover.png create mode 100644 tests/respect-positioned-dialog.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d0f7d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 20edcf5..2a4649a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,40 @@ -dialog-polyfill -=============== +dialog-polyfill.js is a polyfill for ``. -Polyfill for the <dialog> element +### Example + + + + + + + I'm a dialog! + + + +### ::backdrop + +In native ``, the backdrop is a pseudo-element: + + #mydialog::backdrop { + background-color: green; + } + +With the polyfill, you do it like: + + #mydialog + .backdrop { + background-color: green; + } + +### Known limitations + +- Modality isn't bulletproof (you can tab to inert elements) +- The polyfill `` should always be a child of `` +- Polyfill top layer stacking can be ruined by playing with z-index. +- The polyfill `` does not retain dynamically set CSS top/bottom values +upon close diff --git a/dialog-polyfill.css b/dialog-polyfill.css new file mode 100644 index 0000000..8228588 --- /dev/null +++ b/dialog-polyfill.css @@ -0,0 +1,16 @@ +dialog { + position: absolute; + left: 0; right: 0; + margin: auto; + border: solid; + padding: 1em; + background: white; + color: black; + + display: none; +} + +.backdrop { + position: absolute; + background: rgba(0,0,0,0.1); +} diff --git a/dialog-polyfill.js b/dialog-polyfill.js new file mode 100644 index 0000000..1d38cc8 --- /dev/null +++ b/dialog-polyfill.js @@ -0,0 +1,185 @@ +var dialogPolyfill = {} + +dialogPolyfill.reposition = function(element) { + var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; + var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2; + element.style.top = topValue + 'px'; + element.dialogPolyfillInfo.isTopOverridden = true; +} + +dialogPolyfill.inNodeList = function(nodeList, node) { + for (var i = 0; i < nodeList.length; ++i) { + if (nodeList[i] == node) + return true; + } + return false; +} + +dialogPolyfill.isInlinePositionSetByStylesheet = function(element) { + for (var i = 0; i < document.styleSheets.length; ++i) { + var styleSheet = document.styleSheets[i]; + var cssRules = null; + // Some browsers throw on cssRules. + try { + cssRules = styleSheet.cssRules; + } catch (e) {} + if (!cssRules) + continue; + for (var j = 0; j < cssRules.length; ++j) { + var rule = cssRules[j]; + var selectedNodes = document.querySelectorAll(rule.selectorText); + if (!dialogPolyfill.inNodeList(selectedNodes, element)) + continue; + var cssTop = rule.style.getPropertyValue('top'); + var cssBottom = rule.style.getPropertyValue('bottom'); + if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto')) + return true; + } + } + return false; +} + +dialogPolyfill.needsCentering = function(dialog) { + var computedStyle = getComputedStyle(dialog); + if (computedStyle.position != 'absolute') + return false; + + // We must determine whether the top/bottom specified value is non-auto. In + // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but + // Firefox returns the used value. So we do this crazy thing instead: check + // the inline style and then go through CSS rules. + if ((dialog.style.top != 'auto' && dialog.style.top != '') || + (dialog.style.bottom != 'auto' && dialog.style.bottom != '')) + return false; + return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog); +} + +dialogPolyfill.showDialog = function(isModal) { + if (this.open) { + throw 'InvalidStateError: showDialog called on open dialog'; + } + this.open = true; + this.style.display = 'block'; + + if (dialogPolyfill.needsCentering(this)) + dialogPolyfill.reposition(this); + if (isModal) { + this.dialogPolyfillInfo.modal = true; + dialogPolyfill.dm.pushDialog(this); + } +}; + +dialogPolyfill.close = function(retval) { + if (!this.open) + throw new InvalidStateError; + this.open = false; + this.style.display = 'none'; + + // This won't match the native exactly because if the user sets top + // on a centered polyfill dialog, that top gets thrown away when the dialog is + // closed. Not sure it's possible to polyfill this perfectly. + if (this.dialogPolyfillInfo.isTopOverridden) { + this.style.top = 'auto'; + } + + if (this.dialogPolyfillInfo.modal) { + dialogPolyfill.dm.removeDialog(this); + } + + if (typeof retval != 'undefined') + return retval; +} + +dialogPolyfill.registerDialog = function(element) { + element.show = dialogPolyfill.showDialog.bind(element, false); + element.showModal = dialogPolyfill.showDialog.bind(element, true); + element.close = dialogPolyfill.close.bind(element); + element.dialogPolyfillInfo = {}; +}; + +// The overlay is used to simulate how a modal dialog blocks the document. The +// blocking dialog is positioned on top of the overlay, and the rest of the +// dialogs on the pending dialog stack are positioned below it. In the actual +// implementation, the modal dialog stacking is controlled by the top layer, +// where z-index has no effect. +TOP_LAYER_ZINDEX = 100000; +MAX_PENDING_DIALOGS = 100000; + +dialogPolyfill.DialogManager = function() { + this.pendingDialogStack = []; + this.overlay = document.createElement('div'); + this.overlay.style.width = '100%'; + this.overlay.style.height = '100%'; + this.overlay.style.position = 'fixed'; + this.overlay.style.left = '0px'; + this.overlay.style.top = '0px'; + this.overlay.style.background = 'rgba(0,0,0,0.0)'; + + this.overlay.addEventListener('click', function(e) { + var redirectedEvent = document.createEvent('MouseEvents'); + redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window, + e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, + e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget); + document.body.dispatchEvent(redirectedEvent); + }); +} + +dialogPolyfill.dm = new dialogPolyfill.DialogManager(); + +dialogPolyfill.DialogManager.prototype.blockDocument = function() { + if (!document.body.contains(this.overlay)) + document.body.appendChild(this.overlay); +} + +dialogPolyfill.DialogManager.prototype.unblockDocument = function() { + document.body.removeChild(this.overlay); +} + +dialogPolyfill.DialogManager.prototype.updateStacking = function() { + if (this.pendingDialogStack.length == 0) { + this.unblockDocument(); + return; + } + this.blockDocument(); + + var zIndex = TOP_LAYER_ZINDEX; + for (var i = 0; i < this.pendingDialogStack.length; i++) { + if (i == this.pendingDialogStack.length - 1) + this.overlay.style.zIndex = zIndex++; + var dialog = this.pendingDialogStack[i]; + dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++; + dialog.style.zIndex = zIndex++; + } + +} + +dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) { + if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) { + throw "Too many modal dialogs"; + } + + var backdrop = document.createElement('div'); + backdrop.classList.add('backdrop'); + backdrop.addEventListener('click', function(e) { + var redirectedEvent = document.createEvent('MouseEvents'); + redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window, + e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, + e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget); + dialog.dispatchEvent(redirectedEvent); + }); + dialog.parentNode.insertBefore(backdrop, dialog.nextSibling); + dialog.dialogPolyfillInfo.backdrop = backdrop; + this.pendingDialogStack.push(dialog); + this.updateStacking(); +} + +dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) { + var index = this.pendingDialogStack.indexOf(dialog); + if (index == -1) + return; + this.pendingDialogStack.splice(index, 1); + var backdrop = dialog.dialogPolyfillInfo.backdrop; + backdrop.parentNode.removeChild(backdrop); + dialog.dialogPolyfillInfo.backdrop = null; + this.updateStacking(); +} diff --git a/tests/backdrop.html b/tests/backdrop.html new file mode 100644 index 0000000..e753363 --- /dev/null +++ b/tests/backdrop.html @@ -0,0 +1,43 @@ + + + + + + + + + +

Test for backdrop. The test passes if you see a green background behind the +box.

+
+ + + + + diff --git a/tests/dialog-centering.html b/tests/dialog-centering.html new file mode 100644 index 0000000..e88fa9a --- /dev/null +++ b/tests/dialog-centering.html @@ -0,0 +1,46 @@ + + + + + + + + + +

Test that dialog is centered in the viewport. The test passes if you see a +box in the center of the screen.

+
+ + + + diff --git a/tests/dialog-recentering.html b/tests/dialog-recentering.html new file mode 100644 index 0000000..372e31c --- /dev/null +++ b/tests/dialog-recentering.html @@ -0,0 +1,65 @@ + + + + + + + + + +

Test that dialog is recentered if reopened. The test passes if you see a +box in the center of the screen.

+
+
+ + + + diff --git a/tests/fancy-modal-dialog.html b/tests/fancy-modal-dialog.html new file mode 100644 index 0000000..64a89e5 --- /dev/null +++ b/tests/fancy-modal-dialog.html @@ -0,0 +1,272 @@ + + + + + + + +
+ +

Reshare

+
+
+ + +
+
+ + +
+
+
+
+ + + diff --git a/tests/modal-dialog-stacking.html b/tests/modal-dialog-stacking.html new file mode 100644 index 0000000..44ace2e --- /dev/null +++ b/tests/modal-dialog-stacking.html @@ -0,0 +1,89 @@ + + + + + + + + + +Test for modal dialog and backdrop stacking order. The test passes if there are +6 boxes enclosed in each other, becoming increasingly smaller and brighter +green. + + + + + + diff --git a/tests/modal-dialog.html b/tests/modal-dialog.html new file mode 100644 index 0000000..9855768 --- /dev/null +++ b/tests/modal-dialog.html @@ -0,0 +1,45 @@ + + + + + + + + + +

Test for modal dialog. The test passes if you can click on "Click me" button, +but can't click or tab to the "Can't click me!" button

+
+ + + + + + + diff --git a/tests/resources/close_dialog.png b/tests/resources/close_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..793f60e180419b98f273d93b98d267b853dee705 GIT binary patch literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh0wlLOK8*rW!JaOTAr)~;&pUD*FyL?r%zqI3 zpI2LD!E~>MoUDd#)y{l6X>jVtNyAG9B8~w)(>^8%Xm}ZH=;^YT`K$YI_CqJibGV1Tqh802FVdQ&MBb@0O@Bk`2YX_ literal 0 HcmV?d00001 diff --git a/tests/resources/close_dialog_hover.png b/tests/resources/close_dialog_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..fe896f91d25a0e5c7b3a4f674244d1d44f1ac467 GIT binary patch literal 215 zcmV;|04V>7P)H$L@_|ZyrS`8-+be%cSao9Zh{@{n#ou}J=WX002ovPDHLkV1mxjSz`bI literal 0 HcmV?d00001 diff --git a/tests/respect-positioned-dialog.html b/tests/respect-positioned-dialog.html new file mode 100644 index 0000000..1276c38 --- /dev/null +++ b/tests/respect-positioned-dialog.html @@ -0,0 +1,59 @@ + + + + + + + + + +

Test that dialogs with an explicit static position don't get auto-centered. +The test passes if there are three boxes aligned in a single row.

+
+ + + + + +