geoproject-app/src/components/editor/PageSettings.vue

493 lines
12 KiB
Vue
Raw Normal View History

<template>
<section class="settings-section">
<h2>Réglage des pages</h2>
<div class="settings-subsection">
<div class="field">
<label for="page-format">Format d'impression</label>
<select id="page-format" v-model="pageFormat">
<option value="A4">A4</option>
<option value="A5">A5</option>
<option value="A3">A3</option>
<option value="letter">Letter</option>
<option value="legal">Legal</option>
</select>
</div>
</div>
<div class="settings-subsection">
<div class="field field--view-only">
<label for="page-width">Largeur</label>
<input
id="page-width"
type="number"
:value="parseInt(pageWidth)"
disabled
/>
<button type="button" disabled>mm</button>
</div>
<div class="field field--view-only">
<label for="page-height">Hauteur</label>
<input
id="page-height"
type="number"
:value="parseInt(pageHeight)"
disabled
/>
<button type="button" disabled>mm</button>
</div>
</div>
<div class="settings-subsection margins">
<h3>Marges</h3>
<div class="field">
<label for="margin-top">Haut</label>
<div class="input-with-unit">
<input
id="margin-top"
type="number"
v-model.number="margins.top.value"
min="0"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.top.unit === 'mm' }"
@click="margins.top.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.top.unit === 'px' }"
@click="margins.top.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div class="field">
<label for="margin-bottom">Bas</label>
<div class="input-with-unit">
<input
id="margin-bottom"
type="number"
v-model.number="margins.bottom.value"
min="0"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.bottom.unit === 'mm' }"
@click="margins.bottom.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.bottom.unit === 'px' }"
@click="margins.bottom.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div class="field">
<label for="margin-left">Gauche</label>
<div class="input-with-unit">
<input
id="margin-left"
type="number"
v-model.number="margins.left.value"
min="0"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.left.unit === 'mm' }"
@click="margins.left.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.left.unit === 'px' }"
@click="margins.left.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
<div class="field">
<label for="margin-right">Droite</label>
<div class="input-with-unit">
<input
id="margin-right"
type="number"
v-model.number="margins.right.value"
min="0"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.right.unit === 'mm' }"
@click="margins.right.unit = 'mm'"
>
mm
</button>
<button
type="button"
:class="{ active: margins.right.unit === 'px' }"
@click="margins.right.unit = 'px'"
>
px
</button>
</div>
</div>
</div>
</div>
<div class="settings-subsection">
<div class="field">
<label for="background">Arrière-plan</label>
<div class="input-with-color">
<input
ref="backgroundColorInput"
type="text"
id="background"
v-model="background.value"
data-coloris
/>
<!-- Temporarily commented out
<div class="unit-toggle">
<button
type="button"
:class="{ active: background.format === 'rgb' }"
@click="background.format = 'rgb'"
>
rgb
</button>
<button
type="button"
:class="{ active: background.format === 'hex' }"
@click="background.format = 'hex'"
>
hex
</button>
</div>
-->
</div>
</div>
</div>
<div class="settings-subsection">
<div class="field">
<label for="pattern">Motif</label>
<select id="pattern" v-model="pattern">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
</div>
</div>
<div class="settings-subsection">
<div class="field checkbox-field">
<input id="page-numbers" type="checkbox" v-model="pageNumbers" />
<label for="page-numbers">Numéro de page</label>
</div>
<div class="field checkbox-field">
<input id="running-title" type="checkbox" v-model="runningTitle" />
<label for="running-title">Titre courant</label>
</div>
</div>
</section>
</template>
<script setup>
import { ref, computed, watch, onMounted, inject } from 'vue';
import { useStylesheetStore } from '../../stores/stylesheet';
import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css';
const stylesheetStore = useStylesheetStore();
const backgroundColorInput = ref(null);
const activeTab = inject('activeTab', ref('document'));
let isUpdatingFromStore = false;
let updateTimer = null;
const pageFormat = ref('A4');
const pageFormats = {
A4: { width: '210mm', height: '297mm' },
A5: { width: '148mm', height: '210mm' },
A3: { width: '297mm', height: '420mm' },
letter: { width: '8.5in', height: '11in' },
legal: { width: '8.5in', height: '14in' },
};
const pageWidth = computed(() => pageFormats[pageFormat.value].width);
const pageHeight = computed(() => pageFormats[pageFormat.value].height);
const margins = ref({
top: { value: 20, unit: 'mm' },
bottom: { value: 20, unit: 'mm' },
left: { value: 20, unit: 'mm' },
right: { value: 20, unit: 'mm' },
});
const background = ref({
value: '',
format: 'hex',
});
const pattern = ref('');
const pageNumbers = ref(false);
const runningTitle = ref(false);
const debouncedUpdate = (callback) => {
clearTimeout(updateTimer);
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => {
callback();
};
watch(pageFormat, (newFormat) => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
});
});
const updateMargins = () => {
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 = stylesheetStore.extractBlock('@page');
const updatedBlock = currentBlock.replace(
/(margin:\s*)[^;]+/,
`$1${marginValue}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
};
// 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);
}
);
const updateBackground = () => {
if (!background.value.value) return;
const currentBlock = stylesheetStore.extractBlock('@page');
if (currentBlock.includes('background:')) {
const updatedBlock = currentBlock.replace(
/(background:\s*)[^;]+/,
`$1${background.value.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` background: ${background.value.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
}
};
// Watch background value (text input) with debounce
watch(
() => background.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateBackground);
}
);
// Watch background format (button clicks) without debounce
watch(
() => background.value.format,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateBackground);
}
);
watch(pattern, (newPattern) => {
if (!newPattern || isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement pattern application
});
});
watch(pageNumbers, (enabled) => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement page numbers toggle
});
});
watch(runningTitle, (enabled) => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement running title toggle
});
});
const syncFromStore = () => {
isUpdatingFromStore = true;
try {
const pageBlock = stylesheetStore.extractBlock('@page');
const sizeMatch = pageBlock.match(/size:\s*([A-Za-z0-9]+)/);
if (sizeMatch) {
pageFormat.value = sizeMatch[1];
}
const marginMatch = pageBlock.match(
/margin:\s*([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)/i
);
if (marginMatch) {
margins.value.top = {
value: parseFloat(marginMatch[1]),
unit: marginMatch[2],
};
margins.value.right = {
value: parseFloat(marginMatch[3]),
unit: marginMatch[4],
};
margins.value.bottom = {
value: parseFloat(marginMatch[5]),
unit: marginMatch[6],
};
margins.value.left = {
value: parseFloat(marginMatch[7]),
unit: marginMatch[8],
};
}
const bgMatch = pageBlock.match(/background:\s*([^;]+)/);
if (bgMatch) {
background.value.value = bgMatch[1].trim();
}
} finally {
isUpdatingFromStore = false;
}
};
watch(
() => stylesheetStore.content,
() => {
if (!isUpdatingFromStore) {
syncFromStore();
}
}
);
const updateColorisButton = () => {
const input = backgroundColorInput.value;
if (input && background.value.value) {
// Force Coloris to update by triggering a change event
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
}
};
// Watch for when the user returns to the "document" tab
watch(activeTab, (newTab, oldTab) => {
if (newTab === 'document' && oldTab !== 'document' && background.value.value) {
// Small delay to ensure DOM is ready
setTimeout(updateColorisButton, 100);
}
});
onMounted(() => {
syncFromStore();
// Initialize Coloris
Coloris.init();
Coloris({
el: '[data-coloris]',
theme: 'pill',
themeMode: 'dark',
formatToggle: true,
alpha: true,
closeButton: true,
closeLabel: 'Fermer',
clearButton: true,
clearLabel: 'Effacer',
swatchesOnly: false,
inline: false,
wrap: true,
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#ffffff',
'#000000',
],
});
// Initialize button color if value exists
if (background.value.value) {
setTimeout(updateColorisButton, 100);
}
});
</script>