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"
>
- {{ selector || '' }}
+ {{ selector.replace('.block-image.', '.') || '' }}
@@ -22,16 +22,10 @@
class="setting__section"
data-setting="width"
:class="{ 'setting-disabled': !widthEnabled }"
- @click="onSectionClick"
+ @click="onWidthSectionClick"
>
@@ -46,6 +40,40 @@
Valeur par défaut : {{ imageDefaults.width.value }}{{ imageDefaults.width.unit }}
+
+
+
+
+
Valeur par défaut : {{ imageDefaults.height.auto ? 'auto' : imageDefaults.height.value + imageDefaults.height.unit }}
+
+
@@ -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 }}
-
+
{
break-before: page;
}
-.carte-image {
+.block-carte {
max-width: 100%;
height: auto;
}
+.block-carte img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
.marker {
margin-top: 1.5rem;
}
@@ -253,10 +265,4 @@ const getBlockComponent = (type) => {
flex-shrink: 0;
object-fit: contain;
}
-
-.marker-cover {
- max-width: 100%;
- height: auto;
- margin: 0.5rem 0;
-}
diff --git a/src/components/editor/ImageSettings.vue b/src/components/editor/ImageSettings.vue
index 474d81b..0dc05c6 100644
--- a/src/components/editor/ImageSettings.vue
+++ b/src/components/editor/ImageSettings.vue
@@ -24,6 +24,27 @@
+
+
+
@@ -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({