feat: implement reactive EditorPanel with bidirectional sync
- Reorganize editor components into dedicated folder - Create PageSettings component with page format, margins, background controls - Create TextSettings component (structure only, to be populated) - Implement debounced updates (1s delay) to stylesheet store - Add bidirectional sync between EditorPanel and StylesheetViewer - Preserve scroll position as percentage when reloading preview - Move @page rules from App.vue to stylesheet.css for unified management - Extend css-parsing utils to handle text values (e.g., 'A4', 'portrait') - Remove unnecessary comments, use explicit naming instead 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b8cb77c0e5
commit
9f10971041
7 changed files with 1104 additions and 166 deletions
|
|
@ -1,3 +1,20 @@
|
|||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 15mm 26mm 15mm;
|
||||
}
|
||||
|
||||
@page {
|
||||
@bottom-center { content: string(title); }
|
||||
}
|
||||
|
||||
h2 {
|
||||
break-before: page;
|
||||
}
|
||||
|
||||
.chapter > h2 {
|
||||
string-set: title content(text);
|
||||
}
|
||||
|
||||
#chapter-2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
|
|
|||
76
src/App.vue
76
src/App.vue
|
|
@ -1,59 +1,33 @@
|
|||
<script setup>
|
||||
import PagedJsWrapper from './components/PagedJsWrapper.vue';
|
||||
import EditorPanel from './components/EditorPanel.vue';
|
||||
import EditorPanel from './components/editor/EditorPanel.vue';
|
||||
import StylesheetViewer from './components/StylesheetViewer.vue';
|
||||
import ElementPopup from './components/ElementPopup.vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useStylesheetStore } from './stores/stylesheet';
|
||||
|
||||
// ============================================================================
|
||||
// Store
|
||||
// ============================================================================
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
|
||||
// ============================================================================
|
||||
// PagedJS configuration
|
||||
// ============================================================================
|
||||
const printStyles = `
|
||||
h2 { break-before: page; }
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 15mm 26mm 15mm;
|
||||
}
|
||||
|
||||
@page {
|
||||
@bottom-center { content: string(title); }
|
||||
}
|
||||
|
||||
.chapter > h2 { string-set: title content(text); }
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// Iframe preview
|
||||
// ============================================================================
|
||||
const previewFrame = ref(null);
|
||||
|
||||
const injectStylesToIframe = () => {
|
||||
const iframe = previewFrame.value;
|
||||
if (!iframe?.contentDocument) return;
|
||||
|
||||
let styleElement = iframe.contentDocument.getElementById('dynamic-styles');
|
||||
if (!styleElement) {
|
||||
styleElement = iframe.contentDocument.createElement('style');
|
||||
styleElement.id = 'dynamic-styles';
|
||||
iframe.contentDocument.head.appendChild(styleElement);
|
||||
}
|
||||
styleElement.textContent = stylesheetStore.content;
|
||||
};
|
||||
|
||||
const elementPopup = ref(null);
|
||||
|
||||
const renderPreview = async () => {
|
||||
let savedScrollPercentage = 0;
|
||||
|
||||
const renderPreview = async (shouldReloadFromFile = false) => {
|
||||
const iframe = previewFrame.value;
|
||||
if (!iframe) return;
|
||||
|
||||
if (iframe.contentWindow && iframe.contentDocument) {
|
||||
const scrollTop = iframe.contentWindow.scrollY || 0;
|
||||
const scrollHeight = iframe.contentDocument.documentElement.scrollHeight;
|
||||
const clientHeight = iframe.contentWindow.innerHeight;
|
||||
const maxScroll = scrollHeight - clientHeight;
|
||||
|
||||
savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0;
|
||||
}
|
||||
|
||||
if (shouldReloadFromFile || !stylesheetStore.content) {
|
||||
await stylesheetStore.loadStylesheet();
|
||||
}
|
||||
|
||||
iframe.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -61,7 +35,6 @@ const renderPreview = async () => {
|
|||
<head>
|
||||
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
||||
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
||||
<style>${printStyles}</style>
|
||||
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
||||
</head>
|
||||
<body>${document.getElementById('content-source').innerHTML}</body>
|
||||
|
|
@ -70,14 +43,23 @@ const renderPreview = async () => {
|
|||
|
||||
iframe.onload = () => {
|
||||
iframe.contentDocument.addEventListener('click', elementPopup.value.handleIframeClick);
|
||||
|
||||
setTimeout(() => {
|
||||
const scrollHeight = iframe.contentDocument.documentElement.scrollHeight;
|
||||
const clientHeight = iframe.contentWindow.innerHeight;
|
||||
const maxScroll = scrollHeight - clientHeight;
|
||||
const targetScroll = savedScrollPercentage * maxScroll;
|
||||
|
||||
iframe.contentWindow.scrollTo(0, targetScroll);
|
||||
}, 500);
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
watch(() => stylesheetStore.content, injectStylesToIframe);
|
||||
onMounted(renderPreview);
|
||||
watch(() => stylesheetStore.content, () => {
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
onMounted(() => renderPreview(true));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<aside id="editor-panel">
|
||||
<h3>Éditeur</h3>
|
||||
<div class="control">
|
||||
<label>Taille de police des paragraphes</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
:value="fontSizeData?.value ?? 1"
|
||||
@input="updateFontSize(parseFloat($event.target.value))"
|
||||
/>
|
||||
<span>{{ fontSizeData?.unit ?? 'rem' }}</span>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
|
||||
const fontSizeData = computed(() => {
|
||||
return stylesheetStore.extractValue('p', 'font-size');
|
||||
});
|
||||
|
||||
const updateFontSize = (value) => {
|
||||
if (!fontSizeData.value) return;
|
||||
stylesheetStore.updateProperty(
|
||||
'p',
|
||||
'font-size',
|
||||
value,
|
||||
fontSizeData.value.unit
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#editor-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.control input {
|
||||
width: 80px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
84
src/components/editor/EditorPanel.vue
Normal file
84
src/components/editor/EditorPanel.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<aside id="editor-panel">
|
||||
<nav class="tabs">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: activeTab === 'document' }"
|
||||
@click="activeTab = 'document'"
|
||||
>
|
||||
Document
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: activeTab === 'code' }"
|
||||
@click="activeTab = 'code'"
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: activeTab === 'contenu' }"
|
||||
@click="activeTab = 'contenu'"
|
||||
>
|
||||
Contenu
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div v-if="activeTab === 'document'" class="tab-panel">
|
||||
<PageSettings />
|
||||
<TextSettings />
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'code'" class="tab-panel">
|
||||
<!-- Code tab content -->
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'contenu'" class="tab-panel">
|
||||
<!-- Contenu tab content -->
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import PageSettings from './PageSettings.vue';
|
||||
import TextSettings from './TextSettings.vue';
|
||||
|
||||
// Tab management
|
||||
const activeTab = ref('document');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#editor-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.control input {
|
||||
width: 80px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
360
src/components/editor/PageSettings.vue
Normal file
360
src/components/editor/PageSettings.vue
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<template>
|
||||
<section class="settings-section">
|
||||
<h2>Réglage des pages</h2>
|
||||
|
||||
<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 class="field">
|
||||
<label for="page-width">Largeur</label>
|
||||
<input
|
||||
id="page-width"
|
||||
type="text"
|
||||
:value="pageWidth"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="page-height">Hauteur</label>
|
||||
<input
|
||||
id="page-height"
|
||||
type="text"
|
||||
:value="pageHeight"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<h3>Marges</h3>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-top">Haut</label>
|
||||
<div class="field-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="field-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="field-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="field-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="field">
|
||||
<label for="background">Arrière-plan</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="background"
|
||||
type="text"
|
||||
v-model="background.value"
|
||||
/>
|
||||
<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 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 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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useStylesheetStore } from '../../stores/stylesheet';
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
watch(pageFormat, (newFormat) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
|
||||
});
|
||||
});
|
||||
|
||||
watch(margins, (newMargins) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
const marginValue = `${newMargins.top.value}${newMargins.top.unit} ${newMargins.right.value}${newMargins.right.unit} ${newMargins.bottom.value}${newMargins.bottom.unit} ${newMargins.left.value}${newMargins.left.unit}`;
|
||||
|
||||
const currentBlock = stylesheetStore.extractBlock('@page');
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(margin:\s*)[^;]+/,
|
||||
`$1${marginValue}`
|
||||
);
|
||||
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
watch(background, (newBg) => {
|
||||
if (!newBg.value) return;
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
const currentBlock = stylesheetStore.extractBlock('@page');
|
||||
|
||||
if (currentBlock.includes('background:')) {
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(background:\s*)[^;]+/,
|
||||
`$1${newBg.value}`
|
||||
);
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
} else {
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(\s*})$/,
|
||||
` background: ${newBg.value};\n$1`
|
||||
);
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
}
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
watch(pattern, (newPattern) => {
|
||||
if (!newPattern || isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// TODO: implement pattern application
|
||||
});
|
||||
});
|
||||
|
||||
watch(pageNumbers, (enabled) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// TODO: implement page numbers toggle
|
||||
});
|
||||
});
|
||||
|
||||
watch(runningTitle, (enabled) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
syncFromStore();
|
||||
});
|
||||
</script>
|
||||
604
src/components/editor/TextSettings.vue
Normal file
604
src/components/editor/TextSettings.vue
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
<template>
|
||||
<section class="settings-section">
|
||||
<h2>Réglage du texte</h2>
|
||||
<p class="infos">
|
||||
Ces réglages s'appliquent à l'ensemble des éléments du document.
|
||||
Vous pouvez modifier ensuite les éléments indépendamment.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label for="text-font">Police</label>
|
||||
<div class="field-with-option">
|
||||
<select id="text-font" v-model="font">
|
||||
<option value="Alegreya Sans">Alegreya Sans</option>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
</select>
|
||||
<div class="field-checkbox">
|
||||
<input
|
||||
id="text-italic"
|
||||
type="checkbox"
|
||||
v-model="italic"
|
||||
/>
|
||||
<label for="text-italic">Italique</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Graisse</label>
|
||||
<div class="weight-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === '200' }"
|
||||
@click="weight = '200'"
|
||||
>
|
||||
200
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === '300' }"
|
||||
@click="weight = '300'"
|
||||
>
|
||||
300
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === '400' }"
|
||||
@click="weight = '400'"
|
||||
>
|
||||
400
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === '600' }"
|
||||
@click="weight = '600'"
|
||||
>
|
||||
600
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === '800' }"
|
||||
@click="weight = '800'"
|
||||
>
|
||||
800
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === 'normal' }"
|
||||
@click="weight = 'normal'"
|
||||
>
|
||||
normal
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: weight === 'bold' }"
|
||||
@click="weight = 'bold'"
|
||||
>
|
||||
bold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="text-size-range">Taille du texte</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="text-size-range"
|
||||
type="range"
|
||||
v-model.number="fontSize.value"
|
||||
min="8"
|
||||
max="72"
|
||||
step="1"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="fontSize.value"
|
||||
min="8"
|
||||
max="72"
|
||||
step="1"
|
||||
class="size-input"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: fontSize.unit === 'px' }"
|
||||
@click="fontSize.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: fontSize.unit === 'em' }"
|
||||
@click="fontSize.unit = 'em'"
|
||||
>
|
||||
em
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: fontSize.unit === 'rem' }"
|
||||
@click="fontSize.unit = 'rem'"
|
||||
>
|
||||
rem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="text-alignment">Alignement</label>
|
||||
<select id="text-alignment" v-model="alignment">
|
||||
<option value="left">Gauche</option>
|
||||
<option value="center">Centre</option>
|
||||
<option value="right">Droite</option>
|
||||
<option value="justify">Justifié</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="text-color">Couleur</label>
|
||||
<div class="field-with-color">
|
||||
<input
|
||||
type="color"
|
||||
v-model="color.picker"
|
||||
class="color-picker"
|
||||
/>
|
||||
<input
|
||||
id="text-color"
|
||||
type="text"
|
||||
v-model="color.value"
|
||||
class="color-input"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: color.format === 'rgb' }"
|
||||
@click="color.format = 'rgb'"
|
||||
>
|
||||
rgb
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: color.format === 'hex' }"
|
||||
@click="color.format = 'hex'"
|
||||
>
|
||||
hex
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="clear-btn"
|
||||
@click="clearColor"
|
||||
title="Réinitialiser"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="text-background">Arrière-plan</label>
|
||||
<div class="field-with-color">
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-btn"
|
||||
:class="{ active: background.enabled }"
|
||||
@click="background.enabled = !background.enabled"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<input
|
||||
id="text-background"
|
||||
type="text"
|
||||
v-model="background.value"
|
||||
:disabled="!background.enabled"
|
||||
class="color-input"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: background.format === 'rgb' }"
|
||||
:disabled="!background.enabled"
|
||||
@click="background.format = 'rgb'"
|
||||
>
|
||||
rgb
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: background.format === 'hex' }"
|
||||
:disabled="!background.enabled"
|
||||
@click="background.format = 'hex'"
|
||||
>
|
||||
hex
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="clear-btn"
|
||||
:disabled="!background.enabled"
|
||||
@click="clearBackground"
|
||||
title="Effacer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-outer">Marges extérieures</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-outer"
|
||||
type="number"
|
||||
v-model.number="marginOuter.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuter.unit === 'mm' }"
|
||||
@click="marginOuter.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuter.unit === 'px' }"
|
||||
@click="marginOuter.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="collapse-toggle"
|
||||
:class="{ expanded: marginOuterExpanded }"
|
||||
@click="marginOuterExpanded = !marginOuterExpanded"
|
||||
title="Réglages détaillés"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="marginOuterExpanded" class="subsection collapsed-section">
|
||||
<div class="field">
|
||||
<label for="margin-outer-top">Haut</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-outer-top"
|
||||
type="number"
|
||||
v-model.number="marginOuterDetailed.top.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.top.unit === 'mm' }"
|
||||
@click="marginOuterDetailed.top.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.top.unit === 'px' }"
|
||||
@click="marginOuterDetailed.top.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-outer-bottom">Bas</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-outer-bottom"
|
||||
type="number"
|
||||
v-model.number="marginOuterDetailed.bottom.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.bottom.unit === 'mm' }"
|
||||
@click="marginOuterDetailed.bottom.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.bottom.unit === 'px' }"
|
||||
@click="marginOuterDetailed.bottom.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-outer-left">Gauche</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-outer-left"
|
||||
type="number"
|
||||
v-model.number="marginOuterDetailed.left.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.left.unit === 'mm' }"
|
||||
@click="marginOuterDetailed.left.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.left.unit === 'px' }"
|
||||
@click="marginOuterDetailed.left.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-outer-right">Droite</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-outer-right"
|
||||
type="number"
|
||||
v-model.number="marginOuterDetailed.right.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.right.unit === 'mm' }"
|
||||
@click="marginOuterDetailed.right.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginOuterDetailed.right.unit === 'px' }"
|
||||
@click="marginOuterDetailed.right.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-inner">Marges intérieures</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-inner"
|
||||
type="number"
|
||||
v-model.number="marginInner.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInner.unit === 'mm' }"
|
||||
@click="marginInner.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInner.unit === 'px' }"
|
||||
@click="marginInner.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="collapse-toggle"
|
||||
:class="{ expanded: marginInnerExpanded }"
|
||||
@click="marginInnerExpanded = !marginInnerExpanded"
|
||||
title="Réglages détaillés"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="marginInnerExpanded" class="subsection collapsed-section">
|
||||
<div class="field">
|
||||
<label for="margin-inner-top">Haut</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-inner-top"
|
||||
type="number"
|
||||
v-model.number="marginInnerDetailed.top.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.top.unit === 'mm' }"
|
||||
@click="marginInnerDetailed.top.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.top.unit === 'px' }"
|
||||
@click="marginInnerDetailed.top.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-inner-bottom">Bas</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-inner-bottom"
|
||||
type="number"
|
||||
v-model.number="marginInnerDetailed.bottom.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.bottom.unit === 'mm' }"
|
||||
@click="marginInnerDetailed.bottom.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.bottom.unit === 'px' }"
|
||||
@click="marginInnerDetailed.bottom.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-inner-left">Gauche</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-inner-left"
|
||||
type="number"
|
||||
v-model.number="marginInnerDetailed.left.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.left.unit === 'mm' }"
|
||||
@click="marginInnerDetailed.left.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.left.unit === 'px' }"
|
||||
@click="marginInnerDetailed.left.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-inner-right">Droite</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-inner-right"
|
||||
type="number"
|
||||
v-model.number="marginInnerDetailed.right.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.right.unit === 'mm' }"
|
||||
@click="marginInnerDetailed.right.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: marginInnerDetailed.right.unit === 'px' }"
|
||||
@click="marginInnerDetailed.right.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Font
|
||||
const font = ref('Alegreya Sans');
|
||||
const italic = ref(false);
|
||||
|
||||
// Weight
|
||||
const weight = ref('400');
|
||||
|
||||
// Font size
|
||||
const fontSize = ref({
|
||||
value: 23,
|
||||
unit: 'px'
|
||||
});
|
||||
|
||||
// Alignment
|
||||
const alignment = ref('left');
|
||||
|
||||
// Color
|
||||
const color = ref({
|
||||
picker: '#000000',
|
||||
value: 'rgb(250, 250, 250)',
|
||||
format: 'rgb'
|
||||
});
|
||||
|
||||
const clearColor = () => {
|
||||
color.value.picker = '#000000';
|
||||
color.value.value = '';
|
||||
};
|
||||
|
||||
// Background
|
||||
const background = ref({
|
||||
enabled: false,
|
||||
value: 'transparent',
|
||||
format: 'hex'
|
||||
});
|
||||
|
||||
const clearBackground = () => {
|
||||
background.value.value = 'transparent';
|
||||
};
|
||||
|
||||
// Margin outer
|
||||
const marginOuter = ref({
|
||||
value: 23,
|
||||
unit: 'mm'
|
||||
});
|
||||
|
||||
const marginOuterExpanded = ref(false);
|
||||
|
||||
const marginOuterDetailed = ref({
|
||||
top: { value: 23, unit: 'mm' },
|
||||
bottom: { value: 23, unit: 'mm' },
|
||||
left: { value: 23, unit: 'mm' },
|
||||
right: { value: 23, unit: 'mm' }
|
||||
});
|
||||
|
||||
// Margin inner
|
||||
const marginInner = ref({
|
||||
value: 23,
|
||||
unit: 'mm'
|
||||
});
|
||||
|
||||
const marginInnerExpanded = ref(false);
|
||||
|
||||
const marginInnerDetailed = ref({
|
||||
top: { value: 23, unit: 'mm' },
|
||||
bottom: { value: 23, unit: 'mm' },
|
||||
left: { value: 23, unit: 'mm' },
|
||||
right: { value: 23, unit: 'mm' }
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,33 +1,9 @@
|
|||
/**
|
||||
* CSS parsing utilities for extracting and manipulating CSS rules
|
||||
* @module css-parsing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts a complete CSS block for a given selector
|
||||
* @param {string} css - The CSS stylesheet content
|
||||
* @param {string} selector - The CSS selector to find (e.g., '@page', '.my-class')
|
||||
* @returns {string} The matched CSS block including selector and braces, or empty string if not found
|
||||
* @example
|
||||
* const css = '@page { margin: 20mm; }';
|
||||
* extractCssBlock(css, '@page'); // '@page { margin: 20mm; }'
|
||||
*/
|
||||
const extractCssBlock = (css, selector) => {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = css.match(new RegExp(`${escaped}\\s*{[^}]*}`, 'gi'));
|
||||
return match ? match[0] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts a CSS property value and its unit from a stylesheet
|
||||
* @param {string} css - The CSS stylesheet content
|
||||
* @param {string} selector - The CSS selector to search within
|
||||
* @param {string} property - The CSS property name to extract
|
||||
* @returns {{value: number, unit: string}|null} Object with numeric value and unit, or null if not found
|
||||
* @example
|
||||
* const css = '@page { margin: 20mm; }';
|
||||
* extractCssValue(css, '@page', 'margin'); // { value: 20, unit: 'mm' }
|
||||
*/
|
||||
const extractCssValue = (css, selector, property) => {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(
|
||||
|
|
@ -38,26 +14,17 @@ const extractCssValue = (css, selector, property) => {
|
|||
return match ? { value: parseFloat(match[1]), unit: match[2] } : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a CSS property value in a stylesheet
|
||||
* @param {Object} options - Configuration object
|
||||
* @param {string} options.css - The CSS stylesheet content to modify
|
||||
* @param {string} options.selector - The CSS selector to target
|
||||
* @param {string} options.property - The CSS property to update
|
||||
* @param {number} options.value - The new numeric value
|
||||
* @param {string} options.unit - The CSS unit (px, rem, em, mm, cm, in, etc.)
|
||||
* @returns {string} The modified CSS stylesheet content
|
||||
* @example
|
||||
* updateCssValue({
|
||||
* css: '@page { margin: 20mm; }',
|
||||
* selector: '@page',
|
||||
* property: 'margin',
|
||||
* value: 30,
|
||||
* unit: 'mm'
|
||||
* }); // '@page { margin: 30mm; }'
|
||||
*/
|
||||
const updateCssValue = ({ css, selector, property, value, unit }) => {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
if (unit === '') {
|
||||
const regex = new RegExp(
|
||||
`(${escaped}\\s*{[^}]*${property}:\\s*)[^;]+`,
|
||||
'gi'
|
||||
);
|
||||
return css.replace(regex, `$1${value}`);
|
||||
}
|
||||
|
||||
const regex = new RegExp(
|
||||
`(${escaped}\\s*{[^}]*${property}:\\s*)[\\d.]+(px|rem|em|mm|cm|in)`,
|
||||
'gi'
|
||||
|
|
@ -65,13 +32,6 @@ const updateCssValue = ({ css, selector, property, value, unit }) => {
|
|||
return css.replace(regex, `$1${value}${unit}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Collection of CSS parsing utilities
|
||||
* @type {Object}
|
||||
* @property {Function} extractCssBlock - Extract a CSS block by selector
|
||||
* @property {Function} extractCssValue - Extract a property value with unit
|
||||
* @property {Function} updateCssValue - Update a property value in CSS
|
||||
*/
|
||||
const cssParsingUtils = { extractCssBlock, extractCssValue, updateCssValue };
|
||||
|
||||
export default cssParsingUtils;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue