移动目录

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,60 @@
import { SliderBase } from './slider-base.js';
import type { Ripple } from '../ripple/index.js';
import type { FormControl } from '@mdui/jq/shared/form.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
/**
* @summary 滑块组件
*
* ```html
* <mdui-slider></mdui-slider>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 在值发生变更,且失去焦点时,将触发该事件
* @event input - 值变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @csspart track-inactive - 未激活状态的轨道
* @csspart track-active - 已激活状态的轨道
* @csspart handle - 操作杆
* @csspart label 提示文本
* @csspart tickmark - 刻度标记
*/
export declare class Slider extends SliderBase<SliderEventMap> implements FormControl {
static styles: CSSResultGroup;
/**
* 滑块的值,将于表单数据一起提交
*/
value: number;
/**
* 默认值。在重置表单时,将重置为该默认值。该属性只能通过 JavaScript 属性设置
*/
defaultValue: number;
private readonly rippleRef;
private readonly handleRef;
private readonly formController;
protected get rippleElement(): Ripple;
private onValueChange;
connectedCallback(): void;
protected firstUpdated(changedProperties: PropertyValues): void;
/**
* <input /> 用于提供拖拽操作
* <input class="invalid" /> 用于提供 html5 自带的表单错误提示
*/
protected render(): TemplateResult;
private updateStyle;
private onInput;
}
export interface SliderEventMap {
focus: FocusEvent;
blur: FocusEvent;
change: CustomEvent<void>;
input: Event;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-slider': Slider;
}
}

View File

@@ -0,0 +1,121 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { live } from 'lit/directives/live.js';
import { map } from 'lit/directives/map.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.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 { SliderBase } from './slider-base.js';
import { style } from './style.js';
/**
* @summary 滑块组件
*
* ```html
* <mdui-slider></mdui-slider>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event change - 在值发生变更,且失去焦点时,将触发该事件
* @event input - 值变更时触发
* @event invalid - 表单字段验证未通过时触发
*
* @csspart track-inactive - 未激活状态的轨道
* @csspart track-active - 已激活状态的轨道
* @csspart handle - 操作杆
* @csspart label 提示文本
* @csspart tickmark - 刻度标记
*/
let Slider = class Slider extends SliderBase {
constructor() {
super(...arguments);
/**
* 滑块的值,将于表单数据一起提交
*/
this.value = 0;
/**
* 默认值。在重置表单时,将重置为该默认值。该属性只能通过 JavaScript 属性设置
*/
this.defaultValue = 0;
this.rippleRef = createRef();
this.handleRef = createRef();
this.formController = new FormController(this);
}
get rippleElement() {
return this.rippleRef.value;
}
async onValueChange() {
this.value = this.fixValue(this.value);
// reset 引起的值变更,不执行验证;直接修改值引起的变更,需要进行验证
const form = this.formController.getForm();
if (form && formResets.get(form)?.has(this)) {
this.invalid = false;
formResets.get(form).delete(this);
}
else {
await this.updateComplete;
this.invalid = !this.inputRef.value.checkValidity();
}
this.updateStyle();
}
connectedCallback() {
super.connectedCallback();
this.value = this.fixValue(this.value);
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
const onTouchStart = () => {
if (!this.disabled) {
this.labelVisible = true;
}
};
const onTouchEnd = () => {
if (!this.disabled) {
this.labelVisible = false;
}
};
this.addEventListener('touchstart', onTouchStart);
this.addEventListener('mousedown', onTouchStart);
this.addEventListener('touchend', onTouchEnd);
this.addEventListener('mouseup', onTouchEnd);
this.updateStyle();
}
/**
* <input /> 用于提供拖拽操作
* <input class="invalid" /> 用于提供 html5 自带的表单错误提示
*/
render() {
return html `<label class="${classMap({ invalid: this.invalid })}"><input ${ref(this.inputRef)} type="range" step="${this.step}" min="${this.min}" max="${this.max}" ?disabled="${this.disabled}" .value="${live(this.value.toString())}" @input="${this.onInput}" @change="${this.onChange}"><div part="track-inactive" class="track-inactive"></div><div ${ref(this.trackActiveRef)} part="track-active" class="track-active"></div><div ${ref(this.handleRef)} part="handle" class="handle"><div class="elevation"></div><mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.renderLabel(this.value)}</div>${when(this.tickmarks, () => map(this.getCandidateValues(), (value) => html `<div part="tickmark" class="tickmark ${classMap({ active: value < this.value })}" style="${styleMap({
left: `${((value - this.min) / this.max) * 100}%`,
display: value === this.value ? 'none' : 'block',
})}"></div>`))}</label>`;
}
updateStyle() {
const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
this.trackActiveRef.value.style.width = `${percent}%`;
this.handleRef.value.style.left = `${percent}%`;
}
onInput() {
this.value = parseFloat(this.inputRef.value.value);
this.updateStyle();
}
};
Slider.styles = [SliderBase.styles, style];
__decorate([
property({ type: Number })
], Slider.prototype, "value", void 0);
__decorate([
defaultValue()
], Slider.prototype, "defaultValue", void 0);
__decorate([
watch('value', true)
], Slider.prototype, "onValueChange", null);
Slider = __decorate([
customElement('mdui-slider')
], Slider);
export { Slider };

View File

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

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const sliderBaseStyle = css `:host{position:relative;display:block;width:100%;-webkit-tap-highlight-color:transparent;height:2.5rem;padding:0 1.25rem}label{position:relative;display:block;width:100%;height:100%}input[type=range]{position:absolute;inset:0;z-index:4;height:100%;cursor:pointer;opacity:0;appearance:none;width:calc(100% + 20rem * 2 / 16);margin:0 -1.25rem;padding:0 .75rem}:host([disabled]:not([disabled=false i])) input[type=range]{cursor:not-allowed}.track-active,.track-inactive{position:absolute;top:50%;height:.25rem;margin-top:-.125rem}.track-inactive{left:-.125rem;right:-.125rem;border-radius:var(--mdui-shape-corner-full);background-color:rgb(var(--mdui-color-surface-container-highest))}.invalid .track-inactive{background-color:rgba(var(--mdui-color-error),.12)}:host([disabled]:not([disabled=false i])) .track-inactive{background-color:rgba(var(--mdui-color-on-surface),.12)}.track-active{background-color:rgb(var(--mdui-color-primary))}.invalid .track-active{background-color:rgb(var(--mdui-color-error))}:host([disabled]:not([disabled=false i])) .track-active{background-color:rgba(var(--mdui-color-on-surface),.38)}.handle{position:absolute;top:50%;transform:translate(-50%);cursor:pointer;z-index:2;width:2.5rem;height:2.5rem;margin-top:-1.25rem;--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}.invalid .handle{--mdui-comp-ripple-state-layer-color:var(--mdui-color-error)}.handle .elevation,.handle::before{position:absolute;display:block;content:' ';left:.625rem;top:.625rem;width:1.25rem;height:1.25rem;border-radius:var(--mdui-shape-corner-full)}.handle .elevation{background-color:rgb(var(--mdui-color-primary));box-shadow:var(--mdui-elevation-level1)}.invalid .handle .elevation{background-color:rgb(var(--mdui-color-error))}:host([disabled]:not([disabled=false i])) .handle .elevation{background-color:rgba(var(--mdui-color-on-surface),.38);box-shadow:var(--mdui-elevation-level0)}.handle::before{background-color:rgb(var(--mdui-color-background))}.handle mdui-ripple{border-radius:var(--mdui-shape-corner-full)}.label{position:absolute;left:50%;transform:translateX(-50%) scale(0);transform-origin:center bottom;display:flex;align-items:center;justify-content:center;cursor:default;white-space:nowrap;-webkit-user-select:none;user-select:none;pointer-events:none;transition:transform var(--mdui-motion-duration-short2) var(--mdui-motion-easing-standard);bottom:2.5rem;min-width:1.75rem;height:1.75rem;padding:.375rem .5rem;border-radius:var(--mdui-shape-corner-full);color:rgb(var(--mdui-color-on-primary));font-size:var(--mdui-typescale-label-medium-size);font-weight:var(--mdui-typescale-label-medium-weight);letter-spacing:var(--mdui-typescale-label-medium-tracking);line-height:var(--mdui-typescale-label-medium-line-height);background-color:rgb(var(--mdui-color-primary))}.invalid .label{color:rgb(var(--mdui-color-on-error));background-color:rgb(var(--mdui-color-error))}.label::after{content:' ';position:absolute;z-index:-1;transform:rotate(45deg);width:.875rem;height:.875rem;bottom:-.125rem;background-color:rgb(var(--mdui-color-primary))}.invalid .label::after{background-color:rgb(var(--mdui-color-error))}.label-visible{transform:translateX(-50%) scale(1);transition:transform var(--mdui-motion-duration-short4) var(--mdui-motion-easing-standard)}.tickmark{position:absolute;top:50%;transform:translate(-50%);width:.125rem;height:.125rem;margin-top:-.0625rem;border-radius:var(--mdui-shape-corner-full);background-color:rgba(var(--mdui-color-on-surface-variant),.38)}.invalid .tickmark{background-color:rgba(var(--mdui-color-error),.38)}.tickmark.active{background-color:rgba(var(--mdui-color-on-primary),.38)}.invalid .tickmark.active{background-color:rgba(var(--mdui-color-on-error),.38)}:host([disabled]:not([disabled=false i])) .tickmark{background-color:rgba(var(--mdui-color-on-surface),.38)}`;

View File

@@ -0,0 +1,96 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
import type { Ref } from 'lit/directives/ref.js';
declare const SliderBase_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;
export declare class SliderBase<E> extends SliderBase_base<E> {
static styles: CSSResultGroup;
/**
* 滑块的最小值,默认为 `0`
*/
min: number;
/**
* 滑块的最大值,默认为 `100`
*/
max: number;
/**
* 步进间隔,默认为 `1`
*/
step: number;
/**
* 是否添加刻度标记
*/
tickmarks: boolean;
/**
* 是否隐藏文本提示
*/
nolabel: boolean;
/**
* 是否被禁用
*/
disabled: boolean;
/**
* 关联的 `<form>` 元素。此属性值应为同一页面中的一个 `<form>` 元素的 `id`。
*
* 如果未指定此属性,则该元素必须是 `<form>` 元素的子元素。通过此属性,你可以将元素放置在页面的任何位置,而不仅仅是 `<form>` 元素的子元素。
*/
form?: string;
/**
* 滑块的名称,该名称将与表单数据一起提交
*/
name: string;
/**
* 是否验证未通过
*
* 该验证为根据是否通过 `setCustomValidity` 方法设置了值,来判断是否验证通过
*/
protected invalid: boolean;
protected labelVisible: boolean;
protected readonly inputRef: Ref<HTMLInputElement>;
protected readonly trackActiveRef: Ref<HTMLElement>;
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity(): ValidityState;
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage(): string;
protected get rippleDisabled(): boolean;
protected get focusElement(): HTMLElement;
protected get focusDisabled(): boolean;
/**
* 用于自定义标签的显示格式的函数。函数参数为滑块的当前值,返回值为期望显示的文本。
*/
labelFormatter: (value: number) => string;
private onDisabledChange;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity(): boolean;
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message: string): void;
/**
* value 不在 min、max 或 step 的限制范围内时,修正 value 的值
*/
protected fixValue(value: number): number;
/**
* 获取候选值组成的数组
*/
protected getCandidateValues(): number[];
/**
* 渲染浮动标签
*/
protected renderLabel(value: number): TemplateResult;
protected onChange(): void;
}
export {};

View File

@@ -0,0 +1,214 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef } from 'lit/directives/ref.js';
import { when } from 'lit/directives/when.js';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { watch } from '@mdui/shared/decorators/watch.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { FocusableMixin } from '@mdui/shared/mixins/focusable.js';
import { RippleMixin } from '../ripple/ripple-mixin.js';
import { sliderBaseStyle } from './slider-base-style.js';
export class SliderBase extends RippleMixin(FocusableMixin(MduiElement)) {
constructor() {
super(...arguments);
/**
* 滑块的最小值,默认为 `0`
*/
this.min = 0;
/**
* 滑块的最大值,默认为 `100`
*/
this.max = 100;
/**
* 步进间隔,默认为 `1`
*/
this.step = 1;
/**
* 是否添加刻度标记
*/
this.tickmarks = false;
/**
* 是否隐藏文本提示
*/
this.nolabel = false;
/**
* 是否被禁用
*/
this.disabled = false;
/**
* 滑块的名称,该名称将与表单数据一起提交
*/
this.name = '';
/**
* 是否验证未通过
*
* 该验证为根据是否通过 `setCustomValidity` 方法设置了值,来判断是否验证通过
*/
this.invalid = false;
// 按下时label 可见
this.labelVisible = false;
this.inputRef = createRef();
this.trackActiveRef = createRef();
/**
* 用于自定义标签的显示格式的函数。函数参数为滑块的当前值,返回值为期望显示的文本。
*/
this.labelFormatter = (value) => value.toString();
}
/**
* 表单验证状态对象,具体参见 [`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 rippleDisabled() {
return this.disabled;
}
get focusElement() {
return this.inputRef.value;
}
get focusDisabled() {
return this.disabled;
}
onDisabledChange() {
this.invalid = !this.inputRef.value.checkValidity();
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity() {
const valid = this.inputRef.value.checkValidity();
if (!valid) {
// @ts-ignore
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
}
return valid;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity() {
this.invalid = !this.inputRef.value.reportValidity();
if (this.invalid) {
// @ts-ignore
const eventProceeded = this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
if (!eventProceeded) {
// 调用了 preventDefault() 时,隐藏默认的表单错误提示
this.blur();
this.focus();
}
}
return !this.invalid;
}
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message) {
this.inputRef.value.setCustomValidity(message);
this.invalid = !this.inputRef.value.checkValidity();
}
/**
* value 不在 min、max 或 step 的限制范围内时,修正 value 的值
*/
fixValue(value) {
const { min, max, step } = this;
// 确保 value 在 min 和 max 范围内
value = Math.min(Math.max(value, min), max);
// 计算最接近 value 的 step 值
const steps = Math.round((value - min) / step);
let fixedValue = min + steps * step;
// 如果修正后的值超出最大值,则减去一个 step
if (fixedValue > max) {
fixedValue -= step;
}
return fixedValue;
}
/**
* 获取候选值组成的数组
*/
getCandidateValues() {
return Array.from({ length: this.max - this.min + 1 }, (_, index) => index + this.min).filter((value) => !((value - this.min) % this.step));
}
/**
* 渲染浮动标签
*/
renderLabel(value) {
return when(!this.nolabel, () => html `<div part="label" class="label ${classMap({ 'label-visible': this.labelVisible })}">${this.labelFormatter(value)}</div>`);
}
onChange() {
// @ts-ignore
this.emit('change');
}
}
SliderBase.styles = [
componentStyle,
sliderBaseStyle,
];
__decorate([
property({ type: Number, reflect: true })
], SliderBase.prototype, "min", void 0);
__decorate([
property({ type: Number, reflect: true })
], SliderBase.prototype, "max", void 0);
__decorate([
property({ type: Number, reflect: true })
], SliderBase.prototype, "step", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], SliderBase.prototype, "tickmarks", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], SliderBase.prototype, "nolabel", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], SliderBase.prototype, "disabled", void 0);
__decorate([
property({ reflect: true })
], SliderBase.prototype, "form", void 0);
__decorate([
property({ reflect: true })
], SliderBase.prototype, "name", void 0);
__decorate([
state()
], SliderBase.prototype, "invalid", void 0);
__decorate([
state()
], SliderBase.prototype, "labelVisible", void 0);
__decorate([
property({ attribute: false })
], SliderBase.prototype, "labelFormatter", void 0);
__decorate([
watch('disabled', true)
], SliderBase.prototype, "onDisabledChange", null);

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 `.track-active{left:-.125rem;border-radius:var(--mdui-shape-corner-full) 0 0 var(--mdui-shape-corner-full)}`;