| <!-- |
| Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| <link rel="import" href="../core-transition/core-transition.html"> |
| <link rel="import" href="../core-resizable/core-resizable.html"> |
| <link rel="import" href="core-key-helper.html"> |
| <link rel="import" href="core-overlay-layer.html"> |
| |
| <!-- |
| The `core-overlay` element displays overlayed on top of other content. It starts |
| out hidden and is displayed by setting its `opened` property to true. |
| A `core-overlay's` opened state can be toggled by calling the `toggle` |
| method. |
| |
| The `core-overlay` will, by default, show/hide itself when it's opened. The |
| `target` property may be set to another element to cause that element to |
| be shown when the overlay is opened. |
| |
| It's common to want a `core-overlay` to animate to its opened |
| position. The `core-overlay` element uses a `core-transition` to handle |
| animation. The default transition is `core-transition-fade` which |
| causes the overlay to fade in when displayed. See |
| <a href="../core-transition/">`core-transition`</a> for more |
| information about customizing a `core-overlay's` opening animation. The |
| `backdrop` property can be set to true to show a backdrop behind the overlay |
| that will darken the rest of the window. |
| |
| An element that should close the `core-overlay` will automatically |
| do so if it's given the `core-overlay-toggle` attribute. This attribute |
| can be customized with the `closeAttribute` property. You can also use |
| `closeSelector` if more general matching is needed. |
| |
| By default `core-overlay` will close whenever the user taps outside it or |
| presses the escape key. This behavior can be turned off via the |
| `autoCloseDisabled` property. |
| |
| <core-overlay> |
| <h2>Dialog</h2> |
| <input placeholder="say something..." autofocus> |
| <div>I agree with this wholeheartedly.</div> |
| <button core-overlay-toggle>OK</button> |
| </core-overlay> |
| |
| `core-overlay` will automatically size and position itself according to the |
| following rules. The overlay's size is constrained such that it does not |
| overflow the screen. This is done by setting maxHeight/maxWidth on the |
| `sizingTarget`. If the `sizingTarget` already has a setting for one of these |
| properties, it will not be overridden. The overlay should |
| be positioned via css or imperatively using the `core-overlay-position` event. |
| If the overlay is not positioned vertically via setting `top` or `bottom`, it |
| will be centered vertically. The same is true horizontally via a setting to |
| `left` or `right`. In addition, css `margin` can be used to provide some space |
| around the overlay. This can be used to ensure |
| that, for example, a drop shadow is always visible around the overlay. |
| |
| @group Core Elements |
| @element core-overlay |
| @mixins Polymer.CoreResizer https://github.com/polymer/core-resizable |
| @homepage github.io |
| --> |
| <!-- |
| Fired when the `core-overlay`'s `opened` property changes. |
| |
| @event core-overlay-open |
| @param {Object} detail |
| @param {Object} detail.opened the opened state |
| --> |
| <!-- |
| Fired when the `core-overlay` has completely opened. |
| |
| @event core-overlay-open-completed |
| --> |
| <!-- |
| Fired when the `core-overlay` has completely closed. |
| |
| @event core-overlay-close-completed |
| --> |
| <!-- |
| Fired when the `core-overlay` needs to position itself. Optionally, implement |
| in order to position an overlay via code. If the overlay was not otherwise |
| positioned, it's important to indicate how the overlay has been positioned by |
| setting the `dimensions.position` object. For example, if the overlay has been |
| positioned via setting `right` and `top`, set dimensions.position to an |
| object like this: `{v: 'top', h: 'right'}`. |
| |
| @event core-overlay-position |
| @param {Object} detail |
| @param {Object} detail.target the overlay target |
| @param {Object} detail.sizingTarget the overlay sizing target |
| @param {Object} detail.opened the opened state |
| --> |
| <style> |
| .core-overlay-backdrop { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100vw; |
| height: 100vh; |
| background-color: black; |
| opacity: 0; |
| transition: opacity 0.2s; |
| } |
| |
| .core-overlay-backdrop.core-opened { |
| opacity: 0.6; |
| } |
| </style> |
| |
| <polymer-element name="core-overlay"> |
| <script> |
| (function() { |
| |
| Polymer(Polymer.mixin({ |
| |
| publish: { |
| /** |
| * The target element that will be shown when the overlay is |
| * opened. If unspecified, the core-overlay itself is the target. |
| * |
| * @attribute target |
| * @type Object |
| * @default the overlay element |
| */ |
| target: null, |
| |
| |
| /** |
| * A `core-overlay`'s size is guaranteed to be |
| * constrained to the window size. To achieve this, the sizingElement |
| * is sized with a max-height/width. By default this element is the |
| * target element, but it can be specifically set to a specific element |
| * inside the target if that is more appropriate. This is useful, for |
| * example, when a region inside the overlay should scroll if needed. |
| * |
| * @attribute sizingTarget |
| * @type Object |
| * @default the target element |
| */ |
| sizingTarget: null, |
| |
| /** |
| * Set opened to true to show an overlay and to false to hide it. |
| * A `core-overlay` may be made initially opened by setting its |
| * `opened` attribute. |
| * @attribute opened |
| * @type boolean |
| * @default false |
| */ |
| opened: false, |
| |
| /** |
| * If true, the overlay has a backdrop darkening the rest of the screen. |
| * The backdrop element is attached to the document body and may be styled |
| * with the class `core-overlay-backdrop`. When opened the `core-opened` |
| * class is applied. |
| * |
| * @attribute backdrop |
| * @type boolean |
| * @default false |
| */ |
| backdrop: false, |
| |
| /** |
| * If true, the overlay is guaranteed to display above page content. |
| * |
| * @attribute layered |
| * @type boolean |
| * @default false |
| */ |
| layered: false, |
| |
| /** |
| * By default an overlay will close automatically if the user |
| * taps outside it or presses the escape key. Disable this |
| * behavior by setting the `autoCloseDisabled` property to true. |
| * @attribute autoCloseDisabled |
| * @type boolean |
| * @default false |
| */ |
| autoCloseDisabled: false, |
| |
| /** |
| * By default an overlay will focus its target or an element inside |
| * it with the `autoFocus` attribute. Disable this |
| * behavior by setting the `autoFocusDisabled` property to true. |
| * @attribute autoFocusDisabled |
| * @type boolean |
| * @default false |
| */ |
| autoFocusDisabled: false, |
| |
| /** |
| * This property specifies an attribute on elements that should |
| * close the overlay on tap. Should not set `closeSelector` if this |
| * is set. |
| * |
| * @attribute closeAttribute |
| * @type string |
| * @default "core-overlay-toggle" |
| */ |
| closeAttribute: 'core-overlay-toggle', |
| |
| /** |
| * This property specifies a selector matching elements that should |
| * close the overlay on tap. Should not set `closeAttribute` if this |
| * is set. |
| * |
| * @attribute closeSelector |
| * @type string |
| * @default "" |
| */ |
| closeSelector: '', |
| |
| /** |
| * The transition property specifies a string which identifies a |
| * <a href="../core-transition/">`core-transition`</a> element that |
| * will be used to help the overlay open and close. The default |
| * `core-transition-fade` will cause the overlay to fade in and out. |
| * |
| * @attribute transition |
| * @type string |
| * @default 'core-transition-fade' |
| */ |
| transition: 'core-transition-fade' |
| |
| }, |
| |
| captureEventName: 'tap', |
| targetListeners: { |
| 'tap': 'tapHandler', |
| 'keydown': 'keydownHandler', |
| 'core-transitionend': 'transitionend' |
| }, |
| |
| attached: function() { |
| this.resizerAttachedHandler(); |
| }, |
| |
| detached: function() { |
| this.resizerDetachedHandler(); |
| }, |
| |
| resizerShouldNotify: function() { |
| return this.opened; |
| }, |
| |
| registerCallback: function(element) { |
| this.layer = document.createElement('core-overlay-layer'); |
| this.keyHelper = document.createElement('core-key-helper'); |
| this.meta = document.createElement('core-transition'); |
| this.scrim = document.createElement('div'); |
| this.scrim.className = 'core-overlay-backdrop'; |
| }, |
| |
| ready: function() { |
| this.target = this.target || this; |
| // flush to ensure styles are installed before paint |
| Polymer.flush(); |
| }, |
| |
| /** |
| * Toggle the opened state of the overlay. |
| * @method toggle |
| */ |
| toggle: function() { |
| this.opened = !this.opened; |
| }, |
| |
| /** |
| * Open the overlay. This is equivalent to setting the `opened` |
| * property to true. |
| * @method open |
| */ |
| open: function() { |
| this.opened = true; |
| }, |
| |
| /** |
| * Close the overlay. This is equivalent to setting the `opened` |
| * property to false. |
| * @method close |
| */ |
| close: function() { |
| this.opened = false; |
| }, |
| |
| domReady: function() { |
| this.ensureTargetSetup(); |
| }, |
| |
| targetChanged: function(old) { |
| if (this.target) { |
| // really make sure tabIndex is set |
| if (this.target.tabIndex < 0) { |
| this.target.tabIndex = -1; |
| } |
| this.addElementListenerList(this.target, this.targetListeners); |
| this.target.style.display = 'none'; |
| this.target.__overlaySetup = false; |
| } |
| if (old) { |
| this.removeElementListenerList(old, this.targetListeners); |
| var transition = this.getTransition(); |
| if (transition) { |
| transition.teardown(old); |
| } else { |
| old.style.position = ''; |
| old.style.outline = ''; |
| } |
| old.style.display = ''; |
| } |
| }, |
| |
| transitionChanged: function(old) { |
| if (!this.target) { |
| return; |
| } |
| if (old) { |
| this.getTransition(old).teardown(this.target); |
| } |
| this.target.__overlaySetup = false; |
| }, |
| |
| // NOTE: wait to call this until we're as sure as possible that target |
| // is styled. |
| ensureTargetSetup: function() { |
| if (!this.target || this.target.__overlaySetup) { |
| return; |
| } |
| if (!this.sizingTarget) { |
| this.sizingTarget = this.target; |
| } |
| this.target.__overlaySetup = true; |
| this.target.style.display = ''; |
| var transition = this.getTransition(); |
| if (transition) { |
| transition.setup(this.target); |
| } |
| var style = this.target.style; |
| var computed = getComputedStyle(this.target); |
| if (computed.position === 'static') { |
| style.position = 'fixed'; |
| } |
| style.outline = 'none'; |
| style.display = 'none'; |
| }, |
| |
| openedChanged: function() { |
| this.transitioning = true; |
| this.ensureTargetSetup(); |
| this.prepareRenderOpened(); |
| // async here to allow overlay layer to become visible. |
| this.async(function() { |
| this.target.style.display = ''; |
| // force layout to ensure transitions will go |
| this.target.offsetWidth; |
| this.renderOpened(); |
| }); |
| this.fire('core-overlay-open', this.opened); |
| }, |
| |
| // tasks which must occur before opening; e.g. making the element visible |
| prepareRenderOpened: function() { |
| if (this.opened) { |
| addOverlay(this); |
| } |
| this.prepareBackdrop(); |
| // async so we don't auto-close immediately via a click. |
| this.async(function() { |
| if (!this.autoCloseDisabled) { |
| this.enableElementListener(this.opened, document, |
| this.captureEventName, 'captureHandler', true); |
| } |
| }); |
| this.enableElementListener(this.opened, window, 'resize', |
| 'resizeHandler'); |
| |
| if (this.opened) { |
| // force layout so SD Polyfill renders |
| this.target.offsetHeight; |
| this.discoverDimensions(); |
| // if we are showing, then take care when positioning |
| this.preparePositioning(); |
| this.positionTarget(); |
| this.updateTargetDimensions(); |
| this.finishPositioning(); |
| if (this.layered) { |
| this.layer.addElement(this.target); |
| this.layer.opened = this.opened; |
| } |
| } |
| }, |
| |
| // tasks which cause the overlay to actually open; typically play an |
| // animation |
| renderOpened: function() { |
| this.notifyResize(); |
| var transition = this.getTransition(); |
| if (transition) { |
| transition.go(this.target, {opened: this.opened}); |
| } else { |
| this.transitionend(); |
| } |
| this.renderBackdropOpened(); |
| }, |
| |
| // finishing tasks; typically called via a transition |
| transitionend: function(e) { |
| // make sure this is our transition event. |
| if (e && e.target !== this.target) { |
| return; |
| } |
| this.transitioning = false; |
| if (!this.opened) { |
| this.resetTargetDimensions(); |
| this.target.style.display = 'none'; |
| this.completeBackdrop(); |
| removeOverlay(this); |
| if (this.layered) { |
| if (!currentOverlay()) { |
| this.layer.opened = this.opened; |
| } |
| this.layer.removeElement(this.target); |
| } |
| } |
| this.fire('core-overlay-' + (this.opened ? 'open' : 'close') + |
| '-completed'); |
| this.applyFocus(); |
| }, |
| |
| prepareBackdrop: function() { |
| if (this.backdrop && this.opened) { |
| if (!this.scrim.parentNode) { |
| document.body.appendChild(this.scrim); |
| this.scrim.style.zIndex = currentOverlayZ() - 1; |
| } |
| trackBackdrop(this); |
| } |
| }, |
| |
| renderBackdropOpened: function() { |
| if (this.backdrop && getBackdrops().length < 2) { |
| this.scrim.classList.toggle('core-opened', this.opened); |
| } |
| }, |
| |
| completeBackdrop: function() { |
| if (this.backdrop) { |
| trackBackdrop(this); |
| if (getBackdrops().length === 0) { |
| this.scrim.parentNode.removeChild(this.scrim); |
| } |
| } |
| }, |
| |
| preparePositioning: function() { |
| this.target.style.transition = this.target.style.webkitTransition = 'none'; |
| this.target.style.transform = this.target.style.webkitTransform = 'none'; |
| this.target.style.display = ''; |
| }, |
| |
| discoverDimensions: function() { |
| if (this.dimensions) { |
| return; |
| } |
| var target = getComputedStyle(this.target); |
| var sizer = getComputedStyle(this.sizingTarget); |
| this.dimensions = { |
| position: { |
| v: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ? |
| 'bottom' : null), |
| h: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ? |
| 'right' : null), |
| css: target.position |
| }, |
| size: { |
| v: sizer.maxHeight !== 'none', |
| h: sizer.maxWidth !== 'none' |
| }, |
| margin: { |
| top: parseInt(target.marginTop) || 0, |
| right: parseInt(target.marginRight) || 0, |
| bottom: parseInt(target.marginBottom) || 0, |
| left: parseInt(target.marginLeft) || 0 |
| } |
| }; |
| }, |
| |
| finishPositioning: function(target) { |
| this.target.style.display = 'none'; |
| this.target.style.transform = this.target.style.webkitTransform = ''; |
| // force layout to avoid application of transform |
| this.target.offsetWidth; |
| this.target.style.transition = this.target.style.webkitTransition = ''; |
| }, |
| |
| getTransition: function(name) { |
| return this.meta.byId(name || this.transition); |
| }, |
| |
| getFocusNode: function() { |
| return this.target.querySelector('[autofocus]') || this.target; |
| }, |
| |
| applyFocus: function() { |
| var focusNode = this.getFocusNode(); |
| if (this.opened) { |
| if (!this.autoFocusDisabled) { |
| focusNode.focus(); |
| } |
| } else { |
| focusNode.blur(); |
| if (currentOverlay() == this) { |
| console.warn('Current core-overlay is attempting to focus itself as next! (bug)'); |
| } else { |
| focusOverlay(); |
| } |
| } |
| }, |
| |
| positionTarget: function() { |
| // fire positioning event |
| this.fire('core-overlay-position', {target: this.target, |
| sizingTarget: this.sizingTarget, opened: this.opened}); |
| if (!this.dimensions.position.v) { |
| this.target.style.top = '0px'; |
| } |
| if (!this.dimensions.position.h) { |
| this.target.style.left = '0px'; |
| } |
| }, |
| |
| updateTargetDimensions: function() { |
| this.sizeTarget(); |
| this.repositionTarget(); |
| }, |
| |
| sizeTarget: function() { |
| this.sizingTarget.style.boxSizing = 'border-box'; |
| var dims = this.dimensions; |
| var rect = this.target.getBoundingClientRect(); |
| if (!dims.size.v) { |
| this.sizeDimension(rect, dims.position.v, 'top', 'bottom', 'Height'); |
| } |
| if (!dims.size.h) { |
| this.sizeDimension(rect, dims.position.h, 'left', 'right', 'Width'); |
| } |
| }, |
| |
| sizeDimension: function(rect, positionedBy, start, end, extent) { |
| var dims = this.dimensions; |
| var flip = (positionedBy === end); |
| var m = flip ? start : end; |
| var ws = window['inner' + extent]; |
| var o = dims.margin[m] + (flip ? ws - rect[end] : |
| rect[start]); |
| var offset = 'offset' + extent; |
| var o2 = this.target[offset] - this.sizingTarget[offset]; |
| this.sizingTarget.style['max' + extent] = (ws - o - o2) + 'px'; |
| }, |
| |
| // vertically and horizontally center if not positioned |
| repositionTarget: function() { |
| // only center if position fixed. |
| if (this.dimensions.position.css !== 'fixed') { |
| return; |
| } |
| if (!this.dimensions.position.v) { |
| var t = (window.innerHeight - this.target.offsetHeight) / 2; |
| t -= this.dimensions.margin.top; |
| this.target.style.top = t + 'px'; |
| } |
| |
| if (!this.dimensions.position.h) { |
| var l = (window.innerWidth - this.target.offsetWidth) / 2; |
| l -= this.dimensions.margin.left; |
| this.target.style.left = l + 'px'; |
| } |
| }, |
| |
| resetTargetDimensions: function() { |
| if (!this.dimensions || !this.dimensions.size.v) { |
| this.sizingTarget.style.maxHeight = ''; |
| this.target.style.top = ''; |
| } |
| if (!this.dimensions || !this.dimensions.size.h) { |
| this.sizingTarget.style.maxWidth = ''; |
| this.target.style.left = ''; |
| } |
| this.dimensions = null; |
| }, |
| |
| tapHandler: function(e) { |
| // closeSelector takes precedence since closeAttribute has a default non-null value. |
| if (e.target && |
| (this.closeSelector && e.target.matches(this.closeSelector)) || |
| (this.closeAttribute && e.target.hasAttribute(this.closeAttribute))) { |
| this.toggle(); |
| } else { |
| if (this.autoCloseJob) { |
| this.autoCloseJob.stop(); |
| this.autoCloseJob = null; |
| } |
| } |
| }, |
| |
| // We use the traditional approach of capturing events on document |
| // to to determine if the overlay needs to close. However, due to |
| // ShadowDOM event retargeting, the event target is not useful. Instead |
| // of using it, we attempt to close asynchronously and prevent the close |
| // if a tap event is immediately heard on the target. |
| // TODO(sorvell): This approach will not work with modal. For |
| // this we need a scrim. |
| captureHandler: function(e) { |
| if (!this.autoCloseDisabled && (currentOverlay() == this)) { |
| this.autoCloseJob = this.job(this.autoCloseJob, function() { |
| this.close(); |
| }); |
| } |
| }, |
| |
| keydownHandler: function(e) { |
| if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) { |
| this.close(); |
| e.stopPropagation(); |
| } |
| }, |
| |
| /** |
| * Extensions of core-overlay should implement the `resizeHandler` |
| * method to adjust the size and position of the overlay when the |
| * browser window resizes. |
| * @method resizeHandler |
| */ |
| resizeHandler: function() { |
| this.updateTargetDimensions(); |
| }, |
| |
| // TODO(sorvell): these utility methods should not be here. |
| addElementListenerList: function(node, events) { |
| for (var i in events) { |
| this.addElementListener(node, i, events[i]); |
| } |
| }, |
| |
| removeElementListenerList: function(node, events) { |
| for (var i in events) { |
| this.removeElementListener(node, i, events[i]); |
| } |
| }, |
| |
| enableElementListener: function(enable, node, event, methodName, capture) { |
| if (enable) { |
| this.addElementListener(node, event, methodName, capture); |
| } else { |
| this.removeElementListener(node, event, methodName, capture); |
| } |
| }, |
| |
| addElementListener: function(node, event, methodName, capture) { |
| var fn = this._makeBoundListener(methodName); |
| if (node && fn) { |
| Polymer.addEventListener(node, event, fn, capture); |
| } |
| }, |
| |
| removeElementListener: function(node, event, methodName, capture) { |
| var fn = this._makeBoundListener(methodName); |
| if (node && fn) { |
| Polymer.removeEventListener(node, event, fn, capture); |
| } |
| }, |
| |
| _makeBoundListener: function(methodName) { |
| var self = this, method = this[methodName]; |
| if (!method) { |
| return; |
| } |
| var bound = '_bound' + methodName; |
| if (!this[bound]) { |
| this[bound] = function(e) { |
| method.call(self, e); |
| }; |
| } |
| return this[bound]; |
| } |
| |
| }, Polymer.CoreResizer)); |
| |
| // TODO(sorvell): This should be an element with private state so it can |
| // be independent of overlay. |
| // track overlays for z-index and focus managemant |
| var overlays = []; |
| function addOverlay(overlay) { |
| var z0 = currentOverlayZ(); |
| overlays.push(overlay); |
| var z1 = currentOverlayZ(); |
| if (z1 <= z0) { |
| applyOverlayZ(overlay, z0); |
| } |
| } |
| |
| function removeOverlay(overlay) { |
| var i = overlays.indexOf(overlay); |
| if (i >= 0) { |
| overlays.splice(i, 1); |
| setZ(overlay, ''); |
| } |
| } |
| |
| function applyOverlayZ(overlay, aboveZ) { |
| setZ(overlay.target, aboveZ + 2); |
| } |
| |
| function setZ(element, z) { |
| element.style.zIndex = z; |
| } |
| |
| function currentOverlay() { |
| return overlays[overlays.length-1]; |
| } |
| |
| var DEFAULT_Z = 10; |
| |
| function currentOverlayZ() { |
| var z; |
| var current = currentOverlay(); |
| if (current) { |
| var z1 = window.getComputedStyle(current.target).zIndex; |
| if (!isNaN(z1)) { |
| z = Number(z1); |
| } |
| } |
| return z || DEFAULT_Z; |
| } |
| |
| function focusOverlay() { |
| var current = currentOverlay(); |
| // We have to be careful to focus the next overlay _after_ any current |
| // transitions are complete (due to the state being toggled prior to the |
| // transition). Otherwise, we risk infinite recursion when a transitioning |
| // (closed) overlay becomes the current overlay. |
| // |
| // NOTE: We make the assumption that any overlay that completes a transition |
| // will call into focusOverlay to kick the process back off. Currently: |
| // transitionend -> applyFocus -> focusOverlay. |
| if (current && !current.transitioning) { |
| current.applyFocus(); |
| } |
| } |
| |
| var backdrops = []; |
| function trackBackdrop(element) { |
| if (element.opened) { |
| backdrops.push(element); |
| } else { |
| var i = backdrops.indexOf(element); |
| if (i >= 0) { |
| backdrops.splice(i, 1); |
| } |
| } |
| } |
| |
| function getBackdrops() { |
| return backdrops; |
| } |
| })(); |
| </script> |
| </polymer-element> |