UNPKG

mdui

Version:

a CSS Framework based on material design

426 lines (363 loc) 10.8 kB
/** * 在桌面设备上默认显示抽屉栏,不显示遮罩层 * 在手机和平板设备上默认不显示抽屉栏,始终显示遮罩层,且覆盖导航栏 */ 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;