Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(155)

Unified Diff: ui/webui/resources/cr_elements/cr_drawer/cr_drawer.js

Issue 2465433002: Create implementation of the side panel using a dialog. (Closed)
Patch Set: Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
+ }
+});

Powered by Google App Engine
This is Rietveld 408576698