自定义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
相关产品推荐
相关产品推荐

