geoproject-app/src/components/PagePopup.vue

544 lines
16 KiB
Vue
Raw Normal View History

<template>
<BasePopup
ref="basePopup"
id="page-popup"
:display-css="displayedCss"
:editable-css="pageCss"
:popup-width="550"
:popup-height="600"
@close="close"
@css-input="handleCssInput"
@toggle-inheritance="toggleInheritance"
>
<template #header-left>
<span class="page-label">@page</span>
<span class="page-name">{{ templateName || 'default' }}</span>
<span class="page-count">{{ pageCount }} page{{ pageCount > 1 ? 's' : '' }}</span>
</template>
<template #controls>
<!-- Margins -->
<div class="settings-subsection">
<h4>Marges</h4>
<div class="margin-grid">
<div
class="field"
:class="{ 'field--view-only': inheritanceLocked }"
>
<label class="label-with-tooltip" data-css="margin-top">Haut</label>
<div class="input-with-unit">
<NumberInput
v-model="margins.top.value"
:min="0"
:step="1"
:disabled="inheritanceLocked"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.top.unit === 'mm' }"
:disabled="inheritanceLocked"
@click="margins.top.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.top.unit === 'px' }"
:disabled="inheritanceLocked"
@click="margins.top.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div
class="field"
:class="{ 'field--view-only': inheritanceLocked }"
>
<label class="label-with-tooltip" data-css="margin-bottom">Bas</label>
<div class="input-with-unit">
<NumberInput
v-model="margins.bottom.value"
:min="0"
:step="1"
:disabled="inheritanceLocked"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.bottom.unit === 'mm' }"
:disabled="inheritanceLocked"
@click="margins.bottom.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.bottom.unit === 'px' }"
:disabled="inheritanceLocked"
@click="margins.bottom.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div
class="field"
:class="{ 'field--view-only': inheritanceLocked }"
>
<label class="label-with-tooltip" data-css="margin-left">Gauche</label>
<div class="input-with-unit">
<NumberInput
v-model="margins.left.value"
:min="0"
:step="1"
:disabled="inheritanceLocked"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.left.unit === 'mm' }"
:disabled="inheritanceLocked"
@click="margins.left.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.left.unit === 'px' }"
:disabled="inheritanceLocked"
@click="margins.left.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div
class="field"
:class="{ 'field--view-only': inheritanceLocked }"
>
<label class="label-with-tooltip" data-css="margin-right">Droite</label>
<div class="input-with-unit">
<NumberInput
v-model="margins.right.value"
:min="0"
:step="1"
:disabled="inheritanceLocked"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.right.unit === 'mm' }"
:disabled="inheritanceLocked"
@click="margins.right.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.right.unit === 'px' }"
:disabled="inheritanceLocked"
@click="margins.right.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Background -->
<div class="settings-subsection">
<div class="field" :class="{ 'field--view-only': inheritanceLocked }">
<label class="label-with-tooltip" data-css="background">Arrière-plan</label>
<div class="input-with-color">
<input
ref="backgroundColorInput"
type="text"
v-model="background.value"
:disabled="inheritanceLocked"
data-coloris
/>
</div>
</div>
</div>
<!-- Patterns -->
<div class="settings-subsection">
<div class="field" :class="{ 'field--view-only': inheritanceLocked }">
<label class="label-with-tooltip" data-css="background-image">Motifs</label>
<select v-model="pattern" :disabled="inheritanceLocked">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
</div>
</div>
</template>
</BasePopup>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet';
import { useDebounce } from '../composables/useDebounce';
import { useCssSync } from '../composables/useCssSync';
2025-12-09 17:08:40 +01:00
import NumberInput from './ui/NumberInput.vue';
import BasePopup from './ui/BasePopup.vue';
const stylesheetStore = useStylesheetStore();
const { extractSpacing } = useCssSync();
const props = defineProps({
iframeRef: Object,
});
const emit = defineEmits(['close']);
const basePopup = ref(null);
const visible = computed(() => basePopup.value?.visible ?? false);
const inheritanceLocked = computed(() => basePopup.value?.inheritanceLocked ?? true);
const selectedPageElement = ref(null);
const pageCount = ref(0);
const templateName = ref('');
const backgroundColorInput = ref(null);
let isUpdatingFromStore = false;
const { debouncedUpdate } = useDebounce(500);
const margins = ref({
top: { value: 0, unit: 'mm' },
bottom: { value: 0, unit: 'mm' },
left: { value: 0, unit: 'mm' },
right: { value: 0, unit: 'mm' },
});
const background = ref({
value: '',
format: 'hex',
});
const pattern = ref('');
const immediateUpdate = (callback) => {
callback();
};
// Get the selector for the current template's @page rule
const getTemplateSelector = () => {
return templateName.value ? `@page ${templateName.value}` : '@page';
};
// Get or create the template-specific @page block
const getOrCreateTemplateBlock = () => {
const selector = getTemplateSelector();
let block = stylesheetStore.extractBlock(selector);
if (!block && templateName.value) {
const baseBlock = stylesheetStore.extractBlock('@page');
if (baseBlock) {
const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
const newBlock = `\n@page ${templateName.value} {\n margin: ${marginValue};${background.value.value ? `\n background: ${background.value.value};` : ''}\n}\n`;
stylesheetStore.replaceInCustomCss(
baseBlock,
baseBlock + newBlock
);
block = stylesheetStore.extractBlock(selector);
}
}
return block;
};
// Remove the template-specific @page block
const removeTemplateBlock = () => {
if (!templateName.value) return;
const selector = `@page ${templateName.value}`;
const block = stylesheetStore.extractBlock(selector);
if (block) {
stylesheetStore.replaceInCustomCss(
new RegExp(`\\n?${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{[^}]*\\}\\n?`),
'\n'
);
}
};
const updateMargins = (force = false) => {
if (!force && inheritanceLocked.value) return;
const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
const currentBlock = getOrCreateTemplateBlock();
if (!currentBlock) return;
if (currentBlock.includes('margin:')) {
const updatedBlock = currentBlock.replace(
/(margin:\s*)[^;]+/,
`$1${marginValue}`
);
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` margin: ${marginValue};\n$1`
);
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
}
};
const updateBackground = (force = false) => {
if (!force && inheritanceLocked.value) return;
if (!background.value.value) return;
const currentBlock = getOrCreateTemplateBlock();
if (!currentBlock) return;
if (currentBlock.includes('background:')) {
const updatedBlock = currentBlock.replace(
/(background:\s*)[^;]+/,
`$1${background.value.value}`
);
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` background: ${background.value.value};\n$1`
);
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
}
};
// Apply all current field values to create/update the CSS block
const applyAllStyles = () => {
updateMargins(true);
updateBackground(true);
};
// Watch margin values (number inputs) with debounce
watch(
() => [
margins.value.top.value,
margins.value.bottom.value,
margins.value.left.value,
margins.value.right.value,
],
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateMargins);
}
);
// Watch margin units (button clicks) without debounce
watch(
() => [
margins.value.top.unit,
margins.value.bottom.unit,
margins.value.left.unit,
margins.value.right.unit,
],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateMargins);
}
);
// Watch background value with debounce
watch(
() => background.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateBackground);
}
);
const loadValuesFromStylesheet = () => {
try {
isUpdatingFromStore = true;
// Use extractSpacing from useCssSync
const spacing = extractSpacing('@page', 'margin');
if (spacing?.detailed) {
margins.value.top = spacing.detailed.top;
margins.value.right = spacing.detailed.right;
margins.value.bottom = spacing.detailed.bottom;
margins.value.left = spacing.detailed.left;
} else if (spacing?.simple) {
margins.value.top = { ...spacing.simple };
margins.value.right = { ...spacing.simple };
margins.value.bottom = { ...spacing.simple };
margins.value.left = { ...spacing.simple };
}
// Extract background
const pageBlock = stylesheetStore.extractBlock('@page');
if (pageBlock) {
const bgMatch = pageBlock.match(/background:\s*([^;]+)/);
if (bgMatch) {
background.value.value = bgMatch[1].trim();
}
}
} catch (error) {
console.error('Error loading values from stylesheet:', error);
} finally {
isUpdatingFromStore = false;
}
};
const open = (pageElement, event, count = 1) => {
selectedPageElement.value = pageElement;
pageCount.value = count;
// Extract template name from data-page-type attribute
templateName.value = pageElement.getAttribute('data-page-type') || '';
// Read inheritance state from page element's data attribute
basePopup.value.inheritanceLocked = pageElement.dataset.inheritanceUnlocked !== 'true';
// Load values from stylesheet (@page block)
loadValuesFromStylesheet();
// Open popup (sets visible, position, inits Coloris)
basePopup.value.open(event);
};
const close = () => {
selectedPageElement.value = null;
basePopup.value?.close();
emit('close');
};
const toggleInheritance = () => {
const wasLocked = basePopup.value.inheritanceLocked;
basePopup.value.inheritanceLocked = !wasLocked;
if (selectedPageElement.value) {
if (basePopup.value.inheritanceLocked) {
delete selectedPageElement.value.dataset.inheritanceUnlocked;
} else {
selectedPageElement.value.dataset.inheritanceUnlocked = 'true';
}
}
if (basePopup.value.inheritanceLocked && !wasLocked) {
removeTemplateBlock();
} else if (!basePopup.value.inheritanceLocked && wasLocked) {
applyAllStyles();
}
};
const pageCss = computed(() => {
if (!basePopup.value?.inheritanceLocked && templateName.value) {
const templateBlock = stylesheetStore.extractBlock(`@page ${templateName.value}`);
if (templateBlock) return templateBlock;
}
return stylesheetStore.extractBlock('@page') || '';
});
const generatePreviewCss = () => {
if (!templateName.value) return '';
const properties = [];
const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
properties.push(` margin: ${marginValue};`);
if (background.value.value) {
properties.push(` background: ${background.value.value};`);
}
if (properties.length === 0) return '';
return `@page ${templateName.value} {\n${properties.join('\n')}\n}`;
};
const displayedCss = computed(() => {
if (!basePopup.value?.inheritanceLocked) {
return pageCss.value;
}
if (!templateName.value) {
return pageCss.value;
}
const preview = generatePreviewCss();
if (!preview) return pageCss.value;
return '/* Héritage verrouillé - déverrouiller pour appliquer */\n/* ' +
preview.split('\n').join('\n ') +
' */';
});
const handleCssInput = (newCss) => {
const oldBlock = pageCss.value;
if (oldBlock) {
stylesheetStore.replaceInCustomCss(oldBlock, newCss);
}
};
// Watch stylesheet changes to sync values
watch(
() => stylesheetStore.content,
() => {
if (basePopup.value?.visible && !stylesheetStore.isEditing) {
loadValuesFromStylesheet();
}
}
);
defineExpose({ open, close, visible });
</script>
<style scoped>
/* PagePopup-specific styles (orange theme) */
.page-label {
background: var(--color-page-highlight);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 600;
}
.page-name {
font-weight: 600;
font-size: 0.875rem;
}
.page-count {
color: var(--color-page-highlight);
font-size: 0.875rem;
}
.margin-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
</style>