Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / polymer / components-chromium / core-overlay / core-overlay-extracted.js
1
2 (function() {
3
4   Polymer('core-overlay', {
5
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,
16
17
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,
41
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,
53
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,
72
73       /**
74        * This property specifies an attribute on elements that should
75        * close the overlay on tap. Should not set `closeSelector` if this
76        * is set.
77        *
78        * @attribute closeAttribute
79        * @type string
80        * @default "core-overlay-toggle"
81        */
82       closeAttribute: 'core-overlay-toggle',
83
84       /**
85        * This property specifies a selector matching elements that should
86        * close the overlay on tap. Should not set `closeAttribute` if this
87        * is set.
88        *
89        * @attribute closeSelector
90        * @type string
91        * @default ""
92        */
93       closeSelector: '',
94
95       /**
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.
100        *
101        * @attribute margin
102        * @type number
103        * @default 0
104        */
105       margin: 0,
106
107       /**
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.
112        *
113        * @attribute transition
114        * @type string
115        * @default 'core-transition-fade'
116        */
117       transition: 'core-transition-fade'
118
119     },
120
121     captureEventName: 'tap',
122     targetListeners: {
123       'tap': 'tapHandler',
124       'keydown': 'keydownHandler',
125       'core-transitionend': 'transitionend'
126     },
127     
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';
134     },
135
136     ready: function() {
137       this.target = this.target || this;
138       // flush to ensure styles are installed before paint
139       Platform.flush();
140     },
141
142     /** 
143      * Toggle the opened state of the overlay.
144      * @method toggle
145      */
146     toggle: function() {
147       this.opened = !this.opened;
148     },
149
150     /** 
151      * Open the overlay. This is equivalent to setting the `opened`
152      * property to true.
153      * @method open
154      */
155     open: function() {
156       this.opened = true;
157     },
158
159     /** 
160      * Close the overlay. This is equivalent to setting the `opened` 
161      * property to false.
162      * @method close
163      */
164     close: function() {
165       this.opened = false;
166     },
167
168     domReady: function() {
169       this.ensureTargetSetup();
170     },
171
172     targetChanged: function(old) {
173       if (this.target) {
174         // really make sure tabIndex is set
175         if (this.target.tabIndex < 0) {
176           this.target.tabIndex = -1;
177         }
178         this.addElementListenerList(this.target, this.targetListeners);
179         this.target.style.display = 'none';
180       }
181       if (old) {
182         this.removeElementListenerList(old, this.targetListeners);
183         var transition = this.getTransition();
184         if (transition) {
185           transition.teardown(old);
186         } else {
187           old.style.position = '';
188           old.style.outline = '';
189         }
190         old.style.display = '';
191       }
192     },
193
194     // NOTE: wait to call this until we're as sure as possible that target
195     // is styled.
196     ensureTargetSetup: function() {
197       if (!this.target || this.target.__overlaySetup) {
198         return;
199       }
200       this.target.__overlaySetup = true;
201       this.target.style.display = '';
202       var transition = this.getTransition();
203       if (transition) {
204         transition.setup(this.target);
205       }
206       var computed = getComputedStyle(this.target);
207       this.targetStyle = {
208         position: computed.position === 'static' ? 'fixed' :
209             computed.position
210       }
211       if (!transition) {
212         this.target.style.position = this.targetStyle.position;
213         this.target.style.outline = 'none';
214       }
215       this.target.style.display = 'none';
216     },
217
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');
229       });
230       this.fire('core-overlay-open', this.opened);
231     },
232
233     // tasks which must occur before opening; e.g. making the element visible
234     prepareRenderOpened: function() {
235       if (this.opened) {
236         addOverlay(this);
237       }
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);
244         }
245       });
246       this.enableElementListener(this.opened, window, 'resize',
247           'resizeHandler');
248
249       if (this.opened) {
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};
259         }
260         // if we are showing, then take care when measuring
261         this.prepareMeasure(this.target);
262         this.updateTargetDimensions();
263         this.finishMeasure(this.target);
264         if (this.layered) {
265           this.layer.addElement(this.target);
266           this.layer.opened = this.opened;
267         }
268       }
269     },
270
271     // tasks which cause the overlay to actually open; typically play an
272     // animation
273     renderOpened: function() {
274       var transition = this.getTransition();
275       if (transition) {
276         transition.go(this.target, {opened: this.opened});
277       } else {
278         this.transitionend();
279       }
280       this.renderBackdropOpened();
281     },
282
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) {
287         return;
288       }
289       this.transitioning = false;
290       if (!this.opened) {
291         this.resetTargetDimensions();
292         this.target.style.display = 'none';
293         this.completeBackdrop();
294         removeOverlay(this);
295         if (this.layered) {
296           if (!currentOverlay()) {
297             this.layer.opened = this.opened;
298           }
299           this.layer.removeElement(this.target);
300         }
301       }
302       this.applyFocus();
303     },
304
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;
310         }
311         trackBackdrop(this);
312       }
313     },
314
315     renderBackdropOpened: function() {
316       if (this.backdrop && getBackdrops().length < 2) {
317         this.scrim.classList.toggle('core-opened', this.opened);
318       }
319     },
320
321     completeBackdrop: function() {
322       if (this.backdrop) {
323         trackBackdrop(this);
324         if (getBackdrops().length === 0) {
325           this.scrim.parentNode.removeChild(this.scrim);
326         }
327       }
328     },
329
330     prepareMeasure: function(target) {
331       target.style.transition = target.style.webkitTransition = 'none';
332       target.style.transform = target.style.webkitTransform = 'none';
333       target.style.display = '';
334     },
335
336     finishMeasure: function(target) {
337       target.style.display = 'none';
338       target.style.transform = target.style.webkitTransform = '';
339       target.style.transition = target.style.webkitTransition = '';
340     },
341
342     getTransition: function() {
343       return this.meta.byId(this.transition);
344     },
345
346     getFocusNode: function() {
347       return this.target.querySelector('[autofocus]') || this.target;
348     },
349
350     applyFocus: function() {
351       var focusNode = this.getFocusNode();
352       if (this.opened) {
353         focusNode.focus();
354       } else {
355         focusNode.blur();
356         if (currentOverlay() == this) {
357           console.warn('Current core-overlay is attempting to focus itself as next! (bug)');
358         } else {
359           focusOverlay();
360         }
361       }
362     },
363
364     updateTargetDimensions: function() {
365       this.positionTarget();
366       this.sizeTarget();
367       //
368       if (this.layered) {
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';
373       }
374     },
375
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';
386     },
387
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';
394       }
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';
399       }
400     },
401
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;
407     },
408
409     tapHandler: function(e) {
410       // closeSelector takes precedence since closeAttribute has a default non-null value.
411       if (e.target &&
412           (this.closeSelector && e.target.matches(this.closeSelector)) ||
413           (this.closeAttribute && e.target.hasAttribute(this.closeAttribute))) {
414         this.toggle();
415       } else {
416         if (this.autoCloseJob) {
417           this.autoCloseJob.stop();
418           this.autoCloseJob = null;
419         }
420       }
421     },
422     
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() {
433           this.close();
434         });
435       }
436     },
437
438     keydownHandler: function(e) {
439       if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) {
440         this.close();
441         e.stopPropagation();
442       }
443     },
444
445     /**
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
450      */
451     resizeHandler: function() {
452       this.updateTargetDimensions();
453     },
454
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]);
459       }
460     },
461
462     removeElementListenerList: function(node, events) {
463       for (var i in events) {
464         this.removeElementListener(node, i, events[i]);
465       }
466     },
467
468     enableElementListener: function(enable, node, event, methodName, capture) {
469       if (enable) {
470         this.addElementListener(node, event, methodName, capture);
471       } else {
472         this.removeElementListener(node, event, methodName, capture);
473       }
474     },
475
476     addElementListener: function(node, event, methodName, capture) {
477       var fn = this._makeBoundListener(methodName);
478       if (node && fn) {
479         Polymer.addEventListener(node, event, fn, capture);
480       }
481     },
482
483     removeElementListener: function(node, event, methodName, capture) {
484       var fn = this._makeBoundListener(methodName);
485       if (node && fn) {
486         Polymer.removeEventListener(node, event, fn, capture);
487       }
488     },
489
490     _makeBoundListener: function(methodName) {
491       var self = this, method = this[methodName];
492       if (!method) {
493         return;
494       }
495       var bound = '_bound' + methodName;
496       if (!this[bound]) {
497         this[bound] = function(e) {
498           method.call(self, e);
499         }
500       }
501       return this[bound];
502     },
503   });
504
505   function forcePolyfillRender(target) {
506     if (window.ShadowDOMPolyfill) {
507       target.offsetHeight;
508     }
509   }
510
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
514   var overlays = [];
515   function addOverlay(overlay) {
516     var z0 = currentOverlayZ();
517     overlays.push(overlay);
518     var z1 = currentOverlayZ();
519     if (z1 <= z0) {
520       applyOverlayZ(overlay, z0);
521     }
522   }
523
524   function removeOverlay(overlay) {
525     var i = overlays.indexOf(overlay);
526     if (i >= 0) {
527       overlays.splice(i, 1);
528       setZ(overlay, '');
529     }
530   }
531   
532   function applyOverlayZ(overlay, aboveZ) {
533     setZ(overlay.target, aboveZ + 2);
534   }
535   
536   function setZ(element, z) {
537     element.style.zIndex = z;
538   }
539
540   function currentOverlay() {
541     return overlays[overlays.length-1];
542   }
543   
544   var DEFAULT_Z = 10;
545   
546   function currentOverlayZ() {
547     var z;
548     var current = currentOverlay();
549     if (current) {
550       var z1 = window.getComputedStyle(current.target).zIndex;
551       if (!isNaN(z1)) {
552         z = Number(z1);
553       }
554     }
555     return z || DEFAULT_Z;
556   }
557   
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.
564     //
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();
570     }
571   }
572
573   var backdrops = [];
574   function trackBackdrop(element) {
575     if (element.opened) {
576       backdrops.push(element);
577     } else {
578       var i = backdrops.indexOf(element);
579       if (i >= 0) {
580         backdrops.splice(i, 1);
581       }
582     }
583   }
584
585   function getBackdrops() {
586     return backdrops;
587   }
588 })();