You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义HTML元素首次拖拽时drag事件不触发问题

自定义滑块首次拖拽不触发问题的解决方法

问题背景

开发自定义HTML元素时,实现了一款垂直滑块组件替代原生range(规避浏览器兼容问题),但首次点击手柄拖拽时drag事件不触发,需点击别处后再次尝试才生效。

问题原因

原生drag事件依赖元素的draggable属性,首次拖拽时浏览器需要初始化可拖拽状态,导致事件延迟触发;且drag事件本身更适合元素整体拖拽场景,并非滑块这类精细位置控制的最优选择。

解决方案

改用mousedown/mousemove/mouseup(及移动端触摸事件)组合实现拖拽逻辑,这种方式更稳定且无需依赖draggable属性:

修改后的核心JS代码

class hsShader extends HTMLElement {
    template = `
        <span class="hs-shader-upper"></span>
        <span class="hs-shader-handle">
            <span class="hs-shader-percent-holder">50%</span>
        </span>`;
    percent = 50;
    handleHeight = 10;

    handleEl = null;
    upperHalfEl = null;
    percentHolderEl = null;
    connectedCallback() {
        this.id = this.getAttribute("id");
        if (!this.id || this.id === "") {
            console.error("[HSH-Shader] - Shader should have an ID.");
            return;
        }

        this.addTemplate();
        this.getElements();
        this.getAttributes();
        this.addEvents();
    }

    getElements() {
        this.handleEl = this.querySelector(".hs-shader-handle");
        this.upperHalfEl = this.querySelector(".hs-shader-upper");
        this.percentHolderEl = this.querySelector(".hs-shader-percent-holder");
        this.handleHeight = this.handleEl.offsetHeight;
    }

    getAttributes() {
        this.percent = parseInt(this.getAttribute("percent")) || 50;
        this.refreshDisplayPercent();
    }

    addTemplate() {
        this.innerHTML = this.template;
    }

    addEvents() {
        this.setupDragHandlers();
    }
  
    setupDragHandlers() {
        const self = this;
        let isDragging = false;

        // 鼠标事件处理
        const handleMouseDown = (e) => {
            isDragging = true;
            window.getSelection().removeAllRanges();
            // 绑定到document避免鼠标移出元素时丢失事件
            document.addEventListener('mousemove', handleMouseMove);
            document.addEventListener('mouseup', handleMouseUp);
        };

        const handleMouseMove = (e) => {
            if (!isDragging) return;
            const rect = self.getBoundingClientRect();
            const relativeTop = e.clientY - rect.top;
            self.updateDisplayPixel(relativeTop);
        };

        const handleMouseUp = () => {
            isDragging = false;
            document.removeEventListener('mousemove', handleMouseMove);
            document.removeEventListener('mouseup', handleMouseUp);
        };

        // 触摸事件处理
        const handleTouchStart = (e) => {
            isDragging = true;
            document.addEventListener('touchmove', handleTouchMove, { passive: false });
            document.addEventListener('touchend', handleTouchEnd);
        };

        const handleTouchMove = (e) => {
            if (!isDragging) return;
            e.preventDefault(); // 阻止页面滚动冲突
            const rect = self.getBoundingClientRect();
            const relativeTop = e.touches[0].clientY - rect.top;
            self.updateDisplayPixel(relativeTop);
        };

        const handleTouchEnd = () => {
            isDragging = false;
            document.removeEventListener('touchmove', handleTouchMove);
            document.removeEventListener('touchend', handleTouchEnd);
        };

        // 绑定初始事件到手柄
        this.handleEl.addEventListener('mousedown', handleMouseDown);
        this.handleEl.addEventListener('touchstart', handleTouchStart);
    }

    getPosPixel() {
        let height = this.offsetHeight;
        return this.clamp(((height / 100) * this.percent) - (this.handleHeight / 2), 0, height - this.handleHeight);
    }

    getPosPercent(pixel) {
        return this.clamp(Math.round((100 / this.offsetHeight) * pixel), 0, 100);
    }

    refreshDisplayPercent() {
        this.upperHalfEl.style.height = `${this.percent}%`;
        this.handleEl.style.top = `${this.getPosPixel()}px`;
        this.percentHolderEl.innerText = `${this.percent}%`;
    }

    updateDisplayPixel(pixel) {
        const clampedPixel = this.clamp(pixel, 0, this.offsetHeight);
        const percent = this.getPosPercent(clampedPixel);
        const posPercentString = `${percent}%`;
        this.upperHalfEl.style.height = `${clampedPixel}px`;
        this.handleEl.style.top = `${clampedPixel - (this.handleHeight / 2)}px`;
        this.percentHolderEl.innerText = posPercentString;
        this.percent = percent;
    }

    clamp(val, min = 0, max = 100) {
        return Math.max(min, Math.min(max, val));
    }

    setPercent(percent) {
        if (percent > 100 || percent < 0) {
            console.error("[HSH-Shader] - Percent must be between 0 and 100%!");
        }
        this.percent = this.clamp(percent);
        this.refreshDisplayPercent();
    }
};
customElements.define('hs-shader', hsShader);

CSS代码(无需修改)

body{
  width: 100%;
  height: 100vh;
  
  display: flex;
  flex-direction: column;
  justify-content:center;
  text-align:center;
}

.shader-container{
  display: flex;
  flex-direction: row;
  gap: 100px;
  justify-content:center;
  align-items:center;
}

:root{
    --shader_top_half_color: cornflowerblue;
    --shader_thumb_color: #5e7cd3;
    
    --shader_top_half_color_animate: coral;
    --shader_thumb_color_animate: #ce5d33;
  
    --group_shader_top_half_color: #4bcca1;
    --group_shader_thumb_color: #4cbb9f;
}

hs-shader{
  position:relative;
  width: 100px;
  height: 150px;
  border-radius: 0.3em;
  box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
  
  display: flex;
  flex-direction: column;
}

.hs-shader-upper{
  width: 100%;
  height: 50%;
  background-color: var(--shader_top_half_color);
  border-radius: 0.3em;
}

.hs-shader-handle{
  position: absolute;
  left: -5px;
  top: calc(50% - 10px);
  
  display: flex;
  justify-content:center;
  align-items:center;
  
  width: 110%;
  height: 20px;
  border-radius: 0.4em;
  background-color: var(--shader_thumb_color);
  color: white;
  
  cursor: pointer;
}

.hs-shader-percent-indicator{
  display: none;
}

.hs-shader-percent-indicator.show{
  position:absolute;
  right: -70px;
  display: flex;
  justify-content:center;
  align-items:center;
  font-weight: bold;
  font-size: 1em;
  opacity: 0;
  animation: hs-shader-show-indicator 0.3s ease-in forwards;
  width: 50px;
  height: 40px;
  background: cornflowerblue;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  border-radius: 10px;
  box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
}

@keyframes hs-shader-show-indicator{
  0%{
    opacity: 0;
  }  
  100%{
    opacity: 1;
  }
}

.hs-shader-percent-indicator::before{
  content: "";
  position: absolute;
  right: 100%;
  top: 10px;
  width: 0;
  height: 0;
  border-top: 10px solid transparent;
  border-right: 20px solid cornflowerblue;
  border-bottom: 10px solid transparent;
}

.hs-shader-handle:hover{
  box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
}

HTML代码(调整提示文本)

<h1>现在首次拖拽可以正常触发了</h1>
<div class="shader-container">
    <hs-shader id="test" percent="50"></hs-shader>
    <hs-shader id="test2" percent="30"></hs-shader>
</div>

关键优化点

  • 替换drag事件为mousedown/mousemove/mouseup组合,绑定到document避免鼠标移出元素时丢失事件
  • 增加触摸事件支持,处理移动端拖拽场景并阻止默认滚动行为避免冲突
  • 优化clamp方法,明确参数默认值,确保手柄位置不会超出滑块范围
  • 拖拽结束后及时解绑全局事件,避免内存泄漏

内容的提问来源于stack exchange,提问作者Dr.Random

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 06:40:22