Add an exponential backoff to rechecking the app list doodle.
[chromium-blink-merge.git] / third_party / polymer / components-chromium / core-overlay / core-overlay-extracted.js
blobe6a802615577dbc924b969a910a9408fd50d5889
2 (function() {
4   Polymer('core-overlay',Polymer.mixin({
6     publish: {
7       /**
8        * The target element that will be shown when the overlay is 
9        * opened. If unspecified, the core-overlay itself is the target.
10        *
11        * @attribute target
12        * @type Object
13        * @default the overlay element
14        */
15       target: null,
18       /**
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.
25        *
26        * @attribute sizingTarget
27        * @type Object
28        * @default the target element
29        */
30       sizingTarget: null,
31     
32       /**
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
35        * `opened` attribute.
36        * @attribute opened
37        * @type boolean
38        * @default false
39        */
40       opened: false,
42       /**
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`
46        * class is applied.
47        *
48        * @attribute backdrop
49        * @type boolean
50        * @default false
51        */    
52       backdrop: false,
54       /**
55        * If true, the overlay is guaranteed to display above page content.
56        *
57        * @attribute layered
58        * @type boolean
59        * @default false
60       */
61       layered: false,
62     
63       /**
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
68        * @type boolean
69        * @default false
70        */
71       autoCloseDisabled: false,
73       /**
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
78        * @type boolean
79        * @default false
80        */
81       autoFocusDisabled: false,
83       /**
84        * This property specifies an attribute on elements that should
85        * close the overlay on tap. Should not set `closeSelector` if this
86        * is set.
87        *
88        * @attribute closeAttribute
89        * @type string
90        * @default "core-overlay-toggle"
91        */
92       closeAttribute: 'core-overlay-toggle',
94       /**
95        * This property specifies a selector matching elements that should
96        * close the overlay on tap. Should not set `closeAttribute` if this
97        * is set.
98        *
99        * @attribute closeSelector
100        * @type string
101        * @default ""
102        */
103       closeSelector: '',
105       /**
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.
110        *
111        * @attribute transition
112        * @type string
113        * @default 'core-transition-fade'
114        */
115       transition: 'core-transition-fade'
117     },
119     captureEventName: 'tap',
120     targetListeners: {
121       'tap': 'tapHandler',
122       'keydown': 'keydownHandler',
123       'core-transitionend': 'transitionend'
124     },
126     attached: function() {
127       this.resizerAttachedHandler();
128     },
130     detached: function() {
131       this.resizerDetachedHandler();
132     },
134     resizerShouldNotify: function() {
135       return this.opened;
136     },
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';
144     },
146     ready: function() {
147       this.target = this.target || this;
148       // flush to ensure styles are installed before paint
149       Polymer.flush();
150     },
152     /** 
153      * Toggle the opened state of the overlay.
154      * @method toggle
155      */
156     toggle: function() {
157       this.opened = !this.opened;
158     },
160     /** 
161      * Open the overlay. This is equivalent to setting the `opened`
162      * property to true.
163      * @method open
164      */
165     open: function() {
166       this.opened = true;
167     },
169     /** 
170      * Close the overlay. This is equivalent to setting the `opened` 
171      * property to false.
172      * @method close
173      */
174     close: function() {
175       this.opened = false;
176     },
178     domReady: function() {
179       this.ensureTargetSetup();
180     },
182     targetChanged: function(old) {
183       if (this.target) {
184         // really make sure tabIndex is set
185         if (this.target.tabIndex < 0) {
186           this.target.tabIndex = -1;
187         }
188         this.addElementListenerList(this.target, this.targetListeners);
189         this.target.style.display = 'none';
190         this.target.__overlaySetup = false;
191       }
192       if (old) {
193         this.removeElementListenerList(old, this.targetListeners);
194         var transition = this.getTransition();
195         if (transition) {
196           transition.teardown(old);
197         } else {
198           old.style.position = '';
199           old.style.outline = '';
200         }
201         old.style.display = '';
202       }
203     },
205     transitionChanged: function(old) {
206       if (!this.target) {
207         return;
208       }
209       if (old) {
210         this.getTransition(old).teardown(this.target);
211       }
212       this.target.__overlaySetup = false;
213     },
215     // NOTE: wait to call this until we're as sure as possible that target
216     // is styled.
217     ensureTargetSetup: function() {
218       if (!this.target || this.target.__overlaySetup) {
219         return;
220       }
221       if (!this.sizingTarget) {
222         this.sizingTarget = this.target;
223       }
224       this.target.__overlaySetup = true;
225       this.target.style.display = '';
226       var transition = this.getTransition();
227       if (transition) {
228         transition.setup(this.target);
229       }
230       var style = this.target.style;
231       var computed = getComputedStyle(this.target);
232       if (computed.position === 'static') {
233         style.position = 'fixed';
234       }
235       style.outline = 'none';
236       style.display = 'none';
237     },
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;
248         this.renderOpened();
249       });
250       this.fire('core-overlay-open', this.opened);
251     },
253     // tasks which must occur before opening; e.g. making the element visible
254     prepareRenderOpened: function() {
255       if (this.opened) {
256         addOverlay(this);
257       }
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);
264         }
265       });
266       this.enableElementListener(this.opened, window, 'resize',
267           'resizeHandler');
269       if (this.opened) {
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();
278         if (this.layered) {
279           this.layer.addElement(this.target);
280           this.layer.opened = this.opened;
281         }
282       }
283     },
285     // tasks which cause the overlay to actually open; typically play an
286     // animation
287     renderOpened: function() {
288       this.notifyResize();
289       var transition = this.getTransition();
290       if (transition) {
291         transition.go(this.target, {opened: this.opened});
292       } else {
293         this.transitionend();
294       }
295       this.renderBackdropOpened();
296     },
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) {
302         return;
303       }
304       this.transitioning = false;
305       if (!this.opened) {
306         this.resetTargetDimensions();
307         this.target.style.display = 'none';
308         this.completeBackdrop();
309         removeOverlay(this);
310         if (this.layered) {
311           if (!currentOverlay()) {
312             this.layer.opened = this.opened;
313           }
314           this.layer.removeElement(this.target);
315         }
316       }
317       this.fire('core-overlay-' + (this.opened ? 'open' : 'close') + 
318           '-completed');
319       this.applyFocus();
320     },
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;
327         }
328         trackBackdrop(this);
329       }
330     },
332     renderBackdropOpened: function() {
333       if (this.backdrop && getBackdrops().length < 2) {
334         this.scrim.classList.toggle('core-opened', this.opened);
335       }
336     },
338     completeBackdrop: function() {
339       if (this.backdrop) {
340         trackBackdrop(this);
341         if (getBackdrops().length === 0) {
342           this.scrim.parentNode.removeChild(this.scrim);
343         }
344       }
345     },
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 = '';
351     },
353     discoverDimensions: function() {
354       if (this.dimensions) {
355         return;
356       }
357       var target = getComputedStyle(this.target);
358       var sizer = getComputedStyle(this.sizingTarget);
359       this.dimensions = {
360         position: {
361           v: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
362             'bottom' : null),
363           h: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
364             'right' : null),
365           css: target.position
366         },
367         size: {
368           v: sizer.maxHeight !== 'none',
369           h: sizer.maxWidth !== 'none'
370         },
371         margin: {
372           top: parseInt(target.marginTop) || 0,
373           right: parseInt(target.marginRight) || 0,
374           bottom: parseInt(target.marginBottom) || 0,
375           left: parseInt(target.marginLeft) || 0
376         }
377       };
378     },
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 = '';
386     },
388     getTransition: function(name) {
389       return this.meta.byId(name || this.transition);
390     },
392     getFocusNode: function() {
393       return this.target.querySelector('[autofocus]') || this.target;
394     },
396     applyFocus: function() {
397       var focusNode = this.getFocusNode();
398       if (this.opened) {
399         if (!this.autoFocusDisabled) {
400           focusNode.focus();
401         }
402       } else {
403         focusNode.blur();
404         if (currentOverlay() == this) {
405           console.warn('Current core-overlay is attempting to focus itself as next! (bug)');
406         } else {
407           focusOverlay();
408         }
409       }
410     },
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';
418       }
419       if (!this.dimensions.position.h) {
420         this.target.style.left = '0px';
421       }
422     },
424     updateTargetDimensions: function() {
425       this.sizeTarget();
426       this.repositionTarget();
427     },
429     sizeTarget: function() {
430       this.sizingTarget.style.boxSizing = 'border-box';
431       var dims = this.dimensions;
432       var rect = this.target.getBoundingClientRect();
433       if (!dims.size.v) {
434         this.sizeDimension(rect, dims.position.v, 'top', 'bottom', 'Height');
435       }
436       if (!dims.size.h) {
437         this.sizeDimension(rect, dims.position.h, 'left', 'right', 'Width');
438       }
439     },
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] : 
447           rect[start]);
448       var offset = 'offset' + extent;
449       var o2 = this.target[offset] - this.sizingTarget[offset];
450       this.sizingTarget.style['max' + extent] = (ws - o - o2) + 'px';
451     },
453     // vertically and horizontally center if not positioned
454     repositionTarget: function() {
455       // only center if position fixed.      
456       if (this.dimensions.position.css !== 'fixed') {
457         return; 
458       }
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';
463       }
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';
469       }
470     },
472     resetTargetDimensions: function() {
473       if (!this.dimensions || !this.dimensions.size.v) {
474         this.sizingTarget.style.maxHeight = '';  
475         this.target.style.top = '';
476       }
477       if (!this.dimensions || !this.dimensions.size.h) {
478         this.sizingTarget.style.maxWidth = '';  
479         this.target.style.left = '';
480       }
481       this.dimensions = null;
482     },
484     tapHandler: function(e) {
485       // closeSelector takes precedence since closeAttribute has a default non-null value.
486       if (e.target &&
487           (this.closeSelector && e.target.matches(this.closeSelector)) ||
488           (this.closeAttribute && e.target.hasAttribute(this.closeAttribute))) {
489         this.toggle();
490       } else {
491         if (this.autoCloseJob) {
492           this.autoCloseJob.stop();
493           this.autoCloseJob = null;
494         }
495       }
496     },
497     
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() {
508           this.close();
509         });
510       }
511     },
513     keydownHandler: function(e) {
514       if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) {
515         this.close();
516         e.stopPropagation();
517       }
518     },
520     /**
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
525      */
526     resizeHandler: function() {
527       this.updateTargetDimensions();
528     },
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]);
534       }
535     },
537     removeElementListenerList: function(node, events) {
538       for (var i in events) {
539         this.removeElementListener(node, i, events[i]);
540       }
541     },
543     enableElementListener: function(enable, node, event, methodName, capture) {
544       if (enable) {
545         this.addElementListener(node, event, methodName, capture);
546       } else {
547         this.removeElementListener(node, event, methodName, capture);
548       }
549     },
551     addElementListener: function(node, event, methodName, capture) {
552       var fn = this._makeBoundListener(methodName);
553       if (node && fn) {
554         Polymer.addEventListener(node, event, fn, capture);
555       }
556     },
558     removeElementListener: function(node, event, methodName, capture) {
559       var fn = this._makeBoundListener(methodName);
560       if (node && fn) {
561         Polymer.removeEventListener(node, event, fn, capture);
562       }
563     },
565     _makeBoundListener: function(methodName) {
566       var self = this, method = this[methodName];
567       if (!method) {
568         return;
569       }
570       var bound = '_bound' + methodName;
571       if (!this[bound]) {
572         this[bound] = function(e) {
573           method.call(self, e);
574         };
575       }
576       return this[bound];
577     }
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
584   var overlays = [];
585   function addOverlay(overlay) {
586     var z0 = currentOverlayZ();
587     overlays.push(overlay);
588     var z1 = currentOverlayZ();
589     if (z1 <= z0) {
590       applyOverlayZ(overlay, z0);
591     }
592   }
594   function removeOverlay(overlay) {
595     var i = overlays.indexOf(overlay);
596     if (i >= 0) {
597       overlays.splice(i, 1);
598       setZ(overlay, '');
599     }
600   }
601   
602   function applyOverlayZ(overlay, aboveZ) {
603     setZ(overlay.target, aboveZ + 2);
604   }
605   
606   function setZ(element, z) {
607     element.style.zIndex = z;
608   }
610   function currentOverlay() {
611     return overlays[overlays.length-1];
612   }
613   
614   var DEFAULT_Z = 10;
615   
616   function currentOverlayZ() {
617     var z;
618     var current = currentOverlay();
619     if (current) {
620       var z1 = window.getComputedStyle(current.target).zIndex;
621       if (!isNaN(z1)) {
622         z = Number(z1);
623       }
624     }
625     return z || DEFAULT_Z;
626   }
627   
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.
634     //
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();
640     }
641   }
643   var backdrops = [];
644   function trackBackdrop(element) {
645     if (element.opened) {
646       backdrops.push(element);
647     } else {
648       var i = backdrops.indexOf(element);
649       if (i >= 0) {
650         backdrops.splice(i, 1);
651       }
652     }
653   }
655   function getBackdrops() {
656     return backdrops;
657   }
658 })();