factorisation ElementPopUp
This commit is contained in:
parent
3853d0d6e3
commit
8bf7afddd2
3 changed files with 932 additions and 907 deletions
File diff suppressed because it is too large
Load diff
813
src/composables/useElementSettings.js
Normal file
813
src/composables/useElementSettings.js
Normal file
|
|
@ -0,0 +1,813 @@
|
|||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
import { useDebounce } from './useDebounce';
|
||||
import { useTextDefaults } from './useTextDefaults';
|
||||
import { useProjectFonts } from './useProjectFonts';
|
||||
|
||||
export function useElementSettings({ margin, padding, basePopup }) {
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
const textDefaults = useTextDefaults();
|
||||
const { fonts: projectFonts, loadFont, loadAllFontPreviews } = useProjectFonts();
|
||||
const { debouncedUpdate } = useDebounce(500);
|
||||
|
||||
let isUpdatingFromStore = false;
|
||||
|
||||
// --- Selector state ---
|
||||
const selector = ref('');
|
||||
|
||||
// --- Style refs ---
|
||||
const fontFamily = ref('sans-serif');
|
||||
const italic = ref(false);
|
||||
const bold = ref(false);
|
||||
const textAlign = ref('left');
|
||||
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 }),
|
||||
set: (v) => { fontSize.value = v.value; fontSize.unit = v.unit; },
|
||||
});
|
||||
const lineHeight = reactive({ value: 28, unit: 'px' });
|
||||
const lineHeightModel = computed({
|
||||
get: () => ({ value: lineHeight.value, unit: lineHeight.unit }),
|
||||
set: (v) => { lineHeight.value = v.value; lineHeight.unit = v.unit; },
|
||||
});
|
||||
const borderWidth = reactive({ value: 1, unit: 'px' });
|
||||
const borderStyle = ref('solid');
|
||||
const borderColor = ref('#000000');
|
||||
|
||||
// --- Toggle state ---
|
||||
const settingEnabled = reactive({
|
||||
font: false,
|
||||
fontSize: false,
|
||||
lineHeight: false,
|
||||
textAlign: false,
|
||||
color: false,
|
||||
background: false,
|
||||
border: false,
|
||||
margin: false,
|
||||
padding: false,
|
||||
});
|
||||
|
||||
// --- Cache for special groups ---
|
||||
const settingCache = reactive({
|
||||
font: null,
|
||||
fontSize: null,
|
||||
lineHeight: null,
|
||||
color: null,
|
||||
});
|
||||
|
||||
// --- Persistent state per element selector ---
|
||||
const elementStates = new Map();
|
||||
|
||||
const saveElementState = () => {
|
||||
if (!selector.value) return;
|
||||
elementStates.set(selector.value, {
|
||||
toggles: { ...settingEnabled },
|
||||
cache: {
|
||||
font: settingCache.font ? { ...settingCache.font } : null,
|
||||
fontSize: settingCache.fontSize ? { ...settingCache.fontSize } : null,
|
||||
lineHeight: settingCache.lineHeight ? { ...settingCache.lineHeight } : null,
|
||||
color: settingCache.color ?? null,
|
||||
},
|
||||
values: {
|
||||
fontFamily: fontFamily.value,
|
||||
italic: italic.value,
|
||||
bold: bold.value,
|
||||
fontSize: { value: fontSize.value, unit: fontSize.unit },
|
||||
lineHeight: { value: lineHeight.value, unit: lineHeight.unit },
|
||||
color: color.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// --- Property descriptors ---
|
||||
const styleProps = [
|
||||
{ css: 'font-family', group: 'font', get: () => fontFamily.value === 'sans-serif' ? 'sans-serif' : `"${fontFamily.value}"`, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false, skipWatch: true },
|
||||
{ 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', 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 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'],
|
||||
};
|
||||
|
||||
// --- CSS helpers ---
|
||||
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'),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
const inner = newBlock.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, '');
|
||||
if (!inner.trim()) {
|
||||
stylesheetStore.replaceInCustomCss(block, '');
|
||||
} else {
|
||||
stylesheetStore.replaceBlock(block, newBlock);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProp = (cssProp, value, unit) => {
|
||||
if (!selector.value) return;
|
||||
stylesheetStore.updateProperty(selector.value, cssProp, value, unit);
|
||||
};
|
||||
|
||||
// --- Group apply/toggle ---
|
||||
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) {
|
||||
if (prop.group === group) updateProp(prop.css, prop.ref.value, prop.ref.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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyAllEnabledGroups = () => {
|
||||
for (const group of Object.keys(settingEnabled)) {
|
||||
if (settingEnabled[group]) applyGroup(group);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Special group cache ---
|
||||
const saveToCache = (group) => {
|
||||
if (group === 'fontSize') {
|
||||
settingCache.fontSize = { value: fontSize.value, unit: fontSize.unit };
|
||||
} else if (group === 'lineHeight') {
|
||||
settingCache.lineHeight = { value: lineHeight.value, unit: lineHeight.unit };
|
||||
} else if (group === 'font') {
|
||||
settingCache.font = { fontFamily: fontFamily.value, italic: italic.value, bold: bold.value };
|
||||
} else if (group === 'color') {
|
||||
settingCache.color = color.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 === 'lineHeight' && settingCache.lineHeight) {
|
||||
lineHeight.value = settingCache.lineHeight.value;
|
||||
lineHeight.unit = settingCache.lineHeight.unit;
|
||||
settingCache.lineHeight = 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;
|
||||
}
|
||||
};
|
||||
|
||||
const removeSpecialGroupProps = (group) => {
|
||||
if (!selector.value) return;
|
||||
if (group === 'fontSize') {
|
||||
removeProps(['font-size']);
|
||||
stylesheetStore.updateProperty('p', 'font-size', textDefaults.fontSize.value, textDefaults.fontSize.unit);
|
||||
} else if (group === 'lineHeight') {
|
||||
removeProps(['line-height']);
|
||||
stylesheetStore.updateProperty('p', 'line-height', textDefaults.lineHeight.value, textDefaults.lineHeight.unit);
|
||||
} else if (group === 'color') {
|
||||
removeProps(['color']);
|
||||
stylesheetStore.updateProperty('body', 'color', textDefaults.color);
|
||||
} else if (group === 'font') {
|
||||
removeProps(['font-family', 'font-style', 'font-weight']);
|
||||
const fontVal = textDefaults.fontFamily === 'sans-serif' ? 'sans-serif' : `"${textDefaults.fontFamily}"`;
|
||||
stylesheetStore.updateProperty('body', 'font-family', fontVal);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Toggle actions ---
|
||||
const onSubsectionClick = (group) => {
|
||||
if (settingEnabled[group]) return;
|
||||
onToggleSetting(group, true);
|
||||
};
|
||||
|
||||
const onToggleSetting = (group, enabled) => {
|
||||
settingEnabled[group] = enabled;
|
||||
isUpdatingFromStore = true;
|
||||
if (enabled) {
|
||||
restoreFromCache(group);
|
||||
applyAllEnabledGroups();
|
||||
} else {
|
||||
saveToCache(group);
|
||||
const specialGroups = ['font', 'fontSize', 'lineHeight', 'color'];
|
||||
if (specialGroups.includes(group)) {
|
||||
removeSpecialGroupProps(group);
|
||||
} else {
|
||||
removeProps(settingGroups[group]);
|
||||
}
|
||||
}
|
||||
saveElementState();
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// --- CSS display computeds ---
|
||||
const elementCss = computed(() => {
|
||||
if (!selector.value) return '';
|
||||
return stylesheetStore.extractBlock(selector.value) || '';
|
||||
});
|
||||
|
||||
const displayedCssOrder = [
|
||||
{ css: 'font-family', group: 'font', special: true,
|
||||
getValue: () => { const val = settingEnabled.font ? fontFamily.value : textDefaults.fontFamily; return val === 'sans-serif' ? 'sans-serif' : `"${val}"`; } },
|
||||
{ css: 'font-style', group: 'font',
|
||||
getValue: () => italic.value ? 'italic' : null,
|
||||
skip: () => !settingEnabled.font || !italic.value },
|
||||
{ css: 'font-weight', group: 'font',
|
||||
getValue: () => bold.value ? 'bold' : null,
|
||||
skip: () => !settingEnabled.font || !bold.value },
|
||||
{ css: 'font-size', group: 'fontSize', special: true,
|
||||
getValue: () => `${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).value}${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).unit}` },
|
||||
{ css: 'line-height', group: 'lineHeight', special: true,
|
||||
getValue: () => `${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).value}${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).unit}` },
|
||||
{ css: 'text-align', group: 'textAlign',
|
||||
getValue: () => textAlign.value,
|
||||
skip: () => !settingEnabled.textAlign },
|
||||
{ css: 'color', group: 'color', special: true,
|
||||
getValue: () => settingEnabled.color ? color.value : textDefaults.color },
|
||||
{ css: 'background', group: 'background',
|
||||
getValue: () => background.value,
|
||||
skip: () => !settingEnabled.background },
|
||||
{ css: 'border-width', group: 'border',
|
||||
getValue: () => `${borderWidth.value}${borderWidth.unit}`,
|
||||
skip: () => !settingEnabled.border },
|
||||
{ css: 'border-style', group: 'border',
|
||||
getValue: () => borderStyle.value,
|
||||
skip: () => !settingEnabled.border || borderWidth.value === 0 },
|
||||
{ css: 'border-color', group: 'border',
|
||||
getValue: () => borderColor.value,
|
||||
skip: () => !settingEnabled.border || borderWidth.value === 0 },
|
||||
];
|
||||
|
||||
const displayedCss = computed(() => {
|
||||
if (!selector.value) return '';
|
||||
const lines = [];
|
||||
for (const entry of displayedCssOrder) {
|
||||
if (entry.skip && entry.skip()) continue;
|
||||
const val = entry.getValue();
|
||||
if (val === null || val === undefined) continue;
|
||||
const comment = (entry.special && !settingEnabled[entry.group]) ? ' /* valeur par défaut */' : '';
|
||||
lines.push(` ${entry.css}: ${val};${comment}`);
|
||||
}
|
||||
if (settingEnabled.margin) {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
lines.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
|
||||
}
|
||||
}
|
||||
if (settingEnabled.padding) {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
lines.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
|
||||
}
|
||||
}
|
||||
return `${selector.value} {\n${lines.join('\n')}\n}`;
|
||||
});
|
||||
|
||||
const editableFullCss = computed(() => {
|
||||
return displayedCss.value.replace(/ \/\* valeur par défaut \*\//g, '');
|
||||
});
|
||||
|
||||
// --- CSS parsing & sync ---
|
||||
const parseCssProperties = (cssText) => {
|
||||
const result = {};
|
||||
const match = cssText.match(/\{([\s\S]*)\}/);
|
||||
if (!match) return result;
|
||||
for (const decl of match[1].split(';')) {
|
||||
const clean = decl.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
if (!clean) continue;
|
||||
const idx = clean.indexOf(':');
|
||||
if (idx === -1) continue;
|
||||
const prop = clean.substring(0, idx).trim();
|
||||
const val = clean.substring(idx + 1).trim();
|
||||
if (prop && val) result[prop] = val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const specialGroupDiffersFromDefault = (group, parsed) => {
|
||||
switch (group) {
|
||||
case 'font': {
|
||||
if ('font-family' in parsed) {
|
||||
const val = parsed['font-family'].split(',')[0].trim().replace(/['"]/g, '');
|
||||
if (val !== textDefaults.fontFamily) return true;
|
||||
}
|
||||
if ('font-style' in parsed && parsed['font-style'].trim() === 'italic') return true;
|
||||
if ('font-weight' in parsed) {
|
||||
const v = parsed['font-weight'].trim();
|
||||
if (v === 'bold' || parseInt(v) >= 700) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case 'fontSize': {
|
||||
if ('font-size' in parsed) {
|
||||
const m = parsed['font-size'].match(/([\d.]+)(px|rem|em|mm|cm|in)?/);
|
||||
if (m) return parseFloat(m[1]) !== textDefaults.fontSize.value || (m[2] || 'px') !== textDefaults.fontSize.unit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case 'lineHeight': {
|
||||
if ('line-height' in parsed) {
|
||||
const m = parsed['line-height'].match(/([\d.]+)(px|rem|em|mm|cm|in)?/);
|
||||
if (m) return parseFloat(m[1]) !== textDefaults.lineHeight.value || (m[2] || 'px') !== textDefaults.lineHeight.unit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case 'color': {
|
||||
if ('color' in parsed) return parsed['color'].trim() !== textDefaults.color;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const syncRefsFromCss = (cssText) => {
|
||||
const parsed = parseCssProperties(cssText);
|
||||
const specialGroupNames = ['font', 'fontSize', 'lineHeight', 'color'];
|
||||
|
||||
for (const [group, cssProps] of Object.entries(settingGroups)) {
|
||||
const hasAnyProp = cssProps.some(p => p in parsed);
|
||||
|
||||
if (hasAnyProp) {
|
||||
for (const prop of styleProps) {
|
||||
if (prop.group !== group || !(prop.css in parsed)) continue;
|
||||
if (prop.css === 'font-family') {
|
||||
const firstFont = parsed[prop.css].split(',')[0].trim().replace(/['"]/g, '');
|
||||
fontFamily.value = firstFont;
|
||||
} else {
|
||||
prop.set(parsed[prop.css]);
|
||||
}
|
||||
}
|
||||
for (const prop of unitProps) {
|
||||
if (prop.group !== group || !(prop.css in parsed)) continue;
|
||||
const m = parsed[prop.css].match(/([\d.]+)(px|rem|em|mm|cm|in)?/);
|
||||
if (m) {
|
||||
prop.ref.value = parseFloat(m[1]);
|
||||
prop.ref.unit = m[2] || 'px';
|
||||
}
|
||||
}
|
||||
if (group === 'margin') {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (`margin-${side}` in parsed) {
|
||||
const m = parsed[`margin-${side}`].match(/([\d.]+)(px|rem|em|mm|cm|in)?/);
|
||||
if (m) { margin[side].value = parseFloat(m[1]); margin[side].unit = m[2] || 'mm'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (group === 'padding') {
|
||||
for (const side of ['top', 'right', 'bottom', 'left']) {
|
||||
if (`padding-${side}` in parsed) {
|
||||
const m = parsed[`padding-${side}`].match(/([\d.]+)(px|rem|em|mm|cm|in)?/);
|
||||
if (m) { padding[side].value = parseFloat(m[1]); padding[side].unit = m[2] || 'mm'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!settingEnabled[group]) {
|
||||
if (specialGroupNames.includes(group)) {
|
||||
if (specialGroupDiffersFromDefault(group, parsed)) {
|
||||
settingEnabled[group] = true;
|
||||
settingCache[group] = null;
|
||||
}
|
||||
} else {
|
||||
settingEnabled[group] = true;
|
||||
}
|
||||
}
|
||||
} else if (settingEnabled[group]) {
|
||||
settingEnabled[group] = false;
|
||||
if (group === 'font') {
|
||||
fontFamily.value = textDefaults.fontFamily;
|
||||
italic.value = false;
|
||||
bold.value = false;
|
||||
settingCache.font = null;
|
||||
} else if (group === 'fontSize') {
|
||||
fontSize.value = textDefaults.fontSize.value;
|
||||
fontSize.unit = textDefaults.fontSize.unit;
|
||||
settingCache.fontSize = null;
|
||||
} else if (group === 'lineHeight') {
|
||||
lineHeight.value = textDefaults.lineHeight.value;
|
||||
lineHeight.unit = textDefaults.lineHeight.unit;
|
||||
settingCache.lineHeight = null;
|
||||
} else if (group === 'color') {
|
||||
color.value = textDefaults.color;
|
||||
settingCache.color = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveElementState();
|
||||
};
|
||||
|
||||
const handleCssInput = (newCss) => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
syncRefsFromCss(newCss);
|
||||
|
||||
const parsed = parseCssProperties(newCss);
|
||||
const specialGroupPropsMap = {
|
||||
font: ['font-family', 'font-style', 'font-weight'],
|
||||
fontSize: ['font-size'],
|
||||
lineHeight: ['line-height'],
|
||||
color: ['color'],
|
||||
};
|
||||
const excludedProps = new Set();
|
||||
for (const [group, props] of Object.entries(specialGroupPropsMap)) {
|
||||
if (!settingEnabled[group]) {
|
||||
props.forEach(p => excludedProps.add(p));
|
||||
}
|
||||
}
|
||||
|
||||
const storeParts = [];
|
||||
for (const [prop, val] of Object.entries(parsed)) {
|
||||
if (!excludedProps.has(prop)) {
|
||||
storeParts.push(` ${prop}: ${val};`);
|
||||
}
|
||||
}
|
||||
|
||||
const storeBlock = storeParts.length
|
||||
? `${selector.value} {\n${storeParts.join('\n')}\n}\n`
|
||||
: '';
|
||||
|
||||
const oldBlock = elementCss.value;
|
||||
if (oldBlock && storeBlock) {
|
||||
stylesheetStore.replaceInCustomCss(oldBlock, storeBlock);
|
||||
} else if (oldBlock && !storeBlock) {
|
||||
stylesheetStore.replaceInCustomCss(oldBlock, '');
|
||||
} else if (!oldBlock && storeBlock) {
|
||||
stylesheetStore.setCustomCss(
|
||||
(stylesheetStore.customCss ? stylesheetStore.customCss + '\n' : '') + storeBlock
|
||||
);
|
||||
}
|
||||
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// --- Reapply stored values (shared selector protection) ---
|
||||
const reapplyStoredEnabledGroups = () => {
|
||||
const currentSel = basePopup.value?.visible ? selector.value : null;
|
||||
|
||||
for (const [sel, state] of elementStates) {
|
||||
if (!state.toggles || !state.values) continue;
|
||||
if (sel === currentSel) continue;
|
||||
|
||||
if (state.toggles.font) {
|
||||
const val = state.values.fontFamily === 'sans-serif' ? 'sans-serif' : `"${state.values.fontFamily}"`;
|
||||
stylesheetStore.updateProperty(sel, 'font-family', val);
|
||||
}
|
||||
if (state.toggles.fontSize) {
|
||||
stylesheetStore.updateProperty(sel, 'font-size', state.values.fontSize.value, state.values.fontSize.unit);
|
||||
}
|
||||
if (state.toggles.lineHeight) {
|
||||
stylesheetStore.updateProperty(sel, 'line-height', state.values.lineHeight.value, state.values.lineHeight.unit);
|
||||
}
|
||||
if (state.toggles.color) {
|
||||
stylesheetStore.updateProperty(sel, 'color', state.values.color);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSel) {
|
||||
applyAllEnabledGroups();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Load values from stylesheet ---
|
||||
const loadValuesFromStylesheet = (isInitialLoad = false) => {
|
||||
if (!selector.value) return;
|
||||
if (!isInitialLoad) return;
|
||||
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const spacingSides = ['top', 'right', 'bottom', 'left'];
|
||||
for (const side of spacingSides) {
|
||||
const data = stylesheetStore.extractValue(selector.value, `margin-${side}`);
|
||||
if (data && data.value !== undefined) {
|
||||
margin[side].value = data.value;
|
||||
margin[side].unit = data.unit;
|
||||
}
|
||||
}
|
||||
for (const side of spacingSides) {
|
||||
const data = stylesheetStore.extractValue(selector.value, `padding-${side}`);
|
||||
if (data && data.value !== undefined) {
|
||||
padding[side].value = data.value;
|
||||
padding[side].unit = data.unit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!settingEnabled.font && !settingCache.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 (!settingEnabled.fontSize && !settingCache.fontSize) {
|
||||
const data = stylesheetStore.extractValue('p', 'font-size');
|
||||
if (data && data.value !== undefined) { fontSize.value = data.value; fontSize.unit = data.unit; }
|
||||
}
|
||||
if (!settingEnabled.lineHeight && !settingCache.lineHeight) {
|
||||
const data = stylesheetStore.extractValue('p', 'line-height');
|
||||
if (data && data.value !== undefined) { lineHeight.value = data.value; lineHeight.unit = data.unit; }
|
||||
}
|
||||
if (!settingEnabled.color && settingCache.color === null) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Open / Close ---
|
||||
const open = (element, event, { getSelectorFromElement, getInstanceCount, selectedElement, elementInstanceCount }) => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
selectedElement.value = element;
|
||||
selector.value = getSelectorFromElement(element);
|
||||
elementInstanceCount.value = getInstanceCount(selector.value);
|
||||
|
||||
const stored = elementStates.get(selector.value);
|
||||
if (stored) {
|
||||
Object.assign(settingEnabled, stored.toggles);
|
||||
settingCache.font = stored.cache.font ? { ...stored.cache.font } : null;
|
||||
settingCache.fontSize = stored.cache.fontSize ? { ...stored.cache.fontSize } : null;
|
||||
settingCache.lineHeight = stored.cache.lineHeight ? { ...stored.cache.lineHeight } : null;
|
||||
settingCache.color = stored.cache.color ?? null;
|
||||
} else {
|
||||
for (const group of Object.keys(settingEnabled)) {
|
||||
settingEnabled[group] = false;
|
||||
}
|
||||
settingCache.font = null;
|
||||
settingCache.fontSize = null;
|
||||
settingCache.lineHeight = null;
|
||||
settingCache.color = null;
|
||||
}
|
||||
|
||||
loadValuesFromStylesheet(true);
|
||||
|
||||
if (stored && stored.values) {
|
||||
if (settingEnabled.font) {
|
||||
fontFamily.value = stored.values.fontFamily;
|
||||
italic.value = stored.values.italic;
|
||||
bold.value = stored.values.bold;
|
||||
} else if (settingCache.font) {
|
||||
fontFamily.value = settingCache.font.fontFamily;
|
||||
italic.value = settingCache.font.italic;
|
||||
bold.value = settingCache.font.bold;
|
||||
}
|
||||
if (settingEnabled.fontSize) {
|
||||
fontSize.value = stored.values.fontSize.value;
|
||||
fontSize.unit = stored.values.fontSize.unit;
|
||||
} else if (settingCache.fontSize) {
|
||||
fontSize.value = settingCache.fontSize.value;
|
||||
fontSize.unit = settingCache.fontSize.unit;
|
||||
}
|
||||
if (settingEnabled.lineHeight) {
|
||||
lineHeight.value = stored.values.lineHeight.value;
|
||||
lineHeight.unit = stored.values.lineHeight.unit;
|
||||
} else if (settingCache.lineHeight) {
|
||||
lineHeight.value = settingCache.lineHeight.value;
|
||||
lineHeight.unit = settingCache.lineHeight.unit;
|
||||
}
|
||||
if (settingEnabled.color) {
|
||||
color.value = stored.values.color;
|
||||
} else if (settingCache.color !== null) {
|
||||
color.value = settingCache.color;
|
||||
}
|
||||
}
|
||||
|
||||
basePopup.value.open(event);
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
const close = ({ selectedElement, emit }) => {
|
||||
saveElementState();
|
||||
basePopup.value?.close();
|
||||
selector.value = '';
|
||||
selectedElement.value = null;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// --- Watchers ---
|
||||
|
||||
// Font family: load font then update CSS
|
||||
watch(fontFamily, async (val) => {
|
||||
if (val && val !== 'sans-serif') await loadFont(val);
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled.font) return;
|
||||
const cssValue = val === 'sans-serif' ? 'sans-serif' : `"${val}"`;
|
||||
updateProp('font-family', cssValue);
|
||||
});
|
||||
|
||||
// Sync special groups with TextSettings defaults
|
||||
watch(() => textDefaults.fontFamily, (val) => {
|
||||
if (!settingEnabled.font && settingCache.font === null) fontFamily.value = val;
|
||||
});
|
||||
watch(() => textDefaults.fontSize, (val) => {
|
||||
if (!settingEnabled.fontSize && settingCache.fontSize === null) {
|
||||
fontSize.value = val.value;
|
||||
fontSize.unit = val.unit;
|
||||
}
|
||||
}, { deep: true });
|
||||
watch(() => textDefaults.lineHeight, (val) => {
|
||||
if (!settingEnabled.lineHeight && settingCache.lineHeight === null) {
|
||||
lineHeight.value = val.value;
|
||||
lineHeight.unit = val.unit;
|
||||
}
|
||||
}, { deep: true });
|
||||
watch(() => textDefaults.color, (val) => {
|
||||
if (!settingEnabled.color && settingCache.color === null) color.value = val;
|
||||
});
|
||||
|
||||
// Simple props watchers
|
||||
for (const prop of styleProps) {
|
||||
if (prop.skipWatch) continue;
|
||||
watch(prop.get, () => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (!settingEnabled[prop.group]) return;
|
||||
const fn = () => updateProp(prop.css, prop.get());
|
||||
prop.debounce ? debouncedUpdate(fn) : fn();
|
||||
});
|
||||
}
|
||||
|
||||
// Unit props watchers
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Margin watchers
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Padding watchers
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Stylesheet change watcher (shared selector protection)
|
||||
watch(
|
||||
() => stylesheetStore.customCss,
|
||||
() => {
|
||||
if (isUpdatingFromStore) return;
|
||||
if (stylesheetStore.isEditing) return;
|
||||
isUpdatingFromStore = true;
|
||||
reapplyStoredEnabledGroups();
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
// Watch edit mode exit
|
||||
watch(
|
||||
() => stylesheetStore.isEditing,
|
||||
(isEditing, wasEditing) => {
|
||||
if (basePopup.value?.visible && wasEditing && !isEditing && !isUpdatingFromStore) {
|
||||
isUpdatingFromStore = true;
|
||||
loadValuesFromStylesheet();
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
// State
|
||||
selector,
|
||||
fontFamily,
|
||||
italic,
|
||||
bold,
|
||||
textAlign,
|
||||
color,
|
||||
background,
|
||||
fontSize,
|
||||
fontSizeModel,
|
||||
lineHeight,
|
||||
lineHeightModel,
|
||||
borderWidth,
|
||||
borderStyle,
|
||||
borderColor,
|
||||
settingEnabled,
|
||||
settingCache,
|
||||
textDefaults,
|
||||
projectFonts,
|
||||
loadAllFontPreviews,
|
||||
|
||||
// Computeds
|
||||
elementCss,
|
||||
displayedCss,
|
||||
editableFullCss,
|
||||
|
||||
// Actions
|
||||
onSubsectionClick,
|
||||
onToggleSetting,
|
||||
handleCssInput,
|
||||
open,
|
||||
close,
|
||||
saveElementState,
|
||||
};
|
||||
}
|
||||
79
src/composables/useSpacingEditor.js
Normal file
79
src/composables/useSpacingEditor.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { ref, reactive } from 'vue';
|
||||
import { convertUnit } from '../utils/unit-conversion';
|
||||
|
||||
export function useSpacingEditor() {
|
||||
const sides = [
|
||||
{ key: 'top', label: 'Haut' },
|
||||
{ key: 'bottom', label: 'Bas' },
|
||||
{ key: 'left', label: 'Gauche' },
|
||||
{ key: 'right', label: 'Droite' },
|
||||
];
|
||||
|
||||
const marginLocked = ref(true);
|
||||
const margin = reactive({
|
||||
top: { value: 0, unit: 'mm' },
|
||||
right: { value: 0, unit: 'mm' },
|
||||
bottom: { value: 0, unit: 'mm' },
|
||||
left: { value: 0, unit: 'mm' },
|
||||
});
|
||||
|
||||
const paddingLocked = ref(true);
|
||||
const padding = reactive({
|
||||
top: { value: 0, unit: 'mm' },
|
||||
right: { value: 0, unit: 'mm' },
|
||||
bottom: { value: 0, unit: 'mm' },
|
||||
left: { value: 0, unit: 'mm' },
|
||||
});
|
||||
|
||||
const updateMarginValue = (side, value) => {
|
||||
if (marginLocked.value) {
|
||||
for (const s of ['top', 'right', 'bottom', 'left']) margin[s].value = value;
|
||||
} else {
|
||||
margin[side].value = value;
|
||||
}
|
||||
};
|
||||
|
||||
const updateMarginUnit = (side, unit) => {
|
||||
if (marginLocked.value) {
|
||||
for (const s of ['top', 'right', 'bottom', 'left']) {
|
||||
margin[s].value = convertUnit(margin[s].value, margin[s].unit, unit);
|
||||
margin[s].unit = unit;
|
||||
}
|
||||
} else {
|
||||
margin[side].value = convertUnit(margin[side].value, margin[side].unit, unit);
|
||||
margin[side].unit = unit;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePaddingValue = (side, value) => {
|
||||
if (paddingLocked.value) {
|
||||
for (const s of ['top', 'right', 'bottom', 'left']) padding[s].value = value;
|
||||
} else {
|
||||
padding[side].value = value;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePaddingUnit = (side, unit) => {
|
||||
if (paddingLocked.value) {
|
||||
for (const s of ['top', 'right', 'bottom', 'left']) {
|
||||
padding[s].value = convertUnit(padding[s].value, padding[s].unit, unit);
|
||||
padding[s].unit = unit;
|
||||
}
|
||||
} else {
|
||||
padding[side].value = convertUnit(padding[side].value, padding[side].unit, unit);
|
||||
padding[side].unit = unit;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
sides,
|
||||
margin,
|
||||
marginLocked,
|
||||
padding,
|
||||
paddingLocked,
|
||||
updateMarginValue,
|
||||
updateMarginUnit,
|
||||
updatePaddingValue,
|
||||
updatePaddingUnit,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue