kmaterialize 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/sidenav.js ADDED
@@ -0,0 +1,580 @@
1
+ (function($, anim) {
2
+ 'use strict';
3
+
4
+ let _defaults = {
5
+ edge: 'left',
6
+ draggable: true,
7
+ inDuration: 250,
8
+ outDuration: 200,
9
+ onOpenStart: null,
10
+ onOpenEnd: null,
11
+ onCloseStart: null,
12
+ onCloseEnd: null,
13
+ preventScrolling: true
14
+ };
15
+
16
+ /**
17
+ * @class
18
+ */
19
+ class Sidenav extends Component {
20
+ /**
21
+ * Construct Sidenav instance and set up overlay
22
+ * @constructor
23
+ * @param {Element} el
24
+ * @param {Object} options
25
+ */
26
+ constructor(el, options) {
27
+ super(Sidenav, el, options);
28
+
29
+ this.el.M_Sidenav = this;
30
+ this.id = this.$el.attr('id');
31
+
32
+ /**
33
+ * Options for the Sidenav
34
+ * @member Sidenav#options
35
+ * @prop {String} [edge='left'] - Side of screen on which Sidenav appears
36
+ * @prop {Boolean} [draggable=true] - Allow swipe gestures to open/close Sidenav
37
+ * @prop {Number} [inDuration=250] - Length in ms of enter transition
38
+ * @prop {Number} [outDuration=200] - Length in ms of exit transition
39
+ * @prop {Function} onOpenStart - Function called when sidenav starts entering
40
+ * @prop {Function} onOpenEnd - Function called when sidenav finishes entering
41
+ * @prop {Function} onCloseStart - Function called when sidenav starts exiting
42
+ * @prop {Function} onCloseEnd - Function called when sidenav finishes exiting
43
+ */
44
+ this.options = $.extend({}, Sidenav.defaults, options);
45
+
46
+ /**
47
+ * Describes open/close state of Sidenav
48
+ * @type {Boolean}
49
+ */
50
+ this.isOpen = false;
51
+
52
+ /**
53
+ * Describes if Sidenav is fixed
54
+ * @type {Boolean}
55
+ */
56
+ this.isFixed = this.el.classList.contains('sidenav-fixed');
57
+
58
+ /**
59
+ * Describes if Sidenav is being draggeed
60
+ * @type {Boolean}
61
+ */
62
+ this.isDragged = false;
63
+
64
+ // Window size variables for window resize checks
65
+ this.lastWindowWidth = window.innerWidth;
66
+ this.lastWindowHeight = window.innerHeight;
67
+
68
+ this._createOverlay();
69
+ this._createDragTarget();
70
+ this._setupEventHandlers();
71
+ this._setupClasses();
72
+ this._setupFixed();
73
+
74
+ Sidenav._sidenavs.push(this);
75
+ }
76
+
77
+ static get defaults() {
78
+ return _defaults;
79
+ }
80
+
81
+ static init(els, options) {
82
+ return super.init(this, els, options);
83
+ }
84
+
85
+ /**
86
+ * Get Instance
87
+ */
88
+ static getInstance(el) {
89
+ let domElem = !!el.jquery ? el[0] : el;
90
+ return domElem.M_Sidenav;
91
+ }
92
+
93
+ /**
94
+ * Teardown component
95
+ */
96
+ destroy() {
97
+ this._removeEventHandlers();
98
+ this._enableBodyScrolling();
99
+ this._overlay.parentNode.removeChild(this._overlay);
100
+ this.dragTarget.parentNode.removeChild(this.dragTarget);
101
+ this.el.M_Sidenav = undefined;
102
+ this.el.style.transform = '';
103
+
104
+ let index = Sidenav._sidenavs.indexOf(this);
105
+ if (index >= 0) {
106
+ Sidenav._sidenavs.splice(index, 1);
107
+ }
108
+ }
109
+
110
+ _createOverlay() {
111
+ let overlay = document.createElement('div');
112
+ this._closeBound = this.close.bind(this);
113
+ overlay.classList.add('sidenav-overlay');
114
+
115
+ overlay.addEventListener('click', this._closeBound);
116
+
117
+ document.body.appendChild(overlay);
118
+ this._overlay = overlay;
119
+ }
120
+
121
+ _setupEventHandlers() {
122
+ if (Sidenav._sidenavs.length === 0) {
123
+ document.body.addEventListener('click', this._handleTriggerClick);
124
+ }
125
+
126
+ this._handleDragTargetDragBound = this._handleDragTargetDrag.bind(this);
127
+ this._handleDragTargetReleaseBound = this._handleDragTargetRelease.bind(this);
128
+ this._handleCloseDragBound = this._handleCloseDrag.bind(this);
129
+ this._handleCloseReleaseBound = this._handleCloseRelease.bind(this);
130
+ this._handleCloseTriggerClickBound = this._handleCloseTriggerClick.bind(this);
131
+
132
+ this.dragTarget.addEventListener('touchmove', this._handleDragTargetDragBound);
133
+ this.dragTarget.addEventListener('touchend', this._handleDragTargetReleaseBound);
134
+ this._overlay.addEventListener('touchmove', this._handleCloseDragBound);
135
+ this._overlay.addEventListener('touchend', this._handleCloseReleaseBound);
136
+ this.el.addEventListener('touchmove', this._handleCloseDragBound);
137
+ this.el.addEventListener('touchend', this._handleCloseReleaseBound);
138
+ this.el.addEventListener('click', this._handleCloseTriggerClickBound);
139
+
140
+ // Add resize for side nav fixed
141
+ if (this.isFixed) {
142
+ this._handleWindowResizeBound = this._handleWindowResize.bind(this);
143
+ window.addEventListener('resize', this._handleWindowResizeBound);
144
+ }
145
+ }
146
+
147
+ _removeEventHandlers() {
148
+ if (Sidenav._sidenavs.length === 1) {
149
+ document.body.removeEventListener('click', this._handleTriggerClick);
150
+ }
151
+
152
+ this.dragTarget.removeEventListener('touchmove', this._handleDragTargetDragBound);
153
+ this.dragTarget.removeEventListener('touchend', this._handleDragTargetReleaseBound);
154
+ this._overlay.removeEventListener('touchmove', this._handleCloseDragBound);
155
+ this._overlay.removeEventListener('touchend', this._handleCloseReleaseBound);
156
+ this.el.removeEventListener('touchmove', this._handleCloseDragBound);
157
+ this.el.removeEventListener('touchend', this._handleCloseReleaseBound);
158
+ this.el.removeEventListener('click', this._handleCloseTriggerClickBound);
159
+
160
+ // Remove resize for side nav fixed
161
+ if (this.isFixed) {
162
+ window.removeEventListener('resize', this._handleWindowResizeBound);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Handle Trigger Click
168
+ * @param {Event} e
169
+ */
170
+ _handleTriggerClick(e) {
171
+ let $trigger = $(e.target).closest('.sidenav-trigger');
172
+ if (e.target && $trigger.length) {
173
+ let sidenavId = M.getIdFromTrigger($trigger[0]);
174
+
175
+ let sidenavInstance = document.getElementById(sidenavId).M_Sidenav;
176
+ if (sidenavInstance) {
177
+ sidenavInstance.open($trigger);
178
+ }
179
+ e.preventDefault();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Set variables needed at the beggining of drag
185
+ * and stop any current transition.
186
+ * @param {Event} e
187
+ */
188
+ _startDrag(e) {
189
+ let clientX = e.targetTouches[0].clientX;
190
+ this.isDragged = true;
191
+ this._startingXpos = clientX;
192
+ this._xPos = this._startingXpos;
193
+ this._time = Date.now();
194
+ this._width = this.el.getBoundingClientRect().width;
195
+ this._overlay.style.display = 'block';
196
+ this._initialScrollTop = this.isOpen ? this.el.scrollTop : M.getDocumentScrollTop();
197
+ this._verticallyScrolling = false;
198
+ anim.remove(this.el);
199
+ anim.remove(this._overlay);
200
+ }
201
+
202
+ /**
203
+ * Set variables needed at each drag move update tick
204
+ * @param {Event} e
205
+ */
206
+ _dragMoveUpdate(e) {
207
+ let clientX = e.targetTouches[0].clientX;
208
+ let currentScrollTop = this.isOpen ? this.el.scrollTop : M.getDocumentScrollTop();
209
+ this.deltaX = Math.abs(this._xPos - clientX);
210
+ this._xPos = clientX;
211
+ this.velocityX = this.deltaX / (Date.now() - this._time);
212
+ this._time = Date.now();
213
+ if (this._initialScrollTop !== currentScrollTop) {
214
+ this._verticallyScrolling = true;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Handles Dragging of Sidenav
220
+ * @param {Event} e
221
+ */
222
+ _handleDragTargetDrag(e) {
223
+ // Check if draggable
224
+ if (!this.options.draggable || this._isCurrentlyFixed() || this._verticallyScrolling) {
225
+ return;
226
+ }
227
+
228
+ // If not being dragged, set initial drag start variables
229
+ if (!this.isDragged) {
230
+ this._startDrag(e);
231
+ }
232
+
233
+ // Run touchmove updates
234
+ this._dragMoveUpdate(e);
235
+
236
+ // Calculate raw deltaX
237
+ let totalDeltaX = this._xPos - this._startingXpos;
238
+
239
+ // dragDirection is the attempted user drag direction
240
+ let dragDirection = totalDeltaX > 0 ? 'right' : 'left';
241
+
242
+ // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction
243
+ totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX));
244
+ if (this.options.edge === dragDirection) {
245
+ totalDeltaX = 0;
246
+ }
247
+
248
+ /**
249
+ * transformX is the drag displacement
250
+ * transformPrefix is the initial transform placement
251
+ * Invert values if Sidenav is right edge
252
+ */
253
+ let transformX = totalDeltaX;
254
+ let transformPrefix = 'translateX(-100%)';
255
+ if (this.options.edge === 'right') {
256
+ transformPrefix = 'translateX(100%)';
257
+ transformX = -transformX;
258
+ }
259
+
260
+ // Calculate open/close percentage of sidenav, with open = 1 and close = 0
261
+ this.percentOpen = Math.min(1, totalDeltaX / this._width);
262
+
263
+ // Set transform and opacity styles
264
+ this.el.style.transform = `${transformPrefix} translateX(${transformX}px)`;
265
+ this._overlay.style.opacity = this.percentOpen;
266
+ }
267
+
268
+ /**
269
+ * Handle Drag Target Release
270
+ */
271
+ _handleDragTargetRelease() {
272
+ if (this.isDragged) {
273
+ if (this.percentOpen > 0.2) {
274
+ this.open();
275
+ } else {
276
+ this._animateOut();
277
+ }
278
+
279
+ this.isDragged = false;
280
+ this._verticallyScrolling = false;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Handle Close Drag
286
+ * @param {Event} e
287
+ */
288
+ _handleCloseDrag(e) {
289
+ if (this.isOpen) {
290
+ // Check if draggable
291
+ if (!this.options.draggable || this._isCurrentlyFixed() || this._verticallyScrolling) {
292
+ return;
293
+ }
294
+
295
+ // If not being dragged, set initial drag start variables
296
+ if (!this.isDragged) {
297
+ this._startDrag(e);
298
+ }
299
+
300
+ // Run touchmove updates
301
+ this._dragMoveUpdate(e);
302
+
303
+ // Calculate raw deltaX
304
+ let totalDeltaX = this._xPos - this._startingXpos;
305
+
306
+ // dragDirection is the attempted user drag direction
307
+ let dragDirection = totalDeltaX > 0 ? 'right' : 'left';
308
+
309
+ // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction
310
+ totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX));
311
+ if (this.options.edge !== dragDirection) {
312
+ totalDeltaX = 0;
313
+ }
314
+
315
+ let transformX = -totalDeltaX;
316
+ if (this.options.edge === 'right') {
317
+ transformX = -transformX;
318
+ }
319
+
320
+ // Calculate open/close percentage of sidenav, with open = 1 and close = 0
321
+ this.percentOpen = Math.min(1, 1 - totalDeltaX / this._width);
322
+
323
+ // Set transform and opacity styles
324
+ this.el.style.transform = `translateX(${transformX}px)`;
325
+ this._overlay.style.opacity = this.percentOpen;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Handle Close Release
331
+ */
332
+ _handleCloseRelease() {
333
+ if (this.isOpen && this.isDragged) {
334
+ if (this.percentOpen > 0.8) {
335
+ this._animateIn();
336
+ } else {
337
+ this.close();
338
+ }
339
+
340
+ this.isDragged = false;
341
+ this._verticallyScrolling = false;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Handles closing of Sidenav when element with class .sidenav-close
347
+ */
348
+ _handleCloseTriggerClick(e) {
349
+ let $closeTrigger = $(e.target).closest('.sidenav-close');
350
+ if ($closeTrigger.length && !this._isCurrentlyFixed()) {
351
+ this.close();
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Handle Window Resize
357
+ */
358
+ _handleWindowResize() {
359
+ // Only handle horizontal resizes
360
+ if (this.lastWindowWidth !== window.innerWidth) {
361
+ if (window.innerWidth > 992) {
362
+ this.open();
363
+ } else {
364
+ this.close();
365
+ }
366
+ }
367
+
368
+ this.lastWindowWidth = window.innerWidth;
369
+ this.lastWindowHeight = window.innerHeight;
370
+ }
371
+
372
+ _setupClasses() {
373
+ if (this.options.edge === 'right') {
374
+ this.el.classList.add('right-aligned');
375
+ this.dragTarget.classList.add('right-aligned');
376
+ }
377
+ }
378
+
379
+ _removeClasses() {
380
+ this.el.classList.remove('right-aligned');
381
+ this.dragTarget.classList.remove('right-aligned');
382
+ }
383
+
384
+ _setupFixed() {
385
+ if (this._isCurrentlyFixed()) {
386
+ this.open();
387
+ }
388
+ }
389
+
390
+ _isCurrentlyFixed() {
391
+ return this.isFixed && window.innerWidth > 992;
392
+ }
393
+
394
+ _createDragTarget() {
395
+ let dragTarget = document.createElement('div');
396
+ dragTarget.classList.add('drag-target');
397
+ document.body.appendChild(dragTarget);
398
+ this.dragTarget = dragTarget;
399
+ }
400
+
401
+ _preventBodyScrolling() {
402
+ let body = document.body;
403
+ body.style.overflow = 'hidden';
404
+ }
405
+
406
+ _enableBodyScrolling() {
407
+ let body = document.body;
408
+ body.style.overflow = '';
409
+ }
410
+
411
+ open() {
412
+ if (this.isOpen === true) {
413
+ return;
414
+ }
415
+
416
+ this.isOpen = true;
417
+
418
+ // Run onOpenStart callback
419
+ if (typeof this.options.onOpenStart === 'function') {
420
+ this.options.onOpenStart.call(this, this.el);
421
+ }
422
+
423
+ // Handle fixed Sidenav
424
+ if (this._isCurrentlyFixed()) {
425
+ anim.remove(this.el);
426
+ anim({
427
+ targets: this.el,
428
+ translateX: 0,
429
+ duration: 0,
430
+ easing: 'easeOutQuad'
431
+ });
432
+ this._enableBodyScrolling();
433
+ this._overlay.style.display = 'none';
434
+
435
+ // Handle non-fixed Sidenav
436
+ } else {
437
+ if (this.options.preventScrolling) {
438
+ this._preventBodyScrolling();
439
+ }
440
+
441
+ if (!this.isDragged || this.percentOpen != 1) {
442
+ this._animateIn();
443
+ }
444
+ }
445
+ }
446
+
447
+ close() {
448
+ if (this.isOpen === false) {
449
+ return;
450
+ }
451
+
452
+ this.isOpen = false;
453
+
454
+ // Run onCloseStart callback
455
+ if (typeof this.options.onCloseStart === 'function') {
456
+ this.options.onCloseStart.call(this, this.el);
457
+ }
458
+
459
+ // Handle fixed Sidenav
460
+ if (this._isCurrentlyFixed()) {
461
+ let transformX = this.options.edge === 'left' ? '-105%' : '105%';
462
+ this.el.style.transform = `translateX(${transformX})`;
463
+
464
+ // Handle non-fixed Sidenav
465
+ } else {
466
+ this._enableBodyScrolling();
467
+
468
+ if (!this.isDragged || this.percentOpen != 0) {
469
+ this._animateOut();
470
+ } else {
471
+ this._overlay.style.display = 'none';
472
+ }
473
+ }
474
+ }
475
+
476
+ _animateIn() {
477
+ this._animateSidenavIn();
478
+ this._animateOverlayIn();
479
+ }
480
+
481
+ _animateSidenavIn() {
482
+ let slideOutPercent = this.options.edge === 'left' ? -1 : 1;
483
+ if (this.isDragged) {
484
+ slideOutPercent =
485
+ this.options.edge === 'left'
486
+ ? slideOutPercent + this.percentOpen
487
+ : slideOutPercent - this.percentOpen;
488
+ }
489
+
490
+ anim.remove(this.el);
491
+ anim({
492
+ targets: this.el,
493
+ translateX: [`${slideOutPercent * 100}%`, 0],
494
+ duration: this.options.inDuration,
495
+ easing: 'easeOutQuad',
496
+ complete: () => {
497
+ // Run onOpenEnd callback
498
+ if (typeof this.options.onOpenEnd === 'function') {
499
+ this.options.onOpenEnd.call(this, this.el);
500
+ }
501
+ }
502
+ });
503
+ }
504
+
505
+ _animateOverlayIn() {
506
+ let start = 0;
507
+ if (this.isDragged) {
508
+ start = this.percentOpen;
509
+ } else {
510
+ $(this._overlay).css({
511
+ display: 'block'
512
+ });
513
+ }
514
+
515
+ anim.remove(this._overlay);
516
+ anim({
517
+ targets: this._overlay,
518
+ opacity: [start, 1],
519
+ duration: this.options.inDuration,
520
+ easing: 'easeOutQuad'
521
+ });
522
+ }
523
+
524
+ _animateOut() {
525
+ this._animateSidenavOut();
526
+ this._animateOverlayOut();
527
+ }
528
+
529
+ _animateSidenavOut() {
530
+ let endPercent = this.options.edge === 'left' ? -1 : 1;
531
+ let slideOutPercent = 0;
532
+ if (this.isDragged) {
533
+ slideOutPercent =
534
+ this.options.edge === 'left'
535
+ ? endPercent + this.percentOpen
536
+ : endPercent - this.percentOpen;
537
+ }
538
+
539
+ anim.remove(this.el);
540
+ anim({
541
+ targets: this.el,
542
+ translateX: [`${slideOutPercent * 100}%`, `${endPercent * 105}%`],
543
+ duration: this.options.outDuration,
544
+ easing: 'easeOutQuad',
545
+ complete: () => {
546
+ // Run onOpenEnd callback
547
+ if (typeof this.options.onCloseEnd === 'function') {
548
+ this.options.onCloseEnd.call(this, this.el);
549
+ }
550
+ }
551
+ });
552
+ }
553
+
554
+ _animateOverlayOut() {
555
+ anim.remove(this._overlay);
556
+ anim({
557
+ targets: this._overlay,
558
+ opacity: 0,
559
+ duration: this.options.outDuration,
560
+ easing: 'easeOutQuad',
561
+ complete: () => {
562
+ $(this._overlay).css('display', 'none');
563
+ }
564
+ });
565
+ }
566
+ }
567
+
568
+ /**
569
+ * @static
570
+ * @memberof Sidenav
571
+ * @type {Array.<Sidenav>}
572
+ */
573
+ Sidenav._sidenavs = [];
574
+
575
+ M.Sidenav = Sidenav;
576
+
577
+ if (M.jQueryLoaded) {
578
+ M.initializeJqueryWrapper(Sidenav, 'sidenav', 'M_Sidenav');
579
+ }
580
+ })(cash, M.anime);