refactor: optimize EditorPanel updates with selective debouncing

Implement immediate vs debounced updates based on input type to improve
UX responsiveness while preventing excessive re-renders.

Update strategy:
- Immediate (0ms): select, buttons, checkboxes, color picker
- Debounced (1s): text inputs, number inputs, range sliders

Changes:
- PageSettings.vue: Split watchers for margin values/units and background
  value/format. Extract update logic into reusable functions.
- TextSettings.vue: Add comprehensive watcher system with selective
  debouncing for all settings (font, size, color, margins, etc.)

This ensures button clicks (unit toggles, format switches) apply instantly
while typed values (numbers, text) batch updates to reduce CSS re-parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
isUnknown 2025-12-04 14:03:40 +01:00
parent 7ed57d000b
commit 467ae905bd
2 changed files with 417 additions and 157 deletions

View file

@ -2,43 +2,37 @@
<section class="settings-section">
<h2>Réglage des pages</h2>
<div class="field">
<label for="page-format">Format d'impression</label>
<select id="page-format" v-model="pageFormat">
<option value="A4">A4</option>
<option value="A5">A5</option>
<option value="A3">A3</option>
<option value="letter">Letter</option>
<option value="legal">Legal</option>
</select>
<div class="settings-subsection">
<div class="field">
<label for="page-format">Format d'impression</label>
<select id="page-format" v-model="pageFormat">
<option value="A4">A4</option>
<option value="A5">A5</option>
<option value="A3">A3</option>
<option value="letter">Letter</option>
<option value="legal">Legal</option>
</select>
</div>
</div>
<div class="field">
<label for="page-width">Largeur</label>
<input
id="page-width"
type="text"
:value="pageWidth"
disabled
/>
<div class="settings-subsection">
<div class="field field--view-only">
<label for="page-width">Largeur</label>
<input id="page-width" type="text" :value="pageWidth" disabled />
</div>
<div class="field field--view-only">
<label for="page-height">Hauteur</label>
<input id="page-height" type="text" :value="pageHeight" disabled />
</div>
</div>
<div class="field">
<label for="page-height">Hauteur</label>
<input
id="page-height"
type="text"
:value="pageHeight"
disabled
/>
</div>
<div class="subsection">
<div class="settings-subsection margins">
<h3>Marges</h3>
<div class="field">
<label for="margin-top">Haut</label>
<div class="field-with-unit">
<div class="input-with-unit">
<input
id="margin-top"
type="number"
@ -66,7 +60,7 @@
<div class="field">
<label for="margin-bottom">Bas</label>
<div class="field-with-unit">
<div class="input-with-unit">
<input
id="margin-bottom"
type="number"
@ -94,7 +88,7 @@
<div class="field">
<label for="margin-left">Gauche</label>
<div class="field-with-unit">
<div class="input-with-unit">
<input
id="margin-left"
type="number"
@ -122,7 +116,7 @@
<div class="field">
<label for="margin-right">Droite</label>
<div class="field-with-unit">
<div class="input-with-unit">
<input
id="margin-right"
type="number"
@ -149,59 +143,53 @@
</div>
</div>
<div class="field">
<label for="background">Arrière-plan</label>
<div class="field-with-unit">
<input
id="background"
type="text"
v-model="background.value"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: background.format === 'rgb' }"
@click="background.format = 'rgb'"
>
rgb
</button>
<button
type="button"
:class="{ active: background.format === 'hex' }"
@click="background.format = 'hex'"
>
hex
</button>
<div class="settings-subsection">
<div class="field">
<label for="background">Arrière-plan</label>
<div class="input-with-unit">
<input id="background" type="text" v-model="background.value" />
<div class="unit-toggle">
<button
type="button"
:class="{ active: background.format === 'rgb' }"
@click="background.format = 'rgb'"
>
rgb
</button>
<button
type="button"
:class="{ active: background.format === 'hex' }"
@click="background.format = 'hex'"
>
hex
</button>
</div>
</div>
</div>
</div>
<div class="field">
<label for="pattern">Motif</label>
<select id="pattern" v-model="pattern">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
<div class="settings-subsection">
<div class="field">
<label for="pattern">Motif</label>
<select id="pattern" v-model="pattern">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
</div>
</div>
<div class="field checkbox-field">
<input
id="page-numbers"
type="checkbox"
v-model="pageNumbers"
/>
<label for="page-numbers">Numéro de page</label>
</div>
<div class="settings-subsection">
<div class="field checkbox-field">
<input id="page-numbers" type="checkbox" v-model="pageNumbers" />
<label for="page-numbers">Numéro de page</label>
</div>
<div class="field checkbox-field">
<input
id="running-title"
type="checkbox"
v-model="runningTitle"
/>
<label for="running-title">Titre courant</label>
<div class="field checkbox-field">
<input id="running-title" type="checkbox" v-model="runningTitle" />
<label for="running-title">Titre courant</label>
</div>
</div>
</section>
</template>
@ -222,7 +210,7 @@ const pageFormats = {
A5: { width: '148mm', height: '210mm' },
A3: { width: '297mm', height: '420mm' },
letter: { width: '8.5in', height: '11in' },
legal: { width: '8.5in', height: '14in' }
legal: { width: '8.5in', height: '14in' },
};
const pageWidth = computed(() => pageFormats[pageFormat.value].width);
@ -232,12 +220,12 @@ const margins = ref({
top: { value: 20, unit: 'mm' },
bottom: { value: 20, unit: 'mm' },
left: { value: 20, unit: 'mm' },
right: { value: 20, unit: 'mm' }
right: { value: 20, unit: 'mm' },
});
const background = ref({
value: '',
format: 'hex'
format: 'hex',
});
const pattern = ref('');
@ -249,57 +237,109 @@ const debouncedUpdate = (callback) => {
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => {
callback();
};
watch(pageFormat, (newFormat) => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
immediateUpdate(() => {
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
});
});
watch(margins, (newMargins) => {
if (isUpdatingFromStore) return;
const updateMargins = () => {
const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
debouncedUpdate(() => {
const marginValue = `${newMargins.top.value}${newMargins.top.unit} ${newMargins.right.value}${newMargins.right.unit} ${newMargins.bottom.value}${newMargins.bottom.unit} ${newMargins.left.value}${newMargins.left.unit}`;
const currentBlock = stylesheetStore.extractBlock('@page');
const updatedBlock = currentBlock.replace(
/(margin:\s*)[^;]+/,
`$1${marginValue}`
);
const currentBlock = stylesheetStore.extractBlock('@page');
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
};
// Watch margin values (number inputs) with debounce
watch(
() => [
margins.value.top.value,
margins.value.bottom.value,
margins.value.left.value,
margins.value.right.value,
],
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateMargins);
}
);
// Watch margin units (button clicks) without debounce
watch(
() => [
margins.value.top.unit,
margins.value.bottom.unit,
margins.value.left.unit,
margins.value.right.unit,
],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateMargins);
}
);
const updateBackground = () => {
if (!background.value.value) return;
const currentBlock = stylesheetStore.extractBlock('@page');
if (currentBlock.includes('background:')) {
const updatedBlock = currentBlock.replace(
/(margin:\s*)[^;]+/,
`$1${marginValue}`
/(background:\s*)[^;]+/,
`$1${background.value.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` background: ${background.value.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
}
};
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
});
}, { deep: true });
// Watch background value (text input) with debounce
watch(
() => background.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateBackground);
}
);
watch(background, (newBg) => {
if (!newBg.value) return;
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
const currentBlock = stylesheetStore.extractBlock('@page');
if (currentBlock.includes('background:')) {
const updatedBlock = currentBlock.replace(
/(background:\s*)[^;]+/,
`$1${newBg.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` background: ${newBg.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
}
});
}, { deep: true });
// Watch background format (button clicks) without debounce
watch(
() => background.value.format,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateBackground);
}
);
watch(pattern, (newPattern) => {
if (!newPattern || isUpdatingFromStore) return;
debouncedUpdate(() => {
immediateUpdate(() => {
// TODO: implement pattern application
});
});
@ -307,7 +347,7 @@ watch(pattern, (newPattern) => {
watch(pageNumbers, (enabled) => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
immediateUpdate(() => {
// TODO: implement page numbers toggle
});
});
@ -315,7 +355,7 @@ watch(pageNumbers, (enabled) => {
watch(runningTitle, (enabled) => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
immediateUpdate(() => {
// TODO: implement running title toggle
});
});
@ -331,12 +371,26 @@ const syncFromStore = () => {
pageFormat.value = sizeMatch[1];
}
const marginMatch = pageBlock.match(/margin:\s*([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)/i);
const marginMatch = pageBlock.match(
/margin:\s*([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)/i
);
if (marginMatch) {
margins.value.top = { value: parseFloat(marginMatch[1]), unit: marginMatch[2] };
margins.value.right = { value: parseFloat(marginMatch[3]), unit: marginMatch[4] };
margins.value.bottom = { value: parseFloat(marginMatch[5]), unit: marginMatch[6] };
margins.value.left = { value: parseFloat(marginMatch[7]), unit: marginMatch[8] };
margins.value.top = {
value: parseFloat(marginMatch[1]),
unit: marginMatch[2],
};
margins.value.right = {
value: parseFloat(marginMatch[3]),
unit: marginMatch[4],
};
margins.value.bottom = {
value: parseFloat(marginMatch[5]),
unit: marginMatch[6],
};
margins.value.left = {
value: parseFloat(marginMatch[7]),
unit: marginMatch[8],
};
}
const bgMatch = pageBlock.match(/background:\s*([^;]+)/);
@ -348,11 +402,14 @@ const syncFromStore = () => {
}
};
watch(() => stylesheetStore.content, () => {
if (!isUpdatingFromStore) {
syncFromStore();
watch(
() => stylesheetStore.content,
() => {
if (!isUpdatingFromStore) {
syncFromStore();
}
}
});
);
onMounted(() => {
syncFromStore();