Compare commits

...

28 Commits

Author SHA1 Message Date
CrescentLeaf
f01f3b02f4 ui: 移除延迟设置应用视图大小 2025-10-04 16:02:43 +08:00
CrescentLeaf
ad4e873d2f ui: 用户资料中进入对话, 连带上层对话框关闭 2025-10-04 15:52:22 +08:00
CrescentLeaf
a77e22a3ea feat: 从对话详情打开用户详情 2025-10-04 15:49:19 +08:00
CrescentLeaf
1fa91279e2 fix: 阻止用户头像点击事件传播 2025-10-04 15:35:16 +08:00
CrescentLeaf
debdb93935 feat: 对话中打开用户的资料 2025-10-04 15:32:54 +08:00
CrescentLeaf
81cdb4afd9 feat: Chat.getIdForPrivate 2025-10-04 15:32:29 +08:00
CrescentLeaf
bc08cd3c8c feat: 直接和对方私聊 2025-10-04 15:32:11 +08:00
CrescentLeaf
c24078b29d fix: stupid myId instead of targetUserId 2025-10-04 15:31:22 +08:00
CrescentLeaf
f04748aa5c feat: 在对话中打开对话详情对话框 2025-10-04 14:56:00 +08:00
CrescentLeaf
5ce97283f1 refactor: 抽离 openChatInfoDialog 2025-10-04 14:55:24 +08:00
CrescentLeaf
d6f794a094 fix: Chat.getInfo should return id 2025-10-04 14:55:06 +08:00
CrescentLeaf
47bbf12176 ui: 移动端设备上, 最近列表不会呈现激活状态 2025-10-04 14:40:30 +08:00
CrescentLeaf
2cee988ada feat: 自动更新最近对话
* 接收新消息
* 定时 15s
2025-10-04 14:35:19 +08:00
CrescentLeaf
04989762d9 feat: 最近对话 2025-10-04 14:32:22 +08:00
CrescentLeaf
89db6591a0 feat(api): User.getMyRecentChats 2025-10-04 14:13:06 +08:00
CrescentLeaf
d173fb7842 向消息接收者添加最近对话 2025-10-04 14:12:51 +08:00
CrescentLeaf
4133c13cf8 feat: RecentChats in User & Bean 2025-10-04 14:12:07 +08:00
CrescentLeaf
a1eddf813d chore: declare User.getMyRecentChats 2025-10-04 14:11:35 +08:00
CrescentLeaf
39b4a6d8a6 chore: 添加 Map 序列化相关辅助类 2025-10-04 14:07:21 +08:00
CrescentLeaf
e4cf9d6a68 chore: add reference 2025-10-04 13:02:29 +08:00
CrescentLeaf
f29538762b chore: 添加 Map 序列化相关辅助类 2025-10-04 13:02:13 +08:00
CrescentLeaf
7616a49ff8 chore: Chat (client) 同步伺服器端 Bean 定義 2025-10-04 12:10:32 +08:00
CrescentLeaf
42aefdd2f1 chore: deno ignore ./client/mdui_patched 2025-10-04 12:07:28 +08:00
CrescentLeaf
5fadb76a20 fix: 用戶現在無法任意訪問其他不屬於他的對話 2025-10-04 11:52:34 +08:00
CrescentLeaf
5474eac554 chore: 复制文字 先 trim 一遍 2025-10-04 11:42:03 +08:00
CrescentLeaf
a12a8830d4 ui: 改善 复制到剪贴薄 的用户体验
* 在 Via 浏览器上, writeText 本质上被重写了, 逻辑还是 execCommand copy
* 更换 copy 为 cut
2025-10-04 11:34:56 +08:00
CrescentLeaf
6c5f3aac85 ui: 修正由于 Menu 关闭没有同步状态导致的开关不正常 2025-10-04 11:07:55 +08:00
CrescentLeaf
6e164cbdfb fix: 本地 patch MDUI 以解决 tabindex = 0 导致的一系列玄学问题 2025-10-04 11:07:03 +08:00
502 changed files with 94818 additions and 74 deletions

View File

@@ -1,6 +1,7 @@
{ {
"deno.enable": true, "deno.enable": true,
"deno.disablePaths": [ "deno.disablePaths": [
"./thewhitesilk_data" "./thewhitesilk_data",
"./client/mdui_patched"
] ]
} }

20
client/MapJson.ts Normal file
View File

@@ -0,0 +1,20 @@
// https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map
export default class MapJson {
static replacer(key: unknown, value: unknown) {
if (value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
}
} else {
return value
}
}
static reviver(key: unknown, value: any) {
if (value?.dataType === 'Map') {
return new Map(value.value)
}
return value
}
}

View File

@@ -13,17 +13,23 @@ export type CallMethod =
"User.addContact" | "User.addContact" |
"User.removeContacts" | "User.removeContacts" |
"User.getMyRecentChats" |
"Chat.getInfo" | "Chat.getInfo" |
"Chat.getIdForPrivate" |
"Chat.getAnotherUserIdFromPrivate" |
"Chat.sendMessage" | "Chat.sendMessage" |
"Chat.getMessageHistory" | "Chat.getMessageHistory" |
"Chat.uploadFile" "Chat.uploadFile"
export const CallableMethodBeforeAuth = [ export type ClientEvent =
"Client.onMessage"
export const CallableMethodBeforeAuth = [
"User.auth", "User.auth",
"User.register", "User.register",
"User.login", "User.login",
] ]
export type ClientEvent =
"Client.onMessage"

View File

@@ -1,10 +1,10 @@
import ChatType from "./ChatType.ts"
export default class Chat { export default class Chat {
declare type: "paivate" | "group" declare type: ChatType
declare id: string declare id: string
declare title: string declare title: string
declare avatar_file_hash?: string declare avatar?: string
declare user_a_id?: string
declare user_b_id?: string
[key: string]: unknown [key: string]: unknown
} }

View File

@@ -0,0 +1,3 @@
type ChatType = 'private' | 'group'
export default ChatType

View File

@@ -9,6 +9,11 @@
"jsxImportSource": "react", "jsxImportSource": "react",
"jsxImportSourceTypes": "@types/react" "jsxImportSourceTypes": "@types/react"
}, },
"patch": [
"./mdui_patched",
],
"nodeModulesDir": "auto",
"unstable": ["npm-patch"],
"imports": { "imports": {
"@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.5", "@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.5",
"@types/react": "npm:@types/react@18.3.1", "@types/react": "npm:@types/react@18.3.1",

View File

@@ -26,13 +26,13 @@ import AppMobile from './ui/AppMobile.tsx'
import isMobileUI from "./ui/isMobileUI.ts" import isMobileUI from "./ui/isMobileUI.ts"
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(isMobileUI() ? AppMobile : App, null)) ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(isMobileUI() ? AppMobile : App, null))
const onResize = () => setTimeout(() => { const onResize = () => {
document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%") document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%")
// deno-lint-ignore no-window // deno-lint-ignore no-window
document.body.style.setProperty('--whitesilk-window-width', window.innerWidth + 'px') document.body.style.setProperty('--whitesilk-window-width', window.innerWidth + 'px')
// deno-lint-ignore no-window // deno-lint-ignore no-window
document.body.style.setProperty('--whitesilk-window-height', window.innerHeight + 'px') document.body.style.setProperty('--whitesilk-window-height', window.innerHeight + 'px')
}, 100) }
// deno-lint-ignore no-window no-window-prefix // deno-lint-ignore no-window no-window-prefix
window.addEventListener('resize', onResize) window.addEventListener('resize', onResize)
onResize() onResize()

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-present zdhxiong@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,49 @@
# mdui
[![GitHub version](https://badge.fury.io/gh/zdhxiong%2Fmdui.svg)](https://badge.fury.io/gh/zdhxiong%2Fmdui)
[![npm version](https://img.shields.io/npm/v/mdui.svg)](https://www.npmjs.com/package/mdui)
[![CDNJS](https://img.shields.io/cdnjs/v/mdui.svg)](https://cdnjs.com/libraries/mdui)
[官网](https://www.mdui.org) | [文档](https://www.mdui.org/docs/2/)
使用 Web Components 实现,遵循 Material You 设计规范的 Web 前端组件库。
* **Web Components**mdui 组件全部使用 Web Components 开发,使用组件就像使用 `<div>` 标签一样简单。
* **Material You**:遵循最新的 Material Design 3Material You设计规范使你的产品美观、易用。
* **动态配色**支持根据给定颜色值或给定一张图片mdui 能自动计算出颜色值,生成整套配色方案,并在所有 mdui 组件中生效。
* **暗色模式**:所有组件都支持暗色模式、及支持根据操作系统设置自动切换亮色模式和暗色模式。
* **轻量级**gzip 后的 CSS + JavaScript 仅 85KB使用按需导入可进一步减小体积使加载更迅速。
* **IDE 支持**:在 VSCode 和 WebStorm 中能获得完美的代码提示。且提供了 VSCode 扩展和 WebStorm 插件,使开发更便捷。
* **兼容所有框架**mdui 能兼容 Vue、React、Angular 等框架,只要在浏览器上运行的应用,都能使用 mdui。
* **TypeScript 支持**mdui 完全使用 TypeScript 开发,拥有完美的类型提示。
* **无依赖**:不需要依赖任何第三方库,节约网络流量,使加载更迅速。
* **组件丰富**mdui 包含 30 多个组件,及十余个工具函数,常用组件都有。
* **Material Icons 图标库**:提供了超过 1 万个图标组件,可按需导入所需图标。
* **低学习成本**:只需懂一点 HTML、CSS、JavaScript 的基础知识,就能使用 mdui。
## 安装
```bash
npm install mdui --save
```
### 导入 CSS 及 JS 文件
```js
import 'mdui/mdui.css';
import 'mdui';
```
### 使用组件
```html
<mdui-button>Button</mdui-button>
```
## 赞助
赞助以帮助 mdui 持续更新
![通过支付宝赞助](https://ww1.sinaimg.cn/large/63f511e3gy1ffhw0jj5n4j206o089dge.jpg)
![通过微信赞助](https://ww1.sinaimg.cn/large/63f511e3gy1ffhw0vkaeaj206o0890ta.jpg)
[![通过 Paypal 赞助](https://ww1.sinaimg.cn/large/63f511e3gy1fff6937xzbj203w00y3yc.jpg)](https://www.paypal.me/zdhxiong/5)

View File

@@ -0,0 +1 @@
export * from './avatar/index.js';

View File

@@ -0,0 +1 @@
export * from './avatar/index.js';

View File

@@ -0,0 +1,51 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import '../icon.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
/**
* @summary 头像组件
*
* ```html
* <mdui-avatar src="https://avatars.githubusercontent.com/u/3030330?s=40&v=4"></mdui-avatar>
* ```
*
* @slot - 自定义头像内容,可以为字母、汉字、`<img>` 元素、图标等
*
* @csspart image - 使用图片作为头像时,组件内部的 `<img>` 元素
* @csspart icon - 使用图标作为头像时,组件内部的 `<mdui-icon>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Avatar extends MduiElement<AvatarEventMap> {
static styles: CSSResultGroup;
/**
* 头像图片的 URL 地址
*/
src?: string;
/**
* 图片如何适应容器框,与原生的 [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) 属性相同。可选值包括:
*
* * `contain`:保持图片原有尺寸比例,内容会被等比例缩放
* * `cover`:保持图片原有尺寸比例,但部分内容可能被剪切
* * `fill`:默认值,不保持图片原有尺寸比例,内容会被拉伸以填充整个容器
* * `none`:保留图片原有尺寸,内容不会被缩放或拉伸
* * `scale-down`:保持图片原有尺寸比例,内容尺寸与 `none` 或 `contain` 中较小的一个相同
*/
fit?: /*保持图片原有尺寸比例,内容会被等比例缩放*/ 'contain' | /*保持图片原有尺寸比例,但部分内容可能被剪切*/ 'cover' | /*默认值,不保持图片原有尺寸比例,内容会被拉伸以填充整个容器*/ 'fill' | /*保留图片原有尺寸,内容不会被缩放或拉伸*/ 'none' | /*保持图片原有尺寸比例,内容尺寸与 `none` 或 `contain` 中较小的一个相同*/ 'scale-down';
/**
* 头像的 Material Icons 图标名
*/
icon?: string;
/**
* 头像的替代文本描述
*/
label?: string;
private readonly hasSlotController;
protected render(): TemplateResult;
}
export interface AvatarEventMap {
}
declare global {
interface HTMLElementTagNameMap {
'mdui-avatar': Avatar;
}
}

View File

@@ -0,0 +1,57 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { HasSlotController } from '@mdui/shared/controllers/has-slot.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import '../icon.js';
import { style } from './style.js';
/**
* @summary 头像组件
*
* ```html
* <mdui-avatar src="https://avatars.githubusercontent.com/u/3030330?s=40&v=4"></mdui-avatar>
* ```
*
* @slot - 自定义头像内容,可以为字母、汉字、`<img>` 元素、图标等
*
* @csspart image - 使用图片作为头像时,组件内部的 `<img>` 元素
* @csspart icon - 使用图标作为头像时,组件内部的 `<mdui-icon>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Avatar = class Avatar extends MduiElement {
constructor() {
super(...arguments);
this.hasSlotController = new HasSlotController(this, '[default]');
}
render() {
return this.hasSlotController.test('[default]')
? html `<slot></slot>`
: this.src
? html `<img part="image" alt="${ifDefined(this.label)}" src="${this.src}" style="${styleMap({ objectFit: this.fit })}">`
: this.icon
? html `<mdui-icon part="icon" name="${this.icon}"></mdui-icon>`
: nothingTemplate;
}
};
Avatar.styles = [componentStyle, style];
__decorate([
property({ reflect: true })
], Avatar.prototype, "src", void 0);
__decorate([
property({ reflect: true })
], Avatar.prototype, "fit", void 0);
__decorate([
property({ reflect: true })
], Avatar.prototype, "icon", void 0);
__decorate([
property({ reflect: true })
], Avatar.prototype, "label", void 0);
Avatar = __decorate([
customElement('mdui-avatar')
], Avatar);
export { Avatar };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-full);position:relative;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;white-space:nowrap;vertical-align:middle;border-radius:var(--shape-corner);-webkit-user-select:none;user-select:none;width:2.5rem;height:2.5rem;background-color:rgb(var(--mdui-color-primary-container));color:rgb(var(--mdui-color-on-primary-container));font-size:var(--mdui-typescale-title-medium-size);font-weight:var(--mdui-typescale-title-medium-weight);letter-spacing:var(--mdui-typescale-title-medium-tracking);line-height:var(--mdui-typescale-title-medium-line-height)}img{width:100%;height:100%}::slotted(mdui-icon),mdui-icon{font-size:1.5em}`;

View File

@@ -0,0 +1 @@
export * from './badge/index.js';

View File

@@ -0,0 +1 @@
export * from './badge/index.js';

View File

@@ -0,0 +1,31 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
/**
* @summary 徽标组件
*
* ```html
* <mdui-badge>12</mdui-badge>
* ```
*
* @slot - 徽标中显示的文本
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Badge extends MduiElement<BadgeEventMap> {
static styles: CSSResultGroup;
/**
* 徽标的形状。可选值包括:
*
* * `small`:小型徽标,不显示文本
* * `large`:大型徽标,会显示文本
*/
variant: /*小型徽标,不显示文本*/ 'small' | /*大型徽标,会显示文本*/ 'large';
protected render(): TemplateResult;
}
export interface BadgeEventMap {
}
declare global {
interface HTMLElementTagNameMap {
'mdui-badge': Badge;
}
}

View File

@@ -0,0 +1,44 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { style } from './style.js';
/**
* @summary 徽标组件
*
* ```html
* <mdui-badge>12</mdui-badge>
* ```
*
* @slot - 徽标中显示的文本
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Badge = class Badge extends MduiElement {
constructor() {
super(...arguments);
/**
* 徽标的形状。可选值包括:
*
* * `small`:小型徽标,不显示文本
* * `large`:大型徽标,会显示文本
*/
this.variant = 'large';
}
render() {
if (this.variant === 'small') {
return nothingTemplate;
}
return html `<slot></slot>`;
}
};
Badge.styles = [componentStyle, style];
__decorate([
property({ reflect: true })
], Badge.prototype, "variant", void 0);
Badge = __decorate([
customElement('mdui-badge')
], Badge);
export { Badge };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-full);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;border-radius:var(--shape-corner);padding-left:.25rem;padding-right:.25rem;color:rgb(var(--mdui-color-on-error));background-color:rgb(var(--mdui-color-error));height:1rem;min-width:1rem;font-size:var(--mdui-typescale-label-small-size);font-weight:var(--mdui-typescale-label-small-weight);letter-spacing:var(--mdui-typescale-label-small-tracking);line-height:var(--mdui-typescale-label-small-line-height)}:host([variant=small]){min-width:0;padding:0;width:.375rem;height:.375rem}`;

View File

@@ -0,0 +1 @@
export * from './bottom-app-bar/index.js';

View File

@@ -0,0 +1 @@
export * from './bottom-app-bar/index.js';

View File

@@ -0,0 +1,67 @@
import { LayoutItemBase } from '../layout/layout-item-base.js';
import type { LayoutPlacement } from '../layout/helper.js';
import type { ScrollPaddingPosition } from '@mdui/shared/mixins/scrollBehavior.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
declare const BottomAppBar_base: import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/scrollBehavior.js").ScrollBehaviorMixinInterface> & typeof LayoutItemBase;
/**
* @summary 底部应用栏组件
*
* ```html
* <mdui-bottom-app-bar>
* ..<mdui-button-icon icon="check_box--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="edit--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="mic_none--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="image--outlined"></mdui-button-icon>
* ..<div style="flex-grow: 1"></div>
* ..<mdui-fab icon="add"></mdui-fab>
* </mdui-bottom-app-bar>
* ```
*
* @event show - 开始显示时,事件被触发。可以通过调用 `event.preventDefault()` 阻止显示
* @event shown - 显示动画完成时,事件被触发
* @event hide - 开始隐藏时,事件被触发。可以通过调用 `event.preventDefault()` 阻止隐藏
* @event hidden - 隐藏动画完成时,事件被触发
*
* @slot - 底部应用栏内部的元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
export declare class BottomAppBar extends BottomAppBar_base<BottomAppBarEventMap> {
static styles: CSSResultGroup;
/**
* 是否隐藏
*/
hide: boolean;
/**
* 是否让底部应用栏中的 [`<mdui-fab>`](/docs/2/components/fab) 组件脱离应用栏。如果为 `true`,则当应用栏隐藏后,[`<mdui-fab>`](/docs/2/components/fab) 仍会停留在页面上
*/
fabDetach: boolean;
/**
* 滚动行为。可选值为:
*
* * `hide`:滚动时隐藏
*/
scrollBehavior?: 'hide';
protected get scrollPaddingPosition(): ScrollPaddingPosition;
protected get layoutPlacement(): LayoutPlacement;
protected firstUpdated(_changedProperties: PropertyValues): void;
protected render(): TemplateResult;
/**
* 滚动行为
* 当前仅支持 hide 这一个行为,所以不做行为类型判断
*/
protected runScrollThreshold(isScrollingUp: boolean): void;
}
export interface BottomAppBarEventMap {
show: CustomEvent<void>;
shown: CustomEvent<void>;
hide: CustomEvent<void>;
hidden: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-bottom-app-bar': BottomAppBar;
}
}
export {};

View File

@@ -0,0 +1,105 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { ScrollBehaviorMixin } from '@mdui/shared/mixins/scrollBehavior.js';
import { LayoutItemBase } from '../layout/layout-item-base.js';
import { style } from './style.js';
/**
* @summary 底部应用栏组件
*
* ```html
* <mdui-bottom-app-bar>
* ..<mdui-button-icon icon="check_box--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="edit--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="mic_none--outlined"></mdui-button-icon>
* ..<mdui-button-icon icon="image--outlined"></mdui-button-icon>
* ..<div style="flex-grow: 1"></div>
* ..<mdui-fab icon="add"></mdui-fab>
* </mdui-bottom-app-bar>
* ```
*
* @event show - 开始显示时,事件被触发。可以通过调用 `event.preventDefault()` 阻止显示
* @event shown - 显示动画完成时,事件被触发
* @event hide - 开始隐藏时,事件被触发。可以通过调用 `event.preventDefault()` 阻止隐藏
* @event hidden - 隐藏动画完成时,事件被触发
*
* @slot - 底部应用栏内部的元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
let BottomAppBar = class BottomAppBar extends ScrollBehaviorMixin(LayoutItemBase) {
constructor() {
super(...arguments);
/**
* 是否隐藏
*/
this.hide = false;
/**
* 是否让底部应用栏中的 [`<mdui-fab>`](/docs/2/components/fab) 组件脱离应用栏。如果为 `true`,则当应用栏隐藏后,[`<mdui-fab>`](/docs/2/components/fab) 仍会停留在页面上
*/
this.fabDetach = false;
}
get scrollPaddingPosition() {
return 'bottom';
}
get layoutPlacement() {
return 'bottom';
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
this.addEventListener('transitionend', (event) => {
if (event.target === this) {
this.emit(this.hide ? 'hidden' : 'shown');
}
});
}
render() {
return html `<slot></slot>`;
}
/**
* 滚动行为
* 当前仅支持 hide 这一个行为,所以不做行为类型判断
*/
runScrollThreshold(isScrollingUp) {
// 向下滚动
if (!isScrollingUp && !this.hide) {
const eventProceeded = this.emit('hide', { cancelable: true });
if (eventProceeded) {
this.hide = true;
}
}
// 向上滚动
if (isScrollingUp && this.hide) {
const eventProceeded = this.emit('show', { cancelable: true });
if (eventProceeded) {
this.hide = false;
}
}
}
};
BottomAppBar.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], BottomAppBar.prototype, "hide", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'fab-detach',
})
], BottomAppBar.prototype, "fabDetach", void 0);
__decorate([
property({ reflect: true, attribute: 'scroll-behavior' })
], BottomAppBar.prototype, "scrollBehavior", void 0);
BottomAppBar = __decorate([
customElement('mdui-bottom-app-bar')
], BottomAppBar);
export { BottomAppBar };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-none);--z-index:2000;position:fixed;right:0;bottom:0;left:0;display:flex;flex:0 0 auto;align-items:center;justify-content:flex-start;border-radius:var(--shape-corner) var(--shape-corner) 0 0;z-index:var(--z-index);transition:bottom var(--mdui-motion-duration-long2) var(--mdui-motion-easing-emphasized);padding:0 1rem;height:5rem;background-color:rgb(var(--mdui-color-surface-container));box-shadow:var(--mdui-elevation-level2)}:host([scroll-target]:not([scroll-target=''])){position:absolute}:host([hide]:not([hide=false i])){transition-duration:var(--mdui-motion-duration-short4);bottom:-5.625rem}::slotted(:not(:first-child)){margin-left:.5rem}::slotted(mdui-fab){box-shadow:var(--mdui-elevation-level0)}:host([fab-detach]:not([fab-detach=false i])) ::slotted(mdui-fab){position:absolute;transition:bottom var(--mdui-motion-duration-long2) var(--mdui-motion-easing-standard);right:1rem;bottom:.75rem}:host([fab-detach][hide][scroll-behavior~=hide]:not([hide=false i],[fab-detach=false i])) ::slotted(mdui-fab){transition-duration:var(--mdui-motion-duration-short4);bottom:1rem;box-shadow:var(--mdui-elevation-level2)}:host([fab-detach][hide][scroll-behavior~=hide][scroll-target]:not([fab-detach=false i],[hide=false i],[scroll-target=''])) ::slotted(mdui-fab){bottom:6.625rem}:host([hide]:not([hide=false i])) ::slotted(:not(mdui-fab)),:host([hide]:not([hide=false i],[fab-detach])) ::slotted(mdui-fab),:host([hide][fab-detach=false i]:not([hide=false i])) ::slotted(mdui-fab){transform:translateY(8.75rem);transition:transform var(--mdui-motion-duration-0) var(--mdui-motion-easing-emphasized-accelerate) var(--mdui-motion-duration-short4)}::slotted(:first-child){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-short1)}::slotted(:nth-child(2)){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-short3)}::slotted(:nth-child(3)){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-short4)}::slotted(:nth-child(4)){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-medium1)}::slotted(:nth-child(5)){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-medium2)}::slotted(:nth-child(6)){transition:transform var(--mdui-motion-duration-short3) var(--mdui-motion-easing-emphasized-decelerate) var(--mdui-motion-duration-medium3)}`;

View File

@@ -0,0 +1 @@
export * from './button-icon/index.js';

View File

@@ -0,0 +1 @@
export * from './button-icon/index.js';

View File

@@ -0,0 +1,72 @@
import { ButtonBase } from '../button/button-base.js';
import '../icon.js';
import type { Ripple } from '../ripple/index.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
/**
* @summary 图标按钮组件
*
* ```html
* <mdui-button-icon icon="search"></mdui-button-icon>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 选中状态变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 图标组件
* @slot selected-icon 选中状态显示的图标元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart icon - 未选中状态的图标
* @csspart selected-icon 选中状态的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class ButtonIcon extends ButtonBase<ButtonIconEventMap> {
static styles: CSSResultGroup;
/**
* 图标按钮的形状。可选值包括:
*
* * `standard`:适用于最低优先级的操作
* * `filled`:视觉效果强烈,适用于高优先级的操作
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作
* * `outlined`:适用于中等优先级的操作
*/
variant: /*适用于最低优先级的操作*/ 'standard' | /*视觉效果强烈,适用于高优先级的操作*/ 'filled' | /*视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作*/ 'tonal' | /*适用于中等优先级的操作*/ 'outlined';
/**
* Material Icons 图标名。也可以通过 default slot 设置
*/
icon?: string;
/**
* 选中状态的 Material Icons 图标名。也可以通过 `slot="selected-icon"` 设置
*/
selectedIcon?: string;
/**
* 是否可选中
*/
selectable: boolean;
/**
* 是否已被选中
*/
selected: boolean;
private readonly rippleRef;
private readonly hasSlotController;
protected get rippleElement(): Ripple;
private onSelectedChange;
protected firstUpdated(changedProperties: PropertyValues): void;
protected render(): TemplateResult;
private renderIcon;
}
export interface ButtonIconEventMap {
focus: FocusEvent;
blur: FocusEvent;
change: CustomEvent<void>;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-button-icon': ButtonIcon;
}
}

View File

@@ -0,0 +1,129 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { HasSlotController } from '@mdui/shared/controllers/has-slot.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { ButtonBase } from '../button/button-base.js';
import '../icon.js';
import { style } from './style.js';
/**
* @summary 图标按钮组件
*
* ```html
* <mdui-button-icon icon="search"></mdui-button-icon>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 选中状态变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 图标组件
* @slot selected-icon 选中状态显示的图标元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart icon - 未选中状态的图标
* @csspart selected-icon 选中状态的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let ButtonIcon = class ButtonIcon extends ButtonBase {
constructor() {
super(...arguments);
/**
* 图标按钮的形状。可选值包括:
*
* * `standard`:适用于最低优先级的操作
* * `filled`:视觉效果强烈,适用于高优先级的操作
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作
* * `outlined`:适用于中等优先级的操作
*/
this.variant = 'standard';
/**
* 是否可选中
*/
this.selectable = false;
/**
* 是否已被选中
*/
this.selected = false;
this.rippleRef = createRef();
this.hasSlotController = new HasSlotController(this, '[default]', 'selected-icon');
}
get rippleElement() {
return this.rippleRef.value;
}
onSelectedChange() {
this.emit('change');
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.addEventListener('click', () => {
if (!this.selectable || this.disabled) {
return;
}
this.selected = !this.selected;
});
}
render() {
return html `<mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.isButton()
? this.renderButton({
className: 'button',
part: 'button',
content: this.renderIcon(),
})
: this.disabled || this.loading
? html `<span part="button" class="button _a">${this.renderIcon()}</span>`
: this.renderAnchor({
className: 'button',
part: 'button',
content: this.renderIcon(),
})} ${this.renderLoading()}`;
}
renderIcon() {
const icon = () => this.hasSlotController.test('[default]')
? html `<slot></slot>`
: this.icon
? html `<mdui-icon part="icon" class="icon" name="${this.icon}"></mdui-icon>`
: nothingTemplate;
const selectedIcon = () => this.hasSlotController.test('selected-icon') || this.selectedIcon
? html `<slot name="selected-icon" part="selected-icon" class="selected-icon"><mdui-icon name="${this.selectedIcon}"></mdui-icon></slot>`
: icon();
return this.selected ? selectedIcon() : icon();
}
};
ButtonIcon.styles = [ButtonBase.styles, style];
__decorate([
property({ reflect: true })
], ButtonIcon.prototype, "variant", void 0);
__decorate([
property({ reflect: true })
], ButtonIcon.prototype, "icon", void 0);
__decorate([
property({ reflect: true, attribute: 'selected-icon' })
], ButtonIcon.prototype, "selectedIcon", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonIcon.prototype, "selectable", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonIcon.prototype, "selected", void 0);
__decorate([
watch('selected', true)
], ButtonIcon.prototype, "onSelectedChange", null);
ButtonIcon = __decorate([
customElement('mdui-button-icon')
], ButtonIcon);
export { ButtonIcon };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,4 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-full);position:relative;display:inline-block;flex-shrink:0;overflow:hidden;text-align:center;border-radius:var(--shape-corner);cursor:pointer;-webkit-tap-highlight-color:transparent;font-size:1.5rem;width:2.5rem;height:2.5rem}:host([variant=standard]){color:rgb(var(--mdui-color-on-surface-variant));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface-variant)}:host([variant=filled]){color:rgb(var(--mdui-color-primary));background-color:rgb(var(--mdui-color-surface-container-highest));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=tonal]){color:rgb(var(--mdui-color-on-surface-variant));background-color:rgb(var(--mdui-color-surface-container-highest));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface-variant)}:host([variant=outlined]){border:.0625rem solid rgb(var(--mdui-color-outline));color:rgb(var(--mdui-color-on-surface-variant));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface-variant)}:host([variant=outlined][pressed]){color:rgb(var(--mdui-color-on-surface));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}:host([variant=standard][selected]:not([selected=false i])){color:rgb(var(--mdui-color-primary));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=filled]:not([selectable])),:host([variant=filled][selectable=false i]),:host([variant=filled][selected]:not([selected=false i])){color:rgb(var(--mdui-color-on-primary));background-color:rgb(var(--mdui-color-primary));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-primary)}:host([variant=tonal]:not([selectable])),:host([variant=tonal][selectable=false i]),:host([variant=tonal][selected]:not([selected=false i])){color:rgb(var(--mdui-color-on-secondary-container));background-color:rgb(var(--mdui-color-secondary-container));--mdui-comp-ripple-state-layer-color:var(
--mdui-color-on-secondary-container
)}:host([variant=outlined][selected]:not([selected=false i])){border:none;color:rgb(var(--mdui-color-inverse-on-surface));background-color:rgb(var(--mdui-color-inverse-surface));--mdui-comp-ripple-state-layer-color:var(--mdui-color-inverse-on-surface)}:host([variant=filled][disabled]:not([disabled=false i])),:host([variant=outlined][disabled]:not([disabled=false i])),:host([variant=tonal][disabled]:not([disabled=false i])){background-color:rgba(var(--mdui-color-on-surface),.12);border-color:rgba(var(--mdui-color-on-surface),.12)}:host([disabled]:not([disabled=false i])),:host([loading]:not([loading=false i])){cursor:default;pointer-events:none}:host([disabled]:not([disabled=false i])){color:rgba(var(--mdui-color-on-surface),.38)!important}.button{float:left;width:100%}:host([loading]:not([loading=false i])) .button,:host([loading]:not([loading=false i])) mdui-ripple{opacity:0}.icon,.selected-icon mdui-icon,::slotted(*){font-size:inherit}mdui-circular-progress{display:flex;position:absolute;top:calc(50% - 1.5rem / 2);left:calc(50% - 1.5rem / 2);width:1.5rem;height:1.5rem}:host([variant=filled]:not([disabled])) mdui-circular-progress,:host([variant=filled][disabled=false i]) mdui-circular-progress{stroke:rgb(var(--mdui-color-on-primary))}:host([disabled]:not([disabled=false i])) mdui-circular-progress{stroke:rgba(var(--mdui-color-on-surface),38%)}`;

View File

@@ -0,0 +1 @@
export * from './button/index.js';

View File

@@ -0,0 +1 @@
export * from './button/index.js';

View File

@@ -0,0 +1 @@
export declare const buttonBaseStyle: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const buttonBaseStyle = css `.button{position:relative;display:inline-flex;align-items:center;justify-content:center;height:100%;padding:0;overflow:hidden;color:inherit;font-size:inherit;font-family:inherit;font-weight:inherit;letter-spacing:inherit;white-space:nowrap;text-align:center;text-decoration:none;vertical-align:middle;background:0 0;border:none;outline:0;cursor:inherit;-webkit-user-select:none;user-select:none;touch-action:manipulation;zoom:1;-webkit-user-drag:none}`;

View File

@@ -0,0 +1,137 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import '../circular-progress.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
type RenderButtonOptions = {
id?: string;
className?: string;
part?: string;
content?: TemplateResult | TemplateResult[];
tabindex?: number;
};
declare const ButtonBase_base: import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/anchor.js").AnchorMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("../ripple/ripple-mixin.js").RippleMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/focusable.js").FocusableMixinInterface> & typeof MduiElement;
export declare class ButtonBase<E> extends ButtonBase_base<E> {
static styles: CSSResultGroup;
/**
* 是否禁用
*/
disabled: boolean;
/**
* 是否处于加载中状态
*/
loading: boolean;
/**
* 按钮的名称,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
name: string;
/**
* 按钮的初始值,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
value: string;
/**
* 按钮的类型。默认类型为 `button`。可选类型包括:
*
* * `submit`:点击按钮会提交表单数据到服务器
* * `reset`:点击按钮会将表单中的所有字段重置为初始值
* * `button`:此类型的按钮没有默认行为
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
type: /*此按钮将表单数据提交给服务器*/ 'submit' | /*此按钮重置所有组件为初始值*/ 'reset' | /*此按钮没有默认行为*/ 'button';
/**
* 关联的 `<form>` 元素。此属性值应为同一页面中的一个 `<form>` 元素的 `id`。
*
* 如果未指定此属性,则该元素必须是 `<form>` 元素的子元素。通过此属性,你可以将元素放置在页面的任何位置,而不仅仅是 `<form>` 元素的子元素。
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
form?: string;
/**
* 指定提交表单的 URL。
*
* 如果指定了此属性,将覆盖 `<form>` 元素的 `action` 属性。
*
* **Note**:仅在未指定 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formAction?: string;
/**
* 指定提交表单到服务器的内容类型。可选值包括:
*
* * `application/x-www-form-urlencoded`:未指定该属性时的默认值
* * `multipart/form-data`:当表单包含 `<input type="file">` 元素时使用
* * `text/plain`HTML5 新增,用于调试
*
* 如果指定了此属性,将覆盖 `<form>` 元素的 `enctype` 属性。
*
* **Note**:仅在未指定 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formEnctype?: /*未指定该属性时的默认值*/ 'application/x-www-form-urlencoded' | /*当表单包含 `<input type="file">` 元素时使用*/ 'multipart/form-data' | /*HTML5 新增,用于调试*/ 'text/plain';
/**
* 指定提交表单时使用的 HTTP 方法。可选值包括:
*
* * `post`:表单数据包含在表单内容中,发送到服务器
* * `get`:表单数据以 `?` 作为分隔符附加到表单的 URI 属性中,生成的 URI 发送到服务器。当表单没有副作用,并且仅包含 ASCII 字符时,使用此方法
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `method` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formMethod?: /*表单数据包含在表单内容中,发送到服务器*/ 'post' | /*表单数据以 `?` 作为分隔符附加到表单的 URI 属性中,生成的 URI 发送到服务器。当表单没有副作用,并且仅包含 ASCII 字符时,使用此方法*/ 'get';
/**
* 如果设置了此属性,表单提交时将不执行表单验证。
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `novalidate` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formNoValidate: boolean;
/**
* 提交表单后接收到的响应应显示在何处。可选值包括:
*
* * `_self`:默认选项,在当前框架中打开
* * `_blank`:在新窗口中打开
* * `_parent`:在父框架中打开
* * `_top`:在整个窗口中打开
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `target` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formTarget?: /*默认选项,在当前框架中打开*/ '_self' | /*在新窗口中打开*/ '_blank' | /*在父框架中打开*/ '_parent' | /*在整个窗口中打开*/ '_top';
private readonly formController;
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity(): ValidityState | undefined;
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage(): string | undefined;
protected get rippleDisabled(): boolean;
protected get focusElement(): HTMLElement | null;
protected get focusDisabled(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity(): boolean;
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message: string): void;
protected firstUpdated(_changedProperties: PropertyValues): void;
protected renderLoading(): TemplateResult;
protected renderButton({ id, className, part, content, }: RenderButtonOptions): TemplateResult;
protected isButton(): boolean;
}
export {};

View File

@@ -0,0 +1,209 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import cc from 'classcat';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { FormController } from '@mdui/shared/controllers/form.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { AnchorMixin } from '@mdui/shared/mixins/anchor.js';
import { FocusableMixin } from '@mdui/shared/mixins/focusable.js';
import '../circular-progress.js';
import { RippleMixin } from '../ripple/ripple-mixin.js';
import { buttonBaseStyle } from './button-base-style.js';
export class ButtonBase extends AnchorMixin(RippleMixin(FocusableMixin(MduiElement))) {
constructor() {
super(...arguments);
/**
* 是否禁用
*/
this.disabled = false;
/**
* 是否处于加载中状态
*/
this.loading = false;
/**
* 按钮的名称,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
this.name = '';
/**
* 按钮的初始值,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
this.value = '';
/**
* 按钮的类型。默认类型为 `button`。可选类型包括:
*
* * `submit`:点击按钮会提交表单数据到服务器
* * `reset`:点击按钮会将表单中的所有字段重置为初始值
* * `button`:此类型的按钮没有默认行为
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
this.type = 'button';
/**
* 如果设置了此属性,表单提交时将不执行表单验证。
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `novalidate` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
this.formNoValidate = false;
this.formController = new FormController(this);
}
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity() {
if (this.isButton()) {
return this.focusElement.validity;
}
}
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage() {
if (this.isButton()) {
return this.focusElement.validationMessage;
}
}
get rippleDisabled() {
return this.disabled || this.loading;
}
get focusElement() {
return this.isButton()
? this.renderRoot?.querySelector('._button')
: !this.focusDisabled
? this.renderRoot?.querySelector('._a')
: this;
}
get focusDisabled() {
return this.disabled || this.loading;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity() {
if (this.isButton()) {
const valid = this.focusElement.checkValidity();
if (!valid) {
// @ts-ignore
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
}
return valid;
}
return true;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity() {
if (this.isButton()) {
const invalid = !this.focusElement.reportValidity();
if (invalid) {
// @ts-ignore
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
// todo 考虑是否要支持 preventDefault() 方法,当前 invalid 状态没有样式
}
return !invalid;
}
return true;
}
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message) {
if (this.isButton()) {
this.focusElement.setCustomValidity(message);
}
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
this.addEventListener('click', () => {
if (this.type === 'submit') {
this.formController.submit(this);
}
if (this.type === 'reset') {
this.formController.reset(this);
}
});
}
renderLoading() {
return this.loading
? html `<mdui-circular-progress part="loading"></mdui-circular-progress>`
: nothingTemplate;
}
renderButton({ id, className, part, content = html `<slot></slot>`, }) {
return html `<button id="${ifDefined(id)}" class="${cc(['_button', className])}" part="${ifDefined(part)}" ?disabled="${this.rippleDisabled || this.focusDisabled}">${content}</button>`;
}
isButton() {
return !this.href;
}
}
ButtonBase.styles = [
componentStyle,
buttonBaseStyle,
];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonBase.prototype, "disabled", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonBase.prototype, "loading", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "name", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "value", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "type", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "form", void 0);
__decorate([
property({ reflect: true, attribute: 'formaction' })
], ButtonBase.prototype, "formAction", void 0);
__decorate([
property({ reflect: true, attribute: 'formenctype' })
], ButtonBase.prototype, "formEnctype", void 0);
__decorate([
property({ reflect: true, attribute: 'formmethod' })
], ButtonBase.prototype, "formMethod", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'formnovalidate',
})
], ButtonBase.prototype, "formNoValidate", void 0);
__decorate([
property({ reflect: true, attribute: 'formtarget' })
], ButtonBase.prototype, "formTarget", void 0);

View File

@@ -0,0 +1,69 @@
import '../icon.js';
import { ButtonBase } from './button-base.js';
import type { Ripple } from '../ripple/index.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
/**
* @summary 按钮组件
*
* ```html
* <mdui-button>Button</mdui-button>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 按钮的文本
* @slot icon - 按钮左侧的元素
* @slot end-icon - 按钮右侧的元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 按钮的文本
* @csspart icon - 按钮左侧的图标
* @csspart end-icon - 按钮右侧的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Button extends ButtonBase<ButtonEventMap> {
static styles: CSSResultGroup;
/**
* 按钮的形状。可选值包括:
*
* * `elevated`:带阴影的按钮,适用于需要将按钮与背景视觉分离的场景
* * `filled`:视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”
* * `outlined`:带边框的按钮,适用于中等优先级,且次要的操作,如“返回”
* * `text`:文本按钮,适用于最低优先级的操作
*/
variant: /*带阴影的按钮,适用于需要将按钮与背景视觉分离的场景*/ 'elevated' | /*视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等*/ 'filled' | /*视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”*/ 'tonal' | /*带边框的按钮,适用于中等优先级,且次要的操作,如“返回”*/ 'outlined' | /*文本按钮,适用于最低优先级的操作*/ 'text';
/**
* 是否填满父元素宽度
*/
fullWidth: boolean;
/**
* 左侧的 Material Icons 图标名。也可以通过 `slot="icon"` 设置
*/
icon?: string;
/**
* 右侧的 Material Icons 图标名。也可以通过 `slot="end-icon"` 设置
*/
endIcon?: string;
private readonly rippleRef;
protected get rippleElement(): Ripple;
protected render(): TemplateResult;
private renderIcon;
private renderLabel;
private renderEndIcon;
private renderInner;
}
export interface ButtonEventMap {
focus: FocusEvent;
blur: FocusEvent;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-button': Button;
}
}

View File

@@ -0,0 +1,111 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import '../icon.js';
import { ButtonBase } from './button-base.js';
import { style } from './style.js';
/**
* @summary 按钮组件
*
* ```html
* <mdui-button>Button</mdui-button>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 按钮的文本
* @slot icon - 按钮左侧的元素
* @slot end-icon - 按钮右侧的元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 按钮的文本
* @csspart icon - 按钮左侧的图标
* @csspart end-icon - 按钮右侧的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Button = class Button extends ButtonBase {
constructor() {
super(...arguments);
/**
* 按钮的形状。可选值包括:
*
* * `elevated`:带阴影的按钮,适用于需要将按钮与背景视觉分离的场景
* * `filled`:视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”
* * `outlined`:带边框的按钮,适用于中等优先级,且次要的操作,如“返回”
* * `text`:文本按钮,适用于最低优先级的操作
*/
this.variant = 'filled';
/**
* 是否填满父元素宽度
*/
this.fullWidth = false;
this.rippleRef = createRef();
}
get rippleElement() {
return this.rippleRef.value;
}
render() {
return html `<mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.isButton()
? this.renderButton({
className: 'button',
part: 'button',
content: this.renderInner(),
})
: this.disabled || this.loading
? html `<span part="button" class="button _a">${this.renderInner()}</span>`
: this.renderAnchor({
className: 'button',
part: 'button',
content: this.renderInner(),
})}`;
}
renderIcon() {
if (this.loading) {
return this.renderLoading();
}
return html `<slot name="icon" part="icon" class="icon">${this.icon
? html `<mdui-icon name="${this.icon}"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderLabel() {
return html `<slot part="label" class="label"></slot>`;
}
renderEndIcon() {
return html `<slot name="end-icon" part="end-icon" class="end-icon">${this.endIcon
? html `<mdui-icon name="${this.endIcon}"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderInner() {
return [this.renderIcon(), this.renderLabel(), this.renderEndIcon()];
}
};
Button.styles = [ButtonBase.styles, style];
__decorate([
property({ reflect: true })
], Button.prototype, "variant", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'full-width',
})
], Button.prototype, "fullWidth", void 0);
__decorate([
property({ reflect: true })
], Button.prototype, "icon", void 0);
__decorate([
property({ reflect: true, attribute: 'end-icon' })
], Button.prototype, "endIcon", void 0);
Button = __decorate([
customElement('mdui-button')
], Button);
export { Button };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,4 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-full);position:relative;display:inline-block;flex-shrink:0;overflow:hidden;text-align:center;border-radius:var(--shape-corner);cursor:pointer;-webkit-tap-highlight-color:transparent;transition:box-shadow var(--mdui-motion-duration-short4) var(--mdui-motion-easing-linear);min-width:3rem;height:2.5rem;color:rgb(var(--mdui-color-primary));font-size:var(--mdui-typescale-label-large-size);font-weight:var(--mdui-typescale-label-large-weight);letter-spacing:var(--mdui-typescale-label-large-tracking);line-height:var(--mdui-typescale-label-large-line-height)}.button{width:100%;padding:0 1rem}:host([full-width]:not([full-width=false i])){display:block}:host([variant=elevated]){box-shadow:var(--mdui-elevation-level1);background-color:rgb(var(--mdui-color-surface-container-low));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=filled]){color:rgb(var(--mdui-color-on-primary));background-color:rgb(var(--mdui-color-primary));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-primary)}:host([variant=tonal]){color:rgb(var(--mdui-color-on-secondary-container));background-color:rgb(var(--mdui-color-secondary-container));--mdui-comp-ripple-state-layer-color:var(
--mdui-color-on-secondary-container
)}:host([variant=outlined]){border:.0625rem solid rgb(var(--mdui-color-outline));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=text]){--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=outlined][focus-visible]){border-color:rgb(var(--mdui-color-primary))}:host([variant=elevated][hover]){box-shadow:var(--mdui-elevation-level2)}:host([variant=filled][hover]),:host([variant=tonal][hover]){box-shadow:var(--mdui-elevation-level1)}:host([disabled]:not([disabled=false i])),:host([loading]:not([loading=false i])){cursor:default;pointer-events:none}:host([disabled]:not([disabled=false i])){color:rgba(var(--mdui-color-on-surface),38%);box-shadow:var(--mdui-elevation-level0)}:host([variant=elevated][disabled]:not([disabled=false i])),:host([variant=filled][disabled]:not([disabled=false i])),:host([variant=tonal][disabled]:not([disabled=false i])){background-color:rgba(var(--mdui-color-on-surface),12%)}:host([variant=outlined][disabled]:not([disabled=false i])){border-color:rgba(var(--mdui-color-on-surface),12%)}.label{display:inline-flex;padding-right:.5rem;padding-left:.5rem}.end-icon,.icon{display:inline-flex;font-size:1.28571429em}.end-icon mdui-icon,.icon mdui-icon,::slotted([slot=end-icon]),::slotted([slot=icon]){font-size:inherit}mdui-circular-progress{display:inline-flex;width:1.125rem;height:1.125rem}:host([variant=filled]) mdui-circular-progress{stroke:rgb(var(--mdui-color-on-primary))}:host([variant=tonal]) mdui-circular-progress{stroke:rgb(var(--mdui-color-on-secondary-container))}:host([disabled]:not([disabled=false i])) mdui-circular-progress{stroke:rgba(var(--mdui-color-on-surface),38%)}`;

View File

@@ -0,0 +1 @@
export * from './card/index.js';

View File

@@ -0,0 +1 @@
export * from './card/index.js';

View File

@@ -0,0 +1,53 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { Ripple } from '../ripple/index.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
declare const Card_base: import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/anchor.js").AnchorMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("../ripple/ripple-mixin.js").RippleMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/focusable.js").FocusableMixinInterface> & typeof MduiElement;
/**
* @summary 卡片组件
*
* ```html
* <mdui-card>card content</mdui-card>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
*
* @slot - 卡片的内容
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Card extends Card_base<CardEventMap> {
static styles: CSSResultGroup;
/**
* 卡片的形状。可选值包括:
*
* * `elevated`:带阴影的卡片,与背景的视觉分离度较高
* * `filled`:带填充色的卡片,与背景的视觉分离度较低
* * `outlined`:带边框的卡片,与背景的视觉分离度最高
*/
variant: /*带阴影的卡片,与背景的视觉分离度较高*/ 'elevated' | /*带填充色的卡片,与背景的视觉分离度较低*/ 'filled' | /*带边框的卡片,与背景的视觉分离度最高*/ 'outlined';
/**
* 是否可点击。为 `true` 时,卡片将具有鼠标悬浮效果和点击涟漪效果
*/
clickable: boolean;
/**
* 是否禁用
*/
disabled: boolean;
private readonly rippleRef;
protected get rippleElement(): Ripple;
protected get rippleDisabled(): boolean;
protected get focusElement(): HTMLElement | null;
protected get focusDisabled(): boolean;
protected render(): TemplateResult;
}
export interface CardEventMap {
focus: FocusEvent;
blur: FocusEvent;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-card': Card;
}
}
export {};

View File

@@ -0,0 +1,91 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { AnchorMixin } from '@mdui/shared/mixins/anchor.js';
import { FocusableMixin } from '@mdui/shared/mixins/focusable.js';
import { RippleMixin } from '../ripple/ripple-mixin.js';
import { style } from './style.js';
/**
* @summary 卡片组件
*
* ```html
* <mdui-card>card content</mdui-card>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
*
* @slot - 卡片的内容
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Card = class Card extends AnchorMixin(RippleMixin(FocusableMixin(MduiElement))) {
constructor() {
super(...arguments);
/**
* 卡片的形状。可选值包括:
*
* * `elevated`:带阴影的卡片,与背景的视觉分离度较高
* * `filled`:带填充色的卡片,与背景的视觉分离度较低
* * `outlined`:带边框的卡片,与背景的视觉分离度最高
*/
this.variant = 'elevated';
/**
* 是否可点击。为 `true` 时,卡片将具有鼠标悬浮效果和点击涟漪效果
*/
this.clickable = false;
/**
* 是否禁用
*/
this.disabled = false;
this.rippleRef = createRef();
}
get rippleElement() {
return this.rippleRef.value;
}
get rippleDisabled() {
return this.disabled || (!this.href && !this.clickable);
}
get focusElement() {
return this.href && !this.disabled
? this.renderRoot.querySelector('._a')
: this;
}
get focusDisabled() {
return this.rippleDisabled;
}
render() {
return html `<mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.href && !this.disabled
? this.renderAnchor({
className: 'link',
content: html `<slot></slot>`,
})
: html `<slot></slot>`}`;
}
};
Card.styles = [componentStyle, style];
__decorate([
property({ reflect: true })
], Card.prototype, "variant", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Card.prototype, "clickable", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Card.prototype, "disabled", void 0);
Card = __decorate([
customElement('mdui-card')
], Card);
export { Card };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-medium);position:relative;display:inline-block;overflow:hidden;border-radius:var(--shape-corner);-webkit-tap-highlight-color:transparent;transition:box-shadow var(--mdui-motion-duration-short4) var(--mdui-motion-easing-linear);--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}:host([clickable]:not([clickable=false i])){cursor:pointer}:host([variant=elevated]){background-color:rgb(var(--mdui-color-surface-container-low));box-shadow:var(--mdui-elevation-level1)}:host([variant=filled]){background-color:rgb(var(--mdui-color-surface-container-highest))}:host([variant=outlined]){background-color:rgb(var(--mdui-color-surface));border:.0625rem solid rgb(var(--mdui-color-outline))}:host([variant=elevated][hover]){box-shadow:var(--mdui-elevation-level2)}:host([variant=filled][hover]),:host([variant=outlined][hover]){box-shadow:var(--mdui-elevation-level1)}:host([variant=elevated][dragged]),:host([variant=filled][dragged]),:host([variant=outlined][dragged]){box-shadow:var(--mdui-elevation-level3)}:host([disabled]:not([disabled=false i])){opacity:.38;cursor:default;-webkit-user-select:none;user-select:none}:host([variant=elevated][disabled]:not([disabled=false i])){background-color:rgb(var(--mdui-color-surface-variant));box-shadow:var(--mdui-elevation-level0)}:host([variant=filled][disabled]:not([disabled=false i])){background-color:rgb(var(--mdui-color-surface));box-shadow:var(--mdui-elevation-level1)}:host([variant=outlined][disabled]:not([disabled=false i])){box-shadow:var(--mdui-elevation-level0);border-color:rgba(var(--mdui-color-outline),.32)}.link{position:relative;display:inline-block;width:100%;height:100%;color:inherit;font-size:inherit;letter-spacing:inherit;text-decoration:none;touch-action:manipulation;-webkit-user-drag:none}`;

View File

@@ -0,0 +1 @@
export * from './checkbox/index.js';

View File

@@ -0,0 +1 @@
export * from './checkbox/index.js';

View File

@@ -0,0 +1,137 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import '@mdui/shared/icons/check-box-outline-blank.js';
import '@mdui/shared/icons/check-box.js';
import '@mdui/shared/icons/indeterminate-check-box.js';
import '../icon.js';
import type { Ripple } from '../ripple/index.js';
import type { FormControl } from '@mdui/jq/shared/form.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
declare const Checkbox_base: import("@lit/reactive-element/decorators/base.js").Constructor<import("../ripple/ripple-mixin.js").RippleMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/focusable.js").FocusableMixinInterface> & typeof MduiElement;
/**
* @summary 复选框组件
*
* ```html
* <mdui-checkbox>Checkbox</mdui-checkbox>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 选中状态变更时触发
* @event input - 选中状态变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 复选框文本
* @slot unchecked-icon - 未选中状态的图标
* @slot checked-icon - 选中状态的图标
* @slot indeterminate-icon - 不确定状态的图标
*
* @csspart control - 左侧图标容器
* @csspart unchecked-icon - 未选中状态的图标
* @csspart checked-icon - 选中状态的图标
* @csspart indeterminate-icon - 不确定状态的图标
* @csspart label - 复选框文本
*/
export declare class Checkbox extends Checkbox_base<CheckboxEventMap> implements FormControl {
static styles: CSSResultGroup;
/**
* 是否为禁用状态
*/
disabled: boolean;
/**
* 是否为选中状态
*/
checked: boolean;
/**
* 默认选中状态。在重置表单时,将恢复为此状态。此属性只能通过 JavaScript 属性设置
*/
defaultChecked: boolean;
/**
* 是否处于不确定状态
*/
indeterminate: boolean;
/**
* 提交表单时,是否必须选中此复选框
*/
required: boolean;
/**
* 关联的 `<form>` 元素。此属性值应为同一页面中的一个 `<form>` 元素的 `id`。
*
* 如果未指定此属性,则该元素必须是 `<form>` 元素的子元素。通过此属性,你可以将元素放置在页面的任何位置,而不仅仅是 `<form>` 元素的子元素。
*/
form?: string;
/**
* 复选框名称,将与表单数据一起提交
*/
name: string;
/**
* 复选框的值,将于表单数据一起提交
*/
value: string;
/**
* 未选中状态的 Material Icons 图标名。也可以通过 `slot="unchecked-icon"` 设置
*/
uncheckedIcon?: string;
/**
* 选中状态的 Material Icons 图标名。也可以通过 `slot="checked-icon"` 设置
*/
checkedIcon?: string;
/**
* 不确定状态的 Material Icons 图标名。也可以通过 `slot="indeterminate-icon"` 设置
*/
indeterminateIcon?: string;
/**
* 是否验证未通过
*/
private invalid;
private readonly inputRef;
private readonly rippleRef;
private readonly formController;
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity(): ValidityState;
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage(): string;
protected get rippleElement(): Ripple;
protected get rippleDisabled(): boolean;
protected get focusElement(): HTMLElement | undefined;
protected get focusDisabled(): boolean;
private onDisabledChange;
private onCheckedChange;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity(): boolean;
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message: string): void;
protected render(): TemplateResult;
/**
* input[type="checkbox"] 的 change 事件无法冒泡越过 shadow dom
*/
private onChange;
}
export interface CheckboxEventMap {
focus: FocusEvent;
blur: FocusEvent;
change: CustomEvent<void>;
input: Event;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-checkbox': Checkbox;
}
}
export {};

View File

@@ -0,0 +1,254 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { FormController, formResets } from '@mdui/shared/controllers/form.js';
import { defaultValue } from '@mdui/shared/decorators/default-value.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import '@mdui/shared/icons/check-box-outline-blank.js';
import '@mdui/shared/icons/check-box.js';
import '@mdui/shared/icons/indeterminate-check-box.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { FocusableMixin } from '@mdui/shared/mixins/focusable.js';
import '../icon.js';
import { RippleMixin } from '../ripple/ripple-mixin.js';
import { style } from './style.js';
/**
* @summary 复选框组件
*
* ```html
* <mdui-checkbox>Checkbox</mdui-checkbox>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 选中状态变更时触发
* @event input - 选中状态变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 复选框文本
* @slot unchecked-icon - 未选中状态的图标
* @slot checked-icon - 选中状态的图标
* @slot indeterminate-icon - 不确定状态的图标
*
* @csspart control - 左侧图标容器
* @csspart unchecked-icon - 未选中状态的图标
* @csspart checked-icon - 选中状态的图标
* @csspart indeterminate-icon - 不确定状态的图标
* @csspart label - 复选框文本
*/
let Checkbox = class Checkbox extends RippleMixin(FocusableMixin(MduiElement)) {
constructor() {
super(...arguments);
/**
* 是否为禁用状态
*/
this.disabled = false;
/**
* 是否为选中状态
*/
this.checked = false;
/**
* 默认选中状态。在重置表单时,将恢复为此状态。此属性只能通过 JavaScript 属性设置
*/
this.defaultChecked = false;
/**
* 是否处于不确定状态
*/
this.indeterminate = false;
/**
* 提交表单时,是否必须选中此复选框
*/
this.required = false;
/**
* 复选框名称,将与表单数据一起提交
*/
this.name = '';
/**
* 复选框的值,将于表单数据一起提交
*/
this.value = 'on';
/**
* 是否验证未通过
*/
this.invalid = false;
this.inputRef = createRef();
this.rippleRef = createRef();
this.formController = new FormController(this, {
value: (control) => (control.checked ? control.value : undefined),
defaultValue: (control) => control.defaultChecked,
setValue: (control, checked) => (control.checked = checked),
});
}
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity() {
return this.inputRef.value.validity;
}
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage() {
return this.inputRef.value.validationMessage;
}
get rippleElement() {
return this.rippleRef.value;
}
get rippleDisabled() {
return this.disabled;
}
get focusElement() {
return this.inputRef.value;
}
get focusDisabled() {
return this.disabled;
}
async onDisabledChange() {
await this.updateComplete;
this.invalid = !this.inputRef.value.checkValidity();
}
async onCheckedChange() {
await this.updateComplete;
// reset 引起的值变更,不执行验证;直接修改值引起的变更,需要进行验证
const form = this.formController.getForm();
if (form && formResets.get(form)?.has(this)) {
this.invalid = false;
formResets.get(form).delete(this);
}
else {
this.invalid = !this.inputRef.value.checkValidity();
}
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity() {
const valid = this.inputRef.value.checkValidity();
if (!valid) {
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
}
return valid;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity() {
this.invalid = !this.inputRef.value.reportValidity();
if (this.invalid) {
const eventProceeded = this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
// 调用了 preventDefault() 时,隐藏默认的表单错误提示
if (!eventProceeded) {
this.blur();
this.focus();
}
}
return !this.invalid;
}
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message) {
this.inputRef.value.setCustomValidity(message);
this.invalid = !this.inputRef.value.checkValidity();
}
render() {
return html `<label class="${classMap({ invalid: this.invalid })}"><input ${ref(this.inputRef)} type="checkbox" name="${ifDefined(this.name)}" value="${ifDefined(this.value)}" .indeterminate="${live(this.indeterminate)}" .disabled="${this.disabled}" .checked="${live(this.checked)}" .required="${this.required}" @change="${this.onChange}"> <i part="control"><mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple><slot name="unchecked-icon" part="unchecked-icon" class="icon unchecked-icon">${this.uncheckedIcon
? html `<mdui-icon name="${this.uncheckedIcon}" class="i"></mdui-icon>`
: html `<mdui-icon-check-box-outline-blank class="i"></mdui-icon-check-box-outline-blank>`}</slot><slot name="checked-icon" part="checked-icon" class="icon checked-icon">${this.checkedIcon
? html `<mdui-icon name="${this.checkedIcon}" class="i"></mdui-icon>`
: html `<mdui-icon-check-box class="i"></mdui-icon-check-box>`}</slot><slot name="indeterminate-icon" part="indeterminate-icon" class="icon indeterminate-icon">${this.indeterminateIcon
? html `<mdui-icon name="${this.indeterminateIcon}" class="i"></mdui-icon>`
: html `<mdui-icon-indeterminate-check-box class="i"></mdui-icon-indeterminate-check-box>`}</slot></i><slot part="label" class="label"></slot></label>`;
}
/**
* input[type="checkbox"] 的 change 事件无法冒泡越过 shadow dom
*/
onChange() {
this.checked = this.inputRef.value.checked;
this.indeterminate = false;
this.emit('change');
}
};
Checkbox.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Checkbox.prototype, "disabled", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Checkbox.prototype, "checked", void 0);
__decorate([
defaultValue('checked')
], Checkbox.prototype, "defaultChecked", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Checkbox.prototype, "indeterminate", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Checkbox.prototype, "required", void 0);
__decorate([
property({ reflect: true })
], Checkbox.prototype, "form", void 0);
__decorate([
property({ reflect: true })
], Checkbox.prototype, "name", void 0);
__decorate([
property({ reflect: true })
], Checkbox.prototype, "value", void 0);
__decorate([
property({ reflect: true, attribute: 'unchecked-icon' })
], Checkbox.prototype, "uncheckedIcon", void 0);
__decorate([
property({ reflect: true, attribute: 'checked-icon' })
], Checkbox.prototype, "checkedIcon", void 0);
__decorate([
property({ reflect: true, attribute: 'indeterminate-icon' })
], Checkbox.prototype, "indeterminateIcon", void 0);
__decorate([
state()
], Checkbox.prototype, "invalid", void 0);
__decorate([
watch('disabled', true),
watch('indeterminate', true),
watch('required', true)
], Checkbox.prototype, "onDisabledChange", null);
__decorate([
watch('checked', true)
], Checkbox.prototype, "onCheckedChange", null);
Checkbox = __decorate([
customElement('mdui-checkbox')
], Checkbox);
export { Checkbox };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{position:relative;display:inline-flex;cursor:pointer;-webkit-tap-highlight-color:transparent;border-radius:.125rem;font-size:var(--mdui-typescale-label-large-size);font-weight:var(--mdui-typescale-label-large-weight);letter-spacing:var(--mdui-typescale-label-large-tracking);line-height:var(--mdui-typescale-label-large-line-height)}label{display:inline-flex;align-items:center;width:100%;cursor:inherit;-webkit-user-select:none;user-select:none;touch-action:manipulation;zoom:1;-webkit-user-drag:none}input{position:absolute;padding:0;opacity:0;pointer-events:none;width:1.125rem;height:1.125rem;margin:0 0 0 .6875rem}.icon{display:flex;position:absolute;opacity:1;transform:scale(1);color:rgb(var(--mdui-color-on-surface));font-size:1.5rem;transition:color var(--mdui-motion-duration-short4) var(--mdui-motion-easing-standard)}.checked-icon,.indeterminate-icon{opacity:0;transform:scale(.5);transition-property:color,opacity,transform;transition-duration:var(--mdui-motion-duration-short4);transition-timing-function:var(--mdui-motion-easing-standard)}.icon .i,::slotted([slot=checked-icon]),::slotted([slot=indeterminate-icon]),::slotted([slot=unchecked-icon]){color:inherit;font-size:inherit}i{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;border-radius:50%;width:2.5rem;height:2.5rem;--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}.label{display:flex;width:100%;padding-top:.625rem;padding-bottom:.625rem;color:rgb(var(--mdui-color-on-surface));transition:color var(--mdui-motion-duration-short4) var(--mdui-motion-easing-standard)}:host([checked]:not([checked=false i])) i{--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([checked]:not([checked=false i])) .icon{color:rgb(var(--mdui-color-primary))}:host([checked]:not([checked=false i])) .indeterminate-icon{opacity:0;transform:scale(.5)}:host([checked]:not([checked=false i])) .checked-icon{opacity:1;transform:scale(1)}:host([indeterminate]:not([indeterminate=false i])) i{--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([indeterminate]:not([indeterminate=false i])) .icon{color:rgb(var(--mdui-color-primary))}:host([indeterminate]:not([indeterminate=false i])) .checked-icon{opacity:0;transform:scale(.5)}:host([indeterminate]:not([indeterminate=false i])) .indeterminate-icon{opacity:1;transform:scale(1)}.invalid i{--mdui-comp-ripple-state-layer-color:var(--mdui-color-error)}.invalid .icon{color:rgb(var(--mdui-color-error))}.invalid .label{color:rgb(var(--mdui-color-error))}:host([disabled]:not([disabled=false i])){cursor:default;pointer-events:none}:host([disabled]:not([disabled=false i])) .icon{color:rgba(var(--mdui-color-on-surface),38%)}:host([disabled]:not([disabled=false i])) .label{color:rgba(var(--mdui-color-on-surface),38%)}:host([disabled][checked]:not([disabled=false i],[checked=false i])) .unchecked-icon,:host([disabled][indeterminate]:not([disabled=false i],[indeterminate=false i])) .unchecked-icon{opacity:0}`;

View File

@@ -0,0 +1 @@
export * from './chip/index.js';

View File

@@ -0,0 +1 @@
export * from './chip/index.js';

View File

@@ -0,0 +1,109 @@
import '@mdui/shared/icons/check.js';
import '@mdui/shared/icons/clear.js';
import { ButtonBase } from '../button/button-base.js';
import '../icon.js';
import type { Ripple } from '../ripple/index.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
/**
* @summary 纸片组件
*
* ```html
* <mdui-chip>Chip</mdui-chip>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
* @event change - 选中状态变更时触发
* @event delete - 点击删除图标时触发
*
* @slot - 纸片文本
* @slot icon - 左侧元素
* @slot end-icon - 右侧元素
* @slot selected-icon - 选中状态下的左侧元素
* @slot delete-icon - 可删除时的右侧删除元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 纸片文本
* @csspart icon - 左侧图标
* @csspart end-icon - 右侧图标
* @csspart selected-icon - 选中状态下的左侧图标
* @csspart delete-icon - 可删除时的右侧删除图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Chip extends ButtonBase<ChipEventMap> {
static styles: CSSResultGroup;
/**
* 纸片的形状。可选值包括:
*
* * `assist`:用于显示与当前上下文相关的辅助操作,如在点餐页面提供分享、收藏等功能
* * `filter`:用于对内容进行筛选,如在搜索结果页过滤搜索结果
* * `input`:用于表示用户输入的信息片段,如在 Gmail 的“收件人”字段中的联系人
* * `suggestion`:用于提供动态生成的推荐信息,以简化用户操作,如在聊天应用中预测用户可能想发送的信息
*/
variant: /*用于显示与当前上下文相关的辅助操作,如在点餐页面提供分享、收藏等功能*/ 'assist' | /*用于对内容进行筛选,如在搜索结果页过滤搜索结果*/ 'filter' | /*用于表示用户输入的信息片段,如在 Gmail 的“收件人”字段中的联系人*/ 'input' | /*用于提供动态生成的推荐信息,以简化用户操作,如在聊天应用中预测用户可能想发送的信息*/ 'suggestion';
/**
* 是否显示阴影
*/
elevated: boolean;
/**
* 是否可选中
*/
selectable: boolean;
/**
* 是否已选中
*/
selected: boolean;
/**
* 是否可删除。为 `true` 时,纸片右侧会显示删除图标
*/
deletable: boolean;
/**
* 左侧的 Material Icons 图标名。也可以通过 `slot="icon"` 设置
*/
icon?: string;
/**
* 选中状态下左侧的 Material Icons 图标名。也可以通过 `slot="selected-icon"` 设置
*/
selectedIcon?: string;
/**
* 右侧的 Material Icons 图标名。也可以通过 `slot="end-icon"` 设置
*/
endIcon?: string;
/**
* 可删除时,右侧删除图标的 Material Icons 图标名。也可以通过 `slot="delete-icon"` 设置
*/
deleteIcon?: string;
private readonly rippleRef;
private readonly hasSlotController;
constructor();
protected get rippleElement(): Ripple;
private onSelectedChange;
protected firstUpdated(changedProperties: PropertyValues): void;
protected render(): TemplateResult;
private onClick;
private onKeyDown;
/**
* 点击删除按钮
*/
private onDelete;
private renderIcon;
private renderLabel;
private renderEndIcon;
private renderDeleteIcon;
private renderInner;
}
export interface ChipEventMap {
focus: FocusEvent;
blur: FocusEvent;
invalid: CustomEvent<void>;
change: CustomEvent<void>;
delete: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-chip': Chip;
}
}

View File

@@ -0,0 +1,243 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import cc from 'classcat';
import { HasSlotController } from '@mdui/shared/controllers/has-slot.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import '@mdui/shared/icons/check.js';
import '@mdui/shared/icons/clear.js';
import { ButtonBase } from '../button/button-base.js';
import '../icon.js';
import { style } from './style.js';
/**
* @summary 纸片组件
*
* ```html
* <mdui-chip>Chip</mdui-chip>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
* @event change - 选中状态变更时触发
* @event delete - 点击删除图标时触发
*
* @slot - 纸片文本
* @slot icon - 左侧元素
* @slot end-icon - 右侧元素
* @slot selected-icon - 选中状态下的左侧元素
* @slot delete-icon - 可删除时的右侧删除元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 纸片文本
* @csspart icon - 左侧图标
* @csspart end-icon - 右侧图标
* @csspart selected-icon - 选中状态下的左侧图标
* @csspart delete-icon - 可删除时的右侧删除图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Chip = class Chip extends ButtonBase {
constructor() {
super();
/**
* 纸片的形状。可选值包括:
*
* * `assist`:用于显示与当前上下文相关的辅助操作,如在点餐页面提供分享、收藏等功能
* * `filter`:用于对内容进行筛选,如在搜索结果页过滤搜索结果
* * `input`:用于表示用户输入的信息片段,如在 Gmail 的“收件人”字段中的联系人
* * `suggestion`:用于提供动态生成的推荐信息,以简化用户操作,如在聊天应用中预测用户可能想发送的信息
*/
this.variant = 'assist';
/**
* 是否显示阴影
*/
this.elevated = false;
/**
* 是否可选中
*/
this.selectable = false;
/**
* 是否已选中
*/
this.selected = false;
/**
* 是否可删除。为 `true` 时,纸片右侧会显示删除图标
*/
this.deletable = false;
this.rippleRef = createRef();
this.hasSlotController = new HasSlotController(this, 'icon', 'selected-icon', 'end-icon');
this.onClick = this.onClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
get rippleElement() {
return this.rippleRef.value;
}
onSelectedChange() {
this.emit('change');
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.addEventListener('click', this.onClick);
this.addEventListener('keydown', this.onKeyDown);
}
render() {
const hasIcon = this.icon || this.hasSlotController.test('icon');
const hasEndIcon = this.endIcon || this.hasSlotController.test('end-icon');
const hasSelectedIcon = this.selectedIcon ||
['assist', 'filter'].includes(this.variant) ||
hasIcon ||
this.hasSlotController.test('selected-icon');
const className = cc({
button: true,
'has-icon': this.loading ||
(!this.selected && hasIcon) ||
(this.selected && hasSelectedIcon),
'has-end-icon': hasEndIcon,
});
return html `<mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.isButton()
? this.renderButton({
className,
part: 'button',
content: this.renderInner(),
})
: this.disabled || this.loading
? html `<span part="button" class="${className} _a">${this.renderInner()}</span>`
: this.renderAnchor({
className,
part: 'button',
content: this.renderInner(),
})}`;
}
onClick() {
if (this.disabled || this.loading) {
return;
}
// 点击时,切换选中状态
if (this.selectable) {
this.selected = !this.selected;
}
}
onKeyDown(event) {
if (this.disabled || this.loading) {
return;
}
// 按下空格键时,切换选中状态
if (this.selectable && event.key === ' ') {
event.preventDefault();
this.selected = !this.selected;
}
// 按下 Delete 或 BackSpace 键时,触发 delete 事件
if (this.deletable && ['Delete', 'Backspace'].includes(event.key)) {
this.emit('delete');
}
}
/**
* 点击删除按钮
*/
onDelete(event) {
event.stopPropagation();
this.emit('delete');
}
renderIcon() {
if (this.loading) {
return this.renderLoading();
}
const icon = () => {
return this.icon
? html `<mdui-icon name="${this.icon}" class="i"></mdui-icon>`
: nothingTemplate;
};
const selectedIcon = () => {
if (this.selectedIcon) {
return html `<mdui-icon name="${this.selectedIcon}" class="i"></mdui-icon>`;
}
if (this.variant === 'assist' || this.variant === 'filter') {
return html `<mdui-icon-check class="i"></mdui-icon-check>`;
}
return icon();
};
return !this.selected
? html `<slot name="icon" part="icon" class="icon">${icon()}</slot>`
: html `<slot name="selected-icon" part="selected-icon" class="selected-icon">${selectedIcon()}</slot>`;
}
renderLabel() {
return html `<slot part="label" class="label"></slot>`;
}
renderEndIcon() {
return html `<slot name="end-icon" part="end-icon" class="end-icon">${this.endIcon
? html `<mdui-icon name="${this.endIcon}" class="i"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderDeleteIcon() {
if (!this.deletable) {
return nothingTemplate;
}
return html `<slot name="delete-icon" part="delete-icon" class="delete-icon" @click="${this.onDelete}">${this.deleteIcon
? html `<mdui-icon name="${this.deleteIcon}" class="i"></mdui-icon>`
: html `<mdui-icon-clear class="i"></mdui-icon-clear>`}</slot>`;
}
renderInner() {
return [
this.renderIcon(),
this.renderLabel(),
this.renderEndIcon(),
this.renderDeleteIcon(),
];
}
};
Chip.styles = [ButtonBase.styles, style];
__decorate([
property({ reflect: true })
], Chip.prototype, "variant", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Chip.prototype, "elevated", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Chip.prototype, "selectable", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Chip.prototype, "selected", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Chip.prototype, "deletable", void 0);
__decorate([
property({ reflect: true })
], Chip.prototype, "icon", void 0);
__decorate([
property({ reflect: true, attribute: 'selected-icon' })
], Chip.prototype, "selectedIcon", void 0);
__decorate([
property({ reflect: true, attribute: 'end-icon' })
], Chip.prototype, "endIcon", void 0);
__decorate([
property({ reflect: true, attribute: 'delete-icon' })
], Chip.prototype, "deleteIcon", void 0);
__decorate([
watch('selected', true)
], Chip.prototype, "onSelectedChange", null);
Chip = __decorate([
customElement('mdui-chip')
], Chip);
export { Chip };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,4 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-small);position:relative;display:inline-block;flex-shrink:0;overflow:hidden;border-radius:var(--shape-corner);cursor:pointer;-webkit-tap-highlight-color:transparent;transition:box-shadow var(--mdui-motion-duration-short4) var(--mdui-motion-easing-linear);height:2rem;background-color:rgb(var(--mdui-color-surface));border:.0625rem solid rgb(var(--mdui-color-outline));color:rgb(var(--mdui-color-on-surface-variant));font-size:var(--mdui-typescale-label-large-size);font-weight:var(--mdui-typescale-label-large-weight);letter-spacing:var(--mdui-typescale-label-large-tracking);line-height:var(--mdui-typescale-label-large-line-height);--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface-variant)}.button{padding-right:.4375rem;padding-left:.4375rem}:host([variant=input]) .button{padding-right:.1875rem;padding-left:.1875rem}:host([selected]:not([selected=false i])) .button{padding-right:.5rem;padding-left:.5rem}:host([selected][variant=input]:not([selected=false i])) .button{padding-right:.25rem;padding-left:.25rem}:host([elevated]:not([elevated=false i])) .button{padding-right:.5rem;padding-left:.5rem}:host([variant=assist]){color:rgb(var(--mdui-color-on-surface));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}:host([elevated]:not([elevated=false i])){border-width:0;background-color:rgb(var(--mdui-color-surface-container-low));box-shadow:var(--mdui-elevation-level1)}:host([selected]:not([selected=false i])){color:rgb(var(--mdui-color-on-secondary-container));background-color:rgb(var(--mdui-color-secondary-container));border-width:0;--mdui-comp-ripple-state-layer-color:var(
--mdui-color-on-secondary-container
)}:host([disabled]:not([disabled=false i])),:host([loading]:not([loading=false i])){cursor:default;pointer-events:none}:host([disabled]:not([disabled=false i])){border-color:rgba(var(--mdui-color-on-surface),12%);color:rgba(var(--mdui-color-on-surface),38%);box-shadow:var(--mdui-elevation-level0)}:host([disabled][elevated]:not([disabled=false i],[elevated=false i])),:host([disabled][selected]:not([disabled=false i],[selected=false i])){background-color:rgba(var(--mdui-color-on-surface),12%)}:host([selected][hover]:not([selected=false i])){box-shadow:var(--mdui-elevation-level1)}:host([elevated][hover]:not([elevated=false i])){color:rgb(var(--mdui-color-on-secondary-container));box-shadow:var(--mdui-elevation-level2)}:host([variant=filter][hover]),:host([variant=input][hover]),:host([variant=suggestion][hover]){color:rgb(var(--mdui-color-on-surface-variant))}:host([variant=filter][focus-visible]),:host([variant=input][focus-visible]),:host([variant=suggestion][focus-visible]){border-color:rgb(var(--mdui-color-on-surface-variant))}:host([dragged]),:host([dragged][hover]){box-shadow:var(--mdui-elevation-level4)}.button{overflow:visible}.label{display:inline-flex;padding-right:.5rem;padding-left:.5rem}.end-icon,.icon,.selected-icon{display:inline-flex;font-size:1.28571429em;color:rgb(var(--mdui-color-on-surface-variant))}:host([variant=assist]) .end-icon,:host([variant=assist]) .icon,:host([variant=assist]) .selected-icon{color:rgb(var(--mdui-color-primary))}:host([selected]:not([selected=false i])) .end-icon,:host([selected]:not([selected=false i])) .icon,:host([selected]:not([selected=false i])) .selected-icon{color:rgb(var(--mdui-color-on-secondary-container))}:host([disabled]:not([disabled=false i])) .end-icon,:host([disabled]:not([disabled=false i])) .icon,:host([disabled]:not([disabled=false i])) .selected-icon{opacity:.38;color:rgb(var(--mdui-color-on-surface))}.end-icon .i,.icon .i,.selected-icon .i,::slotted([slot=end-icon]),::slotted([slot=icon]),::slotted([slot=selected-icon]){font-size:inherit}:host([variant=input]) .has-icon .icon,:host([variant=input]) .has-icon .selected-icon,:host([variant=input]) .has-icon mdui-circular-progress{margin-left:.25rem}:host([variant=input]) .has-end-icon .end-icon{margin-right:.25rem}mdui-circular-progress{display:inline-flex;width:1.125rem;height:1.125rem}:host([disabled]:not([disabled=false i])) mdui-circular-progress{stroke:rgba(var(--mdui-color-on-surface),38%)}::slotted(mdui-avatar[slot=end-icon]),::slotted(mdui-avatar[slot=icon]),::slotted(mdui-avatar[slot=selected-icon]){width:1.5rem;height:1.5rem}:host([disabled]:not([disabled=false i])) ::slotted(mdui-avatar[slot=end-icon]),:host([disabled]:not([disabled=false i])) ::slotted(mdui-avatar[slot=icon]),:host([disabled]:not([disabled=false i])) ::slotted(mdui-avatar[slot=selected-icon]){opacity:.38}::slotted(mdui-avatar[slot=icon]),::slotted(mdui-avatar[slot=selected-icon]){margin-left:-.25rem;margin-right:-.125rem}::slotted(mdui-avatar[slot=end-icon]){margin-right:-.25rem;margin-left:-.125rem}.delete-icon{display:inline-flex;font-size:1.28571429em;transition:background-color var(--mdui-motion-duration-short4) var(--mdui-motion-easing-linear);border-radius:var(--mdui-shape-corner-full);margin-right:-.25rem;margin-left:-.25rem;padding:.25rem;color:rgb(var(--mdui-color-on-surface-variant))}.delete-icon:hover{background-color:rgba(var(--mdui-color-on-surface-variant),12%)}.has-end-icon .delete-icon{margin-left:.25rem}:host([variant=assiat]) .delete-icon{color:rgb(var(--mdui-color-primary))}:host([variant=input]) .delete-icon{margin-right:.0625rem}:host([disabled]:not([disabled=false i])) .delete-icon{color:rgba(var(--mdui-color-on-surface),38%)}.delete-icon .i,::slotted([slot=delete-icon]){font-size:inherit}::slotted(mdui-avatar[slot=delete-icon]){width:1.125rem;height:1.125rem}`;

View File

@@ -0,0 +1 @@
export * from './circular-progress/index.js';

View File

@@ -0,0 +1 @@
export * from './circular-progress/index.js';

View File

@@ -0,0 +1,30 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
/**
* @summary 圆形进度指示器组件
*
* ```html
* <mdui-circular-progress></mdui-circular-progress>
* ```
*/
export declare class CircularProgress extends MduiElement<CircularProgressEventMap> {
static styles: CSSResultGroup;
/**
* 进度指示器的最大值。默认为 `1`
*/
max: number;
/**
* 进度指示器的当前值。如果未指定该值,则显示为不确定状态
*/
value?: number;
protected render(): TemplateResult;
private renderDeterminate;
private renderInDeterminate;
}
export interface CircularProgressEventMap {
}
declare global {
interface HTMLElementTagNameMap {
'mdui-circular-progress': CircularProgress;
}
}

View File

@@ -0,0 +1,62 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { isUndefined } from '@mdui/jq/shared/helper.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { style } from './style.js';
/**
* @summary 圆形进度指示器组件
*
* ```html
* <mdui-circular-progress></mdui-circular-progress>
* ```
*/
let CircularProgress = class CircularProgress extends MduiElement {
constructor() {
super(...arguments);
/**
* 进度指示器的最大值。默认为 `1`
*/
this.max = 1;
}
render() {
const isDeterminate = !isUndefined(this.value);
return html `<div class="progress ${classMap({
determinate: isDeterminate,
indeterminate: !isDeterminate,
})}">${isDeterminate ? this.renderDeterminate() : this.renderInDeterminate()}</div>`;
}
renderDeterminate() {
const value = this.value;
const strokeWidth = 4; // 圆环宽度
const circleRadius = 18; // 圆环宽度中心点的半径
const π = 3.1415926;
const center = circleRadius + strokeWidth / 2;
const circumference = 2 * π * circleRadius;
const determinateStrokeDashOffset = (1 - value / Math.max(this.max ?? value, value)) * circumference;
return html `<svg viewBox="0 0 ${center * 2} ${center * 2}"><circle class="track" cx="${center}" cy="${center}" r="${circleRadius}" stroke-width="${strokeWidth}"></circle><circle class="circle" cx="${center}" cy="${center}" r="${circleRadius}" stroke-dasharray="${2 * π * circleRadius}" stroke-dashoffset="${determinateStrokeDashOffset}" stroke-width="${strokeWidth}"></circle></svg>`;
}
renderInDeterminate() {
const strokeWidth = 4; // 圆环宽度
const circleRadius = 18; // 圆环宽度中心点的半径
const π = 3.1415926;
const center = circleRadius + strokeWidth / 2;
const circumference = 2 * π * circleRadius;
const halfCircumference = 0.5 * circumference;
const circle = (thisStrokeWidth) => html `<svg class="circle" viewBox="0 0 ${center * 2} ${center * 2}"><circle cx="${center}" cy="${center}" r="${circleRadius}" stroke-dasharray="${circumference}" stroke-dashoffset="${halfCircumference}" stroke-width="${thisStrokeWidth}"></circle></svg>`;
return html `<div class="layer"><div class="clipper left">${circle(strokeWidth)}</div><div class="gap-patch">${circle(strokeWidth * 0.8)}</div><div class="clipper right">${circle(strokeWidth)}</div></div>`;
}
};
CircularProgress.styles = [componentStyle, style];
__decorate([
property({ type: Number, reflect: true })
], CircularProgress.prototype, "max", void 0);
__decorate([
property({ type: Number })
], CircularProgress.prototype, "value", void 0);
CircularProgress = __decorate([
customElement('mdui-circular-progress')
], CircularProgress);
export { CircularProgress };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{position:relative;display:inline-block;flex-shrink:0;width:2.5rem;height:2.5rem;stroke:rgb(var(--mdui-color-primary))}.progress{position:relative;display:inline-block;width:100%;height:100%;text-align:left;transition:opacity var(--mdui-motion-duration-medium1) var(--mdui-motion-easing-linear)}.determinate svg{transform:rotate(-90deg);fill:transparent}.determinate .track{stroke:transparent}.determinate .circle{stroke:inherit;transition:stroke-dashoffset var(--mdui-motion-duration-long2) var(--mdui-motion-easing-standard)}.indeterminate{font-size:0;letter-spacing:0;white-space:nowrap;animation:mdui-comp-circular-progress-rotate 1568ms var(--mdui-motion-easing-linear) infinite}.indeterminate .circle,.indeterminate .layer{position:absolute;width:100%;height:100%}.indeterminate .layer{animation:mdui-comp-circular-progress-layer-rotate 5332ms var(--mdui-motion-easing-standard) infinite both}.indeterminate .circle{fill:transparent;stroke:inherit}.indeterminate .gap-patch{position:absolute;top:0;left:47.5%;width:5%;height:100%;overflow:hidden}.indeterminate .gap-patch .circle{left:-900%;width:2000%;transform:rotate(180deg)}.indeterminate .clipper{position:relative;display:inline-block;width:50%;height:100%;overflow:hidden}.indeterminate .clipper .circle{width:200%}.indeterminate .clipper.left .circle{animation:mdui-comp-circular-progress-left-spin 1333ms var(--mdui-motion-easing-standard) infinite both}.indeterminate .clipper.right .circle{left:-100%;animation:mdui-comp-circular-progress-right-spin 1333ms var(--mdui-motion-easing-standard) infinite both}@keyframes mdui-comp-circular-progress-rotate{to{transform:rotate(360deg)}}@keyframes mdui-comp-circular-progress-layer-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}100%{transform:rotate(1080deg)}}@keyframes mdui-comp-circular-progress-left-spin{0%{transform:rotate(265deg)}50%{transform:rotate(130deg)}100%{transform:rotate(265deg)}}@keyframes mdui-comp-circular-progress-right-spin{0%{transform:rotate(-265deg)}50%{transform:rotate(-130deg)}100%{transform:rotate(-265deg)}}`;

View File

@@ -0,0 +1 @@
export * from './collapse/collapse-item.js';

View File

@@ -0,0 +1 @@
export * from './collapse/collapse-item.js';

View File

@@ -0,0 +1 @@
export * from './collapse/collapse.js';

View File

@@ -0,0 +1 @@
export * from './collapse/collapse.js';

View File

@@ -0,0 +1 @@
export declare const collapseItemStyle: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const collapseItemStyle = css `:host{display:flex;flex-direction:column}.header{display:block}.body{display:block;overflow:hidden;transition:height var(--mdui-motion-duration-short4) var(--mdui-motion-easing-emphasized)}.body.opened{overflow:visible}.body.active{transition-duration:var(--mdui-motion-duration-medium4)}`;

View File

@@ -0,0 +1,68 @@
import '@mdui/jq/methods/height.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { JQ } from '@mdui/jq/shared/core.js';
import type { CSSResultGroup, TemplateResult, PropertyValues } from 'lit';
/**
* @summary 折叠面板项组件,需配合 `<mdui-collapse>` 组件使用
*
* ```html
* <mdui-collapse>
* ..<mdui-collapse-item header="header-1">content-1</mdui-collapse-item>
* ..<mdui-collapse-item header="header-2">content-2</mdui-collapse-item>
* </mdui-collapse>
* ```
*
* @event open - 开始打开时,事件被触发
* @event opened - 打开动画完成时,事件被触发
* @event close - 开始关闭时,事件被触发
* @event closed - 关闭动画完成时,事件被触发
*
* @slot - 折叠面板项的正文内容
* @slot header - 折叠面板项的头部内容
*
* @csspart header - 折叠面板的头部内容
* @csspart body - 折叠面板的正文内容
*/
export declare class CollapseItem extends MduiElement<CollapseItemEventMap> {
static styles: CSSResultGroup;
/**
* 此折叠面板项的值
*/
value?: string;
/**
* 此折叠面板项的头部文本
*/
header?: string;
/**
* 是否禁用此折叠面板项
*/
disabled: boolean;
/**
* 点击该元素时触发折叠,值可以是 CSS 选择器、DOM 元素、或 [JQ 对象](/docs/2/functions/jq)。默认为点击整个 header 区域触发
*/
trigger?: string | HTMLElement | JQ<HTMLElement>;
/**
* 是否为激活状态,由 `collapse` 组件控制该参数
*/
protected active: boolean;
private state;
protected isInitial: boolean;
protected readonly key: number;
private readonly bodyRef;
private onActiveChange;
protected firstUpdated(changedProperties: PropertyValues): void;
protected render(): TemplateResult;
private onTransitionEnd;
private updateBodyHeight;
}
export interface CollapseItemEventMap {
open: CustomEvent<void>;
opened: CustomEvent<void>;
close: CustomEvent<void>;
closed: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-collapse-item': CollapseItem;
}
}

View File

@@ -0,0 +1,130 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { $ } from '@mdui/jq/$.js';
import '@mdui/jq/methods/height.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { uniqueId } from '@mdui/shared/helpers/uniqueId.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { collapseItemStyle } from './collapse-item-style.js';
/**
* @summary 折叠面板项组件,需配合 `<mdui-collapse>` 组件使用
*
* ```html
* <mdui-collapse>
* ..<mdui-collapse-item header="header-1">content-1</mdui-collapse-item>
* ..<mdui-collapse-item header="header-2">content-2</mdui-collapse-item>
* </mdui-collapse>
* ```
*
* @event open - 开始打开时,事件被触发
* @event opened - 打开动画完成时,事件被触发
* @event close - 开始关闭时,事件被触发
* @event closed - 关闭动画完成时,事件被触发
*
* @slot - 折叠面板项的正文内容
* @slot header - 折叠面板项的头部内容
*
* @csspart header - 折叠面板的头部内容
* @csspart body - 折叠面板的正文内容
*/
let CollapseItem = class CollapseItem extends MduiElement {
constructor() {
super(...arguments);
/**
* 是否禁用此折叠面板项
*/
this.disabled = false;
/**
* 是否为激活状态,由 `collapse` 组件控制该参数
*/
this.active = false;
this.state = 'closed';
// 是否是初始状态,不显示动画
this.isInitial = true;
// 每一个 `collapse-item` 元素都添加一个唯一的 key
this.key = uniqueId();
this.bodyRef = createRef();
}
onActiveChange() {
if (this.isInitial) {
this.state = this.active ? 'opened' : 'closed';
if (this.hasUpdated) {
this.updateBodyHeight();
}
}
else {
this.state = this.active ? 'open' : 'close';
this.emit(this.state);
this.updateBodyHeight();
}
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.updateBodyHeight();
}
render() {
return html `<slot name="header" part="header" class="header">${this.header}</slot><slot part="body" class="body ${classMap({
opened: this.state === 'opened',
active: this.active,
})}" ${ref(this.bodyRef)} @transitionend="${this.onTransitionEnd}"></slot>`;
}
onTransitionEnd(event) {
if (event.target === this.bodyRef.value) {
this.state = this.active ? 'opened' : 'closed';
this.emit(this.state);
this.updateBodyHeight();
}
}
updateBodyHeight() {
const scrollHeight = this.bodyRef.value.scrollHeight;
// 如果是从 opened 状态开始关闭,则先设置高度值,并等重绘完成
if (this.state === 'close') {
$(this.bodyRef.value).height(scrollHeight);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.bodyRef.value.clientLeft;
}
$(this.bodyRef.value).height(this.state === 'opened'
? 'auto'
: this.state === 'open'
? scrollHeight
: 0);
}
};
CollapseItem.styles = [
componentStyle,
collapseItemStyle,
];
__decorate([
property({ reflect: true })
], CollapseItem.prototype, "value", void 0);
__decorate([
property({ reflect: true })
], CollapseItem.prototype, "header", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], CollapseItem.prototype, "disabled", void 0);
__decorate([
property()
], CollapseItem.prototype, "trigger", void 0);
__decorate([
state()
], CollapseItem.prototype, "active", void 0);
__decorate([
state()
], CollapseItem.prototype, "state", void 0);
__decorate([
watch('active')
], CollapseItem.prototype, "onActiveChange", null);
CollapseItem = __decorate([
customElement('mdui-collapse-item')
], CollapseItem);
export { CollapseItem };

View File

@@ -0,0 +1 @@
export declare const collapseStyle: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const collapseStyle = css `:host{display:block}`;

View File

@@ -0,0 +1,54 @@
import '@mdui/jq/methods/is.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
/**
* @summary 折叠面板组件,需配合 `<mdui-collapse-item>` 组件使用
*
* ```html
* <mdui-collapse>
* ..<mdui-collapse-item header="header-1">content-1</mdui-collapse-item>
* ..<mdui-collapse-item header="header-2">content-2</mdui-collapse-item>
* </mdui-collapse>
* ```
*
* @event change - 当前展开的折叠面板项变化时触发
*
* @slot - `<mdui-collapse-item>` 组件
*/
export declare class Collapse extends MduiElement<CollapseEventMap> {
static styles: CSSResultGroup;
/**
* 是否启用手风琴模式
*/
accordion: boolean;
/**
* 当前展开的 `<mdui-collapse-item>` 的值
*
* **Note**:该属性的 HTML 属性始终为字符串,只有在 `accordion` 为 `true` 时,才能设置初始值;该属性的 JavaScript 属性值在 `accordion` 为 `true` 时为字符串,在 `accordion` 为 `false` 时为字符串数组。因此,当 `accordion` 为 `false` 时,只能通过修改 JavaScript 属性值来改变此值。
*/
value?: string | string[];
/**
* 是否禁用此折叠面板
*/
disabled: boolean;
private activeKeys;
private readonly items;
private isInitial;
private definedController;
private onActiveKeysChange;
private onValueChange;
protected render(): TemplateResult;
private setActiveKeys;
private setValue;
private onClick;
private onSlotChange;
private updateItems;
}
export interface CollapseEventMap {
change: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-collapse': Collapse;
}
}

View File

@@ -0,0 +1,198 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, queryAssignedElements, state, } from 'lit/decorators.js';
import { $ } from '@mdui/jq/$.js';
import '@mdui/jq/methods/is.js';
import { isElement, isUndefined } from '@mdui/jq/shared/helper.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { DefinedController } from '@mdui/shared/controllers/defined.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { arraysEqualIgnoreOrder } from '@mdui/shared/helpers/array.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { collapseStyle } from './collapse-style.js';
/**
* @summary 折叠面板组件,需配合 `<mdui-collapse-item>` 组件使用
*
* ```html
* <mdui-collapse>
* ..<mdui-collapse-item header="header-1">content-1</mdui-collapse-item>
* ..<mdui-collapse-item header="header-2">content-2</mdui-collapse-item>
* </mdui-collapse>
* ```
*
* @event change - 当前展开的折叠面板项变化时触发
*
* @slot - `<mdui-collapse-item>` 组件
*/
let Collapse = class Collapse extends MduiElement {
constructor() {
super(...arguments);
/**
* 是否启用手风琴模式
*/
this.accordion = false;
/**
* 是否禁用此折叠面板
*/
this.disabled = false;
// 因为 collapse-item 的 value 可能会重复,所以在每个 collapse-item 元素上都添加了一个唯一的 key通过 activeKey 来记录激活状态的 key
this.activeKeys = [];
// 是否是初始状态,初始状态不触发 change 事件,没有动画
this.isInitial = true;
this.definedController = new DefinedController(this, {
relatedElements: ['mdui-collapse-item'],
});
}
async onActiveKeysChange() {
await this.definedController.whenDefined();
// 根据 activeKeys 读取对应 collapse-item 的值
const value = this.accordion
? this.items.find((item) => this.activeKeys.includes(item.key))?.value
: this.items
.filter((item) => this.activeKeys.includes(item.key))
.map((item) => item.value);
this.setValue(value);
if (!this.isInitial) {
this.emit('change');
}
}
async onValueChange() {
this.isInitial = !this.hasUpdated;
await this.definedController.whenDefined();
if (this.accordion) {
const value = this.value;
if (!value) {
this.setActiveKeys([]);
}
else {
const item = this.items.find((item) => item.value === value);
this.setActiveKeys(item ? [item.key] : []);
}
}
else {
const value = this.value;
if (!value.length) {
this.setActiveKeys([]);
}
else {
const activeKeys = this.items
.filter((item) => value.includes(item.value))
.map((item) => item.key);
this.setActiveKeys(activeKeys);
}
}
this.updateItems();
}
render() {
return html `<slot @slotchange="${this.onSlotChange}" @click="${this.onClick}"></slot>`;
}
setActiveKeys(activeKeys) {
if (!arraysEqualIgnoreOrder(this.activeKeys, activeKeys)) {
this.activeKeys = activeKeys;
}
}
setValue(value) {
if (this.accordion || isUndefined(this.value) || isUndefined(value)) {
this.value = value;
}
else if (!arraysEqualIgnoreOrder(this.value, value)) {
this.value = value;
}
}
onClick(event) {
// 全部禁用
if (this.disabled) {
return;
}
// event.button 为 0 时,为鼠标左键点击。忽略鼠标中键和右键
if (event.button) {
return;
}
const target = event.target;
const item = target.closest('mdui-collapse-item');
// collapse-item 被禁用,忽略
if (!item || item.disabled) {
return;
}
const path = event.composedPath();
// 指定了 trigger 时,点击了其他地方时,忽略
if (item.trigger &&
!path.find((element) => isElement(element) && $(element).is(item.trigger))) {
return;
}
// header 元素,忽略点击 header 以外的元素
if (!path.find((element) => isElement(element) && element.part.contains('header'))) {
return;
}
if (this.accordion) {
if (this.activeKeys.includes(item.key)) {
this.setActiveKeys([]);
}
else {
this.setActiveKeys([item.key]);
}
}
else {
// 直接修改 this.activeKeys 无法被 watch 监听到,需要先克隆一份 this.activeKeys
const activeKeys = [...this.activeKeys];
if (activeKeys.includes(item.key)) {
activeKeys.splice(activeKeys.indexOf(item.key), 1);
}
else {
activeKeys.push(item.key);
}
this.setActiveKeys(activeKeys);
}
this.isInitial = false;
this.updateItems();
}
async onSlotChange() {
await this.definedController.whenDefined();
this.updateItems();
}
// 更新 <mdui-collapse-item> 的状态
updateItems() {
this.items.forEach((item) => {
item.active = this.activeKeys.includes(item.key);
item.isInitial = this.isInitial;
});
}
};
Collapse.styles = [
componentStyle,
collapseStyle,
];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Collapse.prototype, "accordion", void 0);
__decorate([
property()
], Collapse.prototype, "value", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Collapse.prototype, "disabled", void 0);
__decorate([
state()
], Collapse.prototype, "activeKeys", void 0);
__decorate([
queryAssignedElements({ selector: 'mdui-collapse-item', flatten: true })
], Collapse.prototype, "items", void 0);
__decorate([
watch('activeKeys', true)
], Collapse.prototype, "onActiveKeysChange", null);
__decorate([
watch('value')
], Collapse.prototype, "onValueChange", null);
Collapse = __decorate([
customElement('mdui-collapse')
], Collapse);
export { Collapse };

View File

@@ -0,0 +1 @@
export * from './dialog/index.js';

View File

@@ -0,0 +1 @@
export * from './dialog/index.js';

View File

@@ -0,0 +1,107 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import '../icon.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
/**
* @summary 对话框组件
*
* ```html
* <mdui-dialog>content</mdui-dialog>
* ```
*
* @event open - 对话框开始打开时触发。可以通过调用 `event.preventDefault()` 阻止对话框打开
* @event opened - 对话框打开动画完成后触发
* @event close - 对话框开始关闭时触发。可以通过调用 `event.preventDefault()` 阻止对话框关闭
* @event closed - 对话框关闭动画完成后触发
* @event overlay-click - 点击遮罩层时触发
*
* @slot header - 顶部元素,默认包含 `icon` slot 和 `headline` slot
* @slot icon - 顶部图标
* @slot headline - 顶部标题
* @slot description - 标题下方的文本
* @slot - 对话框主体内容
* @slot action - 底部操作栏中的元素
*
* @csspart overlay - 遮罩层
* @csspart panel - 对话框容器
* @csspart header - 对话框 header 部分,包含 icon 和 headline
* @csspart icon - 顶部图标,位于 header 中
* @csspart headline - 顶部标题,位于 header 中
* @csspart body - 对话框 body 部分
* @csspart description - 副文本部分,位于 body 中
* @csspart action - 底部操作按钮
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
export declare class Dialog extends MduiElement<DialogEventMap> {
static styles: CSSResultGroup;
/**
* 顶部的 Material Icons 图标名。也可以通过 `slot="icon"` 设置
*/
icon?: string;
/**
* 标题。也可以通过 `slot="headline"` 设置
*/
headline?: string;
/**
* 标题下方的文本。也可以通过 `slot="description"` 设置
*/
description?: string;
/**
* 是否打开对话框
*/
open: boolean;
/**
* 是否全屏显示对话框
*/
fullscreen: boolean;
/**
* 是否允许按下 ESC 键关闭对话框
*/
closeOnEsc: boolean;
/**
* 是否允许点击遮罩层关闭对话框
*/
closeOnOverlayClick: boolean;
/**
* 是否垂直排列底部操作按钮
*/
stackedActions: boolean;
/**
* 是否可拖拽移动位置
*/
/**
* 是否可拖拽改变大小
*/
/**
* dialog 组件内包含的 mdui-top-app-bar 组件
*/
private readonly topAppBarElements;
private originalTrigger;
private modalHelper;
private readonly overlayRef;
private readonly panelRef;
private readonly bodyRef;
private readonly hasSlotController;
private readonly definedController;
private onOpenChange;
disconnectedCallback(): void;
protected firstUpdated(_changedProperties: PropertyValues): void;
protected render(): TemplateResult;
private onOverlayClick;
private renderIcon;
private renderHeadline;
private renderDescription;
}
export interface DialogEventMap {
open: CustomEvent<void>;
opened: CustomEvent<void>;
close: CustomEvent<void>;
closed: CustomEvent<void>;
'overlay-click': CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-dialog': Dialog;
}
}

View File

@@ -0,0 +1,311 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, queryAssignedElements, } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { when } from 'lit/directives/when.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { DefinedController } from '@mdui/shared/controllers/defined.js';
import { HasSlotController } from '@mdui/shared/controllers/has-slot.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { animateTo, stopAnimations } from '@mdui/shared/helpers/animate.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 { lockScreen, unlockScreen } from '@mdui/shared/helpers/scroll.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { offLocaleReady } from '../../internal/localize.js';
import '../icon.js';
import { style } from './style.js';
/**
* @summary 对话框组件
*
* ```html
* <mdui-dialog>content</mdui-dialog>
* ```
*
* @event open - 对话框开始打开时触发。可以通过调用 `event.preventDefault()` 阻止对话框打开
* @event opened - 对话框打开动画完成后触发
* @event close - 对话框开始关闭时触发。可以通过调用 `event.preventDefault()` 阻止对话框关闭
* @event closed - 对话框关闭动画完成后触发
* @event overlay-click - 点击遮罩层时触发
*
* @slot header - 顶部元素,默认包含 `icon` slot 和 `headline` slot
* @slot icon - 顶部图标
* @slot headline - 顶部标题
* @slot description - 标题下方的文本
* @slot - 对话框主体内容
* @slot action - 底部操作栏中的元素
*
* @csspart overlay - 遮罩层
* @csspart panel - 对话框容器
* @csspart header - 对话框 header 部分,包含 icon 和 headline
* @csspart icon - 顶部图标,位于 header 中
* @csspart headline - 顶部标题,位于 header 中
* @csspart body - 对话框 body 部分
* @csspart description - 副文本部分,位于 body 中
* @csspart action - 底部操作按钮
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
let Dialog = class Dialog extends MduiElement {
constructor() {
super(...arguments);
/**
* 是否打开对话框
*/
this.open = false;
/**
* 是否全屏显示对话框
*/
this.fullscreen = false;
/**
* 是否允许按下 ESC 键关闭对话框
*/
this.closeOnEsc = false;
/**
* 是否允许点击遮罩层关闭对话框
*/
this.closeOnOverlayClick = false;
/**
* 是否垂直排列底部操作按钮
*/
this.stackedActions = false;
this.overlayRef = createRef();
this.panelRef = createRef();
this.bodyRef = createRef();
this.hasSlotController = new HasSlotController(this, 'header', 'icon', 'headline', 'description', 'action', '[default]');
this.definedController = new DefinedController(this, {
relatedElements: ['mdui-top-app-bar'],
});
}
async onOpenChange() {
const hasUpdated = this.hasUpdated;
// 默认为关闭状态。因此首次渲染时,且为关闭状态,不执行
if (!this.open && !hasUpdated) {
return;
}
await this.definedController.whenDefined();
if (!hasUpdated) {
await this.updateComplete;
}
// 内部的 header, body, actions 元素
const children = Array.from(this.panelRef.value.querySelectorAll('.header, .body, .actions'));
const easingLinear = getEasing(this, 'linear');
const easingEmphasizedDecelerate = getEasing(this, 'emphasized-decelerate');
const easingEmphasizedAccelerate = getEasing(this, 'emphasized-accelerate');
const stopAnimation = () => Promise.all([
stopAnimations(this.overlayRef.value),
stopAnimations(this.panelRef.value),
...children.map((child) => stopAnimations(child)),
]);
// 打开
// 要区分是否首次渲染,首次渲染不触发事件,不执行动画;非首次渲染,触发事件,执行动画
if (this.open) {
if (hasUpdated) {
const eventProceeded = this.emit('open', { cancelable: true });
if (!eventProceeded) {
return;
}
}
this.style.display = 'flex';
// 包含 <mdui-top-app-bar slot="header"> 时
const topAppBarElements = this.topAppBarElements ?? [];
if (topAppBarElements.length) {
const topAppBarElement = topAppBarElements[0];
// top-app-bar 未设置 scrollTarget 时,默认设置为 bodyRef
if (!topAppBarElement.scrollTarget) {
topAppBarElement.scrollTarget = this.bodyRef.value;
}
// 移除 header 和 body 之间的 margin
this.bodyRef.value.style.marginTop = '0';
}
this.originalTrigger = document.activeElement;
this.modalHelper.activate();
lockScreen(this);
await stopAnimation();
// 设置聚焦
requestAnimationFrame(() => {
const autoFocusTarget = this.querySelector('[autofocus]');
if (autoFocusTarget) {
autoFocusTarget.focus({ preventScroll: true });
}
else {
this.panelRef.value.focus({ preventScroll: true });
}
});
const duration = getDuration(this, 'medium4');
await Promise.all([
animateTo(this.overlayRef.value, [{ opacity: 0 }, { opacity: 1, offset: 0.3 }, { opacity: 1 }], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
}),
animateTo(this.panelRef.value, [
{ transform: 'translateY(-1.875rem) scaleY(0)' },
{ transform: 'translateY(0) scaleY(1)' },
], {
duration: hasUpdated ? duration : 0,
easing: easingEmphasizedDecelerate,
}),
animateTo(this.panelRef.value, [{ opacity: 0 }, { opacity: 1, offset: 0.1 }, { opacity: 1 }], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
}),
...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');
}
}
else {
const eventProceeded = this.emit('close', { cancelable: true });
if (!eventProceeded) {
return;
}
this.modalHelper.deactivate();
await stopAnimation();
const duration = getDuration(this, 'short4');
await Promise.all([
animateTo(this.overlayRef.value, [{ opacity: 1 }, { opacity: 0 }], {
duration,
easing: easingLinear,
}),
animateTo(this.panelRef.value, [
{ transform: 'translateY(0) scaleY(1)' },
{ transform: 'translateY(-1.875rem) scaleY(0.6)' },
], { duration, easing: easingEmphasizedAccelerate }),
animateTo(this.panelRef.value, [{ opacity: 1 }, { opacity: 1, offset: 0.75 }, { opacity: 0 }], { duration, easing: easingLinear }),
...children.map((child) => animateTo(child, [{ opacity: 1 }, { opacity: 0, offset: 0.75 }, { opacity: 0 }], { duration, easing: easingLinear })),
]);
this.style.display = 'none';
unlockScreen(this);
// 对话框关闭后,恢复焦点到原有的元素上
const trigger = this.originalTrigger;
if (typeof trigger?.focus === 'function') {
setTimeout(() => trigger.focus());
}
this.emit('closed');
}
}
disconnectedCallback() {
super.disconnectedCallback();
unlockScreen(this);
// alert, confirm, prompt 函数支持 localize。这里确保在组件销毁时取消监听 localize ready 事件
offLocaleReady(this);
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
this.modalHelper = new Modal(this);
this.addEventListener('keydown', (event) => {
if (this.open && this.closeOnEsc && event.key === 'Escape') {
event.stopPropagation();
this.open = false;
}
});
}
render() {
const hasActionSlot = this.hasSlotController.test('action');
const hasDefaultSlot = this.hasSlotController.test('[default]');
const hasIcon = !!this.icon || this.hasSlotController.test('icon');
const hasHeadline = !!this.headline || this.hasSlotController.test('headline');
const hasDescription = !!this.description || this.hasSlotController.test('description');
const hasHeader = hasIcon || hasHeadline || this.hasSlotController.test('header');
const hasBody = hasDescription || hasDefaultSlot;
// modify: 移除了 tabindex="0", 换为 tabindex
return html `<div ${ref(this.overlayRef)} part="overlay" class="overlay" @click="${this.onOverlayClick}" tabindex="-1"></div><div ${ref(this.panelRef)} part="panel" class="panel ${classMap({
'has-icon': hasIcon,
'has-description': hasDescription,
'has-default': hasDefaultSlot,
})}" tabindex>${when(hasHeader, () => html `<slot name="header" part="header" class="header">${when(hasIcon, () => this.renderIcon())} ${when(hasHeadline, () => this.renderHeadline())}</slot>`)} ${when(hasBody, () => html `<div ${ref(this.bodyRef)} part="body" class="body">${when(hasDescription, () => this.renderDescription())}<slot></slot></div>`)} ${when(hasActionSlot, () => html `<slot name="action" part="action" class="action"></slot>`)}</div>`;
}
onOverlayClick() {
this.emit('overlay-click');
if (!this.closeOnOverlayClick) {
return;
}
this.open = false;
}
renderIcon() {
return html `<slot name="icon" part="icon" class="icon">${this.icon
? html `<mdui-icon name="${this.icon}"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderHeadline() {
return html `<slot name="headline" part="headline" class="headline">${this.headline}</slot>`;
}
renderDescription() {
return html `<slot name="description" part="description" class="description">${this.description}</slot>`;
}
};
Dialog.styles = [componentStyle, style];
__decorate([
property({ reflect: true })
], Dialog.prototype, "icon", void 0);
__decorate([
property({ reflect: true })
], Dialog.prototype, "headline", void 0);
__decorate([
property({ reflect: true })
], Dialog.prototype, "description", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Dialog.prototype, "open", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Dialog.prototype, "fullscreen", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'close-on-esc',
})
], Dialog.prototype, "closeOnEsc", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'close-on-overlay-click',
})
], Dialog.prototype, "closeOnOverlayClick", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'stacked-actions',
})
], Dialog.prototype, "stackedActions", void 0);
__decorate([
queryAssignedElements({
slot: 'header',
selector: 'mdui-top-app-bar',
flatten: true,
})
], Dialog.prototype, "topAppBarElements", void 0);
__decorate([
watch('open')
], Dialog.prototype, "onOpenChange", null);
Dialog = __decorate([
customElement('mdui-dialog')
], Dialog);
export { Dialog };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-extra-large);--z-index:2300;position:fixed;z-index:var(--z-index);display:none;align-items:center;justify-content:center;inset:0;padding:3rem}::slotted(mdui-top-app-bar[slot=header]){position:absolute;border-top-left-radius:var(--mdui-shape-corner-extra-large);border-top-right-radius:var(--mdui-shape-corner-extra-large);background-color:rgb(var(--mdui-color-surface-container-high))}:host([fullscreen]:not([fullscreen=false i])){--shape-corner:var(--mdui-shape-corner-none);padding:0}:host([fullscreen]:not([fullscreen=false i])) ::slotted(mdui-top-app-bar[slot=header]){border-top-left-radius:var(--mdui-shape-corner-none);border-top-right-radius:var(--mdui-shape-corner-none)}.overlay{position:fixed;inset:0;background-color:rgba(var(--mdui-color-scrim),.4)}.panel{--mdui-color-background:var(--mdui-color-surface-container-high);position:relative;display:flex;flex-direction:column;max-height:100%;border-radius:var(--shape-corner);outline:0;transform-origin:top;min-width:17.5rem;max-width:35rem;padding:1.5rem;background-color:rgb(var(--mdui-color-surface-container-high));box-shadow:var(--mdui-elevation-level3)}:host([fullscreen]:not([fullscreen=false i])) .panel{width:100%;max-width:100%;height:100%;max-height:100%;box-shadow:var(--mdui-elevation-level0)}.header{display:flex;flex-direction:column}.has-icon .header{align-items:center}.icon{display:flex;color:rgb(var(--mdui-color-secondary));font-size:1.5rem}.icon mdui-icon,::slotted([slot=icon]){font-size:inherit}.headline{display:flex;color:rgb(var(--mdui-color-on-surface));font-size:var(--mdui-typescale-headline-small-size);font-weight:var(--mdui-typescale-headline-small-weight);letter-spacing:var(--mdui-typescale-headline-small-tracking);line-height:var(--mdui-typescale-headline-small-line-height)}.icon+.headline{padding-top:1rem}.body{overflow:auto}.header+.body{margin-top:1rem}.description{display:flex;color:rgb(var(--mdui-color-on-surface-variant));font-size:var(--mdui-typescale-body-medium-size);font-weight:var(--mdui-typescale-body-medium-weight);letter-spacing:var(--mdui-typescale-body-medium-tracking);line-height:var(--mdui-typescale-body-medium-line-height)}:host([fullscreen]:not([fullscreen=false i])) .description{color:rgb(var(--mdui-color-on-surface))}.has-description.has-default .description{margin-bottom:1rem}.action{display:flex;justify-content:flex-end;padding-top:1.5rem}.action::slotted(:not(:first-child)){margin-left:.5rem}:host([stacked-actions]:not([stacked-actions=false i])) .action{flex-direction:column;align-items:end}:host([stacked-actions]:not([stacked-actions=false i])) .action::slotted(:not(:first-child)){margin-left:0;margin-top:.5rem}`;

View File

@@ -0,0 +1 @@
export * from './divider/index.js';

View File

@@ -0,0 +1 @@
export * from './divider/index.js';

View File

@@ -0,0 +1,32 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
/**
* @summary 分割线组件
*
* ```html
* <mdui-divider></mdui-divider>
* ```
*/
export declare class Divider extends MduiElement<DividerEventMap> {
static styles: CSSResultGroup;
/**
* 是否为垂直分割线
*/
vertical: boolean;
/**
* 是否进行左侧缩进
*/
inset: boolean;
/**
* 是否进行左右两侧缩进
*/
middle: boolean;
protected render(): TemplateResult;
}
export interface DividerEventMap {
}
declare global {
interface HTMLElementTagNameMap {
'mdui-divider': Divider;
}
}

View File

@@ -0,0 +1,60 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { style } from './style.js';
/**
* @summary 分割线组件
*
* ```html
* <mdui-divider></mdui-divider>
* ```
*/
let Divider = class Divider extends MduiElement {
constructor() {
super(...arguments);
/**
* 是否为垂直分割线
*/
this.vertical = false;
/**
* 是否进行左侧缩进
*/
this.inset = false;
/**
* 是否进行左右两侧缩进
*/
this.middle = false;
}
render() {
return html ``;
}
};
Divider.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Divider.prototype, "vertical", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Divider.prototype, "inset", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Divider.prototype, "middle", void 0);
Divider = __decorate([
customElement('mdui-divider')
], Divider);
export { Divider };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{display:block;height:.0625rem;background-color:rgb(var(--mdui-color-surface-variant))}:host([inset]:not([inset=false i])){margin-left:1rem}:host([middle]:not([middle=false i])){margin-left:1rem;margin-right:1rem}:host([vertical]:not([vertical=false i])){height:100%;width:.0625rem}`;

View File

@@ -0,0 +1 @@
export * from './dropdown/index.js';

View File

@@ -0,0 +1 @@
export * from './dropdown/index.js';

View File

@@ -0,0 +1,137 @@
import '@mdui/jq/methods/height.js';
import '@mdui/jq/methods/is.js';
import '@mdui/jq/methods/width.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
/**
* @summary 下拉组件
*
* ```html
* <mdui-dropdown>
* ..<mdui-button slot="trigger">open dropdown</mdui-button>
* ..<mdui-menu>
* ....<mdui-menu-item>Item 1</mdui-menu-item>
* ....<mdui-menu-item>Item 2</mdui-menu-item>
* ..</mdui-menu>
* </mdui-dropdown>
* ```
*
* @event open - 下拉组件开始打开时,事件被触发。可以通过调用 `event.preventDefault()` 阻止下拉组件打开
* @event opened - 下拉组件打开动画完成时,事件被触发
* @event close - 下拉组件开始关闭时,事件被触发。可以通过调用 `event.preventDefault()` 阻止下拉组件关闭
* @event closed - 下拉组件关闭动画完成时,事件被触发
*
* @slot - 下拉组件的内容
* @slot trigger - 触发下拉组件的元素,例如 [`<mdui-button>`](/docs/2/components/button) 元素
*
* @csspart trigger - 触发下拉组件的元素的容器,即 `trigger` slot 的容器
* @csspart panel - 下拉组件内容的容器
*
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
export declare class Dropdown extends MduiElement<DropdownEventMap> {
static styles: CSSResultGroup;
/**
* 是否打开下拉组件
*/
open: boolean;
/**
* 是否禁用下拉组件
*/
disabled: boolean;
/**
* 下拉组件的触发方式,支持多个值,用空格分隔。可选值包括:
*
* * `click`:点击触发
* * `hover`:鼠标悬浮触发
* * `focus`:聚焦触发
* * `contextmenu`:鼠标右键点击、或触摸长按触发
* * `manual`:仅能通过编程方式打开和关闭下拉组件,不能再指定其他触发方式
*/
trigger: /*点击触发*/ 'click' | /*鼠标悬浮触发*/ 'hover' | /*聚焦触发*/ 'focus' | /*鼠标右键点击、或触摸长按触发*/ 'contextmenu' | /*仅能通过编程方式打开和关闭下拉组件,不能再指定其他触发方式*/ 'manual' | string;
/**
* 下拉组件内容的位置。可选值包括:
*
* * `auto`:自动判断位置
* * `top-start`:上方左对齐
* * `top`:上方居中
* * `top-end`:上方右对齐
* * `bottom-start`:下方左对齐
* * `bottom`:下方居中
* * `bottom-end`:下方右对齐
* * `left-start`:左侧顶部对齐
* * `left`:左侧居中
* * `left-end`:左侧底部对齐
* * `right-start`:右侧顶部对齐
* * `right`:右侧居中
* * `right-end`:右侧底部对齐
*/
placement: /*自动判断位置*/ 'auto' | /*上方左对齐*/ 'top-start' | /*上方居中*/ 'top' | /*上方右对齐*/ 'top-end' | /*下方左对齐*/ 'bottom-start' | /*下方居中*/ 'bottom' | /*下方右对齐*/ 'bottom-end' | /*左侧顶部对齐*/ 'left-start' | /*左侧居中*/ 'left' | /*左侧底部对齐*/ 'left-end' | /*右侧顶部对齐*/ 'right-start' | /*右侧居中*/ 'right' | /*右侧底部对齐*/ 'right-end';
/**
* 点击 [`<mdui-menu-item>`](/docs/2/components/menu#menu-item-api) 后,下拉组件是否保持打开状态
*/
stayOpenOnClick: boolean;
/**
* 鼠标悬浮触发下拉组件打开的延时,单位为毫秒
*/
openDelay: number;
/**
* 鼠标悬浮触发下拉组件关闭的延时,单位为毫秒
*/
closeDelay: number;
/**
* 是否在触发下拉组件的光标位置打开下拉组件,常用于打开鼠标右键菜单
*/
openOnPointer: boolean;
private readonly triggerElements;
private readonly panelElements;
private pointerOffsetX;
private pointerOffsetY;
private animateDirection;
private openTimeout;
private closeTimeout;
private observeResize?;
private overflowAncestors?;
private readonly panelRef;
private readonly definedController;
constructor();
private get triggerElement();
private onPositionChange;
private onOpenChange;
connectedCallback(): void;
disconnectedCallback(): void;
protected firstUpdated(changedProperties: PropertyValues): void;
protected render(): TemplateResult;
/**
* 获取 dropdown 打开、关闭动画的 CSS scaleX 或 scaleY
*/
private getCssScaleName;
/**
* 在 document 上点击时,根据条件判断是否要关闭 dropdown
*/
private onDocumentClick;
/**
* 在 document 上按下按键时,根据条件判断是否要关闭 dropdown
*/
private onDocumentKeydown;
private onWindowScroll;
private hasTrigger;
private onFocus;
private onClick;
private onPanelClick;
private onContextMenu;
private onMouseEnter;
private onMouseLeave;
private updatePositioner;
}
export interface DropdownEventMap {
open: CustomEvent<void>;
opened: CustomEvent<void>;
close: CustomEvent<void>;
closed: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-dropdown': Dropdown;
}
}

View File

@@ -0,0 +1,583 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, queryAssignedElements, } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { getOverflowAncestors } from '@floating-ui/utils/dom';
import { $ } from '@mdui/jq/$.js';
import '@mdui/jq/methods/height.js';
import '@mdui/jq/methods/is.js';
import '@mdui/jq/methods/width.js';
import { isFunction } from '@mdui/jq/shared/helper.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.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 { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { getDuration, getEasing } from '@mdui/shared/helpers/motion.js';
import { observeResize } from '@mdui/shared/helpers/observeResize.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { style } from './style.js';
/**
* @summary 下拉组件
*
* ```html
* <mdui-dropdown>
* ..<mdui-button slot="trigger">open dropdown</mdui-button>
* ..<mdui-menu>
* ....<mdui-menu-item>Item 1</mdui-menu-item>
* ....<mdui-menu-item>Item 2</mdui-menu-item>
* ..</mdui-menu>
* </mdui-dropdown>
* ```
*
* @event open - 下拉组件开始打开时,事件被触发。可以通过调用 `event.preventDefault()` 阻止下拉组件打开
* @event opened - 下拉组件打开动画完成时,事件被触发
* @event close - 下拉组件开始关闭时,事件被触发。可以通过调用 `event.preventDefault()` 阻止下拉组件关闭
* @event closed - 下拉组件关闭动画完成时,事件被触发
*
* @slot - 下拉组件的内容
* @slot trigger - 触发下拉组件的元素,例如 [`<mdui-button>`](/docs/2/components/button) 元素
*
* @csspart trigger - 触发下拉组件的元素的容器,即 `trigger` slot 的容器
* @csspart panel - 下拉组件内容的容器
*
* @cssprop --z-index - 组件的 CSS `z-index` 值
*/
let Dropdown = class Dropdown extends MduiElement {
constructor() {
super();
/**
* 是否打开下拉组件
*/
this.open = false;
/**
* 是否禁用下拉组件
*/
this.disabled = false;
/**
* 下拉组件的触发方式,支持多个值,用空格分隔。可选值包括:
*
* * `click`:点击触发
* * `hover`:鼠标悬浮触发
* * `focus`:聚焦触发
* * `contextmenu`:鼠标右键点击、或触摸长按触发
* * `manual`:仅能通过编程方式打开和关闭下拉组件,不能再指定其他触发方式
*/
this.trigger = 'click';
/**
* 下拉组件内容的位置。可选值包括:
*
* * `auto`:自动判断位置
* * `top-start`:上方左对齐
* * `top`:上方居中
* * `top-end`:上方右对齐
* * `bottom-start`:下方左对齐
* * `bottom`:下方居中
* * `bottom-end`:下方右对齐
* * `left-start`:左侧顶部对齐
* * `left`:左侧居中
* * `left-end`:左侧底部对齐
* * `right-start`:右侧顶部对齐
* * `right`:右侧居中
* * `right-end`:右侧底部对齐
*/
this.placement = 'auto';
/**
* 点击 [`<mdui-menu-item>`](/docs/2/components/menu#menu-item-api) 后,下拉组件是否保持打开状态
*/
this.stayOpenOnClick = false;
/**
* 鼠标悬浮触发下拉组件打开的延时,单位为毫秒
*/
this.openDelay = 150;
/**
* 鼠标悬浮触发下拉组件关闭的延时,单位为毫秒
*/
this.closeDelay = 150;
/**
* 是否在触发下拉组件的光标位置打开下拉组件,常用于打开鼠标右键菜单
*/
this.openOnPointer = false;
this.panelRef = createRef();
this.definedController = new DefinedController(this, {
relatedElements: [''],
});
this.onDocumentClick = this.onDocumentClick.bind(this);
this.onDocumentKeydown = this.onDocumentKeydown.bind(this);
this.onWindowScroll = this.onWindowScroll.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onClick = this.onClick.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onPanelClick = this.onPanelClick.bind(this);
}
get triggerElement() {
return this.triggerElements[0];
}
// 这些属性变更时,需要更新样式
async onPositionChange() {
// 如果是打开状态,则更新 panel 的位置
if (this.open) {
await this.definedController.whenDefined();
this.updatePositioner();
}
}
async onOpenChange() {
const hasUpdated = this.hasUpdated;
// 默认为关闭状态。因此首次渲染时,且为关闭状态,不执行
if (!this.open && !hasUpdated) {
return;
}
await this.definedController.whenDefined();
if (!hasUpdated) {
await this.updateComplete;
}
const easingLinear = getEasing(this, 'linear');
const easingEmphasizedDecelerate = getEasing(this, 'emphasized-decelerate');
const easingEmphasizedAccelerate = getEasing(this, 'emphasized-accelerate');
// 打开
// 要区分是否首次渲染,首次渲染时不触发事件,不执行动画;非首次渲染,触发事件,执行动画
if (this.open) {
if (hasUpdated) {
const eventProceeded = this.emit('open', { cancelable: true });
if (!eventProceeded) {
return;
}
}
// dropdown 打开时,尝试把焦点放到 panel 中
const focusablePanel = this.panelElements.find((panel) => isFunction(panel.focus));
setTimeout(() => {
focusablePanel?.focus();
});
const duration = getDuration(this, 'medium4');
await stopAnimations(this.panelRef.value);
this.panelRef.value.hidden = false;
this.updatePositioner();
await Promise.all([
animateTo(this.panelRef.value, [
{ transform: `${this.getCssScaleName()}(0.45)` },
{ transform: `${this.getCssScaleName()}(1)` },
], {
duration: hasUpdated ? duration : 0,
easing: easingEmphasizedDecelerate,
}),
animateTo(this.panelRef.value, [{ opacity: 0 }, { opacity: 1, offset: 0.125 }, { opacity: 1 }], {
duration: hasUpdated ? duration : 0,
easing: easingLinear,
}),
]);
if (hasUpdated) {
this.emit('opened');
}
}
else {
const eventProceeded = this.emit('close', { cancelable: true });
if (!eventProceeded) {
return;
}
// dropdown 关闭时,如果不支持 focus 触发,且焦点在 dropdown 内,则焦点回到 trigger 上
if (!this.hasTrigger('focus') &&
isFunction(this.triggerElement?.focus) &&
(this.contains(document.activeElement) ||
this.contains(document.activeElement?.assignedSlot ?? null))) {
this.triggerElement.focus();
}
const duration = getDuration(this, 'short4');
await stopAnimations(this.panelRef.value);
await Promise.all([
animateTo(this.panelRef.value, [
{ transform: `${this.getCssScaleName()}(1)` },
{ transform: `${this.getCssScaleName()}(0.45)` },
], { duration, easing: easingEmphasizedAccelerate }),
animateTo(this.panelRef.value, [{ opacity: 1 }, { opacity: 1, offset: 0.875 }, { opacity: 0 }], { duration, easing: easingLinear }),
]);
// 可能关闭 dropdown 时该元素已经不存在了(比如页面直接跳转了)
if (this.panelRef.value) {
this.panelRef.value.hidden = true;
}
this.emit('closed');
}
}
connectedCallback() {
super.connectedCallback();
this.definedController.whenDefined().then(() => {
document.addEventListener('pointerdown', this.onDocumentClick);
document.addEventListener('keydown', this.onDocumentKeydown);
this.overflowAncestors = getOverflowAncestors(this.triggerElement);
this.overflowAncestors.forEach((ancestor) => {
ancestor.addEventListener('scroll', this.onWindowScroll);
});
// triggerElement 的尺寸变化时,重新调整 panel 的位置
this.observeResize = observeResize(this.triggerElement, () => {
this.updatePositioner();
});
});
}
disconnectedCallback() {
// 移除组件时,如果关闭动画正在进行中,则会导致关闭动画无法执行完成,最终组件无法隐藏
// 具体场景为 vue 的 <keep-alive> 中切换走,再切换回来时,面板仍然打开着
if (!this.open && this.panelRef.value) {
this.panelRef.value.hidden = true;
}
super.disconnectedCallback();
document.removeEventListener('pointerdown', this.onDocumentClick);
document.removeEventListener('keydown', this.onDocumentKeydown);
this.overflowAncestors?.forEach((ancestor) => {
ancestor.removeEventListener('scroll', this.onWindowScroll);
});
this.observeResize?.unobserve();
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.addEventListener('mouseleave', this.onMouseLeave);
this.definedController.whenDefined().then(() => {
this.triggerElement.addEventListener('focus', this.onFocus);
this.triggerElement.addEventListener('click', this.onClick);
this.triggerElement.addEventListener('contextmenu', this.onContextMenu);
this.triggerElement.addEventListener('mouseenter', this.onMouseEnter);
});
}
render() {
return html `<slot name="trigger" part="trigger" class="trigger"></slot><slot ${ref(this.panelRef)} part="panel" class="panel" hidden @click="${this.onPanelClick}"></slot>`;
}
/**
* 获取 dropdown 打开、关闭动画的 CSS scaleX 或 scaleY
*/
getCssScaleName() {
return this.animateDirection === 'horizontal' ? 'scaleX' : 'scaleY';
}
/**
* 在 document 上点击时,根据条件判断是否要关闭 dropdown
*/
onDocumentClick(e) {
if (this.disabled || !this.open) {
return;
}
const path = e.composedPath();
// 点击 dropdown 外部区域,直接关闭
if (!path.includes(this)) {
this.open = false;
}
// 当包含 contextmenu 且不包含 click 时,点击 trigger关闭
if (this.hasTrigger('contextmenu') &&
!this.hasTrigger('click') &&
path.includes(this.triggerElement)) {
this.open = false;
}
}
/**
* 在 document 上按下按键时,根据条件判断是否要关闭 dropdown
*/
onDocumentKeydown(event) {
if (this.disabled || !this.open) {
return;
}
// 按下 ESC 键时,关闭 dropdown
if (event.key === 'Escape') {
this.open = false;
return;
}
// 按下 Tab 键时,关闭 dropdown
if (event.key === 'Tab') {
// 如果不支持 focus 触发,则焦点回到 trigger 上(这个会在 onOpenChange 中执行 )这里只需阻止默认的 Tab 行为
if (!this.hasTrigger('focus') && isFunction(this.triggerElement?.focus)) {
event.preventDefault();
}
this.open = false;
}
}
onWindowScroll() {
window.requestAnimationFrame(() => this.onPositionChange());
}
hasTrigger(trigger) {
const triggers = this.trigger.split(' ');
return triggers.includes(trigger);
}
onFocus() {
if (this.disabled || this.open || !this.hasTrigger('focus')) {
return;
}
this.open = true;
}
onClick(e) {
// e.button 为 0 时,为鼠标左键点击。忽略鼠标中间和右键
if (this.disabled || e.button || !this.hasTrigger('click')) {
return;
}
// 支持 hover 或 focus 触发时,点击时,不关闭 dropdown
if (this.open && (this.hasTrigger('hover') || this.hasTrigger('focus'))) {
return;
}
this.pointerOffsetX = e.offsetX;
this.pointerOffsetY = e.offsetY;
this.open = !this.open;
}
onPanelClick(e) {
if (!this.disabled &&
!this.stayOpenOnClick &&
$(e.target).is('mdui-menu-item')) {
this.open = false;
}
}
onContextMenu(e) {
if (this.disabled || !this.hasTrigger('contextmenu')) {
return;
}
e.preventDefault();
this.pointerOffsetX = e.offsetX;
this.pointerOffsetY = e.offsetY;
this.open = true;
}
onMouseEnter() {
// 不做 open 状态的判断,因为可以延时打开和关闭
if (this.disabled || !this.hasTrigger('hover')) {
return;
}
window.clearTimeout(this.closeTimeout);
if (this.openDelay) {
this.openTimeout = window.setTimeout(() => {
this.open = true;
}, this.openDelay);
}
else {
this.open = true;
}
}
onMouseLeave() {
// 不做 open 状态的判断,因为可以延时打开和关闭
if (this.disabled || !this.hasTrigger('hover')) {
return;
}
window.clearTimeout(this.openTimeout);
this.closeTimeout = window.setTimeout(() => {
this.open = false;
}, this.closeDelay || 50);
}
// 更新 panel 的位置
updatePositioner() {
const $panel = $(this.panelRef.value);
const $window = $(window);
const panelElements = this.panelElements;
const panelRect = {
width: Math.max(...(panelElements?.map((panel) => panel.offsetWidth) ?? [])),
height: panelElements
?.map((panel) => panel.offsetHeight)
.reduce((total, height) => total + height, 0),
};
// 在光标位置触发时,假设 triggerElement 的宽高为 0位置位于光标位置
const triggerClientRect = this.triggerElement.getBoundingClientRect();
const triggerRect = this.openOnPointer
? {
top: this.pointerOffsetY + triggerClientRect.top,
left: this.pointerOffsetX + triggerClientRect.left,
width: 0,
height: 0,
}
: triggerClientRect;
// dropdown 与屏幕边界至少保留 8px 间距
const screenMargin = 8;
let transformOriginX;
let transformOriginY;
let top;
let left;
let placement = this.placement;
// 自动判断 dropdown 的方位
// 优先级为 bottom>top>right>leftstart>center>end
if (placement === 'auto') {
const windowWidth = $window.width();
const windowHeight = $window.height();
let position;
let alignment;
if (windowHeight - triggerRect.top - triggerRect.height >
panelRect.height + screenMargin) {
// 下方放得下,放下方
position = 'bottom';
}
else if (triggerRect.top > panelRect.height + screenMargin) {
// 上方放得下,放上方
position = 'top';
}
else if (windowWidth - triggerRect.left - triggerRect.width >
panelRect.width + screenMargin) {
// 右侧放得下,放右侧
position = 'right';
}
else if (triggerRect.left > panelRect.width + screenMargin) {
// 左侧放得下,放左侧
position = 'left';
}
else {
// 默认放下方
position = 'bottom';
}
if (['top', 'bottom'].includes(position)) {
if (windowWidth - triggerRect.left > panelRect.width + screenMargin) {
// 左对齐放得下,左对齐
alignment = 'start';
}
else if (triggerRect.left + triggerRect.width / 2 >
panelRect.width / 2 + screenMargin &&
windowWidth - triggerRect.left - triggerRect.width / 2 >
panelRect.width / 2 + screenMargin) {
// 居中对齐放得下,居中对齐
alignment = undefined;
}
else if (triggerRect.left + triggerRect.width >
panelRect.width + screenMargin) {
// 右对齐放得下,右对齐
alignment = 'end';
}
else {
// 默认左对齐
alignment = 'start';
}
}
else {
if (windowHeight - triggerRect.top > panelRect.height + screenMargin) {
// 顶部对齐放得下,顶部对齐
alignment = 'start';
}
else if (triggerRect.top + triggerRect.height / 2 >
panelRect.height / 2 + screenMargin &&
windowHeight - triggerRect.top - triggerRect.height / 2 >
panelRect.height / 2 + screenMargin) {
// 居中对齐放得下,居中对齐
alignment = undefined;
}
else if (triggerRect.top + triggerRect.height >
panelRect.height + screenMargin) {
// 底部对齐放得下,底部对齐
alignment = 'end';
}
else {
// 默认顶部对齐
alignment = 'start';
}
}
placement = alignment
? [position, alignment].join('-')
: position;
}
// 根据 placement 计算 panel 的位置和方向
const [position, alignment] = placement.split('-');
this.animateDirection = ['top', 'bottom'].includes(position)
? 'vertical'
: 'horizontal';
switch (position) {
case 'top':
transformOriginY = 'bottom';
top = triggerRect.top - panelRect.height;
break;
case 'bottom':
transformOriginY = 'top';
top = triggerRect.top + triggerRect.height;
break;
default:
transformOriginY = 'center';
switch (alignment) {
case 'start':
top = triggerRect.top;
break;
case 'end':
top = triggerRect.top + triggerRect.height - panelRect.height;
break;
default:
top =
triggerRect.top + triggerRect.height / 2 - panelRect.height / 2;
break;
}
break;
}
switch (position) {
case 'left':
transformOriginX = 'right';
left = triggerRect.left - panelRect.width;
break;
case 'right':
transformOriginX = 'left';
left = triggerRect.left + triggerRect.width;
break;
default:
transformOriginX = 'center';
switch (alignment) {
case 'start':
left = triggerRect.left;
break;
case 'end':
left = triggerRect.left + triggerRect.width - panelRect.width;
break;
default:
left =
triggerRect.left + triggerRect.width / 2 - panelRect.width / 2;
break;
}
break;
}
$panel.css({
top,
left,
transformOrigin: [transformOriginX, transformOriginY].join(' '),
});
}
};
Dropdown.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Dropdown.prototype, "open", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], Dropdown.prototype, "disabled", void 0);
__decorate([
property({ reflect: true })
], Dropdown.prototype, "trigger", void 0);
__decorate([
property({ reflect: true })
], Dropdown.prototype, "placement", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'stay-open-on-click',
})
], Dropdown.prototype, "stayOpenOnClick", void 0);
__decorate([
property({ type: Number, reflect: true, attribute: 'open-delay' })
], Dropdown.prototype, "openDelay", void 0);
__decorate([
property({ type: Number, reflect: true, attribute: 'close-delay' })
], Dropdown.prototype, "closeDelay", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'open-on-pointer',
})
], Dropdown.prototype, "openOnPointer", void 0);
__decorate([
queryAssignedElements({ slot: 'trigger', flatten: true })
], Dropdown.prototype, "triggerElements", void 0);
__decorate([
queryAssignedElements({ flatten: true })
], Dropdown.prototype, "panelElements", void 0);
__decorate([
watch('placement', true),
watch('openOnPointer', true)
], Dropdown.prototype, "onPositionChange", null);
__decorate([
watch('open')
], Dropdown.prototype, "onOpenChange", null);
Dropdown = __decorate([
customElement('mdui-dropdown')
], Dropdown);
export { Dropdown };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const style = css `:host{--z-index:2100;display:contents}.panel{display:block;position:fixed;z-index:var(--z-index)}`;

View File

@@ -0,0 +1 @@
export * from './fab/index.js';

View File

@@ -0,0 +1 @@
export * from './fab/index.js';

View File

@@ -0,0 +1,76 @@
import { ButtonBase } from '../button/button-base.js';
import '../icon.js';
import type { Ripple } from '../ripple/index.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
/**
* @summary 浮动操作按钮组件
*
* ```html
* <mdui-fab icon="edit"></mdui-fab>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 文本
* @slot icon - 图标
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 右侧的文本
* @csspart icon - 左侧的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner-small - `size="small"` 时,组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --shape-corner-normal - `size="normal"` 时,组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
* @cssprop --shape-corner-large - `size="large"` 时,组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Fab extends ButtonBase<FabEventMap> {
static styles: CSSResultGroup;
/**
* FAB 的形状,此组件的不同形状之间只有颜色不一样。可选值包括:
*
* * `primary`:使用 Primary container 背景色
* * `surface`:使用 Surface container high 背景色
* * `secondary`:使用 Secondary container 背景色
* * `tertiary`:使用 Tertiary container 背景色
*/
variant: /*使用 Primary container 背景色*/ 'primary' | /*使用 Surface container high 背景色*/ 'surface' | /*使用 Secondary container 背景色*/ 'secondary' | /*使用 Tertiary container 背景色*/ 'tertiary';
/**
* FAB 的大小。可选值包括:
* * `normal`:普通大小 FAB
* * `small`:小型 FAB
* * `large`:大型 FAB
*/
size: /*普通大小 FAB*/ 'normal' | /*小型 FAB*/ 'small' | /*大型 FAB*/ 'large';
/**
* Material Icons 图标名。也可以通过 `slot="icon"` 设置
*/
icon?: string;
/**
* 是否为展开状态
*/
extended: boolean;
private readonly rippleRef;
private readonly hasSlotController;
private readonly definedController;
protected get rippleElement(): Ripple;
/**
* extended 变更时,设置动画
*/
private onExtendedChange;
protected render(): TemplateResult;
private renderLabel;
private renderIcon;
private renderInner;
}
export interface FabEventMap {
focus: FocusEvent;
blur: FocusEvent;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-fab': Fab;
}
}

Some files were not shown because too many files have changed in this diff Show More