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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 Polymer({
6 is: 'cr-drawer',
7
8 properties: {
9 /** The opened state of the drawer. */
10 opened: {
11 type: Boolean,
12 value: false,
13 notify: true,
14 reflectToAttribute: true,
15 observer: 'toggleOpen_',
16 },
17
18 /**
19 * The alignment of the drawer on the screen ('left', 'right', 'start' or
20 * 'end'). 'start' computes to left and 'end' to right in LTR layout and
21 * vice versa in RTL layout.
22 */
23 align: {
24 type: String,
25 value: 'left'
26 },
27
28 /**
29 * The computed, read-only position of the drawer on the screen ('left' or
30 * 'right').
31 */
32 position: {
33 type: String,
34 readOnly: true,
35 value: 'left',
36 reflectToAttribute: true
37 },
38 },
39
40 observers: [
41 'resetLayout(position)',
42 'resetPosition_(align, isAttached)',
43 ],
44
45 listeners: {
46 'track': 'track_',
47 'close': 'close',
48 },
49
50 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.
51
52 trackDetails_: null,
53
54 drawerState_: 0,
55
56 boundEscKeydownHandler_: null,
57
58 ready: function() {
59 // Set the scroll direction so you can vertically scroll inside the drawer.
60 this.setScrollDirection('y');
61
62 // Only transition the drawer after its first render (e.g. app-drawer-layout
63 // may need to set the initial opened state which should not be
64 // transitioned).
65 this.setTransitionDuration_('0s');
66 },
67
68 attached: function() {
69 // Only transition the drawer after its first render (e.g. app-drawer-layout
70 // may need to set the initial opened state which should not be
71 // transitioned).
72 Polymer.RenderStatus.afterNextRender(this, function() {
73 this.setTransitionDuration_('');
74 this.boundEscKeydownHandler_ = this.escKeydownHandler_.bind(this);
75 this.resetDrawerState_();
76
77 this.addEventListener('transitionend', this.transitioned_.bind(this));
78 });
79 },
80
81 detached: function() {
82 document.removeEventListener('keydown', this.boundEscKeydownHandler_);
83 },
84
85 /** Opens the drawer. */
86 open: function() {
87 this.opened = true;
88 },
89
90 /** Closes the drawer. */
91 close: function() {
92 this.opened = false;
93 },
94
95 /** Toggles the drawer open and close. */
96 toggle: function() {
97 this.opened = !this.opened;
98 },
99
100 /**
101 * Gets the width of the drawer.
102 * @return {number} The width of the drawer in pixels.
103 */
104 getWidth: function() {
105 return this.$.contentContainer.offsetWidth;
106 },
107
108 /**
109 * Resets the layout. If you changed the size of app-header via CSS
110 * you can notify the changes by either firing the `iron-resize` event
111 * or calling `resetLayout` directly.
112 * @method resetLayout
113 */
114 resetLayout: function() {
115 this.debounce('resetLayout_', function() {
116 this.fire('app-drawer-reset-layout');
117 }, 1);
118 },
119
120 onDialogTap_: function(event) {
121 var rect = this.$.contentContainer.getBoundingClientRect();
122
123 // We can ignore checking top/bottom because dialog is height 100%.
124 if (event.detail.x < rect.left || event.detail.x > rect.right)
125 this.close();
126 },
127
128 /**
129 * Opens and closes the dialog based on the open status.
130 * @param {boolean} open
131 */
132 toggleOpen_: function(open) {
133 if (open && !this.$.contentContainer.open)
134 this.$.contentContainer.showModal();
135 else if (!open && this.$.contentContainer.open)
136 this.$.contentContainer.close();
137 },
138
139 isRTL_: function() {
140 return window.getComputedStyle(this).direction === 'rtl';
141 },
142
143 resetPosition_: function() {
144 switch (this.align) {
145 case 'start':
146 this._setPosition(this.isRTL_() ? 'right' : 'left');
147 return;
148 case 'end':
149 this._setPosition(this.isRTL_() ? 'left' : 'right');
150 return;
151 }
152 this._setPosition(this.align);
153 },
154
155 escKeydownHandler_: function(event) {
156 var ESC_KEYCODE = 27;
157 if (event.keyCode === ESC_KEYCODE) {
158 event.preventDefault();
159 this.close();
160 }
161 },
162
163 track_: function(event) {
164 event.preventDefault();
165
166 switch (event.detail.state) {
167 case 'start':
168 this.trackStart_(event);
169 break;
170 case 'track':
171 this.trackMove_(event);
172 break;
173 case 'end':
174 this.trackEnd_(event);
175 break;
176 }
177 },
178
179 trackStart_: function(event) {
180 this.toggleOpen_(true); // Always show the dialog when tracking.
181 this.drawerState_ = this.DRAWER_STATE.TRACKING;
182
183 // Disable transitions since style attributes will reflect user tracking.
184 this.setTransitionDuration_('0s');
185 this.style.visibility = 'visible';
186
187 var rect = this.$.contentContainer.getBoundingClientRect();
188 if (this.position === 'left')
189 this.translateOffset_ = rect.left;
190 else
191 this.translateOffset_ = rect.right - window.innerWidth;
192
193 this.trackDetails_ = [];
194 },
195
196 trackMove_: function(event) {
197 this.translateDrawer_(event.detail.dx + this.translateOffset_);
198
199 // Use Date.now() since event.timeStamp is inconsistent across browsers
200 // (e.g. most browsers use milliseconds but FF 44 uses microseconds).
201 this.trackDetails_.push({
202 dx: event.detail.dx,
203 timeStamp: Date.now()
204 });
205 },
206
207 trackEnd_: function(event) {
208 var x = event.detail.dx + this.translateOffset_;
209 var drawerWidth = this.getWidth();
210 var isPositionLeft = this.position === 'left';
211 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
212 (x <= 0 || x >= drawerWidth);
213
214 if (!isInEndState) {
215 // No longer need the track events after this method returns - allow them
216 // to be cleaned up.
217 var trackDetails = this.trackDetails_;
218 this.trackDetails_ = null;
219
220 this.filngDrawer_(event, trackDetails);
221 if (this.drawerState_ === this.DRAWER_STATE.FLINGING)
222 return;
223 }
224
225 // If the drawer is not flinging, toggle the opened state based on the
226 // position of the drawer.
227 var halfWidth = drawerWidth / 2;
228 if (event.detail.dx < -halfWidth)
229 this.opened = this.position === 'right';
230 else if (event.detail.dx > halfWidth)
231 this.opened = this.position === 'left';
232
233 // The dialog open state can go out of sync with the |opened| property.
234 this.toggleOpen_(this.opened);
235
236 if (isInEndState)
237 this.resetDrawerState_();
238
239 this.setTransitionDuration_('');
240 this.resetDrawerTranslate_();
241 this.style.visibility = '';
242 },
243
244 calculateVelocity_: function(event, trackDetails) {
245 // Find the oldest track event that is within 100ms using binary search.
246 var now = Date.now();
247 var timeLowerBound = now - 100;
248 var trackDetail;
249 var min = 0;
250 var max = trackDetails.length - 1;
251
252 while (min <= max) {
253 // Floor of average of min and max.
254 var mid = (min + max) >> 1;
255 var d = trackDetails[mid];
256 if (d.timeStamp >= timeLowerBound) {
257 trackDetail = d;
258 max = mid - 1;
259 } else {
260 min = mid + 1;
261 }
262 }
263
264 if (trackDetail) {
265 var dx = event.detail.dx - trackDetail.dx;
266 var dt = (now - trackDetail.timeStamp) || 1;
267 return dx / dt;
268 }
269 return 0;
270 },
271
272 filngDrawer_: function(event, trackDetails) {
273 var velocity = this.calculateVelocity_(event, trackDetails);
274
275 // Do not fling if velocity is not above a threshold.
276 if (Math.abs(velocity) < this.MIN_FLING_THRESHOLD) {
277 return;
278 }
279
280 this.drawerState_ = this.DRAWER_STATE.FLINGING;
281
282 var x = event.detail.dx + this.translateOffset_;
283 var drawerWidth = this.getWidth();
284 var isPositionLeft = this.position === 'left';
285 var isVelocityPositive = velocity > 0;
286 var isClosingLeft = !isVelocityPositive && isPositionLeft;
287 var isClosingRight = isVelocityPositive && !isPositionLeft;
288 var dx;
289
290 if (isClosingLeft)
291 dx = -(x + drawerWidth);
292 else if (isClosingRight)
293 dx = (drawerWidth - x);
294 else
295 dx = -x;
296
297 // Enforce a minimum transition velocity to make the drawer feel snappy.
298 if (isVelocityPositive) {
299 velocity = Math.max(velocity, this.MIN_TRANSITION_VELOCITY);
300 this.opened = this.position === 'left';
301 } else {
302 velocity = Math.min(velocity, -this.MIN_TRANSITION_VELOCITY);
303 this.opened = this.position === 'right';
304 }
305
306 // The dialog open state can go out of sync with the |opened| property.
307 this.toggleOpen_(this.opened);
308
309 // Calculate the amount of time needed to finish the transition based on the
310 // initial slope of the timing function.
311 this.setTransitionDuration_((this.FLING_INITIAL_SLOPE * dx / velocity)
312 + 'ms');
313 this.setTransiitonTimingFunction_(this.FLING_TIMING_FUNCTION);
314
315 this.resetDrawerTranslate_();
316 },
317
318 transitioned_: function(event) {
319 // contentContainer will transition on opened state changed, and scrim will
320 // transition on persistent state changed when opened - these are the
321 // transitions we are interested in.
322 var target = Polymer.dom(event).rootTarget;
323 if (target === this.$.contentContainer || target === this.$.scrim) {
324
325 // If the drawer was flinging, we need to reset the style attributes.
326 if (this.drawerState_ === this.DRAWER_STATE.FLINGING) {
327 this.setTransitionDuration_('');
328 this.setTransiitonTimingFunction_('');
329 this.style.visibility = '';
330 }
331
332 this.resetDrawerState_();
333 }
334 },
335
336 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.
337 this.$.contentContainer.style.transitionDuration = duration;
338 this.$.scrim.style.transitionDuration = duration;
339 },
340
341 setTransiitonTimingFunction_: function(timingFunction) {
342 this.$.contentContainer.style.transitionTimingFunction = timingFunction;
343 this.$.scrim.style.transitionTimingFunction = timingFunction;
344 },
345
346 translateDrawer_: function(x) {
347 var drawerWidth = this.getWidth();
348
349 if (this.position === 'left') {
350 x = Math.max(-drawerWidth, Math.min(x, 0));
351 this.$.scrim.style.opacity = 1 + x / drawerWidth;
352 } else {
353 x = Math.max(0, Math.min(x, drawerWidth));
354 this.$.scrim.style.opacity = 1 - x / drawerWidth;
355 }
356
357 this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
358 },
359
360 resetDrawerTranslate_: function() {
361 this.$.scrim.style.opacity = '';
362 this.transform('', this.$.contentContainer);
363 },
364
365 resetDrawerState_: function() {
366 var oldState = this.drawerState_;
367 this.drawerState_ = this.opened ? this.DRAWER_STATE.OPENED :
368 this.DRAWER_STATE.CLOSED;
369
370 if (oldState !== this.drawerState_) {
371 if (this.drawerState_ === this.DRAWER_STATE.OPENED) {
372 document.addEventListener('keydown', this.boundEscKeydownHandler_);
373 document.body.style.overflow = 'hidden';
374 } else {
375 document.removeEventListener('keydown', this.boundEscKeydownHandler_);
376 document.body.style.overflow = '';
377 }
378
379 // Don't fire the event on initial load.
380 if (oldState !== this.DRAWER_STATE.INIT)
381 this.fire('app-drawer-transitioned');
382 }
383 },
384
385 MIN_FLING_THRESHOLD: 0.2,
386
387 MIN_TRANSITION_VELOCITY: 1.2,
388
389 FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
390
391 FLING_INITIAL_SLOPE: 1.5,
392
393 DRAWER_STATE: {
394 INIT: 0,
395 OPENED: 1,
396 OPENED_PERSISTENT: 2,
397 CLOSED: 3,
398 TRACKING: 4,
399 FLINGING: 5
400 }
401 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698