Chromium Code Reviews| Index: ui/webui/resources/cr_elements/cr_drawer/cr_drawer.js |
| diff --git a/ui/webui/resources/cr_elements/cr_drawer/cr_drawer.js b/ui/webui/resources/cr_elements/cr_drawer/cr_drawer.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..c2e5ca2091e39be04679f185c302001eb45f306a |
| --- /dev/null |
| +++ b/ui/webui/resources/cr_elements/cr_drawer/cr_drawer.js |
| @@ -0,0 +1,401 @@ |
| +// Copyright 2016 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +Polymer({ |
| + is: 'cr-drawer', |
| + |
| + properties: { |
| + /** The opened state of the drawer. */ |
| + opened: { |
| + type: Boolean, |
| + value: false, |
| + notify: true, |
| + reflectToAttribute: true, |
| + observer: 'toggleOpen_', |
| + }, |
| + |
| + /** |
| + * The alignment of the drawer on the screen ('left', 'right', 'start' or |
| + * 'end'). 'start' computes to left and 'end' to right in LTR layout and |
| + * vice versa in RTL layout. |
| + */ |
| + align: { |
| + type: String, |
| + value: 'left' |
| + }, |
| + |
| + /** |
| + * The computed, read-only position of the drawer on the screen ('left' or |
| + * 'right'). |
| + */ |
| + position: { |
| + type: String, |
| + readOnly: true, |
| + value: 'left', |
| + reflectToAttribute: true |
| + }, |
| + }, |
| + |
| + observers: [ |
| + 'resetLayout(position)', |
| + 'resetPosition_(align, isAttached)', |
| + ], |
| + |
| + listeners: { |
| + 'track': 'track_', |
| + 'close': 'close', |
| + }, |
| + |
| + translateOffset_: 0, |
|
dpapad
2016/11/07 20:13:24
Can you document this member var? Both its type bu
hcarmona
2016/11/16 17:44:54
Done.
|
| + |
| + trackDetails_: null, |
| + |
| + drawerState_: 0, |
| + |
| + boundEscKeydownHandler_: null, |
| + |
| + ready: function() { |
| + // Set the scroll direction so you can vertically scroll inside the drawer. |
| + this.setScrollDirection('y'); |
| + |
| + // Only transition the drawer after its first render (e.g. app-drawer-layout |
| + // may need to set the initial opened state which should not be |
| + // transitioned). |
| + this.setTransitionDuration_('0s'); |
| + }, |
| + |
| + attached: function() { |
| + // Only transition the drawer after its first render (e.g. app-drawer-layout |
| + // may need to set the initial opened state which should not be |
| + // transitioned). |
| + Polymer.RenderStatus.afterNextRender(this, function() { |
| + this.setTransitionDuration_(''); |
| + this.boundEscKeydownHandler_ = this.escKeydownHandler_.bind(this); |
| + this.resetDrawerState_(); |
| + |
| + this.addEventListener('transitionend', this.transitioned_.bind(this)); |
| + }); |
| + }, |
| + |
| + detached: function() { |
| + document.removeEventListener('keydown', this.boundEscKeydownHandler_); |
| + }, |
| + |
| + /** Opens the drawer. */ |
| + open: function() { |
| + this.opened = true; |
| + }, |
| + |
| + /** Closes the drawer. */ |
| + close: function() { |
| + this.opened = false; |
| + }, |
| + |
| + /** Toggles the drawer open and close. */ |
| + toggle: function() { |
| + this.opened = !this.opened; |
| + }, |
| + |
| + /** |
| + * Gets the width of the drawer. |
| + * @return {number} The width of the drawer in pixels. |
| + */ |
| + getWidth: function() { |
| + return this.$.contentContainer.offsetWidth; |
| + }, |
| + |
| + /** |
| + * Resets the layout. If you changed the size of app-header via CSS |
| + * you can notify the changes by either firing the `iron-resize` event |
| + * or calling `resetLayout` directly. |
| + * @method resetLayout |
| + */ |
| + resetLayout: function() { |
| + this.debounce('resetLayout_', function() { |
| + this.fire('app-drawer-reset-layout'); |
| + }, 1); |
| + }, |
| + |
| + onDialogTap_: function(event) { |
| + var rect = this.$.contentContainer.getBoundingClientRect(); |
| + |
| + // We can ignore checking top/bottom because dialog is height 100%. |
| + if (event.detail.x < rect.left || event.detail.x > rect.right) |
| + this.close(); |
| + }, |
| + |
| + /** |
| + * Opens and closes the dialog based on the open status. |
| + * @param {boolean} open |
| + */ |
| + toggleOpen_: function(open) { |
| + if (open && !this.$.contentContainer.open) |
| + this.$.contentContainer.showModal(); |
| + else if (!open && this.$.contentContainer.open) |
| + this.$.contentContainer.close(); |
| + }, |
| + |
| + isRTL_: function() { |
| + return window.getComputedStyle(this).direction === 'rtl'; |
| + }, |
| + |
| + resetPosition_: function() { |
| + switch (this.align) { |
| + case 'start': |
| + this._setPosition(this.isRTL_() ? 'right' : 'left'); |
| + return; |
| + case 'end': |
| + this._setPosition(this.isRTL_() ? 'left' : 'right'); |
| + return; |
| + } |
| + this._setPosition(this.align); |
| + }, |
| + |
| + escKeydownHandler_: function(event) { |
| + var ESC_KEYCODE = 27; |
| + if (event.keyCode === ESC_KEYCODE) { |
| + event.preventDefault(); |
| + this.close(); |
| + } |
| + }, |
| + |
| + track_: function(event) { |
| + event.preventDefault(); |
| + |
| + switch (event.detail.state) { |
| + case 'start': |
| + this.trackStart_(event); |
| + break; |
| + case 'track': |
| + this.trackMove_(event); |
| + break; |
| + case 'end': |
| + this.trackEnd_(event); |
| + break; |
| + } |
| + }, |
| + |
| + trackStart_: function(event) { |
| + this.toggleOpen_(true); // Always show the dialog when tracking. |
| + this.drawerState_ = this.DRAWER_STATE.TRACKING; |
| + |
| + // Disable transitions since style attributes will reflect user tracking. |
| + this.setTransitionDuration_('0s'); |
| + this.style.visibility = 'visible'; |
| + |
| + var rect = this.$.contentContainer.getBoundingClientRect(); |
| + if (this.position === 'left') |
| + this.translateOffset_ = rect.left; |
| + else |
| + this.translateOffset_ = rect.right - window.innerWidth; |
| + |
| + this.trackDetails_ = []; |
| + }, |
| + |
| + trackMove_: function(event) { |
| + this.translateDrawer_(event.detail.dx + this.translateOffset_); |
| + |
| + // Use Date.now() since event.timeStamp is inconsistent across browsers |
| + // (e.g. most browsers use milliseconds but FF 44 uses microseconds). |
| + this.trackDetails_.push({ |
| + dx: event.detail.dx, |
| + timeStamp: Date.now() |
| + }); |
| + }, |
| + |
| + trackEnd_: function(event) { |
| + var x = event.detail.dx + this.translateOffset_; |
| + var drawerWidth = this.getWidth(); |
| + var isPositionLeft = this.position === 'left'; |
| + var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : |
| + (x <= 0 || x >= drawerWidth); |
| + |
| + if (!isInEndState) { |
| + // No longer need the track events after this method returns - allow them |
| + // to be cleaned up. |
| + var trackDetails = this.trackDetails_; |
| + this.trackDetails_ = null; |
| + |
| + this.filngDrawer_(event, trackDetails); |
| + if (this.drawerState_ === this.DRAWER_STATE.FLINGING) |
| + return; |
| + } |
| + |
| + // If the drawer is not flinging, toggle the opened state based on the |
| + // position of the drawer. |
| + var halfWidth = drawerWidth / 2; |
| + if (event.detail.dx < -halfWidth) |
| + this.opened = this.position === 'right'; |
| + else if (event.detail.dx > halfWidth) |
| + this.opened = this.position === 'left'; |
| + |
| + // The dialog open state can go out of sync with the |opened| property. |
| + this.toggleOpen_(this.opened); |
| + |
| + if (isInEndState) |
| + this.resetDrawerState_(); |
| + |
| + this.setTransitionDuration_(''); |
| + this.resetDrawerTranslate_(); |
| + this.style.visibility = ''; |
| + }, |
| + |
| + calculateVelocity_: function(event, trackDetails) { |
| + // Find the oldest track event that is within 100ms using binary search. |
| + var now = Date.now(); |
| + var timeLowerBound = now - 100; |
| + var trackDetail; |
| + var min = 0; |
| + var max = trackDetails.length - 1; |
| + |
| + while (min <= max) { |
| + // Floor of average of min and max. |
| + var mid = (min + max) >> 1; |
| + var d = trackDetails[mid]; |
| + if (d.timeStamp >= timeLowerBound) { |
| + trackDetail = d; |
| + max = mid - 1; |
| + } else { |
| + min = mid + 1; |
| + } |
| + } |
| + |
| + if (trackDetail) { |
| + var dx = event.detail.dx - trackDetail.dx; |
| + var dt = (now - trackDetail.timeStamp) || 1; |
| + return dx / dt; |
| + } |
| + return 0; |
| + }, |
| + |
| + filngDrawer_: function(event, trackDetails) { |
| + var velocity = this.calculateVelocity_(event, trackDetails); |
| + |
| + // Do not fling if velocity is not above a threshold. |
| + if (Math.abs(velocity) < this.MIN_FLING_THRESHOLD) { |
| + return; |
| + } |
| + |
| + this.drawerState_ = this.DRAWER_STATE.FLINGING; |
| + |
| + var x = event.detail.dx + this.translateOffset_; |
| + var drawerWidth = this.getWidth(); |
| + var isPositionLeft = this.position === 'left'; |
| + var isVelocityPositive = velocity > 0; |
| + var isClosingLeft = !isVelocityPositive && isPositionLeft; |
| + var isClosingRight = isVelocityPositive && !isPositionLeft; |
| + var dx; |
| + |
| + if (isClosingLeft) |
| + dx = -(x + drawerWidth); |
| + else if (isClosingRight) |
| + dx = (drawerWidth - x); |
| + else |
| + dx = -x; |
| + |
| + // Enforce a minimum transition velocity to make the drawer feel snappy. |
| + if (isVelocityPositive) { |
| + velocity = Math.max(velocity, this.MIN_TRANSITION_VELOCITY); |
| + this.opened = this.position === 'left'; |
| + } else { |
| + velocity = Math.min(velocity, -this.MIN_TRANSITION_VELOCITY); |
| + this.opened = this.position === 'right'; |
| + } |
| + |
| + // The dialog open state can go out of sync with the |opened| property. |
| + this.toggleOpen_(this.opened); |
| + |
| + // Calculate the amount of time needed to finish the transition based on the |
| + // initial slope of the timing function. |
| + this.setTransitionDuration_((this.FLING_INITIAL_SLOPE * dx / velocity) |
| + + 'ms'); |
| + this.setTransiitonTimingFunction_(this.FLING_TIMING_FUNCTION); |
| + |
| + this.resetDrawerTranslate_(); |
| + }, |
| + |
| + transitioned_: function(event) { |
| + // contentContainer will transition on opened state changed, and scrim will |
| + // transition on persistent state changed when opened - these are the |
| + // transitions we are interested in. |
| + var target = Polymer.dom(event).rootTarget; |
| + if (target === this.$.contentContainer || target === this.$.scrim) { |
| + |
| + // If the drawer was flinging, we need to reset the style attributes. |
| + if (this.drawerState_ === this.DRAWER_STATE.FLINGING) { |
| + this.setTransitionDuration_(''); |
| + this.setTransiitonTimingFunction_(''); |
| + this.style.visibility = ''; |
| + } |
| + |
| + this.resetDrawerState_(); |
| + } |
| + }, |
| + |
| + setTransitionDuration_: function(duration) { |
|
dpapad
2016/11/07 20:13:24
Let's annotate all methods in this class with @par
hcarmona
2016/11/16 17:44:54
Done.
|
| + this.$.contentContainer.style.transitionDuration = duration; |
| + this.$.scrim.style.transitionDuration = duration; |
| + }, |
| + |
| + setTransiitonTimingFunction_: function(timingFunction) { |
| + this.$.contentContainer.style.transitionTimingFunction = timingFunction; |
| + this.$.scrim.style.transitionTimingFunction = timingFunction; |
| + }, |
| + |
| + translateDrawer_: function(x) { |
| + var drawerWidth = this.getWidth(); |
| + |
| + if (this.position === 'left') { |
| + x = Math.max(-drawerWidth, Math.min(x, 0)); |
| + this.$.scrim.style.opacity = 1 + x / drawerWidth; |
| + } else { |
| + x = Math.max(0, Math.min(x, drawerWidth)); |
| + this.$.scrim.style.opacity = 1 - x / drawerWidth; |
| + } |
| + |
| + this.translate3d(x + 'px', '0', '0', this.$.contentContainer); |
| + }, |
| + |
| + resetDrawerTranslate_: function() { |
| + this.$.scrim.style.opacity = ''; |
| + this.transform('', this.$.contentContainer); |
| + }, |
| + |
| + resetDrawerState_: function() { |
| + var oldState = this.drawerState_; |
| + this.drawerState_ = this.opened ? this.DRAWER_STATE.OPENED : |
| + this.DRAWER_STATE.CLOSED; |
| + |
| + if (oldState !== this.drawerState_) { |
| + if (this.drawerState_ === this.DRAWER_STATE.OPENED) { |
| + document.addEventListener('keydown', this.boundEscKeydownHandler_); |
| + document.body.style.overflow = 'hidden'; |
| + } else { |
| + document.removeEventListener('keydown', this.boundEscKeydownHandler_); |
| + document.body.style.overflow = ''; |
| + } |
| + |
| + // Don't fire the event on initial load. |
| + if (oldState !== this.DRAWER_STATE.INIT) |
| + this.fire('app-drawer-transitioned'); |
| + } |
| + }, |
| + |
| + MIN_FLING_THRESHOLD: 0.2, |
| + |
| + MIN_TRANSITION_VELOCITY: 1.2, |
| + |
| + FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', |
| + |
| + FLING_INITIAL_SLOPE: 1.5, |
| + |
| + DRAWER_STATE: { |
| + INIT: 0, |
| + OPENED: 1, |
| + OPENED_PERSISTENT: 2, |
| + CLOSED: 3, |
| + TRACKING: 4, |
| + FLINGING: 5 |
| + } |
| +}); |