From bdda8dc8a53f2d7b42732782b835a64ea08aa8a9 Mon Sep 17 00:00:00 2001 From: Julie Blanc Date: Mon, 23 Mar 2026 12:26:34 +0100 Subject: [PATCH] add all images has .block-image --- src/components/ImagePopup.vue | 309 +++++++++++++++++++----- src/components/PagedJsWrapper.vue | 24 +- src/components/editor/ImageSettings.vue | 80 +++++- src/composables/useImageDefaults.js | 1 + src/stores/stylesheet.js | 10 + src/utils/defaults.js | 1 + 6 files changed, 352 insertions(+), 73 deletions(-) diff --git a/src/components/ImagePopup.vue b/src/components/ImagePopup.vue index a8c07be..a8bb1c1 100644 --- a/src/components/ImagePopup.vue +++ b/src/components/ImagePopup.vue @@ -6,13 +6,13 @@ :display-css="displayedCss" :editable-css="editableCss" :popup-width="440" - :popup-height="200" + :popup-height="320" :show-inheritance="false" @close="handleClose" @css-input="handleCssInput" > @@ -56,39 +84,68 @@ import InputWithUnit from './ui/InputWithUnit.vue'; import BasePopup from './ui/BasePopup.vue'; import { useStylesheetStore } from '../stores/stylesheet'; import { useImageDefaults } from '../composables/useImageDefaults'; +import { IMAGE_DEFAULTS } from '../utils/defaults'; +import { useCssUpdater } from '../composables/useCssUpdater'; +import { useDebounce } from '../composables/useDebounce'; const emit = defineEmits(['close']); const stylesheetStore = useStylesheetStore(); const imageDefaults = useImageDefaults(); +const { updateStyle, removeProperty } = useCssUpdater(); +const { debouncedUpdate } = useDebounce(400); const basePopup = ref(null); const visible = computed(() => basePopup.value?.visible ?? false); +const currentFigure = ref(null); const selector = ref(''); + +// --- Width state --- const width = reactive({ value: 100, unit: '%' }); const widthEnabled = ref(false); -const widthCache = ref(null); // { value, unit } | null — persists user value when toggle is OFF +const widthCache = ref(null); + +// --- Height state --- +const heightAuto = ref(true); +const heightPx = reactive({ value: IMAGE_DEFAULTS.height.value, unit: IMAGE_DEFAULTS.height.unit }); +const heightEnabled = ref(false); +const heightCache = ref(null); // { auto, value, unit } | null +const measuredHeight = ref(null); // Actual height captured on first uncheck, per open session -// Per-selector persistent state const elementStates = new Map(); let isUpdatingFromStore = false; -// InputWithUnit model +// --- Models --- const widthModel = computed({ get: () => ({ value: width.value, unit: width.unit }), set: (v) => { width.value = v.value; width.unit = v.unit; }, }); -// Watch width changes → update CSS when toggle is ON + +// --- Watchers --- + watch(() => [width.value, width.unit], () => { if (isUpdatingFromStore || !widthEnabled.value || !selector.value) return; - stylesheetStore.updateProperty(selector.value, 'width', width.value, width.unit); - saveState(); + debouncedUpdate(() => { + stylesheetStore.updateProperty(selector.value, 'width', width.value, width.unit); + saveState(); + }); }); -// Sync greyed field with imageDefaults when toggle is OFF and no cache +const onHeightValueChange = (v) => { + heightPx.value = v.value; + heightPx.unit = v.unit; + if (isUpdatingFromStore || !heightEnabled.value || heightAuto.value || !selector.value) return; + measuredHeight.value = v.value; + debouncedUpdate(() => { + stylesheetStore.updateProperty(selector.value, 'height', heightPx.value, heightPx.unit); + saveState(); + }); +}; + +// Sync greyed width field with imageDefaults when toggle OFF and no cache watch(() => imageDefaults.width, (val) => { if (!widthEnabled.value && widthCache.value === null) { isUpdatingFromStore = true; @@ -98,16 +155,26 @@ watch(() => imageDefaults.width, (val) => { } }, { deep: true }); -// --- Toggle --- +// Sync greyed height field with imageDefaults when toggle OFF and no cache +watch(() => imageDefaults.height, (val) => { + if (!heightEnabled.value && heightCache.value === null) { + isUpdatingFromStore = true; + heightAuto.value = val.auto; + heightPx.value = val.value; + heightPx.unit = val.unit; + nextTick(() => { isUpdatingFromStore = false; }); + } +}, { deep: true }); -const onSectionClick = () => { +// --- Width toggle --- + +const onWidthSectionClick = () => { if (!widthEnabled.value) onToggleWidth(true); }; const onToggleWidth = (enabled) => { isUpdatingFromStore = true; widthEnabled.value = enabled; - if (enabled) { if (widthCache.value) { width.value = widthCache.value.value; @@ -117,23 +184,97 @@ const onToggleWidth = (enabled) => { stylesheetStore.updateProperty(selector.value, 'width', width.value, width.unit); } else { widthCache.value = { value: width.value, unit: width.unit }; - removeWidthProp(); + removeProp('width'); width.value = imageDefaults.width.value; width.unit = imageDefaults.width.unit; } - saveState(); nextTick(() => { isUpdatingFromStore = false; }); }; +// --- Height toggle --- + +const onHeightSectionClick = () => { + if (!heightEnabled.value) onToggleHeight(true); +}; + +const onToggleHeight = (enabled) => { + isUpdatingFromStore = true; + heightEnabled.value = enabled; + if (enabled) { + if (heightCache.value) { + heightAuto.value = heightCache.value.auto; + heightPx.value = heightCache.value.value; + heightPx.unit = heightCache.value.unit; + heightCache.value = null; + } + applyHeight(); + if (!heightAuto.value) updateStyle(`${selector.value} img`, 'height', '100%'); + } else { + heightCache.value = { auto: heightAuto.value, value: heightPx.value, unit: heightPx.unit }; + removeProp('height'); + removeProperty(`${selector.value} img`, 'height'); + heightAuto.value = imageDefaults.height.auto; + heightPx.value = imageDefaults.height.value; + heightPx.unit = imageDefaults.height.unit; + } + saveState(); + nextTick(() => { isUpdatingFromStore = false; }); +}; + +// Auto checkbox (only active when heightEnabled) +const onToggleHeightAuto = (auto) => { + if (!heightEnabled.value) return; + isUpdatingFromStore = true; + if (!auto && heightAuto.value) { + // Unchecking auto → use measured height (capture once, reuse on subsequent unchecks) + if (measuredHeight.value === null) { + measuredHeight.value = getActualImageHeight(); + } + heightPx.value = measuredHeight.value; + heightPx.unit = 'px'; + } + heightAuto.value = auto; + applyHeight(); + if (!auto) { + updateStyle(`${selector.value} img`, 'height', '100%'); + } else { + removeProperty(`${selector.value} img`, 'height'); + } + saveState(); + nextTick(() => { isUpdatingFromStore = false; }); +}; + +const getActualImageHeight = () => { + if (currentFigure.value) { + const img = currentFigure.value.querySelector('img'); + if (img) { + const h = Math.round(img.getBoundingClientRect().height); + if (h > 0) return Math.min(h, 800); + } + const figH = Math.round(currentFigure.value.getBoundingClientRect().height); + if (figH > 0) return Math.min(figH, 800); + } + return IMAGE_DEFAULTS.height.value; +}; + // --- CSS helpers --- -const removeWidthProp = () => { +const applyHeight = () => { + if (!selector.value) return; + if (heightAuto.value) { + stylesheetStore.updateProperty(selector.value, 'height', 'auto'); + } else { + stylesheetStore.updateProperty(selector.value, 'height', heightPx.value, heightPx.unit); + } +}; + +const removeProp = (prop) => { if (!selector.value) return; const block = stylesheetStore.extractBlock(selector.value); if (!block || !stylesheetStore.customCss.includes(block)) return; - - const newBlock = block.replace(/[ \t]*width\s*:[^;]*;[ \t]*\n?/g, ''); + const escaped = prop.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const newBlock = block.replace(new RegExp(`[ \\t]*${escaped}\\s*:[^;]*;[ \\t]*\\n?`, 'g'), ''); const inner = newBlock.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, ''); if (!inner.trim()) { stylesheetStore.replaceInCustomCss(block, ''); @@ -146,10 +287,21 @@ const removeWidthProp = () => { const displayedCss = computed(() => { if (!selector.value) return ''; + const lines = []; + + // Width const widthVal = `${width.value}${width.unit}`; - const defaultVal = `${imageDefaults.width.value}${imageDefaults.width.unit}`; - const comment = (!widthEnabled.value && widthVal === defaultVal) ? ' /* hérité de .block-image */' : ''; - return `${selector.value} {\n width: ${widthVal};${comment}\n}`; + const defaultWidthVal = `${imageDefaults.width.value}${imageDefaults.width.unit}`; + const widthComment = (!widthEnabled.value && widthVal === defaultWidthVal) ? ' /* hérité de .block-image */' : ''; + lines.push(` width: ${widthVal};${widthComment}`); + + // Height + const heightVal = heightAuto.value ? 'auto' : `${heightPx.value}${heightPx.unit}`; + const defaultHeightVal = imageDefaults.height.auto ? 'auto' : `${imageDefaults.height.value}${imageDefaults.height.unit}`; + const heightComment = (!heightEnabled.value && heightVal === defaultHeightVal) ? ' /* hérité de .block-image */' : ''; + lines.push(` height: ${heightVal};${heightComment}`); + + return `${selector.value} {\n${lines.join('\n')}\n}`; }); const editableCss = computed(() => { @@ -161,13 +313,30 @@ const editableCss = computed(() => { const handleCssInput = (newCss) => { isUpdatingFromStore = true; - const match = newCss.match(/width\s*:\s*([\d.]+)(%|px)/i); - if (match) { - width.value = parseFloat(match[1]); - width.unit = match[2]; + const widthMatch = newCss.match(/width\s*:\s*([\d.]+)(%|px)/i); + if (widthMatch) { + width.value = parseFloat(widthMatch[1]); + width.unit = widthMatch[2]; } - const newBlock = `${selector.value} {\n width: ${width.value}${width.unit};\n}\n`; + const heightMatch = newCss.match(/height\s*:\s*([^;]+)/i); + if (heightMatch) { + const hVal = heightMatch[1].trim(); + if (hVal === 'auto') { + heightAuto.value = true; + } else { + const m = hVal.match(/([\d.]+)px/i); + if (m) { + heightAuto.value = false; + heightPx.value = parseFloat(m[1]); + heightPx.unit = 'px'; + } + } + } + + const widthLine = ` width: ${width.value}${width.unit};`; + const heightLine = ` height: ${heightAuto.value ? 'auto' : `${heightPx.value}${heightPx.unit}`};`; + const newBlock = `${selector.value} {\n${widthLine}\n${heightLine}\n}\n`; const oldBlock = stylesheetStore.extractBlock(selector.value) || ''; if (oldBlock) { stylesheetStore.replaceInCustomCss(oldBlock, newBlock); @@ -183,9 +352,13 @@ const handleCssInput = (newCss) => { const saveState = () => { if (!selector.value) return; elementStates.set(selector.value, { - toggle: widthEnabled.value, - cache: widthCache.value ? { ...widthCache.value } : null, - value: { value: width.value, unit: width.unit }, + widthToggle: widthEnabled.value, + widthCache: widthCache.value ? { ...widthCache.value } : null, + widthValue: { value: width.value, unit: width.unit }, + heightToggle: heightEnabled.value, + heightCache: heightCache.value ? { ...heightCache.value } : null, + heightAuto: heightAuto.value, + heightValue: { value: heightPx.value, unit: heightPx.unit }, }); }; @@ -194,39 +367,58 @@ const saveState = () => { const handleImageClick = (figure, event) => { const uniqueClass = Array.from(figure.classList).find(cls => cls.startsWith('block-image--')); const sel = uniqueClass - ? `.${uniqueClass}` + ? `.block-image.${uniqueClass}` : figure.classList.contains('geoformat-cover-image') ? '.geoformat-cover-image' : null; if (!sel) return; isUpdatingFromStore = true; selector.value = sel; + currentFigure.value = figure; + measuredHeight.value = null; - const stored = elementStates.get(selector.value); + const stored = elementStates.get(sel); if (stored) { - widthEnabled.value = stored.toggle; - widthCache.value = stored.cache ? { ...stored.cache } : null; - if (stored.toggle) { - width.value = stored.value.value; - width.unit = stored.value.unit; - } else if (stored.cache) { - width.value = stored.cache.value; - width.unit = stored.cache.unit; - } else { - width.value = imageDefaults.width.value; - width.unit = imageDefaults.width.unit; - } + widthEnabled.value = stored.widthToggle; + widthCache.value = stored.widthCache ? { ...stored.widthCache } : null; + width.value = stored.widthToggle ? stored.widthValue.value : (stored.widthCache ? stored.widthCache.value : imageDefaults.width.value); + width.unit = stored.widthToggle ? stored.widthValue.unit : (stored.widthCache ? stored.widthCache.unit : imageDefaults.width.unit); + + heightEnabled.value = stored.heightToggle; + heightCache.value = stored.heightCache ? { ...stored.heightCache } : null; + heightAuto.value = stored.heightAuto; + heightPx.value = stored.heightToggle ? stored.heightValue.value : (stored.heightCache ? stored.heightCache.value : imageDefaults.height.value); + heightPx.unit = stored.heightToggle ? stored.heightValue.unit : (stored.heightCache ? stored.heightCache.unit : imageDefaults.height.unit); } else { + // Width widthEnabled.value = false; widthCache.value = null; - - const cssVal = stylesheetStore.extractValue(selector.value, 'width'); - if (cssVal && cssVal.value !== undefined) { - width.value = cssVal.value; - width.unit = cssVal.unit; - widthEnabled.value = true; + const wCss = stylesheetStore.extractValue(sel, 'width'); + if (wCss && wCss.value !== undefined) { + width.value = wCss.value; + width.unit = wCss.unit; + const isDefaultWidth = wCss.value === imageDefaults.width.value && wCss.unit === imageDefaults.width.unit; + if (!isDefaultWidth) widthEnabled.value = true; } else { - width.value = imageDefaults.width.value; - width.unit = imageDefaults.width.unit; + width.value = imageDefaults.width.value; width.unit = imageDefaults.width.unit; + } + + // Height + heightEnabled.value = false; + heightCache.value = null; + const hCss = stylesheetStore.extractValue(sel, 'height'); + const hStr = typeof hCss === 'string' ? hCss : null; + if (hStr === 'auto') { + heightAuto.value = true; // toggle reste OFF — c'est la valeur par défaut + } else if (hStr) { + const m = hStr.match(/([\d.]+)px/i); + if (m) { heightAuto.value = false; heightPx.value = parseFloat(m[1]); heightPx.unit = 'px'; heightEnabled.value = true; } + else { heightAuto.value = imageDefaults.height.auto; heightPx.value = imageDefaults.height.value; heightPx.unit = imageDefaults.height.unit; } + } else if (hCss && hCss.value !== undefined) { + heightAuto.value = false; heightPx.value = hCss.value; heightPx.unit = hCss.unit; heightEnabled.value = true; + } else { + heightAuto.value = imageDefaults.height.auto; + heightPx.value = imageDefaults.height.value; + heightPx.unit = imageDefaults.height.unit; } } @@ -239,6 +431,7 @@ const handleImageClick = (figure, event) => { const handleClose = () => { saveState(); selector.value = ''; + currentFigure.value = null; if (basePopup.value?.visible) basePopup.value.close(); emit('close'); }; diff --git a/src/components/PagedJsWrapper.vue b/src/components/PagedJsWrapper.vue index 50c4d07..f479394 100644 --- a/src/components/PagedJsWrapper.vue +++ b/src/components/PagedJsWrapper.vue @@ -69,7 +69,9 @@ :data-page-type="item.template" >

{{ item.title }}

- +
+ +
{{ tag }}
@@ -93,7 +95,11 @@ {{ marker.title }} - + @@ -38,36 +59,83 @@ import { useCssSync } from '../../composables/useCssSync'; import { useDebounce } from '../../composables/useDebounce'; import { useImageDefaults } from '../../composables/useImageDefaults'; -const { updateStyle } = useCssUpdater(); -const { extractNumericValue } = useCssSync(); +const { updateStyle, removeProperty } = useCssUpdater(); +const { extractNumericValue, extractValue } = useCssSync(); const { debouncedUpdate } = useDebounce(500); const imageDefaults = useImageDefaults(); -// State — initial value from defaults.js (overwritten by syncFromStore) +const IMAGE_SELECTORS = ['.block-image', '.geoformat-cover-image']; + +// Width const width = ref({ ...IMAGE_DEFAULTS.width }); -// Start true to block immediate watchers during setup +// Height +const heightAuto = ref(IMAGE_DEFAULTS.height.auto); +const height = ref({ value: IMAGE_DEFAULTS.height.value, unit: IMAGE_DEFAULTS.height.unit }); + let isUpdatingFromStore = true; +// Width watcher watch(width, (val) => { if (isUpdatingFromStore) return; imageDefaults.width = { value: val.value, unit: val.unit }; debouncedUpdate(() => { - updateStyle('.block-image', 'width', `${val.value}${val.unit}`); - updateStyle('.geoformat-cover-image', 'width', `${val.value}${val.unit}`); + IMAGE_SELECTORS.forEach(sel => updateStyle(sel, 'width', `${val.value}${val.unit}`)); }); }, { deep: true, immediate: true }); +// Height auto toggle +const onHeightAutoChange = (auto) => { + if (isUpdatingFromStore) return; + heightAuto.value = auto; + const cssVal = auto ? 'auto' : `${height.value.value}${height.value.unit}`; + IMAGE_SELECTORS.forEach(sel => { + updateStyle(sel, 'height', cssVal); + if (auto) { + removeProperty(`${sel} img`, 'height'); + } else { + updateStyle(`${sel} img`, 'height', '100%'); + } + }); + imageDefaults.height = { auto, value: height.value.value, unit: height.value.unit }; +}; + +// Height value watcher (only when not auto) +watch(height, (val) => { + if (isUpdatingFromStore || heightAuto.value) return; + imageDefaults.height = { auto: false, value: val.value, unit: val.unit }; + debouncedUpdate(() => { + IMAGE_SELECTORS.forEach(sel => { + updateStyle(sel, 'height', `${val.value}${val.unit}`); + updateStyle(`${sel} img`, 'height', '100%'); + }); + }); +}, { deep: true }); + const syncFromStore = () => { isUpdatingFromStore = true; if (imageDefaults._initialized) { width.value = { value: imageDefaults.width.value, unit: imageDefaults.width.unit }; + heightAuto.value = imageDefaults.height.auto; + height.value = { value: imageDefaults.height.value, unit: imageDefaults.height.unit }; } else { const widthVal = extractNumericValue('.block-image', 'width', ['%', 'px']); if (widthVal) width.value = widthVal; + const heightVal = extractValue('.block-image', 'height'); + if (!heightVal || heightVal === 'auto') { + heightAuto.value = true; + } else { + const m = heightVal.match(/([\d.]+)px/i); + if (m) { + heightAuto.value = false; + height.value = { value: parseFloat(m[1]), unit: 'px' }; + } + } + imageDefaults.width = { value: width.value.value, unit: width.value.unit }; + imageDefaults.height = { auto: heightAuto.value, value: height.value.value, unit: height.value.unit }; imageDefaults._initialized = true; } diff --git a/src/composables/useImageDefaults.js b/src/composables/useImageDefaults.js index 175d90c..6f0cf1f 100644 --- a/src/composables/useImageDefaults.js +++ b/src/composables/useImageDefaults.js @@ -4,6 +4,7 @@ import { IMAGE_DEFAULTS } from '../utils/defaults'; // Singleton reactive — ImageSettings writes here, ImagePopup reads when toggle is disabled const defaults = reactive({ width: { ...IMAGE_DEFAULTS.width }, + height: { auto: IMAGE_DEFAULTS.height.auto, value: IMAGE_DEFAULTS.height.value, unit: IMAGE_DEFAULTS.height.unit }, _initialized: false, }); diff --git a/src/stores/stylesheet.js b/src/stores/stylesheet.js index 475cd96..bc59568 100644 --- a/src/stores/stylesheet.js +++ b/src/stores/stylesheet.js @@ -261,7 +261,17 @@ export const useStylesheetStore = defineStore('stylesheet', () => { // Image defaults set('.block-image', 'width', IMAGE_DEFAULTS.width.value, IMAGE_DEFAULTS.width.unit); + set('.block-image', 'height', 'auto'); + set('.block-image img', 'object-fit', 'cover'); + set('.block-image img', 'display', 'flex'); set('.geoformat-cover-image', 'width', IMAGE_DEFAULTS.width.value, IMAGE_DEFAULTS.width.unit); + set('.geoformat-cover-image', 'height', 'auto'); + set('.geoformat-cover-image img', 'object-fit', 'cover'); + set('.geoformat-cover-image img', 'display', 'flex'); + set('.block-carte', 'width', IMAGE_DEFAULTS.width.value, IMAGE_DEFAULTS.width.unit); + set('.block-carte', 'height', 'auto'); + set('.block-carte img', 'object-fit', 'cover'); + set('.block-carte img', 'display', 'flex'); // Inline element defaults (em, i, strong, b, a) for (const [tag, props] of Object.entries(INLINE_DEFAULTS)) { diff --git a/src/utils/defaults.js b/src/utils/defaults.js index 07f6c2b..6171278 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -128,6 +128,7 @@ export const PARAGRAPH_CLASS_DEFAULTS = Object.freeze({ export const IMAGE_DEFAULTS = Object.freeze({ width: Object.freeze({ value: 100, unit: '%' }), + height: Object.freeze({ auto: true, value: 400, unit: 'px' }), }); export const INLINE_DEFAULTS = Object.freeze({