| OLD | NEW |
| (Empty) | |
| 1 Polymer({ |
| 2 is: 'app-drawer', |
| 3 |
| 4 properties: { |
| 5 /** |
| 6 * The opened state of the drawer. |
| 7 */ |
| 8 opened: { |
| 9 type: Boolean, |
| 10 value: false, |
| 11 notify: true, |
| 12 reflectToAttribute: true |
| 13 }, |
| 14 |
| 15 /** |
| 16 * The drawer does not have a scrim and cannot be swiped close. |
| 17 */ |
| 18 persistent: { |
| 19 type: Boolean, |
| 20 value: false, |
| 21 reflectToAttribute: true |
| 22 }, |
| 23 |
| 24 /** |
| 25 * The alignment of the drawer on the screen ('left', 'right', 'start' o
r 'end'). |
| 26 * 'start' computes to left and 'end' to right in LTR layout and vice ve
rsa in RTL |
| 27 * layout. |
| 28 */ |
| 29 align: { |
| 30 type: String, |
| 31 value: 'left' |
| 32 }, |
| 33 |
| 34 /** |
| 35 * The computed, read-only position of the drawer on the screen ('left'
or 'right'). |
| 36 */ |
| 37 position: { |
| 38 type: String, |
| 39 readOnly: true, |
| 40 value: 'left', |
| 41 reflectToAttribute: true |
| 42 }, |
| 43 |
| 44 /** |
| 45 * Create an area at the edge of the screen to swipe open the drawer. |
| 46 */ |
| 47 swipeOpen: { |
| 48 type: Boolean, |
| 49 value: false, |
| 50 reflectToAttribute: true |
| 51 }, |
| 52 |
| 53 /** |
| 54 * Trap keyboard focus when the drawer is opened and not persistent. |
| 55 */ |
| 56 noFocusTrap: { |
| 57 type: Boolean, |
| 58 value: false |
| 59 } |
| 60 }, |
| 61 |
| 62 observers: [ |
| 63 'resetLayout(position)', |
| 64 '_resetPosition(align, isAttached)' |
| 65 ], |
| 66 |
| 67 _translateOffset: 0, |
| 68 |
| 69 _trackDetails: null, |
| 70 |
| 71 _drawerState: 0, |
| 72 |
| 73 _boundEscKeydownHandler: null, |
| 74 |
| 75 _firstTabStop: null, |
| 76 |
| 77 _lastTabStop: null, |
| 78 |
| 79 ready: function() { |
| 80 // Set the scroll direction so you can vertically scroll inside the draw
er. |
| 81 this.setScrollDirection('y'); |
| 82 |
| 83 // Only transition the drawer after its first render (e.g. app-drawer-la
yout |
| 84 // may need to set the initial opened state which should not be transiti
oned). |
| 85 this._setTransitionDuration('0s'); |
| 86 }, |
| 87 |
| 88 attached: function() { |
| 89 // Only transition the drawer after its first render (e.g. app-drawer-la
yout |
| 90 // may need to set the initial opened state which should not be transiti
oned). |
| 91 Polymer.RenderStatus.afterNextRender(this, function() { |
| 92 this._setTransitionDuration(''); |
| 93 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); |
| 94 this._resetDrawerState(); |
| 95 |
| 96 this.listen(this, 'track', '_track'); |
| 97 this.addEventListener('transitionend', this._transitionend.bind(this))
; |
| 98 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) |
| 99 }); |
| 100 }, |
| 101 |
| 102 detached: function() { |
| 103 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
| 104 }, |
| 105 |
| 106 /** |
| 107 * Opens the drawer. |
| 108 */ |
| 109 open: function() { |
| 110 this.opened = true; |
| 111 }, |
| 112 |
| 113 /** |
| 114 * Closes the drawer. |
| 115 */ |
| 116 close: function() { |
| 117 this.opened = false; |
| 118 }, |
| 119 |
| 120 /** |
| 121 * Toggles the drawer open and close. |
| 122 */ |
| 123 toggle: function() { |
| 124 this.opened = !this.opened; |
| 125 }, |
| 126 |
| 127 /** |
| 128 * Gets the width of the drawer. |
| 129 * |
| 130 * @return {number} The width of the drawer in pixels. |
| 131 */ |
| 132 getWidth: function() { |
| 133 return this.$.contentContainer.offsetWidth; |
| 134 }, |
| 135 |
| 136 /** |
| 137 * Resets the layout. If you changed the size of app-header via CSS |
| 138 * you can notify the changes by either firing the `iron-resize` event |
| 139 * or calling `resetLayout` directly. |
| 140 * |
| 141 * @method resetLayout |
| 142 */ |
| 143 resetLayout: function() { |
| 144 this.debounce('_resetLayout', function() { |
| 145 this.fire('app-drawer-reset-layout'); |
| 146 }, 1); |
| 147 }, |
| 148 |
| 149 _isRTL: function() { |
| 150 return window.getComputedStyle(this).direction === 'rtl'; |
| 151 }, |
| 152 |
| 153 _resetPosition: function() { |
| 154 switch (this.align) { |
| 155 case 'start': |
| 156 this._setPosition(this._isRTL() ? 'right' : 'left'); |
| 157 return; |
| 158 case 'end': |
| 159 this._setPosition(this._isRTL() ? 'left' : 'right'); |
| 160 return; |
| 161 } |
| 162 this._setPosition(this.align); |
| 163 }, |
| 164 |
| 165 _escKeydownHandler: function(event) { |
| 166 var ESC_KEYCODE = 27; |
| 167 if (event.keyCode === ESC_KEYCODE) { |
| 168 // Prevent any side effects if app-drawer closes. |
| 169 event.preventDefault(); |
| 170 this.close(); |
| 171 } |
| 172 }, |
| 173 |
| 174 _track: function(event) { |
| 175 if (this.persistent) { |
| 176 return; |
| 177 } |
| 178 |
| 179 // Disable user selection on desktop. |
| 180 event.preventDefault(); |
| 181 |
| 182 switch (event.detail.state) { |
| 183 case 'start': |
| 184 this._trackStart(event); |
| 185 break; |
| 186 case 'track': |
| 187 this._trackMove(event); |
| 188 break; |
| 189 case 'end': |
| 190 this._trackEnd(event); |
| 191 break; |
| 192 } |
| 193 }, |
| 194 |
| 195 _trackStart: function(event) { |
| 196 this._drawerState = this._DRAWER_STATE.TRACKING; |
| 197 |
| 198 // Disable transitions since style attributes will reflect user track ev
ents. |
| 199 this._setTransitionDuration('0s'); |
| 200 this.style.visibility = 'visible'; |
| 201 |
| 202 var rect = this.$.contentContainer.getBoundingClientRect(); |
| 203 if (this.position === 'left') { |
| 204 this._translateOffset = rect.left; |
| 205 } else { |
| 206 this._translateOffset = rect.right - window.innerWidth; |
| 207 } |
| 208 |
| 209 this._trackDetails = []; |
| 210 }, |
| 211 |
| 212 _trackMove: function(event) { |
| 213 this._translateDrawer(event.detail.dx + this._translateOffset); |
| 214 |
| 215 // Use Date.now() since event.timeStamp is inconsistent across browsers
(e.g. most |
| 216 // browsers use milliseconds but FF 44 uses microseconds). |
| 217 this._trackDetails.push({ |
| 218 dx: event.detail.dx, |
| 219 timeStamp: Date.now() |
| 220 }); |
| 221 }, |
| 222 |
| 223 _trackEnd: function(event) { |
| 224 var x = event.detail.dx + this._translateOffset; |
| 225 var drawerWidth = this.getWidth(); |
| 226 var isPositionLeft = this.position === 'left'; |
| 227 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : |
| 228 (x <= 0 || x >= drawerWidth); |
| 229 |
| 230 if (!isInEndState) { |
| 231 // No longer need the track events after this method returns - allow t
hem to be GC'd. |
| 232 var trackDetails = this._trackDetails; |
| 233 this._trackDetails = null; |
| 234 |
| 235 this._flingDrawer(event, trackDetails); |
| 236 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 237 return; |
| 238 } |
| 239 } |
| 240 |
| 241 // If the drawer is not flinging, toggle the opened state based on the p
osition of |
| 242 // the drawer. |
| 243 var halfWidth = drawerWidth / 2; |
| 244 if (event.detail.dx < -halfWidth) { |
| 245 this.opened = this.position === 'right'; |
| 246 } else if (event.detail.dx > halfWidth) { |
| 247 this.opened = this.position === 'left'; |
| 248 } |
| 249 |
| 250 // Trigger app-drawer-transitioned now since there will be no transition
end event. |
| 251 if (isInEndState) { |
| 252 this._resetDrawerState(); |
| 253 } |
| 254 |
| 255 this._setTransitionDuration(''); |
| 256 this._resetDrawerTranslate(); |
| 257 this.style.visibility = ''; |
| 258 }, |
| 259 |
| 260 _calculateVelocity: function(event, trackDetails) { |
| 261 // Find the oldest track event that is within 100ms using binary search. |
| 262 var now = Date.now(); |
| 263 var timeLowerBound = now - 100; |
| 264 var trackDetail; |
| 265 var min = 0; |
| 266 var max = trackDetails.length - 1; |
| 267 |
| 268 while (min <= max) { |
| 269 // Floor of average of min and max. |
| 270 var mid = (min + max) >> 1; |
| 271 var d = trackDetails[mid]; |
| 272 if (d.timeStamp >= timeLowerBound) { |
| 273 trackDetail = d; |
| 274 max = mid - 1; |
| 275 } else { |
| 276 min = mid + 1; |
| 277 } |
| 278 } |
| 279 |
| 280 if (trackDetail) { |
| 281 var dx = event.detail.dx - trackDetail.dx; |
| 282 var dt = (now - trackDetail.timeStamp) || 1; |
| 283 return dx / dt; |
| 284 } |
| 285 return 0; |
| 286 }, |
| 287 |
| 288 _flingDrawer: function(event, trackDetails) { |
| 289 var velocity = this._calculateVelocity(event, trackDetails); |
| 290 |
| 291 // Do not fling if velocity is not above a threshold. |
| 292 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { |
| 293 return; |
| 294 } |
| 295 |
| 296 this._drawerState = this._DRAWER_STATE.FLINGING; |
| 297 |
| 298 var x = event.detail.dx + this._translateOffset; |
| 299 var drawerWidth = this.getWidth(); |
| 300 var isPositionLeft = this.position === 'left'; |
| 301 var isVelocityPositive = velocity > 0; |
| 302 var isClosingLeft = !isVelocityPositive && isPositionLeft; |
| 303 var isClosingRight = isVelocityPositive && !isPositionLeft; |
| 304 var dx; |
| 305 if (isClosingLeft) { |
| 306 dx = -(x + drawerWidth); |
| 307 } else if (isClosingRight) { |
| 308 dx = (drawerWidth - x); |
| 309 } else { |
| 310 dx = -x; |
| 311 } |
| 312 |
| 313 // Enforce a minimum transition velocity to make the drawer feel snappy. |
| 314 if (isVelocityPositive) { |
| 315 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); |
| 316 this.opened = this.position === 'left'; |
| 317 } else { |
| 318 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); |
| 319 this.opened = this.position === 'right'; |
| 320 } |
| 321 |
| 322 // Calculate the amount of time needed to finish the transition based on
the |
| 323 // initial slope of the timing function. |
| 324 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity)
+ 'ms'); |
| 325 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); |
| 326 |
| 327 this._resetDrawerTranslate(); |
| 328 }, |
| 329 |
| 330 _transitionend: function(event) { |
| 331 // contentContainer will transition on opened state changed, and scrim w
ill |
| 332 // transition on persistent state changed when opened - these are the |
| 333 // transitions we are interested in. |
| 334 var target = Polymer.dom(event).rootTarget; |
| 335 if (target === this.$.contentContainer || target === this.$.scrim) { |
| 336 |
| 337 // If the drawer was flinging, we need to reset the style attributes. |
| 338 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 339 this._setTransitionDuration(''); |
| 340 this._setTransitionTimingFunction(''); |
| 341 this.style.visibility = ''; |
| 342 } |
| 343 |
| 344 this._resetDrawerState(); |
| 345 } |
| 346 }, |
| 347 |
| 348 _setTransitionDuration: function(duration) { |
| 349 this.$.contentContainer.style.transitionDuration = duration; |
| 350 this.$.scrim.style.transitionDuration = duration; |
| 351 }, |
| 352 |
| 353 _setTransitionTimingFunction: function(timingFunction) { |
| 354 this.$.contentContainer.style.transitionTimingFunction = timingFunction; |
| 355 this.$.scrim.style.transitionTimingFunction = timingFunction; |
| 356 }, |
| 357 |
| 358 _translateDrawer: function(x) { |
| 359 var drawerWidth = this.getWidth(); |
| 360 |
| 361 if (this.position === 'left') { |
| 362 x = Math.max(-drawerWidth, Math.min(x, 0)); |
| 363 this.$.scrim.style.opacity = 1 + x / drawerWidth; |
| 364 } else { |
| 365 x = Math.max(0, Math.min(x, drawerWidth)); |
| 366 this.$.scrim.style.opacity = 1 - x / drawerWidth; |
| 367 } |
| 368 |
| 369 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); |
| 370 }, |
| 371 |
| 372 _resetDrawerTranslate: function() { |
| 373 this.$.scrim.style.opacity = ''; |
| 374 this.transform('', this.$.contentContainer); |
| 375 }, |
| 376 |
| 377 _resetDrawerState: function() { |
| 378 var oldState = this._drawerState; |
| 379 if (this.opened) { |
| 380 this._drawerState = this.persistent ? |
| 381 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED; |
| 382 } else { |
| 383 this._drawerState = this._DRAWER_STATE.CLOSED; |
| 384 } |
| 385 |
| 386 if (oldState !== this._drawerState) { |
| 387 if (this._drawerState === this._DRAWER_STATE.OPENED) { |
| 388 this._setKeyboardFocusTrap(); |
| 389 document.addEventListener('keydown', this._boundEscKeydownHandler); |
| 390 document.body.style.overflow = 'hidden'; |
| 391 } else { |
| 392 document.removeEventListener('keydown', this._boundEscKeydownHandler
); |
| 393 document.body.style.overflow = ''; |
| 394 } |
| 395 |
| 396 // Don't fire the event on initial load. |
| 397 if (oldState !== this._DRAWER_STATE.INIT) { |
| 398 this.fire('app-drawer-transitioned'); |
| 399 } |
| 400 } |
| 401 }, |
| 402 |
| 403 _setKeyboardFocusTrap: function() { |
| 404 if (this.noFocusTrap) { |
| 405 return; |
| 406 } |
| 407 |
| 408 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated)
, this will |
| 409 // not select focusable elements inside shadow roots. |
| 410 var focusableElementsSelector = [ |
| 411 'a[href]:not([tabindex="-1"])', |
| 412 'area[href]:not([tabindex="-1"])', |
| 413 'input:not([disabled]):not([tabindex="-1"])', |
| 414 'select:not([disabled]):not([tabindex="-1"])', |
| 415 'textarea:not([disabled]):not([tabindex="-1"])', |
| 416 'button:not([disabled]):not([tabindex="-1"])', |
| 417 'iframe:not([tabindex="-1"])', |
| 418 '[tabindex]:not([tabindex="-1"])', |
| 419 '[contentEditable=true]:not([tabindex="-1"])' |
| 420 ].join(','); |
| 421 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem
entsSelector); |
| 422 |
| 423 if (focusableElements.length > 0) { |
| 424 this._firstTabStop = focusableElements[0]; |
| 425 this._lastTabStop = focusableElements[focusableElements.length - 1]; |
| 426 } else { |
| 427 // Reset saved tab stops when there are no focusable elements in the d
rawer. |
| 428 this._firstTabStop = null; |
| 429 this._lastTabStop = null; |
| 430 } |
| 431 |
| 432 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the
first focusable |
| 433 // element in the drawer, if it exists. Use the tabindex attribute since
the this.tabIndex |
| 434 // property in IE/Edge returns 0 (instead of -1) when the attribute is n
ot set. |
| 435 var tabindex = this.getAttribute('tabindex'); |
| 436 if (tabindex && parseInt(tabindex, 10) > -1) { |
| 437 this.focus(); |
| 438 } else if (this._firstTabStop) { |
| 439 this._firstTabStop.focus(); |
| 440 } |
| 441 }, |
| 442 |
| 443 _tabKeydownHandler: function(event) { |
| 444 if (this.noFocusTrap) { |
| 445 return; |
| 446 } |
| 447 |
| 448 var TAB_KEYCODE = 9; |
| 449 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode ===
TAB_KEYCODE) { |
| 450 if (event.shiftKey) { |
| 451 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f
irstTabStop) { |
| 452 event.preventDefault(); |
| 453 this._lastTabStop.focus(); |
| 454 } |
| 455 } else { |
| 456 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la
stTabStop) { |
| 457 event.preventDefault(); |
| 458 this._firstTabStop.focus(); |
| 459 } |
| 460 } |
| 461 } |
| 462 }, |
| 463 |
| 464 _MIN_FLING_THRESHOLD: 0.2, |
| 465 |
| 466 _MIN_TRANSITION_VELOCITY: 1.2, |
| 467 |
| 468 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', |
| 469 |
| 470 _FLING_INITIAL_SLOPE: 1.5, |
| 471 |
| 472 _DRAWER_STATE: { |
| 473 INIT: 0, |
| 474 OPENED: 1, |
| 475 OPENED_PERSISTENT: 2, |
| 476 CLOSED: 3, |
| 477 TRACKING: 4, |
| 478 FLINGING: 5 |
| 479 } |
| 480 |
| 481 /** |
| 482 * Fired when the layout of app-drawer has changed. |
| 483 * |
| 484 * @event app-drawer-reset-layout |
| 485 */ |
| 486 |
| 487 /** |
| 488 * Fired when app-drawer has finished transitioning. |
| 489 * |
| 490 * @event app-drawer-transitioned |
| 491 */ |
| 492 }); |
| OLD | NEW |