4 Polymer('core-overlay',Polymer.mixin({
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 * By default an overlay will focus its target or an element inside
75 * it with the `autoFocus` attribute. Disable this
76 * behavior by setting the `autoFocusDisabled` property to true.
77 * @attribute autoFocusDisabled
81 autoFocusDisabled: false,
84 * This property specifies an attribute on elements that should
85 * close the overlay on tap. Should not set `closeSelector` if this
88 * @attribute closeAttribute
90 * @default "core-overlay-toggle"
92 closeAttribute: 'core-overlay-toggle',
95 * This property specifies a selector matching elements that should
96 * close the overlay on tap. Should not set `closeAttribute` if this
99 * @attribute closeSelector
106 * The transition property specifies a string which identifies a
107 * <a href="../core-transition/">`core-transition`</a> element that
108 * will be used to help the overlay open and close. The default
109 * `core-transition-fade` will cause the overlay to fade in and out.
111 * @attribute transition
113 * @default 'core-transition-fade'
115 transition: 'core-transition-fade'
119 captureEventName: 'tap',
122 'keydown': 'keydownHandler',
123 'core-transitionend': 'transitionend'
126 attached: function() {
127 this.resizerAttachedHandler();
130 detached: function() {
131 this.resizerDetachedHandler();
134 resizerShouldNotify: function() {
138 registerCallback: function(element) {
139 this.layer = document.createElement('core-overlay-layer');
140 this.keyHelper = document.createElement('core-key-helper');
141 this.meta = document.createElement('core-transition');
142 this.scrim = document.createElement('div');
143 this.scrim.className = 'core-overlay-backdrop';
147 this.target = this.target || this;
148 // flush to ensure styles are installed before paint
153 * Toggle the opened state of the overlay.
157 this.opened = !this.opened;
161 * Open the overlay. This is equivalent to setting the `opened`
170 * Close the overlay. This is equivalent to setting the `opened`
178 domReady: function() {
179 this.ensureTargetSetup();
182 targetChanged: function(old) {
184 // really make sure tabIndex is set
185 if (this.target.tabIndex < 0) {
186 this.target.tabIndex = -1;
188 this.addElementListenerList(this.target, this.targetListeners);
189 this.target.style.display = 'none';
190 this.target.__overlaySetup = false;
193 this.removeElementListenerList(old, this.targetListeners);
194 var transition = this.getTransition();
196 transition.teardown(old);
198 old.style.position = '';
199 old.style.outline = '';
201 old.style.display = '';
205 transitionChanged: function(old) {
210 this.getTransition(old).teardown(this.target);
212 this.target.__overlaySetup = false;
215 // NOTE: wait to call this until we're as sure as possible that target
217 ensureTargetSetup: function() {
218 if (!this.target || this.target.__overlaySetup) {
221 if (!this.sizingTarget) {
222 this.sizingTarget = this.target;
224 this.target.__overlaySetup = true;
225 this.target.style.display = '';
226 var transition = this.getTransition();
228 transition.setup(this.target);
230 var style = this.target.style;
231 var computed = getComputedStyle(this.target);
232 if (computed.position === 'static') {
233 style.position = 'fixed';
235 style.outline = 'none';
236 style.display = 'none';
239 openedChanged: function() {
240 this.transitioning = true;
241 this.ensureTargetSetup();
242 this.prepareRenderOpened();
243 // async here to allow overlay layer to become visible.
244 this.async(function() {
245 this.target.style.display = '';
246 // force layout to ensure transitions will go
247 this.target.offsetWidth;
250 this.fire('core-overlay-open', this.opened);
253 // tasks which must occur before opening; e.g. making the element visible
254 prepareRenderOpened: function() {
258 this.prepareBackdrop();
259 // async so we don't auto-close immediately via a click.
260 this.async(function() {
261 if (!this.autoCloseDisabled) {
262 this.enableElementListener(this.opened, document,
263 this.captureEventName, 'captureHandler', true);
266 this.enableElementListener(this.opened, window, 'resize',
270 // force layout so SD Polyfill renders
271 this.target.offsetHeight;
272 this.discoverDimensions();
273 // if we are showing, then take care when positioning
274 this.preparePositioning();
275 this.positionTarget();
276 this.updateTargetDimensions();
277 this.finishPositioning();
279 this.layer.addElement(this.target);
280 this.layer.opened = this.opened;
285 // tasks which cause the overlay to actually open; typically play an
287 renderOpened: function() {
289 var transition = this.getTransition();
291 transition.go(this.target, {opened: this.opened});
293 this.transitionend();
295 this.renderBackdropOpened();
298 // finishing tasks; typically called via a transition
299 transitionend: function(e) {
300 // make sure this is our transition event.
301 if (e && e.target !== this.target) {
304 this.transitioning = false;
306 this.resetTargetDimensions();
307 this.target.style.display = 'none';
308 this.completeBackdrop();
311 if (!currentOverlay()) {
312 this.layer.opened = this.opened;
314 this.layer.removeElement(this.target);
317 this.fire('core-overlay-' + (this.opened ? 'open' : 'close') +
322 prepareBackdrop: function() {
323 if (this.backdrop && this.opened) {
324 if (!this.scrim.parentNode) {
325 document.body.appendChild(this.scrim);
326 this.scrim.style.zIndex = currentOverlayZ() - 1;
332 renderBackdropOpened: function() {
333 if (this.backdrop && getBackdrops().length < 2) {
334 this.scrim.classList.toggle('core-opened', this.opened);
338 completeBackdrop: function() {
341 if (getBackdrops().length === 0) {
342 this.scrim.parentNode.removeChild(this.scrim);
347 preparePositioning: function() {
348 this.target.style.transition = this.target.style.webkitTransition = 'none';
349 this.target.style.transform = this.target.style.webkitTransform = 'none';
350 this.target.style.display = '';
353 discoverDimensions: function() {
354 if (this.dimensions) {
357 var target = getComputedStyle(this.target);
358 var sizer = getComputedStyle(this.sizingTarget);
361 v: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
363 h: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
368 v: sizer.maxHeight !== 'none',
369 h: sizer.maxWidth !== 'none'
372 top: parseInt(target.marginTop) || 0,
373 right: parseInt(target.marginRight) || 0,
374 bottom: parseInt(target.marginBottom) || 0,
375 left: parseInt(target.marginLeft) || 0
380 finishPositioning: function(target) {
381 this.target.style.display = 'none';
382 this.target.style.transform = this.target.style.webkitTransform = '';
383 // force layout to avoid application of transform
384 this.target.offsetWidth;
385 this.target.style.transition = this.target.style.webkitTransition = '';
388 getTransition: function(name) {
389 return this.meta.byId(name || this.transition);
392 getFocusNode: function() {
393 return this.target.querySelector('[autofocus]') || this.target;
396 applyFocus: function() {
397 var focusNode = this.getFocusNode();
399 if (!this.autoFocusDisabled) {
404 if (currentOverlay() == this) {
405 console.warn('Current core-overlay is attempting to focus itself as next! (bug)');
412 positionTarget: function() {
413 // fire positioning event
414 this.fire('core-overlay-position', {target: this.target,
415 sizingTarget: this.sizingTarget, opened: this.opened});
416 if (!this.dimensions.position.v) {
417 this.target.style.top = '0px';
419 if (!this.dimensions.position.h) {
420 this.target.style.left = '0px';
424 updateTargetDimensions: function() {
426 this.repositionTarget();
429 sizeTarget: function() {
430 this.sizingTarget.style.boxSizing = 'border-box';
431 var dims = this.dimensions;
432 var rect = this.target.getBoundingClientRect();
434 this.sizeDimension(rect, dims.position.v, 'top', 'bottom', 'Height');
437 this.sizeDimension(rect, dims.position.h, 'left', 'right', 'Width');
441 sizeDimension: function(rect, positionedBy, start, end, extent) {
442 var dims = this.dimensions;
443 var flip = (positionedBy === end);
444 var m = flip ? start : end;
445 var ws = window['inner' + extent];
446 var o = dims.margin[m] + (flip ? ws - rect[end] :
448 var offset = 'offset' + extent;
449 var o2 = this.target[offset] - this.sizingTarget[offset];
450 this.sizingTarget.style['max' + extent] = (ws - o - o2) + 'px';
453 // vertically and horizontally center if not positioned
454 repositionTarget: function() {
455 // only center if position fixed.
456 if (this.dimensions.position.css !== 'fixed') {
459 if (!this.dimensions.position.v) {
460 var t = (window.innerHeight - this.target.offsetHeight) / 2;
461 t -= this.dimensions.margin.top;
462 this.target.style.top = t + 'px';
465 if (!this.dimensions.position.h) {
466 var l = (window.innerWidth - this.target.offsetWidth) / 2;
467 l -= this.dimensions.margin.left;
468 this.target.style.left = l + 'px';
472 resetTargetDimensions: function() {
473 if (!this.dimensions || !this.dimensions.size.v) {
474 this.sizingTarget.style.maxHeight = '';
475 this.target.style.top = '';
477 if (!this.dimensions || !this.dimensions.size.h) {
478 this.sizingTarget.style.maxWidth = '';
479 this.target.style.left = '';
481 this.dimensions = null;
484 tapHandler: function(e) {
485 // closeSelector takes precedence since closeAttribute has a default non-null value.
487 (this.closeSelector && e.target.matches(this.closeSelector)) ||
488 (this.closeAttribute && e.target.hasAttribute(this.closeAttribute))) {
491 if (this.autoCloseJob) {
492 this.autoCloseJob.stop();
493 this.autoCloseJob = null;
498 // We use the traditional approach of capturing events on document
499 // to to determine if the overlay needs to close. However, due to
500 // ShadowDOM event retargeting, the event target is not useful. Instead
501 // of using it, we attempt to close asynchronously and prevent the close
502 // if a tap event is immediately heard on the target.
503 // TODO(sorvell): This approach will not work with modal. For
504 // this we need a scrim.
505 captureHandler: function(e) {
506 if (!this.autoCloseDisabled && (currentOverlay() == this)) {
507 this.autoCloseJob = this.job(this.autoCloseJob, function() {
513 keydownHandler: function(e) {
514 if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) {
521 * Extensions of core-overlay should implement the `resizeHandler`
522 * method to adjust the size and position of the overlay when the
523 * browser window resizes.
524 * @method resizeHandler
526 resizeHandler: function() {
527 this.updateTargetDimensions();
530 // TODO(sorvell): these utility methods should not be here.
531 addElementListenerList: function(node, events) {
532 for (var i in events) {
533 this.addElementListener(node, i, events[i]);
537 removeElementListenerList: function(node, events) {
538 for (var i in events) {
539 this.removeElementListener(node, i, events[i]);
543 enableElementListener: function(enable, node, event, methodName, capture) {
545 this.addElementListener(node, event, methodName, capture);
547 this.removeElementListener(node, event, methodName, capture);
551 addElementListener: function(node, event, methodName, capture) {
552 var fn = this._makeBoundListener(methodName);
554 Polymer.addEventListener(node, event, fn, capture);
558 removeElementListener: function(node, event, methodName, capture) {
559 var fn = this._makeBoundListener(methodName);
561 Polymer.removeEventListener(node, event, fn, capture);
565 _makeBoundListener: function(methodName) {
566 var self = this, method = this[methodName];
570 var bound = '_bound' + methodName;
572 this[bound] = function(e) {
573 method.call(self, e);
579 }, Polymer.CoreResizer));
581 // TODO(sorvell): This should be an element with private state so it can
582 // be independent of overlay.
583 // track overlays for z-index and focus managemant
585 function addOverlay(overlay) {
586 var z0 = currentOverlayZ();
587 overlays.push(overlay);
588 var z1 = currentOverlayZ();
590 applyOverlayZ(overlay, z0);
594 function removeOverlay(overlay) {
595 var i = overlays.indexOf(overlay);
597 overlays.splice(i, 1);
602 function applyOverlayZ(overlay, aboveZ) {
603 setZ(overlay.target, aboveZ + 2);
606 function setZ(element, z) {
607 element.style.zIndex = z;
610 function currentOverlay() {
611 return overlays[overlays.length-1];
616 function currentOverlayZ() {
618 var current = currentOverlay();
620 var z1 = window.getComputedStyle(current.target).zIndex;
625 return z || DEFAULT_Z;
628 function focusOverlay() {
629 var current = currentOverlay();
630 // We have to be careful to focus the next overlay _after_ any current
631 // transitions are complete (due to the state being toggled prior to the
632 // transition). Otherwise, we risk infinite recursion when a transitioning
633 // (closed) overlay becomes the current overlay.
635 // NOTE: We make the assumption that any overlay that completes a transition
636 // will call into focusOverlay to kick the process back off. Currently:
637 // transitionend -> applyFocus -> focusOverlay.
638 if (current && !current.transitioning) {
639 current.applyFocus();
644 function trackBackdrop(element) {
645 if (element.opened) {
646 backdrops.push(element);
648 var i = backdrops.indexOf(element);
650 backdrops.splice(i, 1);
655 function getBackdrops() {