| 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 |