Files
LingChair/client/mdui_patched/components/navigation-drawer/index.js

406 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { when } from 'lit/directives/when.js';
import { $ } from '@mdui/jq/$.js';
import '@mdui/jq/methods/css.js';
import '@mdui/jq/methods/innerWidth.js';
import { isFunction, isNull } from '@mdui/jq/shared/helper.js';
import { DefinedController } from '@mdui/shared/controllers/defined.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { animateTo, stopAnimations } from '@mdui/shared/helpers/animate.js';
import { breakpoint } from '@mdui/shared/helpers/breakpoint.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { Modal } from '@mdui/shared/helpers/modal.js';
import { getDuration, getEasing } from '@mdui/shared/helpers/motion.js';
import { observeResize } from '@mdui/shared/helpers/observeResize.js';
import { lockScreen, unlockScreen } from '@mdui/shared/helpers/scroll.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { LayoutItemBase } from '../layout/layout-item-base.js';
import { style } from './style.js';
/**
* 在手机端,`modal` 始终为 `true`;大于手机端时,`modal` 属性才开始生效
*
* @summary 侧边抽屉栏组件
*
* ```html
* <mdui-navigation-drawer>content</mdui-navigation-drawer>
* ```
*
* @event open - 抽屉栏打开之前触发。可以通过调用 `event.preventDefault()` 阻止抽屉栏打开
* @event opened - 抽屉栏打开动画完成之后触发
* @event close - 抽屉栏关闭之前触发。可以通过调用 `event.preventDefault()` 阻止抽屉栏关闭
* @event closed - 抽屉栏关闭动画完成之后触发
* @event overlay-click - 点击遮罩层时触发
*
* @slot - 抽屉栏中的内容
*
* @csspart overlay - 遮罩层
* @csspart panel - 抽屉栏容器
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
let NavigationDrawer = class NavigationDrawer extends LayoutItemBase {
constructor() {
super(...arguments);
/**
* 是否打开抽屉栏
*/
this.open = false;
/**
* 抽屉栏打开时,是否显示遮罩层
*
* 在窄屏设备上(屏幕宽度小于 [`--mdui-breakpoint-md`](/docs/2/styles/design-tokens#breakpoint)),会始终显示遮罩层,无视该参数
*/
this.modal = false;
/**
* 在有遮罩层的情况下,按下 ESC 键是否关闭抽屉栏
*/
this.closeOnEsc = false;
/**
* 点击遮罩层时,是否关闭抽屉栏
*/
this.closeOnOverlayClick = false;
/**
* 抽屉栏的位置。可选值包括:
*
* * `left`:左侧
* * `right`:右侧
*/
this.placement = 'left';
/**
* 默认情况下,抽屉栏相对于 `body` 元素显示。当该参数设置为 `true` 时,抽屉栏将相对于其父元素显示。
*
* **Note**:设置该属性时,必须在父元素上手动设置样式 `position: relative; overflow: hidden;`。
*/
this.contained = false;
// 断点为 mobile 时为 `true` 时,强制使用遮罩层
this.mobile = false;
this.overlayRef = createRef();
this.panelRef = createRef();
this.definedController = new DefinedController(this, {
needDomReady: true,
});
}
get layoutPlacement() {
return this.placement;
}
get lockTarget() {
return this.contained || this.isParentLayout
? this.parentElement
: document.documentElement;
}
get isModal() {
return this.mobile || this.modal;
}
// contained 变更后,修改监听尺寸变化的元素。为 true 时,监听父元素;为 false 时,监听 body
async onContainedChange() {
await this.definedController.whenDefined();
this.observeResize?.unobserve();
this.setObserveResize();
}
onPlacementChange() {
if (this.isParentLayout) {
this.layoutManager.updateLayout(this);
}
}
async onMobileChange() {
if (!this.open || this.isParentLayout || this.contained) {
return;
}
await this.definedController.whenDefined();
if (this.isModal) {
lockScreen(this, this.lockTarget);
await this.getLockTargetAnimate(false, 0);
}
else {
unlockScreen(this, this.lockTarget);
await this.getLockTargetAnimate(true, 0);
}
}
async onOpenChange() {
let panel = this.panelRef.value;
let overlay = this.overlayRef.value;
const isRight = this.placement === 'right';
const easingLinear = getEasing(this, 'linear');
const easingEmphasized = getEasing(this, 'emphasized');
// 在当前 drawer 位于 layout 中时,设置所有 layout-item 和 layout-main 元素的 transition 样式
const setLayoutTransition = (duration, easing) => {
$(this.layoutManager.getItemsAndMain()).css('transition', isNull(duration) ? null : `all ${duration}ms ${easing}`);
};
// 停止原有动画
const stopOldAnimations = async () => {
const elements = [];
if (this.isModal) {
elements.push(overlay, panel);
}
else if (!this.isParentLayout) {
elements.push(this.lockTarget);
}
if (this.isParentLayout) {
const layoutItems = this.layoutManager.getItemsAndMain();
const layoutIndex = layoutItems.indexOf(this);
elements.push(...layoutItems.slice(layoutIndex));
}
if (!this.isModal && !elements.includes(this)) {
elements.push(this);
}
await Promise.all(elements.map((element) => stopAnimations(element)));
};
// 打开
// 要区分是否首次渲染,首次渲染时不触发事件,不执行动画;非首次渲染,触发事件,执行动画
if (this.open) {
const hasUpdated = this.hasUpdated;
if (!hasUpdated) {
await this.updateComplete;
panel = this.panelRef.value;
overlay = this.overlayRef.value;
}
if (hasUpdated) {
const eventProceeded = this.emit('open', { cancelable: true });
if (!eventProceeded) {
return;
}
}
await this.definedController.whenDefined();
this.style.display = 'block';
this.originalTrigger = document.activeElement;
if (this.isModal) {
this.modalHelper.activate();
if (!this.contained) {
lockScreen(this, this.lockTarget);
}
}
await stopOldAnimations();
// 设置聚焦
requestAnimationFrame(() => {
const autoFocusTarget = this.querySelector('[autofocus]');
if (autoFocusTarget) {
autoFocusTarget.focus({ preventScroll: true });
}
else {
panel.focus({ preventScroll: true });
}
});
const duration = getDuration(this, 'long2');
const animations = [];
// 模态框 drawer显示 overlay 动画
if (this.isModal) {
animations.push(animateTo(overlay, [{ opacity: 0 }, { opacity: 1, offset: 0.3 }, { opacity: 1 }], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
}));
}
// 不位于 layout 中,父元素 padding 变化的动画
else if (!this.isParentLayout) {
animations.push(this.getLockTargetAnimate(true, hasUpdated ? duration : 0));
}
// 若位于 layout 中,则 layout-main 的 padding 变化需要有和 drawer 相同的动画
// 但首次渲染不执行动画
if (this.isParentLayout && hasUpdated) {
setLayoutTransition(duration, easingEmphasized);
this.layoutManager.updateLayout(this);
}
// drawer 显示动画
animations.push(animateTo(this.isModal ? panel : this, [
{ transform: `translateX(${isRight ? '' : '-'}100%)` },
{ transform: 'translateX(0)' },
], {
duration: hasUpdated ? duration : 0,
easing: easingEmphasized,
}));
await Promise.all(animations);
if (!this.open) {
return;
}
// 若位于 layout 中,则 drawer 动画完成后,移除 layout-main 的动画
if (this.isParentLayout && hasUpdated) {
setLayoutTransition(null);
}
if (hasUpdated) {
this.emit('opened');
}
}
else if (this.hasUpdated) {
// 关闭
const eventProceeded = this.emit('close', { cancelable: true });
if (!eventProceeded) {
return;
}
await this.definedController.whenDefined();
if (this.isModal) {
this.modalHelper.deactivate();
}
await stopOldAnimations();
const duration = getDuration(this, 'short4');
const animations = [];
// 模态框 drawer显示 overlay 动画
if (this.isModal) {
animations.push(animateTo(overlay, [{ opacity: 1 }, { opacity: 0 }], {
duration,
easing: easingLinear,
}));
}
// 不位于 layout 中,父元素 padding 变化的动画
else if (!this.isParentLayout) {
animations.push(this.getLockTargetAnimate(false, duration));
}
// 若位于 layout 中,则 layout-main 的 padding 变化需要有和 drawer 相同的动画
if (this.isParentLayout) {
setLayoutTransition(duration, easingEmphasized);
// 关闭动画开始时drawer 的宽度不变。等到关闭动画结束drawer 的宽度才变为 0
// 为了 layout-main 的动画能在关闭动画开始时就执行,强制调用 updateLayout 更新布局
this.layoutManager.updateLayout(this, { width: 0 });
}
// drawer 显示动画
animations.push(animateTo(this.isModal ? panel : this, [
{ transform: 'translateX(0)' },
{ transform: `translateX(${isRight ? '' : '-'}100%)` },
], { duration, easing: easingEmphasized }));
await Promise.all(animations);
if (this.open) {
return;
}
// 若位于 layout 中,则 drawer 动画结束后,移除 layout-main 的动画
if (this.isParentLayout) {
setLayoutTransition(null);
}
this.style.display = 'none';
if (this.isModal && !this.contained) {
unlockScreen(this, this.lockTarget);
}
// 抽屉栏关闭后,恢复焦点到原有的元素上
const trigger = this.originalTrigger;
if (isFunction(trigger?.focus)) {
setTimeout(() => trigger.focus());
}
this.emit('closed');
}
}
connectedCallback() {
super.connectedCallback();
this.modalHelper = new Modal(this);
this.definedController.whenDefined().then(() => {
this.setObserveResize();
});
}
disconnectedCallback() {
super.disconnectedCallback();
unlockScreen(this, this.lockTarget);
this.observeResize?.unobserve();
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
this.addEventListener('keydown', (event) => {
if (this.open &&
this.closeOnEsc &&
event.key === 'Escape' &&
this.isModal) {
event.stopPropagation();
this.open = false;
}
});
}
render() {
return html `${when(this.isModal, () => html `<div ${ref(this.overlayRef)} part="overlay" class="overlay" @click="${this.onOverlayClick}"></div>`)}<slot ${ref(this.panelRef)} part="panel" class="panel" tabindex="0"></slot>`;
}
setObserveResize() {
this.observeResize = observeResize(this.contained ? this.parentElement : document.documentElement, () => {
const target = this.contained ? this.parentElement : undefined;
this.mobile = breakpoint(target).down('md');
// 若位于 layout 中,且为模态化,则重新布局时,占据的宽度为 0
if (this.isParentLayout) {
this.layoutManager.updateLayout(this, {
width: this.isModal ? 0 : undefined,
});
}
});
}
onOverlayClick() {
this.emit('overlay-click');
if (this.closeOnOverlayClick) {
this.open = false;
}
}
getLockTargetAnimate(open, duration) {
const paddingName = this.placement === 'right' ? 'paddingRight' : 'paddingLeft';
const panelWidth = $(this.panelRef.value).innerWidth() + 'px';
return animateTo(this.lockTarget, [
{ [paddingName]: open ? 0 : panelWidth },
{ [paddingName]: open ? panelWidth : 0 },
], {
duration,
easing: getEasing(this, 'emphasized'),
fill: 'forwards',
});
}
};
NavigationDrawer.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], NavigationDrawer.prototype, "open", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], NavigationDrawer.prototype, "modal", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'close-on-esc',
})
], NavigationDrawer.prototype, "closeOnEsc", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'close-on-overlay-click',
})
], NavigationDrawer.prototype, "closeOnOverlayClick", void 0);
__decorate([
property({ reflect: true })
// eslint-disable-next-line prettier/prettier
], NavigationDrawer.prototype, "placement", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], NavigationDrawer.prototype, "contained", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], NavigationDrawer.prototype, "mobile", void 0);
__decorate([
watch('contained', true)
], NavigationDrawer.prototype, "onContainedChange", null);
__decorate([
watch('placement', true)
], NavigationDrawer.prototype, "onPlacementChange", null);
__decorate([
watch('mobile', true),
watch('modal', true)
], NavigationDrawer.prototype, "onMobileChange", null);
__decorate([
watch('open')
], NavigationDrawer.prototype, "onOpenChange", null);
NavigationDrawer = __decorate([
customElement('mdui-navigation-drawer')
], NavigationDrawer);
export { NavigationDrawer };