4 Polymer('core-overlay', {
8 * The target element that will be shown when the overlay is
9 * opened. If unspecified, the core-overlay itself is the target.
13 * @default the overlay element
19 * A `core-overlay`'s size is guaranteed to be
20 * constrained to the window size. To achieve this, the sizingElement
21 * is sized with a max-height/width. By default this element is the
22 * target element, but it can be specifically set to a specific element
23 * inside the target if that is more appropriate. This is useful, for
24 * example, when a region inside the overlay should scroll if needed.
26 * @attribute sizingTarget
28 * @default the target element
33 * Set opened to true to show an overlay and to false to hide it.
34 * A `core-overlay` may be made initially opened by setting its
43 * If true, the overlay has a backdrop darkening the rest of the screen.
44 * The backdrop element is attached to the document body and may be styled
45 * with the class `core-overlay-backdrop`. When opened the `core-opened`
55 * If true, the overlay is guaranteed to display above page content.
64 * By default an overlay will close automatically if the user
65 * taps outside it or presses the escape key. Disable this
66 * behavior by setting the `autoCloseDisabled` property to true.
67 * @attribute autoCloseDisabled
71 autoCloseDisabled
: false,
74 * This property specifies an attribute on elements that should
75 * close the overlay on tap. Should not set `closeSelector` if this
78 * @attribute closeAttribute
80 * @default "core-overlay-toggle"
82 closeAttribute
: 'core-overlay-toggle',
85 * This property specifies a selector matching elements that should
86 * close the overlay on tap. Should not set `closeAttribute` if this
89 * @attribute closeSelector
96 * A `core-overlay` target's size is constrained to the window size.
97 * The `margin` property specifies a pixel amount around the overlay
98 * that will be reserved. It's useful for ensuring that, for example,
99 * a shadow displayed outside the target will always be visible.
108 * The transition property specifies a string which identifies a
109 * <a href="../core-transition/">`core-transition`</a> element that
110 * will be used to help the overlay open and close. The default
111 * `core-transition-fade` will cause the overlay to fade in and out.
113 * @attribute transition
115 * @default 'core-transition-fade'
117 transition
: 'core-transition-fade'
121 captureEventName
: 'tap',
124 'keydown': 'keydownHandler',
125 'core-transitionend': 'transitionend'
128 registerCallback: function(element
) {
129 this.layer
= document
.createElement('core-overlay-layer');
130 this.keyHelper
= document
.createElement('core-key-helper');
131 this.meta
= document
.createElement('core-transition');
132 this.scrim
= document
.createElement('div');
133 this.scrim
.className
= 'core-overlay-backdrop';
137 this.target
= this.target
|| this;
138 // flush to ensure styles are installed before paint
143 * Toggle the opened state of the overlay.
147 this.opened
= !this.opened
;
151 * Open the overlay. This is equivalent to setting the `opened`
160 * Close the overlay. This is equivalent to setting the `opened`
168 domReady: function() {
169 this.ensureTargetSetup();
172 targetChanged: function(old
) {
174 // really make sure tabIndex is set
175 if (this.target
.tabIndex
< 0) {
176 this.target
.tabIndex
= -1;
178 this.addElementListenerList(this.target
, this.targetListeners
);
179 this.target
.style
.display
= 'none';
182 this.removeElementListenerList(old
, this.targetListeners
);
183 var transition
= this.getTransition();
185 transition
.teardown(old
);
187 old
.style
.position
= '';
188 old
.style
.outline
= '';
190 old
.style
.display
= '';
194 // NOTE: wait to call this until we're as sure as possible that target
196 ensureTargetSetup: function() {
197 if (!this.target
|| this.target
.__overlaySetup
) {
200 this.target
.__overlaySetup
= true;
201 this.target
.style
.display
= '';
202 var transition
= this.getTransition();
204 transition
.setup(this.target
);
206 var computed
= getComputedStyle(this.target
);
208 position
: computed
.position
=== 'static' ? 'fixed' :
212 this.target
.style
.position
= this.targetStyle
.position
;
213 this.target
.style
.outline
= 'none';
215 this.target
.style
.display
= 'none';
218 openedChanged: function() {
219 this.transitioning
= true;
220 this.ensureTargetSetup();
221 this.prepareRenderOpened();
222 // continue styling after delay so display state can change
223 // without aborting transitions
224 // note: we wait a full frame so that transition changes executed
225 // during measuring do not cause transition
226 this.async(function() {
227 this.target
.style
.display
= '';
228 this.async('renderOpened');
230 this.fire('core-overlay-open', this.opened
);
233 // tasks which must occur before opening; e.g. making the element visible
234 prepareRenderOpened: function() {
238 this.prepareBackdrop();
239 // async so we don't auto-close immediately via a click.
240 this.async(function() {
241 if (!this.autoCloseDisabled
) {
242 this.enableElementListener(this.opened
, document
,
243 this.captureEventName
, 'captureHandler', true);
246 this.enableElementListener(this.opened
, window
, 'resize',
250 // TODO(sorvell): force SD Polyfill to render
251 forcePolyfillRender(this.target
);
252 if (!this._shouldPosition
) {
253 this.target
.style
.position
= 'absolute';
254 var computed
= getComputedStyle(this.target
);
255 var t
= (computed
.top
=== 'auto' && computed
.bottom
=== 'auto');
256 var l
= (computed
.left
=== 'auto' && computed
.right
=== 'auto');
257 this.target
.style
.position
= this.targetStyle
.position
;
258 this._shouldPosition
= {top
: t
, left
: l
};
260 // if we are showing, then take care when measuring
261 this.prepareMeasure(this.target
);
262 this.updateTargetDimensions();
263 this.finishMeasure(this.target
);
265 this.layer
.addElement(this.target
);
266 this.layer
.opened
= this.opened
;
271 // tasks which cause the overlay to actually open; typically play an
273 renderOpened: function() {
274 var transition
= this.getTransition();
276 transition
.go(this.target
, {opened
: this.opened
});
278 this.transitionend();
280 this.renderBackdropOpened();
283 // finishing tasks; typically called via a transition
284 transitionend: function(e
) {
285 // make sure this is our transition event.
286 if (e
&& e
.target
!== this.target
) {
289 this.transitioning
= false;
291 this.resetTargetDimensions();
292 this.target
.style
.display
= 'none';
293 this.completeBackdrop();
296 if (!currentOverlay()) {
297 this.layer
.opened
= this.opened
;
299 this.layer
.removeElement(this.target
);
305 prepareBackdrop: function() {
306 if (this.backdrop
&& this.opened
) {
307 if (!this.scrim
.parentNode
) {
308 document
.body
.appendChild(this.scrim
);
309 this.scrim
.style
.zIndex
= currentOverlayZ() - 1;
315 renderBackdropOpened: function() {
316 if (this.backdrop
&& getBackdrops().length
< 2) {
317 this.scrim
.classList
.toggle('core-opened', this.opened
);
321 completeBackdrop: function() {
324 if (getBackdrops().length
=== 0) {
325 this.scrim
.parentNode
.removeChild(this.scrim
);
330 prepareMeasure: function(target
) {
331 target
.style
.transition
= target
.style
.webkitTransition
= 'none';
332 target
.style
.transform
= target
.style
.webkitTransform
= 'none';
333 target
.style
.display
= '';
336 finishMeasure: function(target
) {
337 target
.style
.display
= 'none';
338 target
.style
.transform
= target
.style
.webkitTransform
= '';
339 target
.style
.transition
= target
.style
.webkitTransition
= '';
342 getTransition: function() {
343 return this.meta
.byId(this.transition
);
346 getFocusNode: function() {
347 return this.target
.querySelector('[autofocus]') || this.target
;
350 applyFocus: function() {
351 var focusNode
= this.getFocusNode();
356 if (currentOverlay() == this) {
357 console
.warn('Current core-overlay is attempting to focus itself as next! (bug)');
364 updateTargetDimensions: function() {
365 this.positionTarget();
369 var rect
= this.target
.getBoundingClientRect();
370 this.target
.style
.top
= rect
.top
+ 'px';
371 this.target
.style
.left
= rect
.left
+ 'px';
372 this.target
.style
.right
= this.target
.style
.bottom
= 'auto';
376 sizeTarget: function() {
377 var sizer
= this.sizingTarget
|| this.target
;
378 var rect
= sizer
.getBoundingClientRect();
379 var mt
= rect
.top
=== this.margin
? this.margin
: this.margin
* 2;
380 var ml
= rect
.left
=== this.margin
? this.margin
: this.margin
* 2;
381 var h
= window
.innerHeight
- rect
.top
- mt
;
382 var w
= window
.innerWidth
- rect
.left
- ml
;
383 sizer
.style
.maxHeight
= h
+ 'px';
384 sizer
.style
.maxWidth
= w
+ 'px';
385 sizer
.style
.boxSizing
= 'border-box';
388 positionTarget: function() {
389 // vertically and horizontally center if not positioned
390 if (this._shouldPosition
.top
) {
391 var t
= Math
.max((window
.innerHeight
-
392 this.target
.offsetHeight
- this.margin
*2) / 2, this.margin
);
393 this.target
.style
.top
= t
+ 'px';
395 if (this._shouldPosition
.left
) {
396 var l
= Math
.max((window
.innerWidth
-
397 this.target
.offsetWidth
- this.margin
*2) / 2, this.margin
);
398 this.target
.style
.left
= l
+ 'px';
402 resetTargetDimensions: function() {
403 this.target
.style
.top
= this.target
.style
.left
= '';
404 this.target
.style
.right
= this.target
.style
.bottom
= '';
405 this.target
.style
.width
= this.target
.style
.height
= '';
406 this._shouldPosition
= null;
409 tapHandler: function(e
) {
410 // closeSelector takes precedence since closeAttribute has a default non-null value.
412 (this.closeSelector
&& e
.target
.matches(this.closeSelector
)) ||
413 (this.closeAttribute
&& e
.target
.hasAttribute(this.closeAttribute
))) {
416 if (this.autoCloseJob
) {
417 this.autoCloseJob
.stop();
418 this.autoCloseJob
= null;
423 // We use the traditional approach of capturing events on document
424 // to to determine if the overlay needs to close. However, due to
425 // ShadowDOM event retargeting, the event target is not useful. Instead
426 // of using it, we attempt to close asynchronously and prevent the close
427 // if a tap event is immediately heard on the target.
428 // TODO(sorvell): This approach will not work with modal. For
429 // this we need a scrim.
430 captureHandler: function(e
) {
431 if (!this.autoCloseDisabled
&& (currentOverlay() == this)) {
432 this.autoCloseJob
= this.job(this.autoCloseJob
, function() {
438 keydownHandler: function(e
) {
439 if (!this.autoCloseDisabled
&& (e
.keyCode
== this.keyHelper
.ESCAPE_KEY
)) {
446 * Extensions of core-overlay should implement the `resizeHandler`
447 * method to adjust the size and position of the overlay when the
448 * browser window resizes.
449 * @method resizeHandler
451 resizeHandler: function() {
452 this.updateTargetDimensions();
455 // TODO(sorvell): these utility methods should not be here.
456 addElementListenerList: function(node
, events
) {
457 for (var i
in events
) {
458 this.addElementListener(node
, i
, events
[i
]);
462 removeElementListenerList: function(node
, events
) {
463 for (var i
in events
) {
464 this.removeElementListener(node
, i
, events
[i
]);
468 enableElementListener: function(enable
, node
, event
, methodName
, capture
) {
470 this.addElementListener(node
, event
, methodName
, capture
);
472 this.removeElementListener(node
, event
, methodName
, capture
);
476 addElementListener: function(node
, event
, methodName
, capture
) {
477 var fn
= this._makeBoundListener(methodName
);
479 Polymer
.addEventListener(node
, event
, fn
, capture
);
483 removeElementListener: function(node
, event
, methodName
, capture
) {
484 var fn
= this._makeBoundListener(methodName
);
486 Polymer
.removeEventListener(node
, event
, fn
, capture
);
490 _makeBoundListener: function(methodName
) {
491 var self
= this, method
= this[methodName
];
495 var bound
= '_bound' + methodName
;
497 this[bound
] = function(e
) {
498 method
.call(self
, e
);
505 function forcePolyfillRender(target
) {
506 if (window
.ShadowDOMPolyfill
) {
511 // TODO(sorvell): This should be an element with private state so it can
512 // be independent of overlay.
513 // track overlays for z-index and focus managemant
515 function addOverlay(overlay
) {
516 var z0
= currentOverlayZ();
517 overlays
.push(overlay
);
518 var z1
= currentOverlayZ();
520 applyOverlayZ(overlay
, z0
);
524 function removeOverlay(overlay
) {
525 var i
= overlays
.indexOf(overlay
);
527 overlays
.splice(i
, 1);
532 function applyOverlayZ(overlay
, aboveZ
) {
533 setZ(overlay
.target
, aboveZ
+ 2);
536 function setZ(element
, z
) {
537 element
.style
.zIndex
= z
;
540 function currentOverlay() {
541 return overlays
[overlays
.length
-1];
546 function currentOverlayZ() {
548 var current
= currentOverlay();
550 var z1
= window
.getComputedStyle(current
.target
).zIndex
;
555 return z
|| DEFAULT_Z
;
558 function focusOverlay() {
559 var current
= currentOverlay();
560 // We have to be careful to focus the next overlay _after_ any current
561 // transitions are complete (due to the state being toggled prior to the
562 // transition). Otherwise, we risk infinite recursion when a transitioning
563 // (closed) overlay becomes the current overlay.
565 // NOTE: We make the assumption that any overlay that completes a transition
566 // will call into focusOverlay to kick the process back off. Currently:
567 // transitionend -> applyFocus -> focusOverlay.
568 if (current
&& !current
.transitioning
) {
569 current
.applyFocus();
574 function trackBackdrop(element
) {
575 if (element
.opened
) {
576 backdrops
.push(element
);
578 var i
= backdrops
.indexOf(element
);
580 backdrops
.splice(i
, 1);
585 function getBackdrops() {