674 lines
18 KiB
Vue
674 lines
18 KiB
Vue
<template>
|
|
<section class="settings-section" id="settings-section_page">
|
|
<h2>Réglage des pages</h2>
|
|
<div class="container">
|
|
|
|
|
|
<div class="settings-subsection">
|
|
<div class="field field-simple">
|
|
<label for="page-format" class="label-with-tooltip" data-css="size"
|
|
>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-size field--view-only">
|
|
<label for="page-width" class="label-with-tooltip" data-css="width"
|
|
>Largeur</label
|
|
>
|
|
<input
|
|
id="page-width"
|
|
type="number"
|
|
:value="parseInt(pageWidth)"
|
|
disabled
|
|
/>
|
|
<button type="button" disabled>mm</button>
|
|
</div>
|
|
|
|
<div class="field field-size field--view-only">
|
|
<label for="page-height" class="label-with-tooltip" data-css="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 field-margin">
|
|
<label for="margin-top" class="label-with-tooltip" data-css="margin-top"
|
|
>Haut</label
|
|
>
|
|
<div class="input-with-unit">
|
|
<NumberInput
|
|
id="margin-top"
|
|
v-model="margins.top.value"
|
|
:min="0"
|
|
:step="1"
|
|
/>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
:class="{ active: margins.top.unit === 'rem' }"
|
|
@click="margins.top.unit = 'rem'"
|
|
>
|
|
rem
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field field-margin">
|
|
<label
|
|
for="margin-bottom"
|
|
class="label-with-tooltip"
|
|
data-css="margin-bottom"
|
|
>Bas</label
|
|
>
|
|
<div class="input-with-unit">
|
|
<NumberInput
|
|
id="margin-bottom"
|
|
v-model="margins.bottom.value"
|
|
:min="0"
|
|
:step="1"
|
|
/>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
:class="{ active: margins.bottom.unit === 'rem' }"
|
|
@click="margins.bottom.unit = 'rem'"
|
|
>
|
|
rem
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field field-margin">
|
|
<label
|
|
for="margin-left"
|
|
class="label-with-tooltip"
|
|
data-css="margin-left"
|
|
>Gauche</label
|
|
>
|
|
<div class="input-with-unit">
|
|
<NumberInput
|
|
id="margin-left"
|
|
v-model="margins.left.value"
|
|
:min="0"
|
|
:step="1"
|
|
/>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
:class="{ active: margins.left.unit === 'rem' }"
|
|
@click="margins.left.unit = 'rem'"
|
|
>
|
|
rem
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field field-margin">
|
|
<label
|
|
for="margin-right"
|
|
class="label-with-tooltip"
|
|
data-css="margin-right"
|
|
>Droite</label
|
|
>
|
|
<div class="input-with-unit">
|
|
<NumberInput
|
|
id="margin-right"
|
|
v-model="margins.right.value"
|
|
:min="0"
|
|
:step="1"
|
|
/>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
:class="{ active: margins.right.unit === 'rem' }"
|
|
@click="margins.right.unit = 'rem'"
|
|
>
|
|
rem
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-subsection">
|
|
<div class="field field-simple">
|
|
<label for="background" class="label-with-tooltip" data-css="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 field-simple">
|
|
<label
|
|
for="pattern"
|
|
class="label-with-tooltip"
|
|
data-css="background-image"
|
|
>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"
|
|
class="label-with-tooltip"
|
|
data-css="@bottom-left/right"
|
|
>Numéro de page</label
|
|
>
|
|
</div>
|
|
|
|
<div class="field checkbox-field">
|
|
<input id="running-title" type="checkbox" v-model="runningTitle" />
|
|
<label
|
|
for="running-title"
|
|
class="label-with-tooltip"
|
|
data-css="string-set"
|
|
>Titre courant</label
|
|
>
|
|
</div>
|
|
</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 NumberInput from '../ui/NumberInput.vue';
|
|
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(() => {
|
|
updatePageFooters();
|
|
});
|
|
});
|
|
|
|
watch(runningTitle, (enabled) => {
|
|
if (isUpdatingFromStore) return;
|
|
|
|
immediateUpdate(() => {
|
|
updatePageFooters();
|
|
});
|
|
});
|
|
|
|
const updatePageFooters = () => {
|
|
let currentCss = stylesheetStore.content;
|
|
|
|
// Remove existing @page:left and @page:right rules
|
|
currentCss = currentCss.replace(/@page:left\s*\{[^}]*\}/g, '');
|
|
currentCss = currentCss.replace(/@page:right\s*\{[^}]*\}/g, '');
|
|
|
|
// Remove old @page @bottom-center rule if exists
|
|
currentCss = currentCss.replace(
|
|
/@page\s*\{[^}]*@bottom-center[^}]*\}/g,
|
|
(match) => {
|
|
return match.replace(/@bottom-center\s*\{[^}]*\}/g, '');
|
|
}
|
|
);
|
|
|
|
// Remove string-set rule if running title is disabled
|
|
if (!runningTitle.value) {
|
|
currentCss = currentCss.replace(
|
|
/\.chapter\s*>\s*h2\s*\{[^}]*string-set:[^}]*\}\s*/g,
|
|
''
|
|
);
|
|
} else if (!currentCss.includes('string-set: title')) {
|
|
// Add the string-set rule for h2 titles if running title is enabled
|
|
const stringSetRule =
|
|
'\n.chapter > h2 {\n string-set: title content(text);\n}\n';
|
|
currentCss += stringSetRule;
|
|
}
|
|
|
|
// Build new rules based on checkboxes
|
|
let leftPageRule = '';
|
|
let rightPageRule = '';
|
|
|
|
if (pageNumbers.value || runningTitle.value) {
|
|
// Left pages: page number bottom-left, running title right next to it (bottom-left-corner)
|
|
let leftBottomLeft = '';
|
|
let leftBottomCenter = '';
|
|
|
|
if (pageNumbers.value && runningTitle.value) {
|
|
// Page number on the left, title right next to it
|
|
leftBottomLeft =
|
|
' @bottom-left {\n content: counter(page) " " string(title);\n }\n';
|
|
} else if (pageNumbers.value) {
|
|
leftBottomLeft = ' @bottom-left {\n content: counter(page);\n }\n';
|
|
} else if (runningTitle.value) {
|
|
leftBottomLeft = ' @bottom-left {\n content: string(title);\n }\n';
|
|
}
|
|
|
|
if (leftBottomLeft || leftBottomCenter) {
|
|
leftPageRule = `@page:left {\n${leftBottomLeft}${leftBottomCenter}}\n\n`;
|
|
}
|
|
|
|
// Right pages: title on the left, page number on the right (next to it)
|
|
let rightBottomRight = '';
|
|
|
|
if (pageNumbers.value && runningTitle.value) {
|
|
// Title on the left of page number
|
|
rightBottomRight =
|
|
' @bottom-right {\n content: string(title) " " counter(page);\n }\n';
|
|
} else if (pageNumbers.value) {
|
|
rightBottomRight =
|
|
' @bottom-right {\n content: counter(page);\n }\n';
|
|
} else if (runningTitle.value) {
|
|
rightBottomRight =
|
|
' @bottom-right {\n content: string(title);\n }\n';
|
|
}
|
|
|
|
if (rightBottomRight) {
|
|
rightPageRule = `@page:right {\n${rightBottomRight}}\n\n`;
|
|
}
|
|
}
|
|
|
|
// Insert the new rules after the main @page rule
|
|
const pageRuleMatch = currentCss.match(/@page\s*\{[^}]*\}/);
|
|
if (pageRuleMatch) {
|
|
const insertPosition = pageRuleMatch.index + pageRuleMatch[0].length;
|
|
currentCss =
|
|
currentCss.slice(0, insertPosition) +
|
|
'\n\n' +
|
|
leftPageRule +
|
|
rightPageRule +
|
|
currentCss.slice(insertPosition);
|
|
}
|
|
|
|
stylesheetStore.content = currentCss;
|
|
};
|
|
|
|
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();
|
|
}
|
|
|
|
// Check for page numbers and running title in @page:left and @page:right
|
|
const leftPageMatch = stylesheetStore.content.match(
|
|
/@page:left\s*\{[^}]*\}/
|
|
);
|
|
const rightPageMatch = stylesheetStore.content.match(
|
|
/@page:right\s*\{[^}]*\}/
|
|
);
|
|
|
|
// Check if page numbers exist (counter(page) in either left or right)
|
|
const hasPageNumbers =
|
|
(leftPageMatch && leftPageMatch[0].includes('counter(page)')) ||
|
|
(rightPageMatch && rightPageMatch[0].includes('counter(page)'));
|
|
pageNumbers.value = hasPageNumbers;
|
|
|
|
// Check if running title exists (string(title) in either left or right)
|
|
const hasRunningTitle =
|
|
(leftPageMatch && leftPageMatch[0].includes('string(title)')) ||
|
|
(rightPageMatch && rightPageMatch[0].includes('string(title)'));
|
|
runningTitle.value = hasRunningTitle;
|
|
} 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>
|