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
78
src/App.vue
78
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;
|
||||
|
||||
await stylesheetStore.loadStylesheet();
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue