优化进度条组件;重构滑块组件

master
yutent 2023-11-23 18:59:28 +08:00
parent 0cde6f7c02
commit 8461d75a69
2 changed files with 224 additions and 281 deletions

View File

@ -185,11 +185,12 @@ class Progress extends Component {
} }
#drawLine(v) { #drawLine(v) {
let l = v < 2 ? 2 : v > 98 ? 98 : v
return html` return html`
<div class="progress-bar"> <div class="progress-bar">
<mark class="thumb" style=${styleMap({ width: v + '%' })}></mark> <mark class="thumb" style=${styleMap({ width: v + '%' })}></mark>
</div> </div>
<div class="progress-value" style=${styleMap({ left: v + '%' })}> <div class="progress-value" style=${styleMap({ left: l + '%' })}>
${v}% ${v}%
</div> </div>
` `

View File

@ -1,10 +1,10 @@
/** /**
* {} * {进度条}
* @author yutent<yutent.io@gmail.com> * @author chensbox<chensbox@foxmail.com>
* @date 2023/03/21 16:14:10 * @date 2023/04/28 16:14:10
*/ */
import { css, bind, unbind, html, Component } from 'wkit' import { css, html, Component, range } from 'wkit'
class Slider extends Component { class Slider extends Component {
static props = { static props = {
@ -12,325 +12,267 @@ class Slider extends Component {
type: Number, type: Number,
default: 0, default: 0,
observer(val) { observer(val) {
this.$bar && this.initValue(val) this.value = this.#fix(val)
} }
}, },
type: 'info', step: {
max: 100, type: Number,
min: 0, default: 1,
step: 1, observer(v) {
disabled: false, v += ''
readonly: false, this.#decimal = v.split('.').pop().length
vertical: false }
},
min: {
type: Number,
default: 0,
observer(v) {
this.#updateRange()
}
},
max: {
type: Number,
default: 100,
observer(v) {
this.#updateRange()
}
},
vertical: false,
disabled: false
} }
#len = 100
#stops = 10 // 断点数, 最大只显示10个断点
#point = 10 // 每个断点的百分比
#value = 0 // 内部的值, 固定使用 0-100范围的值
#decimal = 0 //小数位
static styles = [ static styles = [
css` css`
:host { :host {
display: inline-block; display: flex;
align-items: center;
width: 100%; width: 100%;
height: 38px; min-height: 32px;
} }
.slider {
.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; position: relative;
display: flex; width: 100%;
align-items: center; height: var(--line-width);
height: 100%; -webkit-user-select: none;
}
.runway {
flex: 1;
position: relative;
height: 6px;
// width: 100%;
background: #e4e7ed;
border-radius: 99rem;
cursor: pointer;
.bar {
flex: 1;
pointer-events: none;
position: absolute;
height: 100%;
width: 0%;
border-radius: 99rem;
background: var(--color-blue-2, #409eff);
}
}
.dot-wrapper {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: 36px;
width: 36px;
top: 50%;
left: 0%;
cursor: grab;
transform: translate(-50%, -50%);
.dot {
height: 20px;
width: 20px;
border-radius: 50%;
border: 2px solid var(--color-blue-2, #409eff);
background: #fff;
transition: transform 0.2s ease-in-out;
&:hover {
transform: scale(1.2);
}
}
}
.tips {
opacity: 0;
pointer-events: none;
user-select: none; user-select: none;
}
.slider-bar {
position: absolute; position: absolute;
top: -100%; left: 0;
border-radius: 4px; top: 0;
padding: 10px; width: 100%;
z-index: 2000; height: var(--line-width);
font-size: 12px; border-radius: 32px;
line-height: 1.2; background: var(--base-color);
min-width: 10px; i {
white-space: nowrap; display: none;
color: #fff;
background: #303133;
transform: translateX(-50%);
transition: opacity 0.2s ease-in-out;
&.show {
opacity: 1;
}
&:after {
position: absolute;
border: 6px solid transparent;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
content: '';
border-top-color: #303133;
} }
} }
`, `,
//状态 // vertical
css` css`
:host([vertical]) { :host([vertical]) {
display: inline-block; width: auto;
width: 38px; height: 100%;
height: 250px; align-items: flex-end;
.slider {
justify-content: center;
.runway {
flex: 0 0 auto;
display: flex;
flex-direction: column-reverse;
width: 6px;
height: 100%;
cursor: pointer; .container {
.bar { width: var(--line-width);
height: 0%; height: 100%;
width: 100%; min-height: 32px;
}
.dot-wrapper {
left: 50%;
top: 100%;
transform: translate(-50%, -50%);
}
}
} }
.tips {
top: 100%;
left: 50%;
transform: translate(-50%, -180%);
}
}
:host([hide-tooltip]) .tips {
visibility: hidden;
}
:host([loading]), .slider-bar {
:host([disabled]) { width: var(--line-width);
pointer-events: none; height: 100%;
cursor: not-allowed; }
opacity: 0.6;
.thumb {
top: auto;
transform: translate(calc(var(--line-width) / 2 - 16px), 16px);
}
} }
`, `,
//配色
css`
$colors: (
primary: 'teal',
info: 'blue',
success: 'green',
warning: 'orange',
danger: 'red',
secondary: 'dark',
help: 'grey'
);
@loop $t, $c in $colors { // stops
:host([type='#{$t}']) { css`
.bar { :host([show-stops]) {
background-color: var(--color-#{$c}-2); .slider-bar {
i {
position: absolute;
display: block;
width: var(--line-width);
height: var(--line-width);
border-radius: 50%;
background: #fff;
transform: translateX(-50%);
} }
.dot { }
border-color: var(--color-#{$c}-2); }
`,
// thumb
css`
.thumb {
position: absolute;
left: 0;
top: calc((32px - var(--line-width)) / -2);
z-index: 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;
} }
} }
} }
` `
] ]
progress = 0
mounted() { #updateRange() {
this.$bar = this.$refs.bar let { min, max, step } = this
this.$dotWrap = this.$refs.dotWrap if (min >= max) {
this.$runway = this.$refs.runway throw new Error('Slider props "min >= max"')
this.$tips = this.$refs.tips }
this.accuracy = this.step >= 1 ? 0 : String(this.step).split('.')[1].length let len = max - min
this.initValue(this.value) this.#stops = ~~(len / step)
if (this.#stops > 10) {
this.#stops = 10
}
this.#len = len
this.#point = +(100 / this.#stops).toFixed(2)
} }
onMousedown(e) {
let {
value: preValue,
step,
max,
min,
progress,
vertical,
disabled,
readOnly,
accuracy
} = this
if (disabled || readOnly) {
return
}
this.$tips.classList.toggle('show')
preValue = +preValue || min
let start = vertical ? e.clientY : e.clientX #fix(val) {
let onMousemove = bind(document, 'mousemove', e => { let { min, max, step } = this
e.preventDefault() let fix
let distance = (vertical ? e.clientY : e.clientX) - start val = Math.max(min, Math.min(val, max))
let scale = val = +val.toFixed(this.#decimal)
(distance /
(vertical ? this.$runway.clientHeight : this.$runway.clientWidth)) *
(vertical ? -1 : 1)
let diff = fix = +(val % step).toFixed(this.#decimal)
accuracy === 0 ? Math.round(scale * (max - min)) : scale * (max - min) if (fix !== step) {
let newProgress = progress + Math.floor(scale * 100) if (fix < step / 2) {
let newValue = val -= fix
accuracy === 0 ? Math.floor(preValue + diff) : preValue + diff
newValue = Math.max(newValue, min)
newValue = Math.min(newValue, max)
if (accuracy === 0) {
if (newValue % step) {
return
}
} else { } else {
let _newValue = Math.round(newValue * Math.pow(10, accuracy)) val = val - fix + step
let _step = step * Math.pow(10, accuracy)
if (_newValue % _step) {
return
}
}
this.value = newValue.toFixed(accuracy)
requestAnimationFrame(() => {
this.setProgress(vertical ? 100 - newProgress : newProgress)
})
this.$emit('input')
})
let onMouseup = bind(document, 'mouseup', () => {
unbind(document, 'mousemove', onMousemove)
unbind(document, 'mouseup', onMouseup)
this.$tips.classList.toggle('show')
this.$emit('change')
})
}
onClick(e) {
if (e.target !== this.$refs.runway) {
return
}
let { max, min, step, vertical, disabled, readOnly, accuracy } = this
if (disabled || readOnly) {
return
}
let { clientWidth, clientHeight } = e.target
let { offsetX, offsetY } = e
let range = max - min
let scale =
(vertical ? offsetY : offsetX) / (vertical ? clientHeight : clientWidth)
step *= Math.pow(10, accuracy)
let newValue =
+(scale * range + min).toFixed(accuracy) * Math.pow(10, accuracy)
let mod = +(newValue % step).toFixed(accuracy)
if (mod) {
let half = step / 2
if (mod > half) {
newValue += step - mod
} else {
newValue -= mod
} }
} }
newValue *= Math.pow(10, -accuracy) this.#value = Math.round(((val - min) * 100) / (max - min))
this.value = (vertical ? range - newValue : newValue).toFixed(accuracy) return val
let progress = Math.floor(((newValue - min) / range) * 100) }
this.setProgress(progress)
this.$emit('change') #seek(ev) {
this.$emit('input') let { clientWidth, clientHeight } = ev.target
if (!this.timeout) { let { min, max, step } = this
this.$tips.classList.toggle('show') let val = 0
if (this.vertical) {
val = (clientHeight - ev.offsetY) / clientHeight
} else { } else {
clearTimeout(this.timeout) val = ev.offsetX / clientWidth
this.timeout = null
} }
this.timeout = setTimeout(() => { val *= this.#len
this.$tips.classList.toggle('show') val += min
this.timeout = null
}, 1000) this.value = this.#fix(val)
} }
initValue(val) {
let { max, min, vertical, disabled, readOnly } = this get #activeStyle() {
if (disabled || readOnly) { let v = this.#value
return let deg = this.vertical ? 0 : '90deg'
} return `background:linear-gradient(${deg}, var(--active-color) ${v}%, transparent ${v}%)`
let range = max - min }
val = Math.max(val, min)
val = Math.min(val, max) get #thumbStyle() {
this.value = val let v = this.#value
let progress = Math.floor(((val - min) / range) * 100) if (this.vertical) {
progress = vertical ? 100 - progress : progress return `bottom:${v}%`
this.setProgress(progress)
}
setProgress(val) {
let { vertical } = this
val = Math.floor(val)
val = Math.min(val, 100)
val = Math.max(val, 0)
if (vertical) {
this.$bar.style.height = `${100 - val}%`
this.$dotWrap.style.top = `${val}%`
this.$tips.style.top = `${val}%`
this.progress = 100 - val
} else {
this.$bar.style.width = `${val}%`
this.$dotWrap.style.left = `${val}%`
this.$tips.style.left = `${val}%`
this.progress = val
} }
return `left:${v}%`
} }
render() { render() {
return html`<div class="slider" ref="slider"> let n = this.hasAttribute('show-stops') ? this.#stops : 1
<div ref="runway" class="runway" @click=${this.onClick}>
<div class="bar" ref="bar"></div> return html`
<div class="dot-wrapper" ref="dotWrap" @mousedown=${this.onMousedown}> <main class="container">
<div class="dot"></div> <div class="slider-bar">
${range(1, n).map(i => {
return html`<i style="left:${i * this.#point}%"></i>`
})}
</div> </div>
</div> <div
<div class="tips" ref="tips">${this.value}</div> class="slider-bar"
</div>` style=${this.#activeStyle}
@click=${this.#seek}
></div>
<div class="thumb" style=${this.#thumbStyle}>
<mark class="value">${this.value}</mark>
</div>
</main>
`
} }
} }