OLD | NEW |
| (Empty) |
1 | |
2 (function() { | |
3 | |
4 Polymer('core-overlay',Polymer.mixin({ | |
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 * 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, | |
82 | |
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', | |
93 | |
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: '', | |
104 | |
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' | |
116 | |
117 }, | |
118 | |
119 captureEventName: 'tap', | |
120 targetListeners: { | |
121 'tap': 'tapHandler', | |
122 'keydown': 'keydownHandler', | |
123 'core-transitionend': 'transitionend' | |
124 }, | |
125 | |
126 attached: function() { | |
127 this.resizerAttachedHandler(); | |
128 }, | |
129 | |
130 detached: function() { | |
131 this.resizerDetachedHandler(); | |
132 }, | |
133 | |
134 resizerShouldNotify: function() { | |
135 return this.opened; | |
136 }, | |
137 | |
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 }, | |
145 | |
146 ready: function() { | |
147 this.target = this.target || this; | |
148 // flush to ensure styles are installed before paint | |
149 Polymer.flush(); | |
150 }, | |
151 | |
152 /** | |
153 * Toggle the opened state of the overlay. | |
154 * @method toggle | |
155 */ | |
156 toggle: function() { | |
157 this.opened = !this.opened; | |
158 }, | |
159 | |
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 }, | |
168 | |
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 }, | |
177 | |
178 domReady: function() { | |
179 this.ensureTargetSetup(); | |
180 }, | |
181 | |
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 }, | |
204 | |
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 }, | |
214 | |
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 }, | |
238 | |
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 }, | |
252 | |
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'); | |
268 | |
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 }, | |
284 | |
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 }, | |
297 | |
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 }, | |
321 | |
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 }, | |
331 | |
332 renderBackdropOpened: function() { | |
333 if (this.backdrop && getBackdrops().length < 2) { | |
334 this.scrim.classList.toggle('core-opened', this.opened); | |
335 } | |
336 }, | |
337 | |
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 }, | |
346 | |
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 }, | |
352 | |
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 }, | |
379 | |
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 }, | |
387 | |
388 getTransition: function(name) { | |
389 return this.meta.byId(name || this.transition); | |
390 }, | |
391 | |
392 getFocusNode: function() { | |
393 return this.target.querySelector('[autofocus]') || this.target; | |
394 }, | |
395 | |
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 ne
xt! (bug)'); | |
406 } else { | |
407 focusOverlay(); | |
408 } | |
409 } | |
410 }, | |
411 | |
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 }, | |
423 | |
424 updateTargetDimensions: function() { | |
425 this.sizeTarget(); | |
426 this.repositionTarget(); | |
427 }, | |
428 | |
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 }, | |
440 | |
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 }, | |
452 | |
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 } | |
464 | |
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 }, | |
471 | |
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 }, | |
483 | |
484 tapHandler: function(e) { | |
485 // closeSelector takes precedence since closeAttribute has a default non-n
ull 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 }, | |
512 | |
513 keydownHandler: function(e) { | |
514 if (!this.autoCloseDisabled && (e.keyCode == this.keyHelper.ESCAPE_KEY)) { | |
515 this.close(); | |
516 e.stopPropagation(); | |
517 } | |
518 }, | |
519 | |
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 }, | |
529 | |
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 }, | |
536 | |
537 removeElementListenerList: function(node, events) { | |
538 for (var i in events) { | |
539 this.removeElementListener(node, i, events[i]); | |
540 } | |
541 }, | |
542 | |
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 }, | |
550 | |
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 }, | |
557 | |
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 }, | |
564 | |
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 } | |
578 | |
579 }, Polymer.CoreResizer)); | |
580 | |
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 } | |
593 | |
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 } | |
609 | |
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 } | |
642 | |
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 } | |
654 | |
655 function getBackdrops() { | |
656 return backdrops; | |
657 } | |
658 })(); | |
OLD | NEW |