diff options
Diffstat (limited to 'node_modules/materialize-css/js/sidenav.js')
-rw-r--r-- | node_modules/materialize-css/js/sidenav.js | 580 |
1 files changed, 580 insertions, 0 deletions
diff --git a/node_modules/materialize-css/js/sidenav.js b/node_modules/materialize-css/js/sidenav.js new file mode 100644 index 0000000000..4a88dd3901 --- /dev/null +++ b/node_modules/materialize-css/js/sidenav.js @@ -0,0 +1,580 @@ +(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>} + */ + Sidenav._sidenavs = []; + + M.Sidenav = Sidenav; + + if (M.jQueryLoaded) { + M.initializeJqueryWrapper(Sidenav, 'sidenav', 'M_Sidenav'); + } +})(cash, M.anime); |