581 lines
16 KiB
JavaScript
581 lines
16 KiB
JavaScript
|
(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);
|