Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / polymer / components / core-overlay / core-overlay.html
1 <!--
2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
6 Code distributed by Google as part of the polymer project is also
7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
8 -->
9
10 <link rel="import" href="../polymer/polymer.html">
11 <link rel="import" href="../core-transition/core-transition.html">
12 <link rel="import" href="core-key-helper.html">
13 <link rel="import" href="core-overlay-layer.html">
14
15 <!--
16 The `core-overlay` element displays overlayed on top of other content. It starts
17 out hidden and is displayed by setting its `opened` property to true.
18 A `core-overlay's` opened state can be toggled by calling the `toggle`
19 method.
20
21 The `core-overlay` will, by default, show/hide itself when it's opened. The 
22 `target` property may be set to another element to cause that element to 
23 be shown when the overlay is opened.
24
25 It's common to want a `core-overlay` to animate to its opened
26 position. The `core-overlay` element uses a `core-transition` to handle
27 animation. The default transition is `core-transition-fade` which 
28 causes the overlay to fade in when displayed. See 
29 <a href="../core-transition/">`core-transition`</a> for more
30 information about customizing a `core-overlay's` opening animation. The
31 `backdrop` property can be set to true to show a backdrop behind the overlay
32 that will darken the rest of the window.
33
34 An element that should close the `core-overlay` will automatically
35 do so if it's given the `core-overlay-toggle` attribute. This attribute
36 can be customized with the `closeAttribute` property. You can also use
37 `closeSelector` if more general matching is needed.
38
39 By default  `core-overlay` will close whenever the user taps outside it or
40 presses the escape key. This behavior can be turned off via the
41 `autoCloseDisabled` property.
42
43     <core-overlay>
44       <h2>Dialog</h2>
45       <input placeholder="say something..." autofocus>
46       <div>I agree with this wholeheartedly.</div>
47       <button core-overlay-toggle>OK</button>
48     </core-overlay>
49
50 `core-overlay` will automatically size and position itself according to the 
51 following rules. If the target's style.top and style.left are unset, the 
52 target will be centered. The size of the target is constrained to be no larger
53 than the window dimensions. The `margin` property specifies the extra amount
54 of space that should be reserved around the overlay. This can be used to ensure
55 that, for example, a drop shadow is always visible around the overlay.
56
57 @group Core Elements
58 @element core-overlay
59 @homepage github.io
60 -->
61 <!--
62 Fired when the `core-overlay`'s `opened` property changes.
63
64 @event core-overlay-open
65 @param {Object} detail
66 @param {Object} detail.opened the opened state
67 -->
68
69 <style>
70   .core-overlay-backdrop {
71     position: fixed;
72     top: 0;
73     left: 0;
74     width: 100vw;
75     height: 100vh;
76     background-color: black;
77     opacity: 0;
78     transition: opacity 0.2s;
79   }
80
81   .core-overlay-backdrop.core-opened {
82     opacity: 0.6;
83   }
84 </style>
85
86 <polymer-element name="core-overlay">
87 <script>
88 (function() {
89
90   Polymer('core-overlay', {
91
92     publish: {
93       /**
94        * The target element that will be shown when the overlay is 
95        * opened. If unspecified, the core-overlay itself is the target.
96        *
97        * @attribute target
98        * @type Object
99        * @default the overlay element
100        */
101       target: null,
102
103
104       /**
105        * A `core-overlay`'s size is guaranteed to be 
106        * constrained to the window size. To achieve this, the sizingElement
107        * is sized with a max-height/width. By default this element is the 
108        * target element, but it can be specifically set to a specific element
109        * inside the target if that is more appropriate. This is useful, for 
110        * example, when a region inside the overlay should scroll if needed.
111        *
112        * @attribute sizingTarget
113        * @type Object
114        * @default the target element
115        */
116       sizingTarget: null,
117     
118       /**
119        * Set opened to true to show an overlay and to false to hide it.
120        * A `core-overlay` may be made initially opened by setting its
121        * `opened` attribute.
122        * @attribute opened
123        * @type boolean
124        * @default false
125        */
126       opened: false,
127
128       /**
129        * If true, the overlay has a backdrop darkening the rest of the screen.
130        * The backdrop element is attached to the document body and may be styled
131        * with the class `core-overlay-backdrop`. When opened the `core-opened`
132        * class is applied.
133        *
134        * @attribute backdrop
135        * @type boolean
136        * @default false
137        */    
138       backdrop: false,
139
140       /**
141        * If true, the overlay is guaranteed to display above page content.
142        *
143        * @attribute layered
144        * @type boolean
145        * @default false
146       */
147       layered: false,
148     
149       /**
150        * By default an overlay will close automatically if the user
151        * taps outside it or presses the escape key. Disable this
152        * behavior by setting the `autoCloseDisabled` property to true.
153        * @attribute autoCloseDisabled
154        * @type boolean
155        * @default false
156        */
157       autoCloseDisabled: false,
158
159       /**
160        * This property specifies an attribute on elements that should
161        * close the overlay on tap. Should not set `closeSelector` if this
162        * is set.
163        *
164        * @attribute closeAttribute
165        * @type string
166        * @default "core-overlay-toggle"
167        */
168       closeAttribute: 'core-overlay-toggle',
169
170       /**
171        * This property specifies a selector matching elements that should
172        * close the overlay on tap. Should not set `closeAttribute` if this
173        * is set.
174        *
175        * @attribute closeSelector
176        * @type string
177        * @default ""
178        */
179       closeSelector: '',
180
181       /**
182        * A `core-overlay` target's size is constrained to the window size.
183        * The `margin` property specifies a pixel amount around the overlay 
184        * that will be reserved. It's useful for ensuring that, for example, 
185        * a shadow displayed outside the target will always be visible.
186        *
187        * @attribute margin
188        * @type number
189        * @default 0
190        */
191       margin: 0,
192
193       /**
194        * The transition property specifies a string which identifies a 
195        * <a href="../core-transition/">`core-transition`</a> element that 
196        * will be used to help the overlay open and close. The default
197        * `core-transition-fade` will cause the overlay to fade in and out.
198        *
199        * @attribute transition
200        * @type string
201        * @default 'core-transition-fade'
202        */
203       transition: 'core-transition-fade'
204
205     },
206
207     captureEventName: 'tap',
208     targetListeners: {
209       'tap': 'tapHandler',
210       'keydown': 'keydownHandler',
211       'core-transitionend': 'transitionend'
212     },
213     
214     registerCallback: function(element) {
215       this.layer = document.createElement('core-overlay-layer');
216       this.keyHelper = document.createElement('core-key-helper');
217       this.meta = document.createElement('core-transition');
218       this.scrim = document.createElement('div');
219       this.scrim.className = 'core-overlay-backdrop';
220     },
221
222     ready: function() {
223       this.target = this.target || this;
224       // flush to ensure styles are installed before paint
225       Platform.flush();
226     },
227
228     /** 
229      * Toggle the opened state of the overlay.
230      * @method toggle
231      */
232     toggle: function() {
233       this.opened = !this.opened;
234     },
235
236     /** 
237      * Open the overlay. This is equivalent to setting the `opened`
238      * property to true.
239      * @method open
240      */
241     open: function() {
242       this.opened = true;
243     },
244
245     /** 
246      * Close the overlay. This is equivalent to setting the `opened` 
247      * property to false.
248      * @method close
249      */
250     close: function() {
251       this.opened = false;
252     },
253
254     domReady: function() {
255       this.ensureTargetSetup();
256     },
257
258     targetChanged: function(old) {
259       if (this.target) {
260         // really make sure tabIndex is set
261         if (this.target.tabIndex < 0) {
262           this.target.tabIndex = -1;
263         }
264         this.addElementListenerList(this.target, this.targetListeners);
265         this.target.style.display = 'none';
266       }
267       if (old) {
268         this.removeElementListenerList(old, this.targetListeners);
269         var transition = this.getTransition();
270         if (transition) {
271           transition.teardown(old);
272         } else {
273           old.style.position = '';
274           old.style.outline = '';
275         }
276         old.style.display = '';
277       }
278     },
279
280     // NOTE: wait to call this until we're as sure as possible that target
281     // is styled.
282     ensureTargetSetup: function() {
283       if (!this.target || this.target.__overlaySetup) {
284         return;
285       }
286       this.target.__overlaySetup = true;
287       this.target.style.display = '';
288       var transition = this.getTransition();
289       if (transition) {
290         transition.setup(this.target);
291       }
292       var computed = getComputedStyle(this.target);
293       this.targetStyle = {
294         position: computed.position === 'static' ? 'fixed' :
295             computed.position
296       }
297       if (!transition) {
298         this.target.style.position = this.targetStyle.position;
299         this.target.style.outline = 'none';
300       }
301       this.target.style.display = 'none';
302     },
303
304     openedChanged: function() {
305       this.transitioning = true;
306       this.ensureTargetSetup();
307       this.prepareRenderOpened();
308       // continue styling after delay so display state can change
309       // without aborting transitions
310       // note: we wait a full frame so that transition changes executed
311       // during measuring do not cause transition
312       this.async(function() {
313         this.target.style.display = '';
314         this.async('renderOpened');
315       });
316       this.fire('core-overlay-open', this.opened);
317     },
318
319     // tasks which must occur before opening; e.g. making the element visible
320     prepareRenderOpened: function() {
321       if (this.opened) {
322         addOverlay(this);
323       }
324       this.prepareBackdrop();
325       // async so we don't auto-close immediately via a click.
326       this.async(function() {
327         if (!this.autoCloseDisabled) {
328           this.enableElementListener(this.opened, document,
329               this.captureEventName, 'captureHandler', true);
330         }
331       });
332       this.enableElementListener(this.opened, window, 'resize',
333           'resizeHandler');
334
335       if (this.opened) {
336         // TODO(sorvell): force SD Polyfill to render
337         forcePolyfillRender(this.target);
338         if (!this._shouldPosition) {
339           this.target.style.position = 'absolute';
340           var computed = getComputedStyle(this.target);
341           var t = (computed.top === 'auto' && computed.bottom === 'auto');
342           var l = (computed.left === 'auto' && computed.right === 'auto');
343           this.target.style.position = this.targetStyle.position;
344           this._shouldPosition = {top: t, left: l};
345         }
346         // if we are showing, then take care when measuring
347         this.prepareMeasure(this.target);
348         this.updateTargetDimensions();
349         this.finishMeasure(this.target);
350         if (this.layered) {
351           this.layer.addElement(this.target);
352           this.layer.opened = this.opened;
353         }
354       }
355     },
356
357     // tasks which cause the overlay to actually open; typically play an
358     // animation
359     renderOpened: function() {
360       var transition = this.getTransition();
361       if (transition) {
362         transition.go(this.target, {opened: this.opened});
363       } else {
364         this.transitionend();
365       }
366       this.renderBackdropOpened();
367     },
368
369     // finishing tasks; typically called via a transition
370     transitionend: function(e) {
371       // make sure this is our transition event.
372       if (e && e.target !== this.target) {
373         return;
374       }
375       this.transitioning = false;
376       if (!this.opened) {
377         this.resetTargetDimensions();
378         this.target.style.display = 'none';
379         this.completeBackdrop();
380         removeOverlay(this);
381         if (this.layered) {
382           if (!currentOverlay()) {
383             this.layer.opened = this.opened;
384           }
385           this.layer.removeElement(this.target);
386         }
387       }
388       this.applyFocus();
389     },
390
391     prepareBackdrop: function() {
392       if (this.backdrop && this.opened) {
393         if (!this.scrim.parentNode) {
394           document.body.appendChild(this.scrim);
395           this.scrim.style.zIndex = currentOverlayZ() - 1;
396         }
397         trackBackdrop(this);
398       }
399     },
400
401     renderBackdropOpened: function() {
402       if (this.backdrop && getBackdrops().length < 2) {
403         this.scrim.classList.toggle('core-opened', this.opened);
404       }
405     },
406
407     completeBackdrop: function() {
408       if (this.backdrop) {
409         trackBackdrop(this);
410         if (getBackdrops().length === 0) {
411           this.scrim.parentNode.removeChild(this.scrim);
412         }
413       }
414     },
415
416     prepareMeasure: function(target) {
417       target.style.transition = target.style.webkitTransition = 'none';
418       target.style.transform = target.style.webkitTransform = 'none';
419       target.style.display = '';
420     },
421
422     finishMeasure: function(target) {
423       target.style.display = 'none';
424       target.style.transform = target.style.webkitTransform = '';
425       target.style.transition = target.style.webkitTransition = '';
426     },
427
428     getTransition: function() {
429       return this.meta.byId(this.transition);
430     },
431
432     getFocusNode: function() {
433       return this.target.querySelector('[autofocus]') || this.target;
434     },
435
436     applyFocus: function() {
437       var focusNode = this.getFocusNode();
438       if (this.opened) {
439         focusNode.focus();
440       } else {
441         focusNode.blur();
442         if (currentOverlay() == this) {
443           console.warn('Current core-overlay is attempting to focus itself as next! (bug)');
444         } else {
445           focusOverlay();
446         }
447       }
448     },
449
450     updateTargetDimensions: function() {
451       this.positionTarget();
452       this.sizeTarget();
453       //
454       if (this.layered) {
455         var rect = this.target.getBoundingClientRect();
456         this.target.style.top = rect.top + 'px';
457         this.target.style.left = rect.left + 'px';
458         this.target.style.right = this.target.style.bottom = 'auto';
459       }
460     },
461
462     sizeTarget: function() {
463       var sizer = this.sizingTarget || this.target;
464       var rect = sizer.getBoundingClientRect();
465       var mt = rect.top === this.margin ? this.margin : this.margin * 2;
466       var ml = rect.left === this.margin ? this.margin : this.margin * 2;
467       var h = window.innerHeight - rect.top - mt;
468       var w = window.innerWidth - rect.left - ml;
469       sizer.style.maxHeight = h + 'px';
470       sizer.style.maxWidth = w + 'px';
471       sizer.style.boxSizing = 'border-box';
472     },
473
474     positionTarget: function() {
475       // vertically and horizontally center if not positioned
476       if (this._shouldPosition.top) {
477         var t = Math.max((window.innerHeight - 
478             this.target.offsetHeight - this.margin*2) / 2, this.margin);
479         this.target.style.top = t + 'px';
480       }
481       if (this._shouldPosition.left) {
482         var l = Math.max((window.innerWidth - 
483             this.target.offsetWidth - this.margin*2) / 2, this.margin);
484         this.target.style.left = l + 'px';
485       }
486     },
487
488     resetTargetDimensions: function() {
489       this.target.style.top = this.target.style.left = '';
490       this.target.style.right = this.target.style.bottom = '';
491       this.target.style.width = this.target.style.height = '';
492       this._shouldPosition = null;
493     },
494
495     tapHandler: function(e) {
496       // closeSelector takes precedence since closeAttribute has a default non-null value.
497       if (e.target &&
498           (this.closeSelector && e.target.matches(this.closeSelector)) ||
499           (this.closeAttribute && e.target.hasAttribute(this.closeAttribute))) {
500         this.toggle();
501       } else {
502         if (this.autoCloseJob) {
503           this.autoCloseJob.stop();
504           this.autoCloseJob = null;
505         }
506       }
507     },
508     
509     // We use the traditional approach of capturing events on document
510     // to to determine if the overlay needs to close. However, due to 
511     // ShadowDOM event retargeting, the event target is not useful. Instead
512     // of using it, we attempt to close asynchronously and prevent the close
513     // if a tap event is immediately heard on the target.
514     // TODO(sorvell): This approach will not work with modal. For
515     // this we need a scrim.
516     captureHandler: function(e) {
517       if (!this.autoCloseDisabled && (currentOverlay() == this)) {
518         this.autoCloseJob = this.job(this.autoCloseJob, function() {
519           this.close();
520         });
521       }
522     },
523
524     keydownHandler: function(e) {
525       if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) {
526         this.close();
527         e.stopPropagation();
528       }
529     },
530
531     /**
532      * Extensions of core-overlay should implement the `resizeHandler`
533      * method to adjust the size and position of the overlay when the 
534      * browser window resizes.
535      * @method resizeHandler
536      */
537     resizeHandler: function() {
538       this.updateTargetDimensions();
539     },
540
541     // TODO(sorvell): these utility methods should not be here.
542     addElementListenerList: function(node, events) {
543       for (var i in events) {
544         this.addElementListener(node, i, events[i]);
545       }
546     },
547
548     removeElementListenerList: function(node, events) {
549       for (var i in events) {
550         this.removeElementListener(node, i, events[i]);
551       }
552     },
553
554     enableElementListener: function(enable, node, event, methodName, capture) {
555       if (enable) {
556         this.addElementListener(node, event, methodName, capture);
557       } else {
558         this.removeElementListener(node, event, methodName, capture);
559       }
560     },
561
562     addElementListener: function(node, event, methodName, capture) {
563       var fn = this._makeBoundListener(methodName);
564       if (node && fn) {
565         Polymer.addEventListener(node, event, fn, capture);
566       }
567     },
568
569     removeElementListener: function(node, event, methodName, capture) {
570       var fn = this._makeBoundListener(methodName);
571       if (node && fn) {
572         Polymer.removeEventListener(node, event, fn, capture);
573       }
574     },
575
576     _makeBoundListener: function(methodName) {
577       var self = this, method = this[methodName];
578       if (!method) {
579         return;
580       }
581       var bound = '_bound' + methodName;
582       if (!this[bound]) {
583         this[bound] = function(e) {
584           method.call(self, e);
585         }
586       }
587       return this[bound];
588     },
589   });
590
591   function forcePolyfillRender(target) {
592     if (window.ShadowDOMPolyfill) {
593       target.offsetHeight;
594     }
595   }
596
597   // TODO(sorvell): This should be an element with private state so it can
598   // be independent of overlay.
599   // track overlays for z-index and focus managemant
600   var overlays = [];
601   function addOverlay(overlay) {
602     var z0 = currentOverlayZ();
603     overlays.push(overlay);
604     var z1 = currentOverlayZ();
605     if (z1 <= z0) {
606       applyOverlayZ(overlay, z0);
607     }
608   }
609
610   function removeOverlay(overlay) {
611     var i = overlays.indexOf(overlay);
612     if (i >= 0) {
613       overlays.splice(i, 1);
614       setZ(overlay, '');
615     }
616   }
617   
618   function applyOverlayZ(overlay, aboveZ) {
619     setZ(overlay.target, aboveZ + 2);
620   }
621   
622   function setZ(element, z) {
623     element.style.zIndex = z;
624   }
625
626   function currentOverlay() {
627     return overlays[overlays.length-1];
628   }
629   
630   var DEFAULT_Z = 10;
631   
632   function currentOverlayZ() {
633     var z;
634     var current = currentOverlay();
635     if (current) {
636       var z1 = window.getComputedStyle(current.target).zIndex;
637       if (!isNaN(z1)) {
638         z = Number(z1);
639       }
640     }
641     return z || DEFAULT_Z;
642   }
643   
644   function focusOverlay() {
645     var current = currentOverlay();
646     // We have to be careful to focus the next overlay _after_ any current
647     // transitions are complete (due to the state being toggled prior to the
648     // transition). Otherwise, we risk infinite recursion when a transitioning
649     // (closed) overlay becomes the current overlay.
650     //
651     // NOTE: We make the assumption that any overlay that completes a transition
652     // will call into focusOverlay to kick the process back off. Currently:
653     // transitionend -> applyFocus -> focusOverlay.
654     if (current && !current.transitioning) {
655       current.applyFocus();
656     }
657   }
658
659   var backdrops = [];
660   function trackBackdrop(element) {
661     if (element.opened) {
662       backdrops.push(element);
663     } else {
664       var i = backdrops.indexOf(element);
665       if (i >= 0) {
666         backdrops.splice(i, 1);
667       }
668     }
669   }
670
671   function getBackdrops() {
672     return backdrops;
673   }
674 })();
675 </script>
676 </polymer-element>