 Chromium Code Reviews
 Chromium Code Reviews Issue 2465433002:
  Create implementation of the side panel using a dialog.  (Closed)
    
  
    Issue 2465433002:
  Create implementation of the side panel using a dialog.  (Closed) 
  | 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 | 
| + } | 
| +}); |