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

JavaScript画板模式切换问题:hover-draw与click-draw互斥失效

问题:画板hover/click绘图模式切换失效

我正在开发一个画板功能,希望支持用户在hover-draw模式(默认模式,鼠标悬停在div上时改变其颜色)和click-draw模式(需点击或按住鼠标拖动div才能上色)之间切换。
我添加了一个按钮用于切换hoverOn/clickOn变量的值,期望只有当对应变量处于"on"状态时,相应的绘图函数才会生效。
但实际情况是,点击按钮将hoverOn设为false后,hover事件函数仍会执行,div依然会被上色。


完整代码

HTML

<body>
    <div class="main">
        <h1>Draw something nice!</h1>
        <div class="content">
            <div class="settings">
                <div class="grid-resize">
                    <label for="grid-resize">Change the grid size:</label>
                    <input type="range" id="grid-resize" min="4" max="100">
                </div>
                <div class="buttons">
                    <button id="clear" class="settings-buttons" title="Click here to clear the board">Clear All</button>
                    <button id="reset" class="settings=buttons" title="Click here to go back to original settings - this will clear the board">Reset</button>
                    <button id="change-mode" class="settings-buttons" title="Click here to change the mode">Draw Mode</button>
                    <button id="random-color" class="settings-buttons">Random Color</button>
                    <button id="black-shades" class="settings-buttons">Black Shades</button>
                    <button id="eraser" class="settings-buttons"><img src="images/rubber.svg">Eraser</button>
                </div>
            </div>
            <div class="container">
            </div>
        </div>
    </div>
</body>

CSS

html, body {
  margin: 0;
  padding: 20px;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  text-align: center;
}

h1 {
  margin-top: 0;
}

.content {
  display: flex;
  justify-content: center;
  gap: 5em;
}
.content .settings {
  display: flex;
  flex-direction: column;
  width: 15%;
}
.content .grid-resize {
  text-align: start;
  margin-bottom: 2em;
}
.content .buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5em;
}
.content .buttons button {
  border: none;
  box-shadow: 1px 1px rgba(128, 128, 128, 0.41);
  border-radius: 15px;
  width: 70px;
  height: 70px;
}
.content .buttons button:hover {
  box-shadow: 2px 2px #176fae;
  background-color: #72A0C1;
  color: white;
  transform: scale(1.2);
}
.content .buttons button:active {
  box-shadow: none;
  background-color: #72A0C1;
  color: white;
  transform: scale(1);
}
.content .buttons button .eraser-border {
  border: 1px solid black;
}

.container {
  display: grid;
  width: 400px;
  height: 400px;
}
.container .cell {
  background-color: white;
  border: 1px solid rgba(220, 220, 220, 0.3);
}

JavaScript

const container = document.querySelector('.container');
const newSize = document.querySelector('#grid-resize');
const clearBtn = document.querySelector('#clear');
const resetBtn = document.querySelector('#reset');
const randomBtn = document.querySelector('#random-color');
const modeBtn = document.querySelector('#change-mode');
const eraser = document.querySelector('#eraser');

let size = 16;
let hoverOn = true;
let clickOn = false;
let color = '#72A0C1';

newSize.addEventListener('input', changeSize);
clearBtn.addEventListener('click', clearGrid);
resetBtn.addEventListener('click', resetGrid);
modeBtn.addEventListener('click', changeMode);
randomBtn.addEventListener('click', drawRainbow);

eraser.addEventListener('click', () => {
    if (color == 'white') {
        color = '#72A0C1';
        eraser.style.border = 'none';
    } else {
        color = 'white';
        eraser.style.border = '2px dotted #FFC0CB';
    }
});

let isMousedown = false;
container.addEventListener('mousedown', ()=>{isMousedown = true;});
container.addEventListener('mouseup', ()=>{isMousedown = false;});

function changeSize() {
    const cells = document.querySelectorAll('.cell');
    cells.forEach(cell => {
        cell.remove();
    });
    size = newSize.value;
    generateDiv(size);
}

function clearGrid() {
    const cells = document.querySelectorAll('.cell');
    cells.forEach(cell => {
        cell.style.backgroundColor = 'white';
    });
}

function resetGrid() {
    const cells = document.querySelectorAll('.cell');
    cells.forEach(cell => {
        cell.remove();
    });
    size = 16;
    newSize.value = size;
    generateDiv();
}

function changeMode() {
    if (hoverOn) {
        hoverOn = false;
        clickOn = true;
    } else if (clickOn) {
        clickOn = false;
        hoverOn = true;
    }
    console.log (hoverOn, clickOn)
}

function setHoverMode() {
    if (hoverOn == true) {
        container.addEventListener('mouseover', function (e) {
    e.target.style.background = `${color}`;
    });
}
    
}

function setClickMode() {
    if (clickOn == true) {
        container.addEventListener('mousemove', (e) => {
        if (isMousedown) {
            e.target.style.backgroundColor = `${color}`;
        }
    });
    container.addEventListener('mousemove', e => e.preventDefault());
    } 
}

function generateColor() {
    let randomColor = '#';
    let characters = 'ABCDEF0123456789';
    for(let i=0; i < 6; i++) {
        randomColor += characters.charAt(Math.floor(Math.random() * characters.length));
        color = randomColor;
    }
}

function drawRainbow() {
    container.addEventListener('mousemove', ()=>{
        generateColor();
    });
}

function generateDiv() {
    container.style.gridTemplateColumns = `repeat(${size}, 1fr)`;
    container.style.gridTemplateRows = `repeat(${size}, 1fr)`;

    for (let i = 0; i < size * size; i++) {
        let cell = document.createElement('div');
        cell.classList.add('cell');
        container.appendChild(cell);
    }
}

generateDiv(size);

解决方案

问题核心在于你当前的写法是一次性添加事件监听器,一旦监听器绑定到container上,后续修改hoverOn/clickOn变量并不会自动移除这些监听器,所以即使变量变为false,之前绑定的hover事件依然会触发。

修复步骤:

  1. 提取事件处理函数为独立变量:把hover和click模式的处理逻辑单独存成函数,方便后续添加/移除监听器。
  2. 在模式切换时动态添加/移除监听器:每次切换模式时,先移除不需要的模式对应的监听器,再添加当前模式的监听器。
  3. 初始化时绑定默认模式的监听器:页面加载时先绑定hover模式的监听器。

修改后的关键代码如下:

// 提取独立的事件处理函数
function handleHoverDraw(e) {
  // 确保目标是单元格
  if (!e.target.classList.contains('cell')) return;
  if (hoverOn) {
    e.target.style.background = `${color}`;
  }
}

function handleClickDraw(e) {
  // 确保目标是单元格
  if (!e.target.classList.contains('cell')) return;
  if (clickOn && isMousedown) {
    e.target.style.backgroundColor = `${color}`;
  }
}

// 修改changeMode函数,动态切换监听器
function changeMode() {
  if (hoverOn) {
    hoverOn = false;
    clickOn = true;
    // 移除hover监听器
    container.removeEventListener('mouseover', handleHoverDraw);
    // 添加click模式监听器
    container.addEventListener('mousemove', handleClickDraw);
  } else if (clickOn) {
    clickOn = false;
    hoverOn = true;
    // 移除click模式监听器
    container.removeEventListener('mousemove', handleClickDraw);
    // 添加hover监听器
    container.addEventListener('mouseover', handleHoverDraw);
  }
  console.log(hoverOn, clickOn);
}

// 初始化绑定默认的hover模式监听器
container.addEventListener('mouseover', handleHoverDraw);

另外,原代码中的setHoverMode和setClickMode函数没有被调用过,这两个函数可以直接删掉,改用上面的动态切换逻辑。

额外优化点:

  • drawRainbow函数存在重复绑定事件的问题,每次点击按钮都会新增一个mousemove监听器,建议改成切换一个rainbowMode变量,在统一的绘图处理函数里判断是否生成随机颜色。
  • 新增的单元格判断逻辑,避免鼠标误操作到容器本身导致样式异常。

内容的提问来源于stack exchange,提问作者pseudodejapris

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 20:40:31