mdui
Version:
实现 material you 设计规范的 Web Components 组件库
287 lines (286 loc) • 11.3 kB
JavaScript
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.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 { getDuration, getEasing } from '@mdui/shared/helpers/motion.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import '@mdui/shared/icons/clear.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import '../button-icon.js';
import '../button.js';
import '../icon.js';
import { style } from './style.js';
/**
* @summary 消息条组件
*
* ```html
* <mdui-snackbar>message</mdui-snackbar>
* ```
*
* @event open - Snackbar 开始显示时,事件被触发。可以通过调用 `event.preventDefault()` 阻止 Snackbar 显示
* @event opened - Snackbar 显示动画完成时,事件被触发
* @event close - Snackbar 开始隐藏时,事件被触发。可以通过调用 `event.preventDefault()` 阻止 Snackbar 关闭
* @event closed - Snackbar 隐藏动画完成时,事件被触发
* @event action-click - 点击操作按钮时触发
*
* @slot - Snackbar 的消息文本内容
* @slot action - 右侧的操作按钮
* @slot close-button - 右侧的关闭按钮。必须设置 `closeable` 属性为 `true` 才会显示
* @slot close-icon - 关闭按钮中的图标
*
* @csspart message - 消息文本
* @csspart action - 操作按钮
* @csspart close-button - 关闭按钮
* @csspart close-icon - 关闭按钮中的图标
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
let Snackbar = class Snackbar extends MduiElement {
constructor() {
super();
/**
* 是否显示 Snackbar
*/
this.open = false;
/**
* Snackbar 的显示位置。默认为 `bottom`。可选值包括:
*
* * `top`:顶部居中
* * `top-start`:顶部左对齐
* * `top-end`:顶部右对齐
* * `bottom`:底部居中
* * `bottom-start`:底部左对齐
* * `bottom-end`:底部右对齐
*/
this.placement = 'bottom';
/**
* 操作按钮是否处于加载中状态
*/
this.actionLoading = false;
/**
* 是否在右侧显示关闭按钮
*/
this.closeable = false;
/**
* 自动关闭的延迟时间(单位:毫秒)。设置为 `0` 则不自动关闭。默认为 5000 毫秒
*/
this.autoCloseDelay = 5000;
/**
* 点击或触摸 Snackbar 以外的区域时,是否关闭 Snackbar
*/
this.closeOnOutsideClick = false;
this.onDocumentClick = this.onDocumentClick.bind(this);
}
async onOpenChange() {
const isMobile = breakpoint().down('sm');
const isCenteredHorizontally = ['top', 'bottom'].includes(this.placement);
const easingLinear = getEasing(this, 'linear');
const easingEmphasizedDecelerate = getEasing(this, 'emphasized-decelerate');
const children = Array.from(this.renderRoot.querySelectorAll('.message, .action-group'));
// 手机上始终使用全宽的样式,但 @media 选择器中无法使用 CSS 变量,所以使用 js 来设置样式
const commonStyle = isMobile
? { left: '1rem', right: '1rem', minWidth: 0 }
: isCenteredHorizontally
? { left: '50%' }
: {};
// 打开
// 要区分是否首次渲染,首次渲染时不触发事件,不执行动画;非首次渲染,触发事件,执行动画
if (this.open) {
const hasUpdated = this.hasUpdated;
if (!hasUpdated) {
await this.updateComplete;
}
if (hasUpdated) {
const eventProceeded = this.emit('open', { cancelable: true });
if (!eventProceeded) {
return;
}
}
window.clearTimeout(this.closeTimeout);
if (this.autoCloseDelay) {
this.closeTimeout = window.setTimeout(() => {
this.open = false;
}, this.autoCloseDelay);
}
this.style.display = 'flex';
await Promise.all([
stopAnimations(this),
...children.map((child) => stopAnimations(child)),
]);
const duration = getDuration(this, 'medium4');
const getOpenStyle = (ident) => {
const scaleY = `scaleY(${ident === 'start' ? 0 : 1})`;
if (isMobile) {
return { transform: scaleY };
}
else {
return {
transform: [
scaleY,
isCenteredHorizontally ? 'translateX(-50%)' : '',
]
.filter((i) => i)
.join(' '),
};
}
};
await Promise.all([
animateTo(this, [
{ ...getOpenStyle('start'), ...commonStyle },
{ ...getOpenStyle('end'), ...commonStyle },
], {
duration: hasUpdated ? duration : 0,
easing: easingEmphasizedDecelerate,
fill: 'forwards',
}),
animateTo(this, [{ opacity: 0 }, { opacity: 1, offset: 0.5 }, { opacity: 1 }], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
fill: 'forwards',
}),
...children.map((child) => animateTo(child, [
{ opacity: 0 },
{ opacity: 0, offset: 0.2 },
{ opacity: 1, offset: 0.8 },
{ opacity: 1 },
], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
})),
]);
if (hasUpdated) {
this.emit('opened');
}
return;
}
// 关闭
if (!this.open && this.hasUpdated) {
const eventProceeded = this.emit('close', { cancelable: true });
if (!eventProceeded) {
return;
}
window.clearTimeout(this.closeTimeout);
await Promise.all([
stopAnimations(this),
...children.map((child) => stopAnimations(child)),
]);
const duration = getDuration(this, 'short4');
const getCloseStyle = (ident) => {
const opacity = ident === 'start' ? 1 : 0;
const styles = { opacity };
if (!isMobile && isCenteredHorizontally) {
Object.assign(styles, { transform: 'translateX(-50%)' });
}
return styles;
};
await Promise.all([
animateTo(this, [
{ ...getCloseStyle('start'), ...commonStyle },
{ ...getCloseStyle('end'), ...commonStyle },
], {
duration,
easing: easingLinear,
fill: 'forwards',
}),
...children.map((child) => animateTo(child, [{ opacity: 1 }, { opacity: 0, offset: 0.75 }, { opacity: 0 }], { duration, easing: easingLinear })),
]);
this.style.display = 'none';
this.emit('closed');
return;
}
}
connectedCallback() {
super.connectedCallback();
document.addEventListener('pointerdown', this.onDocumentClick);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('pointerdown', this.onDocumentClick);
}
render() {
return html `<slot part="message" class="message"></slot><div class="action-group"><slot name="action" part="action" class="action" ="${this.onActionClick}">${this.action
? html `<mdui-button variant="text" loading="${this.actionLoading}">${this.action}</mdui-button>`
: nothingTemplate}</slot>${when(this.closeable, () => html `<slot name="close-button" part="close-button" class="close-button" ="${this.onCloseClick}"><mdui-button-icon><slot name="close-icon" part="close-icon">${this.closeIcon
? html `<mdui-icon name="${this.closeIcon}" class="i"></mdui-icon>`
: html `<mdui-icon-clear class="i"></mdui-icon-clear>`}</slot></mdui-button-icon></slot>`)}</div>`;
}
/**
* 在 document 上点击时,根据条件判断是否要关闭 snackbar
*/
onDocumentClick(e) {
if (!this.open || !this.closeOnOutsideClick) {
return;
}
const target = e.target;
if (!this.contains(target) && this !== target) {
this.open = false;
}
}
onActionClick(event) {
event.stopPropagation();
this.emit('action-click');
}
onCloseClick() {
this.open = false;
}
};
Snackbar.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Snackbar.prototype, "open", void 0);
__decorate([
property({ reflect: true })
], Snackbar.prototype, "placement", void 0);
__decorate([
property({ reflect: true, attribute: 'action' })
], Snackbar.prototype, "action", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'action-loading',
})
], Snackbar.prototype, "actionLoading", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Snackbar.prototype, "closeable", void 0);
__decorate([
property({ reflect: true, attribute: 'close-icon' })
], Snackbar.prototype, "closeIcon", void 0);
__decorate([
property({ type: Number, reflect: true, attribute: 'message-line' })
// eslint-disable-next-line prettier/prettier
], Snackbar.prototype, "messageLine", void 0);
__decorate([
property({ type: Number, reflect: true, attribute: 'auto-close-delay' })
], Snackbar.prototype, "autoCloseDelay", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
attribute: 'close-on-outside-click',
converter: booleanConverter,
})
], Snackbar.prototype, "closeOnOutsideClick", void 0);
__decorate([
watch('open')
], Snackbar.prototype, "onOpenChange", null);
Snackbar = __decorate([
customElement('mdui-snackbar')
], Snackbar);
export { Snackbar };