328 lines
6.9 KiB
JavaScript
328 lines
6.9 KiB
JavaScript
/**
|
|
* {进度条}
|
|
* @author chensbox<chensbox@foxmail.com>
|
|
* @date 2023/04/28 16:14:10
|
|
*/
|
|
|
|
import { css, html, Component, bind, unbind, range, offset } from 'wkit'
|
|
|
|
class Slider extends Component {
|
|
static props = {
|
|
value: {
|
|
type: Number,
|
|
default: 0,
|
|
observer(val) {
|
|
this.value = this.#fix(val)
|
|
}
|
|
},
|
|
step: {
|
|
type: Number,
|
|
default: 1,
|
|
observer(v) {
|
|
v += ''
|
|
this.#decimal = v.split('.').pop().length
|
|
}
|
|
},
|
|
min: {
|
|
type: Number,
|
|
default: 0,
|
|
observer(v) {
|
|
this.#updateRange()
|
|
}
|
|
},
|
|
max: {
|
|
type: Number,
|
|
default: 100,
|
|
observer(v) {
|
|
this.#updateRange()
|
|
}
|
|
},
|
|
vertical: false,
|
|
disabled: false,
|
|
lazy: 'num!0'
|
|
}
|
|
|
|
#len = 100
|
|
#stops = 10 // 断点数, 最大只显示10个断点
|
|
#point = 10 // 每个断点的百分比
|
|
#value = 0 // 内部的值, 固定使用 0-100范围的值
|
|
#decimal = 0 //小数位
|
|
|
|
#stamp = 0
|
|
|
|
static styles = [
|
|
css`
|
|
:host {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.container {
|
|
--line-width: var(--wc-slider-line-width, 6px);
|
|
--base-color: var(--wc-slider-inactive-color, var(--color-plain-2));
|
|
--active-color: var(--wc-slider-active-color, var(--color-teal-1));
|
|
position: relative;
|
|
width: 100%;
|
|
height: var(--line-width);
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.slider-bar {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: var(--line-width);
|
|
border-radius: 32px;
|
|
background: var(--base-color);
|
|
i {
|
|
display: none;
|
|
}
|
|
}
|
|
`,
|
|
// vertical
|
|
css`
|
|
:host([vertical]) {
|
|
width: 32px;
|
|
height: 100%;
|
|
justify-content: center;
|
|
|
|
.container {
|
|
width: var(--line-width);
|
|
height: 100%;
|
|
min-height: 32px;
|
|
}
|
|
|
|
.slider-bar {
|
|
width: var(--line-width);
|
|
height: 100%;
|
|
}
|
|
|
|
.thumb {
|
|
top: auto;
|
|
transform: translate(calc(var(--line-width) / 2 - 16px), 16px);
|
|
}
|
|
}
|
|
`,
|
|
|
|
// stops
|
|
css`
|
|
:host([show-stops]) {
|
|
.slider-bar {
|
|
i {
|
|
position: absolute;
|
|
display: block;
|
|
width: var(--line-width);
|
|
height: var(--line-width);
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
// thumb
|
|
css`
|
|
.thumb {
|
|
position: absolute;
|
|
left: 0;
|
|
top: calc((32px - var(--line-width)) / -2);
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 7px;
|
|
transform: translateX(-16px);
|
|
|
|
&::before {
|
|
display: block;
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 2px solid var(--active-color);
|
|
border-radius: 50%;
|
|
content: '';
|
|
background: #fff;
|
|
cursor: grab;
|
|
transition: transform 0.1s ease-in-out;
|
|
}
|
|
|
|
.value {
|
|
display: none;
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 32px;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
background: var(--color-dark-2);
|
|
color: #fff;
|
|
transform: translateX(-50%);
|
|
|
|
&::after {
|
|
display: block;
|
|
position: absolute;
|
|
left: 50%;
|
|
margin-left: -3px;
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--color-dark-2);
|
|
content: '';
|
|
transform: rotate(45deg);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
&::before {
|
|
transform: scale(1.15);
|
|
}
|
|
|
|
.value {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
&:active {
|
|
&::before {
|
|
cursor: grabbing;
|
|
}
|
|
}
|
|
}
|
|
:host([hide-thumb]) {
|
|
.thumb::before {
|
|
display: none;
|
|
}
|
|
}
|
|
`,
|
|
// disabled
|
|
css`
|
|
:host([disabled]) {
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
|
|
.thumb::before {
|
|
cursor: not-allowed;
|
|
transform: unset;
|
|
}
|
|
}
|
|
`
|
|
]
|
|
|
|
#updateRange() {
|
|
let { min, max, step } = this
|
|
if (min >= max) {
|
|
throw new Error('Slider props "min >= max"')
|
|
}
|
|
let len = max - min
|
|
this.#stops = ~~(len / step)
|
|
if (this.#stops > 10) {
|
|
this.#stops = 10
|
|
}
|
|
this.#len = len
|
|
this.#point = +(100 / this.#stops).toFixed(2)
|
|
}
|
|
|
|
#fix(val) {
|
|
let { min, max, step } = this
|
|
let fix
|
|
val = Math.max(min, Math.min(val, max))
|
|
val = +val.toFixed(this.#decimal)
|
|
|
|
fix = +(val % step).toFixed(this.#decimal)
|
|
if (fix !== step) {
|
|
if (fix < step / 2) {
|
|
val -= fix
|
|
} else {
|
|
val = val - fix + step
|
|
}
|
|
}
|
|
|
|
this.#value = Math.round(((val - min) * 100) / (max - min))
|
|
return val
|
|
}
|
|
|
|
#seek(ev) {
|
|
let { clientWidth: w, clientHeight: h } = ev.target
|
|
let { min, max, step, lazy } = this
|
|
let val = 0
|
|
let now = Date.now()
|
|
|
|
if (this.vertical) {
|
|
val = (h - ev.y) / h
|
|
} else {
|
|
val = ev.x / w
|
|
}
|
|
val *= this.#len
|
|
val += min
|
|
|
|
this.value = this.#fix(val)
|
|
|
|
// 并发拦截
|
|
if (lazy > 0 && now - this.#stamp < lazy) {
|
|
return
|
|
}
|
|
this.#stamp = now
|
|
this.$emit('input')
|
|
this.$emit('change')
|
|
}
|
|
|
|
#mousedown(ev) {
|
|
let target = ev.currentTarget
|
|
let doc = document
|
|
let opts = { once: true }
|
|
let { left, top } = offset(target)
|
|
let { scrollLeft, scrollTop } = document.documentElement
|
|
let _x = left - scrollLeft
|
|
let _y = top - scrollTop
|
|
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
|
|
this.#seek({ target, x: ev.x - _x, y: ev.y - _y })
|
|
|
|
let callback = bind(doc, 'mousemove', ({ x, y }) => {
|
|
x -= _x
|
|
y -= _y
|
|
this.#seek({ target, x, y })
|
|
})
|
|
|
|
bind(doc, 'mouseup', _ => unbind(doc, 'mousemove', callback), opts)
|
|
}
|
|
|
|
get #activeStyle() {
|
|
let v = this.#value
|
|
let deg = this.vertical ? 0 : '90deg'
|
|
return `background:linear-gradient(${deg}, var(--active-color) ${v}%, transparent ${v}%)`
|
|
}
|
|
|
|
get #thumbStyle() {
|
|
let v = this.#value
|
|
if (this.vertical) {
|
|
return `bottom:${v}%`
|
|
}
|
|
return `left:${v}%`
|
|
}
|
|
|
|
render() {
|
|
let n = this.hasAttribute('show-stops') ? this.#stops : 1
|
|
|
|
return html`
|
|
<main class="container" @mousedown=${this.#mousedown}>
|
|
<div class="slider-bar">
|
|
${range(1, n).map(i => {
|
|
return html`<i style="left:${i * this.#point}%"></i>`
|
|
})}
|
|
</div>
|
|
<div class="slider-bar" style=${this.#activeStyle}></div>
|
|
|
|
<div class="thumb" style=${this.#thumbStyle}>
|
|
<mark class="value">${this.value}</mark>
|
|
</div>
|
|
</main>
|
|
`
|
|
}
|
|
}
|
|
|
|
Slider.reg('slider')
|
JavaScript
98.9%
CSS
1.1%