fix(vue): batch color picker drag updates into one undo entry (#187)

PropertyListRoot's update() and patch() pushed a new undo entry on
every call, so dragging a color slider or color area in the fill,
stroke, or effect pickers filled the undo stack with dozens of
intermediate entries per interaction. Cmd+Z had to be pressed many
times to revert a single conceptual color change.

Wrap update() and patch() in a debounced begin/commit batch keyed by
(op, propKey, index, nodeIds). Rapid events collapse into one undo
entry, and the batch commits after 300ms of idle. Changing prop key,
index, or selection flushes the previous batch immediately, and
add/remove/toggleVisibility flush any pending batch before running so
button actions never mix with drag history. The existing explicit
batch in toggleVisibility for multi-node is preserved.
This commit is contained in:
tae.virus 2026-04-13 15:45:50 +09:00 committed by GitHub
parent a4f30325eb
commit c829f69e1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 3 deletions

View file

@ -2,6 +2,10 @@
## Unreleased
### Fixes
- Fix color picker dragging flooding the undo stack — fill/stroke/effect color and opacity drags now collapse into a single undo entry per interaction via debounced batching in `PropertyListRoot`
## 0.11.6 — 2026-04-08
### Fixes

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onBeforeUnmount } from 'vue'
import { useEditor } from '@open-pencil/vue/context/editorContext'
import { useNodeProps } from '@open-pencil/vue/controls/useNodeProps'
@ -51,7 +51,35 @@ function targetNodes(): SceneNode[] {
return activeNode.value ? [activeNode.value] : []
}
const BATCH_IDLE_MS = 300
let batchKey: string | null = null
let batchTimer: ReturnType<typeof setTimeout> | null = null
function flushBatch() {
if (batchTimer !== null) {
clearTimeout(batchTimer)
batchTimer = null
}
if (batchKey !== null) {
editor.undo.commitBatch()
batchKey = null
}
}
function ensureBatch(key: string, label: string) {
if (batchKey !== key) {
flushBatch()
editor.undo.beginBatch(label)
batchKey = key
}
if (batchTimer !== null) clearTimeout(batchTimer)
batchTimer = setTimeout(flushBatch, BATCH_IDLE_MS)
}
onBeforeUnmount(flushBatch)
function add(defaults: ArrayItemType) {
flushBatch()
emit('add', defaults)
for (const n of targetNodes()) {
const arr = isMulti.value ? [defaults] : [...n[propKey], defaults]
@ -64,6 +92,7 @@ function add(defaults: ArrayItemType) {
}
function remove(index: number) {
flushBatch()
emit('remove', index)
for (const n of targetNodes()) {
editor.updateNodeWithUndo(
@ -78,7 +107,11 @@ function remove(index: number) {
function update(index: number, item: ArrayItemType) {
emit('update', index, item)
for (const n of targetNodes()) {
const nodes = targetNodes()
if (nodes.length === 0) return
const key = `update:${propKey}:${index}:${nodes.map((n) => n.id).join(',')}`
ensureBatch(key, `Change ${propKey}`)
for (const n of nodes) {
const arr = [...n[propKey]] as ArrayItemType[]
arr[index] = item
editor.updateNodeWithUndo(n.id, { [propKey]: arr } as Partial<SceneNode>, `Change ${propKey}`)
@ -87,7 +120,11 @@ function update(index: number, item: ArrayItemType) {
function patch(index: number, changes: Record<string, unknown>) {
emit('patch', index, changes)
for (const n of targetNodes()) {
const nodes = targetNodes()
if (nodes.length === 0) return
const key = `patch:${propKey}:${index}:${nodes.map((n) => n.id).join(',')}`
ensureBatch(key, `Change ${propKey}`)
for (const n of nodes) {
const arr = [...n[propKey]] as ArrayItemType[]
arr[index] = { ...arr[index], ...changes } as ArrayItemType
editor.updateNodeWithUndo(n.id, { [propKey]: arr } as Partial<SceneNode>, `Change ${propKey}`)
@ -95,6 +132,7 @@ function patch(index: number, changes: Record<string, unknown>) {
}
function toggleVisibility(index: number) {
flushBatch()
emit('toggleVisibility', index)
const nodes = targetNodes()
if (nodes.length === 0) return