OLD | NEW |
(Empty) | |
| 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 ne
xt! (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-n
ull 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 })(); |
OLD | NEW |