From a85a810b1e46d78a0056f6ce595e83de0d9dbe39 Mon Sep 17 00:00:00 2001 From: Julie Blanc Date: Mon, 23 Mar 2026 12:40:47 +0100 Subject: [PATCH] reglages cartes --- public/assets/css/pagedjs-interface.css | 20 +- public/assets/svg/carte.svg | 1 + src/App.vue | 10 +- src/components/CartePopup.vue | 357 +++++++++++++++++++++++ src/components/PagedJsWrapper.vue | 6 +- src/components/editor/CarteSettings.vue | 138 +++++++++ src/components/editor/EditorPanel.vue | 2 + src/composables/useCarteDefaults.js | 13 + src/composables/useIframeInteractions.js | 98 ++++++- src/stores/stylesheet.js | 4 +- src/utils/defaults.js | 5 + 11 files changed, 641 insertions(+), 13 deletions(-) create mode 100644 public/assets/svg/carte.svg create mode 100644 src/components/CartePopup.vue create mode 100644 src/components/editor/CarteSettings.vue create mode 100644 src/composables/useCarteDefaults.js diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css index ad8fec3..41c3268 100644 --- a/public/assets/css/pagedjs-interface.css +++ b/public/assets/css/pagedjs-interface.css @@ -193,13 +193,15 @@ } .block-image.element-hovered, -.geoformat-cover-image.element-hovered { +.geoformat-cover-image.element-hovered, +.block-carte.element-hovered { outline: 2px solid #0d996050 !important; cursor: pointer !important; } .block-image.element-selected, -.geoformat-cover-image.element-selected { +.geoformat-cover-image.element-selected, +.block-carte.element-selected { outline: 2px dashed #0d9960 !important; } @@ -217,6 +219,20 @@ font-family: sans-serif; } +.carte-hover-label { + position: absolute; + background: #0d9960; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 600; + opacity: 0.3; + pointer-events: none; + z-index: 9999; + font-family: sans-serif; +} + /* Marks (to delete when merge in paged.js) */ .pagedjs_marks-crop { diff --git a/public/assets/svg/carte.svg b/public/assets/svg/carte.svg new file mode 100644 index 0000000..585776d --- /dev/null +++ b/public/assets/svg/carte.svg @@ -0,0 +1 @@ + diff --git a/src/App.vue b/src/App.vue index 0aaed16..78bd81a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,6 +3,7 @@ import PagedJsWrapper from './components/PagedJsWrapper.vue'; import EditorPanel from './components/editor/EditorPanel.vue'; import ElementPopup from './components/ElementPopup.vue'; import ImagePopup from './components/ImagePopup.vue'; +import CartePopup from './components/CartePopup.vue'; // import PagePopup from './components/PagePopup.vue'; // DISABLED: page template styling feature import PreviewLoader from './components/PreviewLoader.vue'; import SaveButton from './components/SaveButton.vue'; @@ -25,6 +26,7 @@ const previewFrame1 = ref(null); const previewFrame2 = ref(null); const elementPopup = ref(null); const imagePopup = ref(null); +const cartePopup = ref(null); // const pagePopup = ref(null); // DISABLED: page template styling feature const activeTab = ref(''); @@ -41,7 +43,8 @@ const { // handlePagePopupClose, // DISABLED: page template styling feature handleElementPopupClose, handleImagePopupClose, -} = useIframeInteractions({ elementPopup, imagePopup }); + handleCartePopupClose, +} = useIframeInteractions({ elementPopup, imagePopup, cartePopup }); // Setup preview renderer with double buffering const { @@ -134,6 +137,11 @@ onMounted(async () => { ref="imagePopup" @close="handleImagePopupClose" /> + + +
+
+ + +
+
+ +
+

Valeur par défaut : {{ carteDefaults.width.value }}{{ carteDefaults.width.unit }}

+
+ + +
+
+ + +
+
+
+ + +
+ +
+

Valeur par défaut : {{ carteDefaults.height.auto ? 'auto' : carteDefaults.height.value + carteDefaults.height.unit }}

+
+ + + + + + + + diff --git a/src/components/PagedJsWrapper.vue b/src/components/PagedJsWrapper.vue index f479394..a0d3c9a 100644 --- a/src/components/PagedJsWrapper.vue +++ b/src/components/PagedJsWrapper.vue @@ -69,7 +69,7 @@ :data-page-type="item.template" >

{{ item.title }}

-
+
@@ -133,6 +133,10 @@ import { const narrativeStore = useNarrativeStore(); const hasNarrativeData = computed(() => narrativeStore.data !== null); + +const getCarteBlockId = (itemId) => { + return (itemId || '').split('/').pop()?.replace(/[^a-z0-9]/gi, '').slice(0, 8) || ''; +}; const flattenedContent = computed(() => narrativeStore.flattenedContent); // Filter out hidden blocks diff --git a/src/components/editor/CarteSettings.vue b/src/components/editor/CarteSettings.vue new file mode 100644 index 0000000..6832280 --- /dev/null +++ b/src/components/editor/CarteSettings.vue @@ -0,0 +1,138 @@ + + + diff --git a/src/components/editor/EditorPanel.vue b/src/components/editor/EditorPanel.vue index 826bd73..da75abc 100644 --- a/src/components/editor/EditorPanel.vue +++ b/src/components/editor/EditorPanel.vue @@ -54,6 +54,7 @@ +
@@ -72,6 +73,7 @@ import { inject } from 'vue'; import PageSettings from './PageSettings.vue'; import TextSettings from './TextSettings.vue'; import ImageSettings from './ImageSettings.vue'; +import CarteSettings from './CarteSettings.vue'; import StylesheetViewer from '../StylesheetViewer.vue'; const activeTab = inject('activeTab'); diff --git a/src/composables/useCarteDefaults.js b/src/composables/useCarteDefaults.js new file mode 100644 index 0000000..bc84f91 --- /dev/null +++ b/src/composables/useCarteDefaults.js @@ -0,0 +1,13 @@ +import { reactive } from 'vue'; +import { CARTE_DEFAULTS } from '../utils/defaults'; + +// Singleton reactive — CarteSettings writes here, CartePopup reads when toggle is disabled +const defaults = reactive({ + width: { ...CARTE_DEFAULTS.width }, + height: { auto: CARTE_DEFAULTS.height.auto, value: CARTE_DEFAULTS.height.value, unit: CARTE_DEFAULTS.height.unit }, + _initialized: false, +}); + +export function useCarteDefaults() { + return defaults; +} diff --git a/src/composables/useIframeInteractions.js b/src/composables/useIframeInteractions.js index dbf5e25..76ea3c7 100644 --- a/src/composables/useIframeInteractions.js +++ b/src/composables/useIframeInteractions.js @@ -4,14 +4,16 @@ import { ref } from 'vue'; * Composable for managing interactions with pages and elements in the iframe * Handles hover effects, labels, and click events for both pages and content elements */ -export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup // DISABLED: page template styling feature */ }) { +export function useIframeInteractions({ elementPopup, imagePopup, cartePopup /*, pagePopup // DISABLED: page template styling feature */ }) { // DISABLED: page template styling feature // const hoveredPage = ref(null); // const selectedPages = ref([]); // Pages with active border (when popup is open) - const hoveredElement = ref(null); // Currently hovered content element - const hoveredFigure = ref(null); // Currently hovered block-image figure - const selectedElement = ref(null); // Selected element (when popup is open) - const selectedFigure = ref(null); // Selected figure (when image popup is open) + const hoveredElement = ref(null); + const hoveredFigure = ref(null); // block-image or geoformat-cover-image + const hoveredCarte = ref(null); // block-carte + const selectedElement = ref(null); + const selectedFigure = ref(null); // selected block-image + const selectedCarte = ref(null); // selected block-carte // const EDGE_THRESHOLD = 30; // px from edge to trigger hover // DISABLED: page template styling feature // Text elements that can trigger ElementPopup (excluding containers, images, etc.) @@ -197,6 +199,42 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / return null; }; + // Find closest block-carte figure ancestor + const getCarteFigure = (element) => { + let current = element; + while (current && current.tagName !== 'BODY') { + if (current.tagName === 'FIGURE' && current.classList.contains('block-carte')) return current; + current = current.parentElement; + } + return null; + }; + + // Create and position carte label on hover + const createCarteLabel = (figure) => { + const doc = figure.ownerDocument; + const existing = doc.querySelector('.carte-hover-label'); + if (existing) existing.remove(); + + const uniqueClass = Array.from(figure.classList).find(cls => cls.startsWith('block-carte--')); + const labelText = uniqueClass ? `.${uniqueClass}` : '.block-carte'; + + const rect = figure.getBoundingClientRect(); + const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop; + const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft; + + const label = doc.createElement('div'); + label.className = 'carte-hover-label'; + label.textContent = labelText; + label.style.top = `${rect.top + scrollTop - 32}px`; + label.style.left = `${rect.left + scrollLeft}px`; + doc.body.appendChild(label); + }; + + const removeCarteLabel = (doc) => { + const label = doc.querySelector('.carte-hover-label'); + if (label) label.remove(); + }; + // Check if element is a content element (or find closest content parent) const getContentElement = (element) => { let current = element; @@ -285,8 +323,22 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / hoveredFigure.value = imageFigure; } + // Check for block-carte figure hover + const carteFigure = (!imageFigure) ? getCarteFigure(event.target) : null; + if (carteFigure !== hoveredCarte.value) { + if (hoveredCarte.value && hoveredCarte.value !== selectedCarte.value) { + hoveredCarte.value.classList.remove('element-hovered'); + } + removeCarteLabel(doc); + if (carteFigure && carteFigure !== selectedCarte.value) { + carteFigure.classList.add('element-hovered'); + createCarteLabel(carteFigure); + } + hoveredCarte.value = carteFigure; + } + // Check for content element hover - const contentElement = imageFigure ? null : getContentElement(event.target); + const contentElement = (imageFigure || carteFigure) ? null : getContentElement(event.target); if (contentElement !== hoveredElement.value) { // Remove highlight from previous element (only if not selected) @@ -347,6 +399,7 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / clearSelectedElement(); if (elementPopup.value.visible) elementPopup.value.close(); imagePopup.value?.close(); + cartePopup.value?.close(); return; } @@ -376,8 +429,31 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / return; } - // Close image popup if open when clicking a text element + // Check if clicking a block-carte figure + const carteFigure = getCarteFigure(element); + if (carteFigure) { + clearSelectedElement(); + if (elementPopup.value.visible) elementPopup.value.close(); + imagePopup.value?.close(); + const doc2 = event.target.ownerDocument; + removeElementLabel(doc2); + removeCarteLabel(doc2); + if (cartePopup.value?.visible) { + if (selectedCarte.value) { selectedCarte.value.classList.remove('element-selected'); selectedCarte.value = null; } + cartePopup.value.close(); + return; + } + carteFigure.classList.remove('element-hovered'); + carteFigure.classList.add('element-selected'); + hoveredCarte.value = null; + selectedCarte.value = carteFigure; + cartePopup.value?.handleCarteClick(carteFigure, event); + return; + } + + // Close image and carte popups when clicking a text element imagePopup.value?.close(); + cartePopup.value?.close(); // Only show ElementPopup for content elements, not divs const contentElement = getContentElement(element); @@ -437,6 +513,13 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / } }; + const handleCartePopupClose = () => { + if (selectedCarte.value) { + selectedCarte.value.classList.remove('element-selected'); + selectedCarte.value = null; + } + }; + return { // State // hoveredPage, // DISABLED: page template styling feature @@ -449,6 +532,7 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup / // handlePagePopupClose, // DISABLED: page template styling feature handleElementPopupClose, handleImagePopupClose, + handleCartePopupClose, // Utilities // clearSelectedPages, // DISABLED: page template styling feature clearSelectedElement, diff --git a/src/stores/stylesheet.js b/src/stores/stylesheet.js index bc59568..000436e 100644 --- a/src/stores/stylesheet.js +++ b/src/stores/stylesheet.js @@ -5,7 +5,7 @@ import * as cssComments from '../utils/css-comments'; import prettier from 'prettier/standalone'; import parserPostcss from 'prettier/plugins/postcss'; import { getCsrfToken } from '../utils/kirby-auth'; -import { PAGE_DEFAULTS, TEXT_DEFAULTS, IMAGE_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults'; +import { PAGE_DEFAULTS, TEXT_DEFAULTS, IMAGE_DEFAULTS, CARTE_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults'; export const useStylesheetStore = defineStore('stylesheet', () => { // Base state @@ -268,7 +268,7 @@ export const useStylesheetStore = defineStore('stylesheet', () => { 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', 'width', CARTE_DEFAULTS.width.value, CARTE_DEFAULTS.width.unit); set('.block-carte', 'height', 'auto'); set('.block-carte img', 'object-fit', 'cover'); set('.block-carte img', 'display', 'flex'); diff --git a/src/utils/defaults.js b/src/utils/defaults.js index 6171278..daa3a47 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -131,6 +131,11 @@ export const IMAGE_DEFAULTS = Object.freeze({ height: Object.freeze({ auto: true, value: 400, unit: 'px' }), }); +export const CARTE_DEFAULTS = Object.freeze({ + width: Object.freeze({ value: 100, unit: '%' }), + height: Object.freeze({ auto: true, value: 400, unit: 'px' }), +}); + export const INLINE_DEFAULTS = Object.freeze({ em: Object.freeze({ fontStyle: 'italic' }), i: Object.freeze({ fontStyle: 'italic' }),