移动目录

This commit is contained in:
CrescentLeaf
2025-11-23 13:27:15 +08:00
parent f13623f4fc
commit 1cb8ac3fff
479 changed files with 49 additions and 49 deletions

View File

@@ -0,0 +1,46 @@
import '@mdui/jq/methods/addClass.js';
import '@mdui/jq/methods/children.js';
import '@mdui/jq/methods/css.js';
import '@mdui/jq/methods/data.js';
import '@mdui/jq/methods/each.js';
import '@mdui/jq/methods/filter.js';
import '@mdui/jq/methods/innerHeight.js';
import '@mdui/jq/methods/innerWidth.js';
import '@mdui/jq/methods/offset.js';
import '@mdui/jq/methods/on.js';
import '@mdui/jq/methods/prependTo.js';
import '@mdui/jq/methods/remove.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
/**
* 处理点击时的涟漪动画;及添加 hover、focused、dragged 的背景色
* 背景色通过在 .surface 元素上添加对应的 class 实现
* 阴影在 ripple-mixin 中处理,通过在 :host 元素上添加 attribute 供 CSS 选择器添加样式
*/
export declare class Ripple extends MduiElement<RippleEventMap> {
static styles: CSSResultGroup;
/**
* 是否禁用涟漪动画
*/
noRipple: boolean;
private hover;
private focused;
private dragged;
private readonly surfaceRef;
startPress(event?: Event): void;
endPress(): void;
startHover(): void;
endHover(): void;
startFocus(): void;
endFocus(): void;
startDrag(): void;
endDrag(): void;
protected render(): TemplateResult;
}
export interface RippleEventMap {
}
declare global {
interface HTMLElementTagNameMap {
'mdui-ripple': Ripple;
}
}

View File

@@ -0,0 +1,175 @@
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/addClass.js';
import '@mdui/jq/methods/children.js';
import '@mdui/jq/methods/css.js';
import '@mdui/jq/methods/data.js';
import '@mdui/jq/methods/each.js';
import '@mdui/jq/methods/filter.js';
import '@mdui/jq/methods/innerHeight.js';
import '@mdui/jq/methods/innerWidth.js';
import '@mdui/jq/methods/offset.js';
import '@mdui/jq/methods/on.js';
import '@mdui/jq/methods/prependTo.js';
import '@mdui/jq/methods/remove.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';
/**
* 处理点击时的涟漪动画;及添加 hover、focused、dragged 的背景色
* 背景色通过在 .surface 元素上添加对应的 class 实现
* 阴影在 ripple-mixin 中处理,通过在 :host 元素上添加 attribute 供 CSS 选择器添加样式
*/
let Ripple = class Ripple extends MduiElement {
constructor() {
super(...arguments);
/**
* 是否禁用涟漪动画
*/
this.noRipple = false;
this.hover = false;
this.focused = false;
this.dragged = false;
this.surfaceRef = createRef();
}
startPress(event) {
if (this.noRipple) {
return;
}
const $surface = $(this.surfaceRef.value);
const surfaceHeight = $surface.innerHeight();
const surfaceWidth = $surface.innerWidth();
// 点击位置坐标
let touchStartX;
let touchStartY;
if (!event) {
// 未传入事件对象,涟漪从中间扩散
touchStartX = surfaceWidth / 2;
touchStartY = surfaceHeight / 2;
}
else {
// 传入了事件对象,涟漪从点击位置扩散
const touchPosition = typeof TouchEvent !== 'undefined' &&
event instanceof TouchEvent &&
event.touches.length
? event.touches[0]
: event;
const offset = $surface.offset();
// 点击位置不在 surface 内,不执行
if (touchPosition.pageX < offset.left ||
touchPosition.pageX > offset.left + surfaceWidth ||
touchPosition.pageY < offset.top ||
touchPosition.pageY > offset.top + surfaceHeight) {
return;
}
touchStartX = touchPosition.pageX - offset.left;
touchStartY = touchPosition.pageY - offset.top;
}
// 涟漪直径
const diameter = Math.max(Math.pow(Math.pow(surfaceHeight, 2) + Math.pow(surfaceWidth, 2), 0.5), 48);
// 涟漪扩散动画
const translateX = `${-touchStartX + surfaceWidth / 2}px`;
const translateY = `${-touchStartY + surfaceHeight / 2}px`;
const translate = `translate3d(${translateX}, ${translateY}, 0) scale(1)`;
// 涟漪 DOM 元素
$('<div class="wave"></div>')
.css({
width: diameter,
height: diameter,
marginTop: -diameter / 2,
marginLeft: -diameter / 2,
left: touchStartX,
top: touchStartY,
})
.each((_, wave) => {
wave.style.setProperty('--mdui-comp-ripple-transition-x', translateX);
wave.style.setProperty('--mdui-comp-ripple-transition-y', translateY);
})
.prependTo(this.surfaceRef.value)
.each((_, wave) => wave.clientLeft) // 重绘
.css('transform', translate)
.on('animationend', function (e) {
const event = e;
if (event.animationName === 'mdui-comp-ripple-radius-in') {
$(this).data('filled', true); // 扩散动画完成后,添加标记
}
});
}
endPress() {
const $waves = $(this.surfaceRef.value)
.children()
.filter((_, wave) => !$(wave).data('removing'))
.data('removing', true);
const hideAndRemove = ($waves) => {
$waves
.addClass('out')
.each((_, wave) => wave.clientLeft) // 重绘
.on('animationend', function () {
$(this).remove();
});
};
// 扩散动画未完成,先完成扩散,再隐藏并移除
$waves
.filter((_, wave) => !$(wave).data('filled'))
.on('animationend', function (e) {
const event = e;
if (event.animationName === 'mdui-comp-ripple-radius-in') {
hideAndRemove($(this));
}
});
// 扩散动画已完成,直接隐藏并移除
hideAndRemove($waves.filter((_, wave) => !!$(wave).data('filled')));
}
startHover() {
this.hover = true;
}
endHover() {
this.hover = false;
}
startFocus() {
this.focused = true;
}
endFocus() {
this.focused = false;
}
startDrag() {
this.dragged = true;
}
endDrag() {
this.dragged = false;
}
render() {
return html `<div ${ref(this.surfaceRef)} class="surface ${classMap({
hover: this.hover,
focused: this.focused,
dragged: this.dragged,
})}"></div>`;
}
};
Ripple.styles = [componentStyle, style];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'no-ripple',
})
], Ripple.prototype, "noRipple", void 0);
__decorate([
state()
], Ripple.prototype, "hover", void 0);
__decorate([
state()
], Ripple.prototype, "focused", void 0);
__decorate([
state()
], Ripple.prototype, "dragged", void 0);
Ripple = __decorate([
customElement('mdui-ripple')
], Ripple);
export { Ripple };

View File

@@ -0,0 +1,28 @@
import '@mdui/jq/methods/index.js';
import './index.js';
import type { Ripple } from './index.js';
import type { Constructor } from '@lit/reactive-element/decorators/base.js';
import type { LitElement } from 'lit';
export declare class RippleMixinInterface {
noRipple: boolean;
protected getRippleIndex: () => number | undefined;
protected get rippleElement(): Ripple | Ripple[] | NodeListOf<Ripple>;
protected get rippleDisabled(): boolean | boolean[];
protected get rippleTarget(): HTMLElement | HTMLElement[] | NodeListOf<HTMLElement>;
protected startHover(event: PointerEvent): void;
protected endHover(event: PointerEvent): void;
}
/**
* hover, pressed, dragged 三个属性用于添加到 rippleTarget 属性指定的元素上,供 CSS 选择题添加样式
*
* TODO: dragged 功能
*
* NOTE:
* 不支持触控的屏幕上事件顺序为pointerdown -> (8ms) -> mousedown -> pointerup -> (1ms) -> mouseup -> (1ms) -> click
*
* 支持触控的屏幕上事件顺序为pointerdown -> (8ms) -> touchstart -> pointerup -> (1ms) -> touchend -> (5ms) -> mousedown -> mouseup -> click
* pointermove 比较灵敏,有可能触发了 pointermove 但没有触发 touchmove
*
* 支持触摸笔的屏幕上事件顺序为todo
*/
export declare const RippleMixin: <T extends Constructor<LitElement>>(superclass: T) => Constructor<RippleMixinInterface> & T;

View File

@@ -0,0 +1,257 @@
import { __decorate } from "tslib";
import { property } from 'lit/decorators.js';
import { $ } from '@mdui/jq/$.js';
import '@mdui/jq/methods/index.js';
import { isArrayLike } from '@mdui/jq/shared/helper.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import './index.js';
/**
* hover, pressed, dragged 三个属性用于添加到 rippleTarget 属性指定的元素上,供 CSS 选择题添加样式
*
* TODO: dragged 功能
*
* NOTE:
* 不支持触控的屏幕上事件顺序为pointerdown -> (8ms) -> mousedown -> pointerup -> (1ms) -> mouseup -> (1ms) -> click
*
* 支持触控的屏幕上事件顺序为pointerdown -> (8ms) -> touchstart -> pointerup -> (1ms) -> touchend -> (5ms) -> mousedown -> mouseup -> click
* pointermove 比较灵敏,有可能触发了 pointermove 但没有触发 touchmove
*
* 支持触摸笔的屏幕上事件顺序为todo
*/
export const RippleMixin = (superclass) => {
class Mixin extends superclass {
constructor() {
super(...arguments);
/**
* 是否禁用涟漪动画
*/
this.noRipple = false;
/**
* 当前激活的是第几个 <mdui-ripple>。仅一个组件中有多个 <mdui-ripple> 时可以使用该属性
* 若值为 undefined则组件中所有 <mdui-ripple> 都激活
*/
this.rippleIndex = undefined;
/**
* 获取当前激活的是第几个 <mdui-ripple>。仅一个组件中有多个 <mdui-ripple> 时可以使用该属性
* 若值为 undefined则组件中所有 <mdui-ripple> 都激活
* 可在子类中手动指定该方法,指定需要激活的 ripple
*/
this.getRippleIndex = () => this.rippleIndex;
}
/**
* 子类要添加该属性,指向 <mdui-ripple> 元素
* 如果一个组件中包含多个 <mdui-ripple> 元素,则这里可以是一个数组或 NodeList
*/
get rippleElement() {
throw new Error('Must implement rippleElement getter!');
}
/**
* 子类要实现该属性,表示是否禁用 ripple
* 如果一个组件中包含多个 <mdui-ripple> 元素,则这里可以是一个数组;也可以是单个值,同时控制多个 <mdui-ripple> 元素
*/
get rippleDisabled() {
throw new Error('Must implement rippleDisabled getter!');
}
/**
* 当前 <mdui-ripple> 元素相对于哪个元素存在,即 hover、pressed、dragged 属性要添加到哪个元素上,默认为 :host
* 如果需要修改该属性,则子类可以实现该属性
* 如果一个组件中包含多个 <mdui-ripple> 元素,则这里可以是一个数组;也可以是单个值,同时控制多个 <mdui-ripple> 元素
*/
get rippleTarget() {
return this;
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
const $rippleTarget = $(this.rippleTarget);
// 监听到事件时,是在第几个 <mdui-ripple> 上触发的事件,记录到 this.rippleIndex 中
const setRippleIndex = (event) => {
if (isArrayLike(this.rippleTarget)) {
this.rippleIndex = $rippleTarget.index(event.target);
}
};
const rippleTargetArr = isArrayLike(this.rippleTarget)
? this.rippleTarget
: [this.rippleTarget];
rippleTargetArr.forEach((rippleTarget) => {
rippleTarget.addEventListener('pointerdown', (event) => {
setRippleIndex(event);
this.startPress(event);
});
rippleTarget.addEventListener('pointerenter', (event) => {
setRippleIndex(event);
this.startHover(event);
});
rippleTarget.addEventListener('pointerleave', (event) => {
setRippleIndex(event);
this.endHover(event);
});
rippleTarget.addEventListener('focus', (event) => {
setRippleIndex(event);
this.startFocus();
});
rippleTarget.addEventListener('blur', (event) => {
setRippleIndex(event);
this.endFocus();
});
});
}
/**
* 若存在多个 <mdui-ripple>,但 rippleTarget 为同一个,则 hover 状态无法在多个 <mdui-ripple> 之间切换
* 所以把 startHover 和 endHover 设置为 protected供子类调用
* 子类中,在 getRippleIndex() 的返回值变更前调用 endHover(event),变更后调用 startHover(event)
*/
startHover(event) {
if (event.pointerType !== 'mouse' || this.isRippleDisabled()) {
return;
}
this.getRippleTarget().setAttribute('hover', '');
this.getRippleElement().startHover();
}
endHover(event) {
if (event.pointerType !== 'mouse' || this.isRippleDisabled()) {
return;
}
this.getRippleTarget().removeAttribute('hover');
this.getRippleElement().endHover();
}
/**
* 当前激活的 <mdui-ripple> 元素是否被禁用
*/
isRippleDisabled() {
const disabled = this.rippleDisabled;
if (!Array.isArray(disabled)) {
return disabled;
}
const rippleIndex = this.getRippleIndex();
if (rippleIndex !== undefined) {
return disabled[rippleIndex];
}
return disabled.length ? disabled[0] : false;
}
/**
* 获取当前激活的 <mdui-ripple> 元素实例
*/
getRippleElement() {
const ripple = this.rippleElement;
if (!isArrayLike(ripple)) {
return ripple;
}
const rippleIndex = this.getRippleIndex();
if (rippleIndex !== undefined) {
return ripple[rippleIndex];
}
return ripple[0];
}
/**
* 获取当前激活的 <mdui-ripple> 元素相对于哪个元素存在
*/
getRippleTarget() {
const target = this.rippleTarget;
if (!isArrayLike(target)) {
return target;
}
const rippleIndex = this.getRippleIndex();
if (rippleIndex !== undefined) {
return target[rippleIndex];
}
return target[0];
}
startFocus() {
if (this.isRippleDisabled()) {
return;
}
this.getRippleElement().startFocus();
}
endFocus() {
if (this.isRippleDisabled()) {
return;
}
this.getRippleElement().endFocus();
}
startPress(event) {
// 为鼠标时操作,仅响应鼠标左键点击
if (this.isRippleDisabled() || event.button) {
return;
}
const target = this.getRippleTarget();
target.setAttribute('pressed', '');
// 手指触摸触发涟漪
if (['touch', 'pen'].includes(event.pointerType)) {
let hidden = false;
// 手指触摸后,延迟一段时间触发涟漪,避免手指滑动时也触发涟漪
let timer = setTimeout(() => {
timer = 0;
this.getRippleElement().startPress(event);
}, 70);
const hideRipple = () => {
// 如果手指没有移动,且涟漪动画还没有开始,则开始涟漪动画
if (timer) {
clearTimeout(timer);
timer = 0;
this.getRippleElement().startPress(event);
}
if (!hidden) {
hidden = true;
this.endPress();
}
target.removeEventListener('pointerup', hideRipple);
target.removeEventListener('pointercancel', hideRipple);
};
// 手指移动后,移除涟漪动画
const touchMove = () => {
if (timer) {
clearTimeout(timer);
timer = 0;
}
target.removeEventListener('touchmove', touchMove);
};
// pointermove 事件过于灵敏,可能在未触发 touchmove 的情况下,触发了 pointermove 事件,导致正常的点击操作没有显示涟漪
// 因此这里监听 touchmove 事件
target.addEventListener('touchmove', touchMove);
target.addEventListener('pointerup', hideRipple);
target.addEventListener('pointercancel', hideRipple);
}
// 鼠标点击触发涟漪,点击后立即触发涟漪(仅鼠标左键能触发涟漪)
if (event.pointerType === 'mouse' && event.button === 0) {
const hideRipple = () => {
this.endPress();
target.removeEventListener('pointerup', hideRipple);
target.removeEventListener('pointercancel', hideRipple);
target.removeEventListener('pointerleave', hideRipple);
};
this.getRippleElement().startPress(event);
target.addEventListener('pointerup', hideRipple);
target.addEventListener('pointercancel', hideRipple);
target.addEventListener('pointerleave', hideRipple);
}
}
endPress() {
if (this.isRippleDisabled()) {
return;
}
this.getRippleTarget().removeAttribute('pressed');
this.getRippleElement().endPress();
}
startDrag() {
if (this.isRippleDisabled()) {
return;
}
this.getRippleElement().startDrag();
}
endDrag() {
if (this.isRippleDisabled()) {
return;
}
this.getRippleElement().endDrag();
}
}
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'no-ripple',
})
], Mixin.prototype, "noRipple", void 0);
return Mixin;
};

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:absolute;top:0;left:0;display:block;width:100%;height:100%;overflow:hidden;pointer-events:none}.surface{position:absolute;top:0;left:0;width:100%;height:100%;transition-duration:280ms;transition-property:background-color;pointer-events:none;transition-timing-function:var(--mdui-motion-easing-standard)}.hover{background-color:rgba(var(--mdui-comp-ripple-state-layer-color,var(--mdui-color-on-surface)),var(--mdui-state-layer-hover))}:host-context([focus-visible]) .focused{background-color:rgba(var(--mdui-comp-ripple-state-layer-color,var(--mdui-color-on-surface)),var(--mdui-state-layer-focus))}.dragged{background-color:rgba(var(--mdui-comp-ripple-state-layer-color,var(--mdui-color-on-surface)),var(--mdui-state-layer-dragged))}.wave{position:absolute;z-index:1;background-color:rgb(var(--mdui-comp-ripple-state-layer-color,var(--mdui-color-on-surface)));border-radius:50%;transform:translate3d(0,0,0) scale(.4);opacity:0;animation:225ms ease 0s 1 normal forwards running mdui-comp-ripple-radius-in,75ms ease 0s 1 normal forwards running mdui-comp-ripple-opacity-in;pointer-events:none}.out{transform:translate3d(var(--mdui-comp-ripple-transition-x,0),var(--mdui-comp-ripple-transition-y,0),0) scale(1);animation:150ms ease 0s 1 normal none running mdui-comp-ripple-opacity-out}@keyframes mdui-comp-ripple-radius-in{from{transform:translate3d(0,0,0) scale(.4);animation-timing-function:var(--mdui-motion-easing-standard)}to{transform:translate3d(var(--mdui-comp-ripple-transition-x,0),var(--mdui-comp-ripple-transition-y,0),0) scale(1)}}@keyframes mdui-comp-ripple-opacity-in{from{opacity:0;animation-timing-function:linear}to{opacity:var(--mdui-state-layer-pressed)}}@keyframes mdui-comp-ripple-opacity-out{from{animation-timing-function:linear;opacity:var(--mdui-state-layer-pressed)}to{opacity:0}}`;