toggle all values
This commit is contained in:
parent
203ed18aba
commit
fa56118e75
4 changed files with 376 additions and 106 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -30,3 +30,5 @@ api/.env
|
|||
# Claude settings
|
||||
.claude
|
||||
/.claude/*
|
||||
|
||||
doc-conception.md
|
||||
|
|
|
|||
|
|
@ -17,19 +17,20 @@
|
|||
|
||||
<template #controls>
|
||||
<!-- Font Family -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-font" >
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.font }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.font" @change="onToggleSetting('font', $event.target.checked)" />
|
||||
<div class="field field-font">
|
||||
<label class="label-with-tooltip" data-css="font-family">Police</label>
|
||||
<div class="field-font__options">
|
||||
<select v-model="fontFamily" >
|
||||
<select v-model="fontFamily">
|
||||
<option v-for="f in fonts" :key="f" :value="f">{{ f }}</option>
|
||||
</select>
|
||||
<div class="field-checkbox">
|
||||
<input type="checkbox" v-model="italic" />
|
||||
<input type="checkbox" v-model="italic" />
|
||||
<label class="label-with-tooltip" data-css="font-style">Italique</label>
|
||||
</div>
|
||||
<div class="field-checkbox">
|
||||
<input type="checkbox" v-model="bold" />
|
||||
<input type="checkbox" v-model="bold" />
|
||||
<label class="label-with-tooltip" data-css="font-weight">Gras</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -37,8 +38,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Font Size -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-text-size" >
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.fontSize }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.fontSize" @change="onToggleSetting('fontSize', $event.target.checked)" />
|
||||
<div class="field field-text-size">
|
||||
<label class="label-with-tooltip" data-css="font-size">Taille du texte</label>
|
||||
<InputWithUnit
|
||||
v-model="fontSizeModel"
|
||||
|
|
@ -50,10 +52,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- LineHeight -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-text-size" >
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.lineHeight }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.lineHeight" @change="onToggleSetting('lineHeight', $event.target.checked)" />
|
||||
<div class="field field-text-size">
|
||||
<label class="label-with-tooltip" data-css="line-height">Interlignage</label>
|
||||
<InputWithUnit
|
||||
v-model="lineHeightModel"
|
||||
|
|
@ -65,12 +67,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Alignment -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field" >
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.textAlign }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.textAlign" @change="onToggleSetting('textAlign', $event.target.checked)" />
|
||||
<div class="field">
|
||||
<label class="label-with-tooltip" data-css="text-align">Alignement</label>
|
||||
<select v-model="textAlign" >
|
||||
<select v-model="textAlign">
|
||||
<option value="left">Gauche</option>
|
||||
<option value="center">Centre</option>
|
||||
<option value="right">Droite</option>
|
||||
|
|
@ -79,11 +81,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.color }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.color" @change="onToggleSetting('color', $event.target.checked)" />
|
||||
<div class="field field-simple">
|
||||
<label class="label-with-tooltip" data-css="color">Couleur du texte</label>
|
||||
<div class="input-with-color">
|
||||
<input
|
||||
ref="colorInput"
|
||||
type="text"
|
||||
v-model="color"
|
||||
data-coloris
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.background }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.background" @change="onToggleSetting('background', $event.target.checked)" />
|
||||
<div class="field field-simple">
|
||||
<label class="label-with-tooltip" data-css="background">Arrière-plan</label>
|
||||
<div class="input-with-color">
|
||||
<input
|
||||
ref="backgroundInput"
|
||||
type="text"
|
||||
v-model="background"
|
||||
data-coloris
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bordure -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-border" >
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.border }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.border" @change="onToggleSetting('border', $event.target.checked)" />
|
||||
<div class="field field-border">
|
||||
<div class="settings-subsection-header">
|
||||
<label class="label-with-tooltip" data-css="border">Bordure</label>
|
||||
<label class="label-with-tooltip" data-css="border">Bordure</label>
|
||||
</div>
|
||||
<div class="field-border__options">
|
||||
<div class="field-border__option">
|
||||
|
|
@ -93,15 +128,15 @@
|
|||
v-model="borderWidth.value"
|
||||
:min="0"
|
||||
:step="1"
|
||||
/>
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button type="button" class="active" >px</button>
|
||||
<button type="button" class="active">px</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-border__option">
|
||||
<label class="label-with-tooltip" data-css="border-style">Style</label>
|
||||
<select v-model="borderStyle" >
|
||||
<select v-model="borderStyle">
|
||||
<option value="solid">Plein</option>
|
||||
<option value="dotted">Pointillés</option>
|
||||
<option value="dashed">Tirets</option>
|
||||
|
|
@ -115,7 +150,7 @@
|
|||
ref="borderColorInput"
|
||||
type="text"
|
||||
v-model="borderColor"
|
||||
data-coloris
|
||||
data-coloris
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -123,45 +158,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-simple" >
|
||||
<label class="label-with-tooltip" data-css="color">Couleur du texte</label>
|
||||
<div class="input-with-color">
|
||||
<input
|
||||
ref="colorInput"
|
||||
type="text"
|
||||
v-model="color"
|
||||
data-coloris
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-simple" >
|
||||
<label class="label-with-tooltip" data-css="background">Arrière-plan</label>
|
||||
<div class="input-with-color">
|
||||
<input
|
||||
ref="backgroundInput"
|
||||
type="text"
|
||||
v-model="background"
|
||||
data-coloris
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outer Margins -->
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.margin }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.margin" @change="onToggleSetting('margin', $event.target.checked)" />
|
||||
<div class="settings-subsection-header">
|
||||
<span class="label-with-tooltip" data-css="margin">Marges extérieures</span>
|
||||
<button
|
||||
type="button"
|
||||
class="lock-toggle"
|
||||
:class="{ locked: marginLocked }"
|
||||
@click="marginLocked = !marginLocked"
|
||||
@click="marginLocked = !marginLocked"
|
||||
:title="marginLocked ? 'Déverrouiller (modifier indépendamment)' : 'Verrouiller (modifier ensemble)'"
|
||||
>
|
||||
<svg v-if="marginLocked" width="11" height="13" viewBox="0 0 11 13" fill="none">
|
||||
|
|
@ -178,25 +184,25 @@
|
|||
v-for="side in sides"
|
||||
:key="side.key"
|
||||
class="field field-margin"
|
||||
>
|
||||
>
|
||||
<label class="label-with-tooltip" :data-css="`margin-${side.key}`">{{ side.label }}</label>
|
||||
<div class="input-with-unit">
|
||||
<NumberInput
|
||||
:modelValue="margin[side.key].value"
|
||||
:min="0"
|
||||
:step="1"
|
||||
@update:modelValue="(v) => updateMarginValue(side.key, v)"
|
||||
@update:modelValue="(v) => updateMarginValue(side.key, v)"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margin[side.key].unit === 'mm' }"
|
||||
@click="updateMarginUnit(side.key, 'mm')"
|
||||
@click="updateMarginUnit(side.key, 'mm')"
|
||||
>mm</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margin[side.key].unit === 'px' }"
|
||||
@click="updateMarginUnit(side.key, 'px')"
|
||||
@click="updateMarginUnit(side.key, 'px')"
|
||||
>px</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -204,14 +210,15 @@
|
|||
</div>
|
||||
|
||||
<!-- Inner Margins (Padding) -->
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection" :class="{ 'setting-disabled': !settingEnabled.padding }">
|
||||
<input type="checkbox" class="toggle-setting" :checked="settingEnabled.padding" @change="onToggleSetting('padding', $event.target.checked)" />
|
||||
<div class="settings-subsection-header">
|
||||
<span class="label-with-tooltip" data-css="padding">Marges intérieures</span>
|
||||
<button
|
||||
type="button"
|
||||
class="lock-toggle"
|
||||
:class="{ locked: paddingLocked }"
|
||||
@click="paddingLocked = !paddingLocked"
|
||||
@click="paddingLocked = !paddingLocked"
|
||||
:title="paddingLocked ? 'Déverrouiller (modifier indépendamment)' : 'Verrouiller (modifier ensemble)'"
|
||||
>
|
||||
<svg v-if="paddingLocked" width="11" height="13" viewBox="0 0 11 13" fill="none">
|
||||
|
|
@ -228,25 +235,25 @@
|
|||
v-for="side in sides"
|
||||
:key="side.key"
|
||||
class="field field-margin"
|
||||
>
|
||||
>
|
||||
<label class="label-with-tooltip" :data-css="`padding-${side.key}`">{{ side.label }}</label>
|
||||
<div class="input-with-unit">
|
||||
<NumberInput
|
||||
:modelValue="padding[side.key].value"
|
||||
:min="0"
|
||||
:step="1"
|
||||
@update:modelValue="(v) => updatePaddingValue(side.key, v)"
|
||||
@update:modelValue="(v) => updatePaddingValue(side.key, v)"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: padding[side.key].unit === 'mm' }"
|
||||
@click="updatePaddingUnit(side.key, 'mm')"
|
||||
@click="updatePaddingUnit(side.key, 'mm')"
|
||||
>mm</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: padding[side.key].unit === 'px' }"
|
||||
@click="updatePaddingUnit(side.key, 'px')"
|
||||
@click="updatePaddingUnit(side.key, 'px')"
|
||||
>px</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -260,12 +267,16 @@
|
|||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
import { useDebounce } from '../composables/useDebounce';
|
||||
import { useCssSync } from '../composables/useCssSync';
|
||||
import { useTextDefaults } from '../composables/useTextDefaults';
|
||||
import NumberInput from './ui/NumberInput.vue';
|
||||
import InputWithUnit from './ui/InputWithUnit.vue';
|
||||
import BasePopup from './ui/BasePopup.vue';
|
||||
import { convertUnit } from '../utils/unit-conversion';
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
const { extractValue: cssExtractValue, extractNumericValue } = useCssSync();
|
||||
const textDefaults = useTextDefaults();
|
||||
|
||||
const props = defineProps({
|
||||
iframeRef: Object,
|
||||
|
|
@ -296,7 +307,13 @@ const color = ref('rgb(0, 0, 0)');
|
|||
const background = ref('transparent');
|
||||
const fontSize = reactive({ value: 23, unit: 'px' });
|
||||
const fontSizeModel = computed({
|
||||
get: () => ({ value: fontSize.value, unit: fontSize.unit }),
|
||||
get: () => {
|
||||
if (!settingEnabled.fontSize) {
|
||||
// When disabled, display TextSettings' current value reactively
|
||||
return { ...textDefaults.fontSize };
|
||||
}
|
||||
return { value: fontSize.value, unit: fontSize.unit };
|
||||
},
|
||||
set: (v) => { fontSize.value = v.value; fontSize.unit = v.unit; },
|
||||
});
|
||||
const lineHeight = reactive({ value: 28, unit: 'px' });
|
||||
|
|
@ -304,7 +321,7 @@ const lineHeightModel = computed({
|
|||
get: () => ({ value: lineHeight.value, unit: lineHeight.unit }),
|
||||
set: (v) => { lineHeight.value = v.value; lineHeight.unit = v.unit; },
|
||||
});
|
||||
const borderWidth = reactive({ value: 0, unit: 'px' });
|
||||
const borderWidth = reactive({ value: 1, unit: 'px' });
|
||||
const borderStyle = ref('solid');
|
||||
const borderColor = ref('#000000');
|
||||
|
||||
|
|
@ -324,25 +341,47 @@ const padding = reactive({
|
|||
left: { value: 0, unit: 'mm' },
|
||||
});
|
||||
|
||||
// Cache for special groups values when unchecked (to restore on re-check)
|
||||
const settingCache = reactive({
|
||||
font: null, // { fontFamily, italic, bold }
|
||||
fontSize: null, // { value, unit }
|
||||
color: null, // string
|
||||
});
|
||||
|
||||
// Per-subsection toggle state
|
||||
// Special groups (font, fontSize, color): checked by default
|
||||
// Other groups: unchecked by default
|
||||
const settingEnabled = reactive({
|
||||
font: true,
|
||||
fontSize: true,
|
||||
lineHeight: false,
|
||||
textAlign: false,
|
||||
color: true,
|
||||
background: false,
|
||||
border: false,
|
||||
margin: false,
|
||||
padding: false,
|
||||
});
|
||||
|
||||
// Constants
|
||||
const fonts = ['Alegreya Sans', 'Alegreya', 'Arial', 'Georgia', 'Times New Roman'];
|
||||
|
||||
// Style property descriptors
|
||||
// Style property descriptors (with group field)
|
||||
const styleProps = [
|
||||
{ css: 'font-family', get: () => fontFamily.value, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
|
||||
{ css: 'font-style', get: () => italic.value ? 'italic' : 'normal', set: v => italic.value = v === 'italic', debounce: false },
|
||||
{ css: 'font-weight', get: () => bold.value ? 'bold' : 'normal', set: v => bold.value = v === 'bold' || parseInt(v) >= 700, debounce: false },
|
||||
{ css: 'text-align', get: () => textAlign.value, set: v => textAlign.value = v, debounce: false },
|
||||
{ css: 'color', get: () => color.value, set: v => color.value = v, debounce: true },
|
||||
{ css: 'background', get: () => background.value, set: v => background.value = v, debounce: true },
|
||||
{ css: 'border-style', get: () => borderStyle.value, set: v => borderStyle.value = v || 'solid', debounce: false },
|
||||
{ css: 'border-color', get: () => borderColor.value, set: v => borderColor.value = v, debounce: true },
|
||||
{ css: 'font-family', group: 'font', get: () => fontFamily.value, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
|
||||
{ css: 'font-style', group: 'font', get: () => italic.value ? 'italic' : 'normal', set: v => italic.value = v === 'italic', debounce: false, skipWhenDefault: v => v !== 'italic' },
|
||||
{ css: 'font-weight', group: 'font', get: () => bold.value ? 'bold' : 'normal', set: v => bold.value = v === 'bold' || parseInt(v) >= 700, debounce: false, skipWhenDefault: v => v === 'normal' },
|
||||
{ css: 'text-align', group: 'textAlign', get: () => textAlign.value, set: v => textAlign.value = v, debounce: false },
|
||||
{ css: 'color', group: 'color', get: () => color.value, set: v => color.value = v, debounce: true },
|
||||
{ css: 'background', group: 'background', get: () => background.value, set: v => background.value = v, debounce: true },
|
||||
{ css: 'border-style', group: 'border', get: () => borderStyle.value, set: v => borderStyle.value = v || 'solid', debounce: false },
|
||||
{ css: 'border-color', group: 'border', get: () => borderColor.value, set: v => borderColor.value = v, debounce: true },
|
||||
];
|
||||
|
||||
const unitProps = [
|
||||
{ css: 'font-size', ref: fontSize, debounce: true },
|
||||
{ css: 'line-height', ref: lineHeight, debounce: true },
|
||||
{ css: 'border-width', ref: borderWidth, debounce: true },
|
||||
{ css: 'font-size', group: 'fontSize', ref: fontSize, debounce: true },
|
||||
{ css: 'line-height', group: 'lineHeight', ref: lineHeight, debounce: true },
|
||||
{ css: 'border-width', group: 'border', ref: borderWidth, debounce: true },
|
||||
];
|
||||
|
||||
const sides = [
|
||||
|
|
@ -392,17 +431,50 @@ const updatePaddingUnit = (side, unit) => {
|
|||
}
|
||||
};
|
||||
|
||||
// CSS properties covered by each group
|
||||
const settingGroups = {
|
||||
font: ['font-family', 'font-style', 'font-weight'],
|
||||
fontSize: ['font-size'],
|
||||
lineHeight:['line-height'],
|
||||
textAlign: ['text-align'],
|
||||
color: ['color'],
|
||||
background:['background'],
|
||||
border: ['border-width', 'border-style', 'border-color'],
|
||||
margin: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
|
||||
padding: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],
|
||||
};
|
||||
|
||||
// Remove CSS properties from the element's custom CSS block
|
||||
const removeProps = (cssProps) => {
|
||||
if (!selector.value) return;
|
||||
|
||||
const block = stylesheetStore.extractBlock(selector.value);
|
||||
if (!block || !stylesheetStore.customCss.includes(block)) return;
|
||||
|
||||
let newBlock = block;
|
||||
for (const prop of cssProps) {
|
||||
const escaped = prop.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
newBlock = newBlock.replace(
|
||||
new RegExp('[ \\t]*' + escaped + '\\s*:[^;]*;[ \\t]*\\n?', 'g'),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
// If the block is now empty, remove it entirely
|
||||
const inner = newBlock.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, '');
|
||||
if (!inner.trim()) {
|
||||
stylesheetStore.replaceInCustomCss(block, '');
|
||||
} else {
|
||||
stylesheetStore.replaceBlock(block, newBlock);
|
||||
}
|
||||
};
|
||||
|
||||
// Generic update: push a single property to the stylesheet store
|
||||
const updateProp = (cssProp, value, unit) => {
|
||||
if (!selector.value) return;
|
||||
stylesheetStore.updateProperty(selector.value, cssProp, value, unit);
|
||||
};
|
||||
|
||||
const updateUnitPropUnit = (prop, newUnit) => {
|
||||
prop.value = convertUnit(prop.value, prop.unit, newUnit);
|
||||
prop.unit = newUnit;
|
||||
};
|
||||
|
||||
const getSelectorFromElement = (element) => {
|
||||
if (element.id) {
|
||||
return `#${element.id}`;
|
||||
|
|
@ -441,6 +513,7 @@ const generatePreviewCss = () => {
|
|||
const properties = [];
|
||||
|
||||
for (const prop of styleProps) {
|
||||
if (!settingEnabled[prop.group]) continue;
|
||||
const val = prop.get();
|
||||
if (!val) continue;
|
||||
if (prop.css === 'font-style' && val !== 'italic') continue;
|
||||
|
|
@ -450,19 +523,25 @@ const generatePreviewCss = () => {
|
|||
}
|
||||
|
||||
for (const prop of unitProps) {
|
||||
if (!settingEnabled[prop.group]) continue;
|
||||
if (prop.ref.value !== undefined && prop.ref.value !== null) {
|
||||
properties.push(` ${prop.css}: ${prop.ref.value}${prop.ref.unit};`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (margin[side].value !== undefined && margin[side].value !== null) {
|
||||
properties.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
|
||||
if (settingEnabled.margin) {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (margin[side].value !== undefined && margin[side].value !== null) {
|
||||
properties.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (padding[side].value !== undefined && padding[side].value !== null) {
|
||||
properties.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
|
||||
|
||||
if (settingEnabled.padding) {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (padding[side].value !== undefined && padding[side].value !== null) {
|
||||
properties.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -476,38 +555,109 @@ const displayedCss = computed(() => {
|
|||
return elementCss.value || generatePreviewCss();
|
||||
});
|
||||
|
||||
const applyAllStyles = () => {
|
||||
// Apply all properties for a given group to the stylesheet
|
||||
const applyGroup = (group) => {
|
||||
if (!selector.value) return;
|
||||
|
||||
for (const prop of styleProps) {
|
||||
if (prop.group !== group) continue;
|
||||
if (prop.skipWhenDefault && prop.skipWhenDefault(prop.get())) continue;
|
||||
updateProp(prop.css, prop.get());
|
||||
}
|
||||
for (const prop of unitProps) {
|
||||
updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
||||
if (prop.group === group) updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
||||
}
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
||||
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
||||
if (group === 'margin') {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
||||
}
|
||||
}
|
||||
if (group === 'padding') {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Watchers — simple props
|
||||
// Apply all currently-enabled groups to keep the CSS block consistent
|
||||
const applyAllEnabledGroups = () => {
|
||||
for (const group of Object.keys(settingEnabled)) {
|
||||
if (settingEnabled[group]) applyGroup(group);
|
||||
}
|
||||
};
|
||||
|
||||
// When unchecking a special group: save element value, load TextSettings fallback for display
|
||||
const saveToCacheAndLoadFallback = (group) => {
|
||||
if (group === 'fontSize') {
|
||||
settingCache.fontSize = { value: fontSize.value, unit: fontSize.unit };
|
||||
// Display is handled reactively by fontSizeModel computed
|
||||
} else if (group === 'font') {
|
||||
settingCache.font = { fontFamily: fontFamily.value, italic: italic.value, bold: bold.value };
|
||||
const ff = cssExtractValue('body', 'font-family');
|
||||
if (ff) fontFamily.value = ff.replace(/['"]/g, '');
|
||||
const fs = cssExtractValue('p', 'font-style');
|
||||
if (fs) italic.value = fs === 'italic';
|
||||
const fw = cssExtractValue('p', 'font-weight');
|
||||
if (fw) bold.value = fw === 'bold' || parseInt(fw) >= 700;
|
||||
} else if (group === 'color') {
|
||||
settingCache.color = color.value;
|
||||
const c = cssExtractValue('body', 'color');
|
||||
if (c) color.value = c;
|
||||
}
|
||||
};
|
||||
|
||||
// When re-checking a special group: restore cached element value
|
||||
const restoreFromCache = (group) => {
|
||||
if (group === 'fontSize' && settingCache.fontSize) {
|
||||
fontSize.value = settingCache.fontSize.value;
|
||||
fontSize.unit = settingCache.fontSize.unit;
|
||||
settingCache.fontSize = null;
|
||||
} else if (group === 'font' && settingCache.font) {
|
||||
fontFamily.value = settingCache.font.fontFamily;
|
||||
italic.value = settingCache.font.italic;
|
||||
bold.value = settingCache.font.bold;
|
||||
settingCache.font = null;
|
||||
} else if (group === 'color' && settingCache.color !== null) {
|
||||
color.value = settingCache.color;
|
||||
settingCache.color = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle a setting group on/off
|
||||
const onToggleSetting = (group, enabled) => {
|
||||
settingEnabled[group] = enabled;
|
||||
if (enabled) {
|
||||
isUpdatingFromStore = true;
|
||||
restoreFromCache(group);
|
||||
isUpdatingFromStore = false;
|
||||
applyAllEnabledGroups();
|
||||
} else {
|
||||
saveToCacheAndLoadFallback(group);
|
||||
removeProps(settingGroups[group]);
|
||||
}
|
||||
};
|
||||
|
||||
// Watchers — simple props (with group guard)
|
||||
for (const prop of styleProps) {
|
||||
watch(prop.get, () => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled[prop.group]) return;
|
||||
const fn = () => updateProp(prop.css, prop.get());
|
||||
prop.debounce ? debouncedUpdate(fn) : fn();
|
||||
});
|
||||
}
|
||||
|
||||
// Watchers — unit props (watch both value and unit)
|
||||
// Watchers — unit props (with group guard)
|
||||
for (const prop of unitProps) {
|
||||
watch(() => prop.ref.value, () => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled[prop.group]) return;
|
||||
const fn = () => updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
||||
prop.debounce ? debouncedUpdate(fn) : fn();
|
||||
});
|
||||
watch(() => prop.ref.unit, () => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled[prop.group]) return;
|
||||
updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
||||
});
|
||||
}
|
||||
|
|
@ -517,6 +667,7 @@ watch(
|
|||
() => [margin.top.value, margin.right.value, margin.bottom.value, margin.left.value],
|
||||
() => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled.margin) return;
|
||||
debouncedUpdate(() => {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
||||
|
|
@ -528,6 +679,7 @@ watch(
|
|||
() => [margin.top.unit, margin.right.unit, margin.bottom.unit, margin.left.unit],
|
||||
() => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled.margin) return;
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
||||
}
|
||||
|
|
@ -539,6 +691,7 @@ watch(
|
|||
() => [padding.top.value, padding.right.value, padding.bottom.value, padding.left.value],
|
||||
() => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled.padding) return;
|
||||
debouncedUpdate(() => {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
||||
|
|
@ -550,6 +703,7 @@ watch(
|
|||
() => [padding.top.unit, padding.right.unit, padding.bottom.unit, padding.left.unit],
|
||||
() => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled.padding) return;
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
||||
}
|
||||
|
|
@ -587,16 +741,46 @@ watch(
|
|||
}
|
||||
);
|
||||
|
||||
const loadValuesFromStylesheet = () => {
|
||||
const loadValuesFromStylesheet = (isInitialLoad = false) => {
|
||||
if (!selector.value) return;
|
||||
|
||||
if (isInitialLoad) {
|
||||
// Reset settingEnabled to defaults only on initial open
|
||||
settingEnabled.font = true;
|
||||
settingEnabled.fontSize = true;
|
||||
settingEnabled.lineHeight = false;
|
||||
settingEnabled.textAlign = false;
|
||||
settingEnabled.color = true;
|
||||
settingEnabled.background = false;
|
||||
settingEnabled.border = false;
|
||||
settingEnabled.margin = false;
|
||||
settingEnabled.padding = false;
|
||||
}
|
||||
|
||||
const groupsFound = new Set();
|
||||
|
||||
// Only detect settingEnabled from the custom CSS block (not baseCss fallback)
|
||||
const customCssBlock = (() => {
|
||||
const css = stylesheetStore.customCss;
|
||||
if (!css) return '';
|
||||
const escaped = selector.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = new RegExp(escaped + '\\s*\\{([^}]*)\\}').exec(css);
|
||||
return match ? match[1] : '';
|
||||
})();
|
||||
const isInCustomCss = (cssProp) =>
|
||||
customCssBlock.includes(cssProp + ':') || customCssBlock.includes(cssProp + ' :');
|
||||
|
||||
try {
|
||||
// Simple props
|
||||
for (const prop of styleProps) {
|
||||
const data = stylesheetStore.extractValue(selector.value, prop.css);
|
||||
if (data) {
|
||||
const value = typeof data === 'string' ? data : data.value;
|
||||
prop.set(value);
|
||||
if (isInCustomCss(prop.css)) groupsFound.add(prop.group);
|
||||
// During live sync, don't overwrite refs for disabled groups (they show cached/fallback values)
|
||||
if (isInitialLoad || settingEnabled[prop.group]) {
|
||||
const value = typeof data === 'string' ? data : data.value;
|
||||
prop.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -604,8 +788,11 @@ const loadValuesFromStylesheet = () => {
|
|||
for (const prop of unitProps) {
|
||||
const data = stylesheetStore.extractValue(selector.value, prop.css);
|
||||
if (data && data.value !== undefined) {
|
||||
prop.ref.value = data.value;
|
||||
prop.ref.unit = data.unit;
|
||||
if (isInCustomCss(prop.css)) groupsFound.add(prop.group);
|
||||
if (isInitialLoad || settingEnabled[prop.group]) {
|
||||
prop.ref.value = data.value;
|
||||
prop.ref.unit = data.unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -617,18 +804,20 @@ const loadValuesFromStylesheet = () => {
|
|||
if (data && data.value !== undefined) {
|
||||
margin[side].value = data.value;
|
||||
margin[side].unit = data.unit;
|
||||
anyMarginFound = true;
|
||||
if (isInCustomCss(`margin-${side}`)) anyMarginFound = true;
|
||||
}
|
||||
}
|
||||
if (!anyMarginFound) {
|
||||
if (!anyMarginFound && isInCustomCss('margin')) {
|
||||
const data = stylesheetStore.extractValue(selector.value, 'margin');
|
||||
if (data && data.value !== undefined) {
|
||||
for (const side of spacingSides) {
|
||||
margin[side].value = data.value;
|
||||
margin[side].unit = data.unit;
|
||||
}
|
||||
anyMarginFound = true;
|
||||
}
|
||||
}
|
||||
if (anyMarginFound) groupsFound.add('margin');
|
||||
|
||||
// Padding sides — try individual first, fallback to shorthand
|
||||
let anyPaddingFound = false;
|
||||
|
|
@ -637,24 +826,71 @@ const loadValuesFromStylesheet = () => {
|
|||
if (data && data.value !== undefined) {
|
||||
padding[side].value = data.value;
|
||||
padding[side].unit = data.unit;
|
||||
anyPaddingFound = true;
|
||||
if (isInCustomCss(`padding-${side}`)) anyPaddingFound = true;
|
||||
}
|
||||
}
|
||||
if (!anyPaddingFound) {
|
||||
if (!anyPaddingFound && isInCustomCss('padding')) {
|
||||
const data = stylesheetStore.extractValue(selector.value, 'padding');
|
||||
if (data && data.value !== undefined) {
|
||||
for (const side of spacingSides) {
|
||||
padding[side].value = data.value;
|
||||
padding[side].unit = data.unit;
|
||||
}
|
||||
anyPaddingFound = true;
|
||||
}
|
||||
}
|
||||
if (anyPaddingFound) groupsFound.add('padding');
|
||||
|
||||
// Update settingEnabled based on what was found in the element's CSS
|
||||
if (isInitialLoad) {
|
||||
for (const group of ['lineHeight', 'textAlign', 'background', 'border', 'margin', 'padding']) {
|
||||
settingEnabled[group] = groupsFound.has(group);
|
||||
}
|
||||
} else {
|
||||
// During live sync: only enable groups newly found in CSS, never override user's manual toggles
|
||||
for (const group of Object.keys(settingEnabled)) {
|
||||
if (groupsFound.has(group)) settingEnabled[group] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special groups: font, fontSize, color — always enabled
|
||||
// If not found in element CSS, load fallback values from TextSettings selectors
|
||||
// Only on initial open — during live sync, ElementPopup is independent from TextSettings
|
||||
if (isInitialLoad) {
|
||||
if (!groupsFound.has('font')) {
|
||||
const ff = stylesheetStore.extractValue('body', 'font-family');
|
||||
if (ff) fontFamily.value = (typeof ff === 'string' ? ff : ff.value).replace(/['"]/g, '');
|
||||
const fs = stylesheetStore.extractValue('p', 'font-style');
|
||||
if (fs) italic.value = (typeof fs === 'string' ? fs : fs.value) === 'italic';
|
||||
const fw = stylesheetStore.extractValue('p', 'font-weight');
|
||||
if (fw) { const v = typeof fw === 'string' ? fw : fw.value; bold.value = v === 'bold' || parseInt(v) >= 700; }
|
||||
}
|
||||
|
||||
if (!groupsFound.has('fontSize')) {
|
||||
const data = stylesheetStore.extractValue('p', 'font-size');
|
||||
if (data && data.value !== undefined) {
|
||||
fontSize.value = data.value;
|
||||
fontSize.unit = data.unit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupsFound.has('color')) {
|
||||
const c = stylesheetStore.extractValue('body', 'color');
|
||||
if (c) color.value = typeof c === 'string' ? c : c.value;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading values from stylesheet:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const open = (element, event, count = null) => {
|
||||
// Clear cache from any previous element
|
||||
settingCache.font = null;
|
||||
settingCache.fontSize = null;
|
||||
settingCache.color = null;
|
||||
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
selectedElement.value = element;
|
||||
|
|
@ -662,7 +898,7 @@ const open = (element, event, count = null) => {
|
|||
|
||||
elementInstanceCount.value = count !== null ? count : getInstanceCount(selector.value);
|
||||
|
||||
loadValuesFromStylesheet();
|
||||
loadValuesFromStylesheet(true);
|
||||
basePopup.value.open(event);
|
||||
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
|
|
@ -717,6 +953,21 @@ defineExpose({ handleIframeClick, close, visible });
|
|||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Toggle setting checkbox */
|
||||
.toggle-setting {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-purple, #7c3aed);
|
||||
}
|
||||
|
||||
/* Disabled state: grey out fields but keep checkbox interactive */
|
||||
.setting-disabled .field,
|
||||
.setting-disabled .settings-subsection-header {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lock-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ import InputWithUnit from '../ui/InputWithUnit.vue';
|
|||
import { useCssUpdater } from '../../composables/useCssUpdater';
|
||||
import { useCssSync } from '../../composables/useCssSync';
|
||||
import { useDebounce } from '../../composables/useDebounce';
|
||||
import { useTextDefaults } from '../../composables/useTextDefaults';
|
||||
|
||||
const { updateStyle } = useCssUpdater();
|
||||
const { extractValue, extractNumericValue } = useCssSync();
|
||||
const { debouncedUpdate } = useDebounce(500);
|
||||
const textDefaults = useTextDefaults();
|
||||
|
||||
// Constants
|
||||
const fonts = ['Alegreya Sans', 'Arial', 'Georgia', 'Helvetica', 'Times New Roman'];
|
||||
|
|
@ -112,11 +114,12 @@ watch(bold, (val) => {
|
|||
});
|
||||
|
||||
watch(fontSize, (val) => {
|
||||
textDefaults.fontSize = { value: val.value, unit: val.unit };
|
||||
if (isUpdatingFromStore) return;
|
||||
debouncedUpdate(() => {
|
||||
updateStyle('p', 'font-size', `${val.value}${val.unit}`);
|
||||
});
|
||||
}, { deep: true });
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
|
||||
// Sync from store
|
||||
|
|
|
|||
14
src/composables/useTextDefaults.js
Normal file
14
src/composables/useTextDefaults.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { reactive } from 'vue';
|
||||
|
||||
// Singleton reactive — TextSettings writes here, ElementPopup reads when disabled
|
||||
const defaults = reactive({
|
||||
fontSize: { value: 16, unit: 'px' },
|
||||
fontFamily: 'Alegreya Sans',
|
||||
italic: false,
|
||||
bold: false,
|
||||
color: 'rgb(0, 0, 0)',
|
||||
});
|
||||
|
||||
export function useTextDefaults() {
|
||||
return defaults;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue