(function($, anim) { 'use strict'; let _defaults = { edge: 'left', draggable: true, inDuration: 250, outDuration: 200, onOpenStart: null, onOpenEnd: null, onCloseStart: null, onCloseEnd: null, preventScrolling: true }; /** * @class */ class Sidenav extends Component { /** * Construct Sidenav instance and set up overlay * @constructor * @param {Element} el * @param {Object} options */ constructor(el, options) { super(Sidenav, el, options); this.el.M_Sidenav = this; this.id = this.$el.attr('id'); /** * Options for the Sidenav * @member Sidenav#options * @prop {String} [edge='left'] - Side of screen on which Sidenav appears * @prop {Boolean} [draggable=true] - Allow swipe gestures to open/close Sidenav * @prop {Number} [inDuration=250] - Length in ms of enter transition * @prop {Number} [outDuration=200] - Length in ms of exit transition * @prop {Function} onOpenStart - Function called when sidenav starts entering * @prop {Function} onOpenEnd - Function called when sidenav finishes entering * @prop {Function} onCloseStart - Function called when sidenav starts exiting * @prop {Function} onCloseEnd - Function called when sidenav finishes exiting */ this.options = $.extend({}, Sidenav.defaults, options); /** * Describes open/close state of Sidenav * @type {Boolean} */ this.isOpen = false; /** * Describes if Sidenav is fixed * @type {Boolean} */ this.isFixed = this.el.classList.contains('sidenav-fixed'); /** * Describes if Sidenav is being draggeed * @type {Boolean} */ this.isDragged = false; // Window size variables for window resize checks this.lastWindowWidth = window.innerWidth; this.lastWindowHeight = window.innerHeight; this._createOverlay(); this._createDragTarget(); this._setupEventHandlers(); this._setupClasses(); this._setupFixed(); Sidenav._sidenavs.push(this); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Sidenav; } /** * Teardown component */ destroy() { this._removeEventHandlers(); this._enableBodyScrolling(); this._overlay.parentNode.removeChild(this._overlay); this.dragTarget.parentNode.removeChild(this.dragTarget); this.el.M_Sidenav = undefined; this.el.style.transform = ''; let index = Sidenav._sidenavs.indexOf(this); if (index >= 0) { Sidenav._sidenavs.splice(index, 1); } } _createOverlay() { let overlay = document.createElement('div'); this._closeBound = this.close.bind(this); overlay.classList.add('sidenav-overlay'); overlay.addEventListener('click', this._closeBound); document.body.appendChild(overlay); this._overlay = overlay; } _setupEventHandlers() { if (Sidenav._sidenavs.length === 0) { document.body.addEventListener('click', this._handleTriggerClick); } this._handleDragTargetDragBound = this._handleDragTargetDrag.bind(this); this._handleDragTargetReleaseBound = this._handleDragTargetRelease.bind(this); this._handleCloseDragBound = this._handleCloseDrag.bind(this); this._handleCloseReleaseBound = this._handleCloseRelease.bind(this); this._handleCloseTriggerClickBound = this._handleCloseTriggerClick.bind(this); this.dragTarget.addEventListener('touchmove', this._handleDragTargetDragBound); this.dragTarget.addEventListener('touchend', this._handleDragTargetReleaseBound); this._overlay.addEventListener('touchmove', this._handleCloseDragBound); this._overlay.addEventListener('touchend', this._handleCloseReleaseBound); this.el.addEventListener('touchmove', this._handleCloseDragBound); this.el.addEventListener('touchend', this._handleCloseReleaseBound); this.el.addEventListener('click', this._handleCloseTriggerClickBound); // Add resize for side nav fixed if (this.isFixed) { this._handleWindowResizeBound = this._handleWindowResize.bind(this); window.addEventListener('resize', this._handleWindowResizeBound); } } _removeEventHandlers() { if (Sidenav._sidenavs.length === 1) { document.body.removeEventListener('click', this._handleTriggerClick); } this.dragTarget.removeEventListener('touchmove', this._handleDragTargetDragBound); this.dragTarget.removeEventListener('touchend', this._handleDragTargetReleaseBound); this._overlay.removeEventListener('touchmove', this._handleCloseDragBound); this._overlay.removeEventListener('touchend', this._handleCloseReleaseBound); this.el.removeEventListener('touchmove', this._handleCloseDragBound); this.el.removeEventListener('touchend', this._handleCloseReleaseBound); this.el.removeEventListener('click', this._handleCloseTriggerClickBound); // Remove resize for side nav fixed if (this.isFixed) { window.removeEventListener('resize', this._handleWindowResizeBound); } } /** * Handle Trigger Click * @param {Event} e */ _handleTriggerClick(e) { let $trigger = $(e.target).closest('.sidenav-trigger'); if (e.target && $trigger.length) { let sidenavId = M.getIdFromTrigger($trigger[0]); let sidenavInstance = document.getElementById(sidenavId).M_Sidenav; if (sidenavInstance) { sidenavInstance.open($trigger); } e.preventDefault(); } } /** * Set variables needed at the beggining of drag * and stop any current transition. * @param {Event} e */ _startDrag(e) { let clientX = e.targetTouches[0].clientX; this.isDragged = true; this._startingXpos = clientX; this._xPos = this._startingXpos; this._time = Date.now(); this._width = this.el.getBoundingClientRect().width; this._overlay.style.display = 'block'; this._initialScrollTop = this.isOpen ? this.el.scrollTop : M.getDocumentScrollTop(); this._verticallyScrolling = false; anim.remove(this.el); anim.remove(this._overlay); } /** * Set variables needed at each drag move update tick * @param {Event} e */ _dragMoveUpdate(e) { let clientX = e.targetTouches[0].clientX; let currentScrollTop = this.isOpen ? this.el.scrollTop : M.getDocumentScrollTop(); this.deltaX = Math.abs(this._xPos - clientX); this._xPos = clientX; this.velocityX = this.deltaX / (Date.now() - this._time); this._time = Date.now(); if (this._initialScrollTop !== currentScrollTop) { this._verticallyScrolling = true; } } /** * Handles Dragging of Sidenav * @param {Event} e */ _handleDragTargetDrag(e) { // Check if draggable if (!this.options.draggable || this._isCurrentlyFixed() || this._verticallyScrolling) { return; } // If not being dragged, set initial drag start variables if (!this.isDragged) { this._startDrag(e); } // Run touchmove updates this._dragMoveUpdate(e); // Calculate raw deltaX let totalDeltaX = this._xPos - this._startingXpos; // dragDirection is the attempted user drag direction let dragDirection = totalDeltaX > 0 ? 'right' : 'left'; // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); if (this.options.edge === dragDirection) { totalDeltaX = 0; } /** * transformX is the drag displacement * transformPrefix is the initial transform placement * Invert values if Sidenav is right edge */ let transformX = totalDeltaX; let transformPrefix = 'translateX(-100%)'; if (this.options.edge === 'right') { transformPrefix = 'translateX(100%)'; transformX = -transformX; } // Calculate open/close percentage of sidenav, with open = 1 and close = 0 this.percentOpen = Math.min(1, totalDeltaX / this._width); // Set transform and opacity styles this.el.style.transform = `${transformPrefix} translateX(${transformX}px)`; this._overlay.style.opacity = this.percentOpen; } /** * Handle Drag Target Release */ _handleDragTargetRelease() { if (this.isDragged) { if (this.percentOpen > 0.2) { this.open(); } else { this._animateOut(); } this.isDragged = false; this._verticallyScrolling = false; } } /** * Handle Close Drag * @param {Event} e */ _handleCloseDrag(e) { if (this.isOpen) { // Check if draggable if (!this.options.draggable || this._isCurrentlyFixed() || this._verticallyScrolling) { return; } // If not being dragged, set initial drag start variables if (!this.isDragged) { this._startDrag(e); } // Run touchmove updates this._dragMoveUpdate(e); // Calculate raw deltaX let totalDeltaX = this._xPos - this._startingXpos; // dragDirection is the attempted user drag direction let dragDirection = totalDeltaX > 0 ? 'right' : 'left'; // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); if (this.options.edge !== dragDirection) { totalDeltaX = 0; } let transformX = -totalDeltaX; if (this.options.edge === 'right') { transformX = -transformX; } // Calculate open/close percentage of sidenav, with open = 1 and close = 0 this.percentOpen = Math.min(1, 1 - totalDeltaX / this._width); // Set transform and opacity styles this.el.style.transform = `translateX(${transformX}px)`; this._overlay.style.opacity = this.percentOpen; } } /** * Handle Close Release */ _handleCloseRelease() { if (this.isOpen && this.isDragged) { if (this.percentOpen > 0.8) { this._animateIn(); } else { this.close(); } this.isDragged = false; this._verticallyScrolling = false; } } /** * Handles closing of Sidenav when element with class .sidenav-close */ _handleCloseTriggerClick(e) { let $closeTrigger = $(e.target).closest('.sidenav-close'); if ($closeTrigger.length && !this._isCurrentlyFixed()) { this.close(); } } /** * Handle Window Resize */ _handleWindowResize() { // Only handle horizontal resizes if (this.lastWindowWidth !== window.innerWidth) { if (window.innerWidth > 992) { this.open(); } else { this.close(); } } this.lastWindowWidth = window.innerWidth; this.lastWindowHeight = window.innerHeight; } _setupClasses() { if (this.options.edge === 'right') { this.el.classList.add('right-aligned'); this.dragTarget.classList.add('right-aligned'); } } _removeClasses() { this.el.classList.remove('right-aligned'); this.dragTarget.classList.remove('right-aligned'); } _setupFixed() { if (this._isCurrentlyFixed()) { this.open(); } } _isCurrentlyFixed() { return this.isFixed && window.innerWidth > 992; } _createDragTarget() { let dragTarget = document.createElement('div'); dragTarget.classList.add('drag-target'); document.body.appendChild(dragTarget); this.dragTarget = dragTarget; } _preventBodyScrolling() { let body = document.body; body.style.overflow = 'hidden'; } _enableBodyScrolling() { let body = document.body; body.style.overflow = ''; } open() { if (this.isOpen === true) { return; } this.isOpen = true; // Run onOpenStart callback if (typeof this.options.onOpenStart === 'function') { this.options.onOpenStart.call(this, this.el); } // Handle fixed Sidenav if (this._isCurrentlyFixed()) { anim.remove(this.el); anim({ targets: this.el, translateX: 0, duration: 0, easing: 'easeOutQuad' }); this._enableBodyScrolling(); this._overlay.style.display = 'none'; // Handle non-fixed Sidenav } else { if (this.options.preventScrolling) { this._preventBodyScrolling(); } if (!this.isDragged || this.percentOpen != 1) { this._animateIn(); } } } close() { if (this.isOpen === false) { return; } this.isOpen = false; // Run onCloseStart callback if (typeof this.options.onCloseStart === 'function') { this.options.onCloseStart.call(this, this.el); } // Handle fixed Sidenav if (this._isCurrentlyFixed()) { let transformX = this.options.edge === 'left' ? '-105%' : '105%'; this.el.style.transform = `translateX(${transformX})`; // Handle non-fixed Sidenav } else { this._enableBodyScrolling(); if (!this.isDragged || this.percentOpen != 0) { this._animateOut(); } else { this._overlay.style.display = 'none'; } } } _animateIn() { this._animateSidenavIn(); this._animateOverlayIn(); } _animateSidenavIn() { let slideOutPercent = this.options.edge === 'left' ? -1 : 1; if (this.isDragged) { slideOutPercent = this.options.edge === 'left' ? slideOutPercent + this.percentOpen : slideOutPercent - this.percentOpen; } anim.remove(this.el); anim({ targets: this.el, translateX: [`${slideOutPercent * 100}%`, 0], duration: this.options.inDuration, easing: 'easeOutQuad', complete: () => { // Run onOpenEnd callback if (typeof this.options.onOpenEnd === 'function') { this.options.onOpenEnd.call(this, this.el); } } }); } _animateOverlayIn() { let start = 0; if (this.isDragged) { start = this.percentOpen; } else { $(this._overlay).css({ display: 'block' }); } anim.remove(this._overlay); anim({ targets: this._overlay, opacity: [start, 1], duration: this.options.inDuration, easing: 'easeOutQuad' }); } _animateOut() { this._animateSidenavOut(); this._animateOverlayOut(); } _animateSidenavOut() { let endPercent = this.options.edge === 'left' ? -1 : 1; let slideOutPercent = 0; if (this.isDragged) { slideOutPercent = this.options.edge === 'left' ? endPercent + this.percentOpen : endPercent - this.percentOpen; } anim.remove(this.el); anim({ targets: this.el, translateX: [`${slideOutPercent * 100}%`, `${endPercent * 105}%`], duration: this.options.outDuration, easing: 'easeOutQuad', complete: () => { // Run onOpenEnd callback if (typeof this.options.onCloseEnd === 'function') { this.options.onCloseEnd.call(this, this.el); } } }); } _animateOverlayOut() { anim.remove(this._overlay); anim({ targets: this._overlay, opacity: 0, duration: this.options.outDuration, easing: 'easeOutQuad', complete: () => { $(this._overlay).css('display', 'none'); } }); } } /** * @static * @memberof Sidenav * @type {Array.} */ Sidenav._sidenavs = []; M.Sidenav = Sidenav; if (M.jQueryLoaded) { M.initializeJqueryWrapper(Sidenav, 'sidenav', 'M_Sidenav'); } })(cash, M.anime);