mdui
Version:
a CSS Framework based on material design
426 lines (363 loc) • 10.8 kB
text/typescript
/**
* 在桌面设备上默认显示抽屉栏,不显示遮罩层
* 在手机和平板设备上默认不显示抽屉栏,始终显示遮罩层,且覆盖导航栏
*/
import $ from 'mdui.jq/es/$';
import extend from 'mdui.jq/es/functions/extend';
import { JQ } from 'mdui.jq/es/JQ';
import 'mdui.jq/es/methods/addClass';
import 'mdui.jq/es/methods/each';
import 'mdui.jq/es/methods/find';
import 'mdui.jq/es/methods/first';
import 'mdui.jq/es/methods/hasClass';
import 'mdui.jq/es/methods/off';
import 'mdui.jq/es/methods/on';
import 'mdui.jq/es/methods/one';
import 'mdui.jq/es/methods/removeClass';
import 'mdui.jq/es/methods/width';
import Selector from 'mdui.jq/es/types/Selector';
import mdui from '../../mdui';
import '../../jq_extends/methods/transitionEnd';
import '../../jq_extends/static/hideOverlay';
import '../../jq_extends/static/lockScreen';
import '../../jq_extends/static/showOverlay';
import '../../jq_extends/static/throttle';
import '../../jq_extends/static/unlockScreen';
import { componentEvent } from '../../utils/componentEvent';
import { $window } from '../../utils/dom';
declare module '../../interfaces/MduiStatic' {
interface MduiStatic {
/**
* Drawer 组件
*
* 请通过 `new mdui.Drawer()` 调用
*/
Drawer: {
/**
* 实例化 Drawer 组件
* @param selector CSS 选择器、或 DOM 元素、或 JQ 对象
* @param options 配置参数
*/
new (
selector: Selector | HTMLElement | ArrayLike<HTMLElement>,
options?: OPTIONS,
): Drawer;
};
}
}
type OPTIONS = {
/**
* 打开抽屉栏时是否显示遮罩层。该参数只对中等屏幕及以上的设备有效,在超小屏和小屏设备上始终会显示遮罩层。
*/
overlay?: boolean;
/**
* 是否启用滑动手势。
*/
swipe?: boolean;
};
type STATE = 'opening' | 'opened' | 'closing' | 'closed';
type EVENT = 'open' | 'opened' | 'close' | 'closed';
const DEFAULT_OPTIONS: OPTIONS = {
overlay: false,
swipe: false,
};
class Drawer {
/**
* drawer 元素的 JQ 对象
*/
public $element: JQ;
/**
* 配置参数
*/
public options: OPTIONS = extend({}, DEFAULT_OPTIONS);
/**
* 当前是否显示着遮罩层
*/
private overlay = false;
/**
* 抽屉栏的位置
*/
private position: 'left' | 'right';
/**
* 当前抽屉栏状态
*/
private state: STATE;
public constructor(
selector: Selector | HTMLElement | ArrayLike<HTMLElement>,
options: OPTIONS = {},
) {
this.$element = $(selector).first();
extend(this.options, options);
this.position = this.$element.hasClass('mdui-drawer-right')
? 'right'
: 'left';
if (this.$element.hasClass('mdui-drawer-close')) {
this.state = 'closed';
} else if (this.$element.hasClass('mdui-drawer-open')) {
this.state = 'opened';
} else if (this.isDesktop()) {
this.state = 'opened';
} else {
this.state = 'closed';
}
// 浏览器窗口大小调整时
$window.on(
'resize',
$.throttle(() => {
if (this.isDesktop()) {
// 由手机平板切换到桌面时
// 如果显示着遮罩,则隐藏遮罩
if (this.overlay && !this.options.overlay) {
$.hideOverlay();
this.overlay = false;
$.unlockScreen();
}
// 没有强制关闭,则状态为打开状态
if (!this.$element.hasClass('mdui-drawer-close')) {
this.state = 'opened';
}
} else if (!this.overlay && this.state === 'opened') {
// 由桌面切换到手机平板时。如果抽屉栏是打开着的且没有遮罩层,则关闭抽屉栏
if (this.$element.hasClass('mdui-drawer-open')) {
$.showOverlay();
this.overlay = true;
$.lockScreen();
$('.mdui-overlay').one('click', () => this.close());
} else {
this.state = 'closed';
}
}
}, 100),
);
// 绑定关闭按钮事件
this.$element.find('[mdui-drawer-close]').each((_, close) => {
$(close).on('click', () => this.close());
});
this.swipeSupport();
}
/**
* 是否是桌面设备
*/
private isDesktop(): boolean {
return $window.width() >= 1024;
}
/**
* 滑动手势支持
*/
private swipeSupport(): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
// 抽屉栏滑动手势控制
let openNavEventHandler: (event: Event) => void;
let touchStartX: number;
let touchStartY: number;
let swipeStartX: number;
let swiping: null | 'opening' | 'closing' = null;
let maybeSwiping = false;
const $body = $('body');
// 手势触发的范围
const swipeAreaWidth = 24;
function setPosition(translateX: number): void {
const rtlTranslateMultiplier = that.position === 'right' ? -1 : 1;
const transformCSS = `translate(${
-1 * rtlTranslateMultiplier * translateX
}px, 0) !important;`;
const transitionCSS = 'initial !important;';
that.$element.css(
'cssText',
`transform: ${transformCSS}; transition: ${transitionCSS};`,
);
}
function cleanPosition(): void {
that.$element[0].style.transform = '';
that.$element[0].style.webkitTransform = '';
that.$element[0].style.transition = '';
that.$element[0].style.webkitTransition = '';
}
function getMaxTranslateX(): number {
return that.$element.width() + 10;
}
function getTranslateX(currentX: number): number {
return Math.min(
Math.max(
swiping === 'closing'
? swipeStartX - currentX
: getMaxTranslateX() + swipeStartX - currentX,
0,
),
getMaxTranslateX(),
);
}
function onBodyTouchEnd(event?: Event): void {
if (swiping) {
let touchX = (event as TouchEvent).changedTouches[0].pageX;
if (that.position === 'right') {
touchX = $body.width() - touchX;
}
const translateRatio = getTranslateX(touchX) / getMaxTranslateX();
maybeSwiping = false;
const swipingState = swiping;
swiping = null;
if (swipingState === 'opening') {
if (translateRatio < 0.92) {
cleanPosition();
that.open();
} else {
cleanPosition();
}
} else {
if (translateRatio > 0.08) {
cleanPosition();
that.close();
} else {
cleanPosition();
}
}
$.unlockScreen();
} else {
maybeSwiping = false;
}
$body.off({
// eslint-disable-next-line @typescript-eslint/no-use-before-define
touchmove: onBodyTouchMove,
touchend: onBodyTouchEnd,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
touchcancel: onBodyTouchMove,
});
}
function onBodyTouchMove(event: Event): void {
let touchX = (event as TouchEvent).touches[0].pageX;
if (that.position === 'right') {
touchX = $body.width() - touchX;
}
const touchY = (event as TouchEvent).touches[0].pageY;
if (swiping) {
setPosition(getTranslateX(touchX));
} else if (maybeSwiping) {
const dXAbs = Math.abs(touchX - touchStartX);
const dYAbs = Math.abs(touchY - touchStartY);
const threshold = 8;
if (dXAbs > threshold && dYAbs <= threshold) {
swipeStartX = touchX;
swiping = that.state === 'opened' ? 'closing' : 'opening';
$.lockScreen();
setPosition(getTranslateX(touchX));
} else if (dXAbs <= threshold && dYAbs > threshold) {
onBodyTouchEnd();
}
}
}
function onBodyTouchStart(event: Event): void {
touchStartX = (event as TouchEvent).touches[0].pageX;
if (that.position === 'right') {
touchStartX = $body.width() - touchStartX;
}
touchStartY = (event as TouchEvent).touches[0].pageY;
if (that.state !== 'opened') {
if (
touchStartX > swipeAreaWidth ||
openNavEventHandler !== onBodyTouchStart
) {
return;
}
}
maybeSwiping = true;
$body.on({
touchmove: onBodyTouchMove,
touchend: onBodyTouchEnd,
touchcancel: onBodyTouchMove,
});
}
function enableSwipeHandling(): void {
if (!openNavEventHandler) {
$body.on('touchstart', onBodyTouchStart);
openNavEventHandler = onBodyTouchStart;
}
}
if (this.options.swipe) {
enableSwipeHandling();
}
}
/**
* 触发组件事件
* @param name
*/
private triggerEvent(name: EVENT): void {
componentEvent(name, 'drawer', this.$element, this);
}
/**
* 动画结束回调
*/
private transitionEnd(): void {
if (this.$element.hasClass('mdui-drawer-open')) {
this.state = 'opened';
this.triggerEvent('opened');
} else {
this.state = 'closed';
this.triggerEvent('closed');
}
}
/**
* 是否处于打开状态
*/
private isOpen(): boolean {
return this.state === 'opening' || this.state === 'opened';
}
/**
* 打开抽屉栏
*/
public open(): void {
if (this.isOpen()) {
return;
}
this.state = 'opening';
this.triggerEvent('open');
if (!this.options.overlay) {
$('body').addClass(`mdui-drawer-body-${this.position}`);
}
this.$element
.removeClass('mdui-drawer-close')
.addClass('mdui-drawer-open')
.transitionEnd(() => this.transitionEnd());
if (!this.isDesktop() || this.options.overlay) {
this.overlay = true;
$.showOverlay().one('click', () => this.close());
$.lockScreen();
}
}
/**
* 关闭抽屉栏
*/
public close(): void {
if (!this.isOpen()) {
return;
}
this.state = 'closing';
this.triggerEvent('close');
if (!this.options.overlay) {
$('body').removeClass(`mdui-drawer-body-${this.position}`);
}
this.$element
.addClass('mdui-drawer-close')
.removeClass('mdui-drawer-open')
.transitionEnd(() => this.transitionEnd());
if (this.overlay) {
$.hideOverlay();
this.overlay = false;
$.unlockScreen();
}
}
/**
* 切换抽屉栏打开/关闭状态
*/
public toggle(): void {
this.isOpen() ? this.close() : this.open();
}
/**
* 返回当前抽屉栏的状态。共包含四种状态:`opening`、`opened`、`closing`、`closed`
*/
public getState(): STATE {
return this.state;
}
}
mdui.Drawer = Drawer;