2025-11-24 14:01:48 +01:00
|
|
|
<script setup>
|
|
|
|
|
import PagedJsWrapper from './components/PagedJsWrapper.vue';
|
2025-12-03 15:20:49 +01:00
|
|
|
import EditorPanel from './components/editor/EditorPanel.vue';
|
2025-11-24 16:51:55 +01:00
|
|
|
import ElementPopup from './components/ElementPopup.vue';
|
2025-12-05 11:02:18 +01:00
|
|
|
import { onMounted, ref, watch, computed, provide } from 'vue';
|
2025-11-24 17:55:42 +01:00
|
|
|
import { useStylesheetStore } from './stores/stylesheet';
|
2025-12-04 16:14:50 +01:00
|
|
|
import Coloris from '@melloware/coloris';
|
2025-11-24 17:55:42 +01:00
|
|
|
|
|
|
|
|
const stylesheetStore = useStylesheetStore();
|
2025-12-04 15:27:23 +01:00
|
|
|
const previewFrame1 = ref(null);
|
|
|
|
|
const previewFrame2 = ref(null);
|
2025-11-24 18:18:27 +01:00
|
|
|
const elementPopup = ref(null);
|
2025-12-05 11:02:18 +01:00
|
|
|
const activeTab = ref('');
|
|
|
|
|
|
|
|
|
|
provide('activeTab', activeTab);
|
2025-11-24 18:18:27 +01:00
|
|
|
|
2025-12-03 15:20:49 +01:00
|
|
|
let savedScrollPercentage = 0;
|
2025-12-04 15:27:23 +01:00
|
|
|
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
|
|
|
|
|
let isTransitioning = false;
|
|
|
|
|
|
|
|
|
|
const activeFrame = computed(() => {
|
2025-12-04 15:55:52 +01:00
|
|
|
return currentFrameIndex.value === 1
|
|
|
|
|
? previewFrame1.value
|
|
|
|
|
: previewFrame2.value;
|
2025-12-04 15:27:23 +01:00
|
|
|
});
|
2025-12-03 15:20:49 +01:00
|
|
|
|
|
|
|
|
const renderPreview = async (shouldReloadFromFile = false) => {
|
2025-12-04 15:27:23 +01:00
|
|
|
if (isTransitioning) return;
|
|
|
|
|
isTransitioning = true;
|
|
|
|
|
|
|
|
|
|
// Determine which iframe is currently visible and which to render to
|
2025-12-04 15:55:52 +01:00
|
|
|
const visibleFrame =
|
|
|
|
|
currentFrameIndex.value === 1 ? previewFrame1.value : previewFrame2.value;
|
|
|
|
|
const hiddenFrame =
|
|
|
|
|
currentFrameIndex.value === 1 ? previewFrame2.value : previewFrame1.value;
|
2025-12-04 15:27:23 +01:00
|
|
|
|
|
|
|
|
if (!hiddenFrame) {
|
|
|
|
|
isTransitioning = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Save scroll position from visible frame
|
2025-12-04 15:55:52 +01:00
|
|
|
if (
|
|
|
|
|
visibleFrame &&
|
|
|
|
|
visibleFrame.contentWindow &&
|
|
|
|
|
visibleFrame.contentDocument
|
|
|
|
|
) {
|
2025-12-04 15:27:23 +01:00
|
|
|
const scrollTop = visibleFrame.contentWindow.scrollY || 0;
|
2025-12-04 15:55:52 +01:00
|
|
|
const scrollHeight =
|
|
|
|
|
visibleFrame.contentDocument.documentElement.scrollHeight;
|
2025-12-04 15:27:23 +01:00
|
|
|
const clientHeight = visibleFrame.contentWindow.innerHeight;
|
2025-12-03 15:20:49 +01:00
|
|
|
const maxScroll = scrollHeight - clientHeight;
|
|
|
|
|
|
|
|
|
|
savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldReloadFromFile || !stylesheetStore.content) {
|
|
|
|
|
await stylesheetStore.loadStylesheet();
|
|
|
|
|
}
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Render to the hidden frame
|
|
|
|
|
hiddenFrame.srcdoc = `
|
2025-11-24 16:51:55 +01:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
2025-11-24 17:55:42 +01:00
|
|
|
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
2025-11-24 16:51:55 +01:00
|
|
|
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
|
|
|
|
</head>
|
2025-11-24 17:55:42 +01:00
|
|
|
<body>${document.getElementById('content-source').innerHTML}</body>
|
2025-11-24 16:51:55 +01:00
|
|
|
</html>
|
2025-11-24 17:55:42 +01:00
|
|
|
`;
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
hiddenFrame.onload = () => {
|
|
|
|
|
hiddenFrame.contentDocument.addEventListener(
|
2025-12-04 13:34:33 +01:00
|
|
|
'click',
|
|
|
|
|
elementPopup.value.handleIframeClick
|
|
|
|
|
);
|
2025-12-03 15:20:49 +01:00
|
|
|
|
2025-12-04 16:14:50 +01:00
|
|
|
// Close Coloris when clicking in the iframe
|
|
|
|
|
hiddenFrame.contentDocument.addEventListener('click', () => {
|
|
|
|
|
Coloris.close();
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Wait for PagedJS to finish rendering
|
2025-12-03 15:20:49 +01:00
|
|
|
setTimeout(() => {
|
2025-12-04 15:27:23 +01:00
|
|
|
// Restore scroll position
|
2025-12-04 15:55:52 +01:00
|
|
|
const scrollHeight =
|
|
|
|
|
hiddenFrame.contentDocument.documentElement.scrollHeight;
|
2025-12-04 15:27:23 +01:00
|
|
|
const clientHeight = hiddenFrame.contentWindow.innerHeight;
|
2025-12-03 15:20:49 +01:00
|
|
|
const maxScroll = scrollHeight - clientHeight;
|
|
|
|
|
const targetScroll = savedScrollPercentage * maxScroll;
|
|
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
hiddenFrame.contentWindow.scrollTo(0, targetScroll);
|
|
|
|
|
|
|
|
|
|
// Start crossfade transition
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// Make hidden frame visible (it's already behind)
|
|
|
|
|
hiddenFrame.style.opacity = '1';
|
|
|
|
|
hiddenFrame.style.zIndex = '1';
|
|
|
|
|
|
|
|
|
|
// Fade out visible frame
|
|
|
|
|
if (visibleFrame) {
|
|
|
|
|
visibleFrame.style.opacity = '0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// After fade completes, swap the frames
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (visibleFrame) {
|
|
|
|
|
visibleFrame.style.zIndex = '0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Swap current frame
|
|
|
|
|
currentFrameIndex.value = currentFrameIndex.value === 1 ? 2 : 1;
|
|
|
|
|
isTransitioning = false;
|
2025-12-04 15:55:52 +01:00
|
|
|
}, 100); // Match CSS transition duration
|
|
|
|
|
}, 50); // Small delay to ensure scroll is set
|
|
|
|
|
}, 200); // Wait for PagedJS
|
2025-11-24 18:01:47 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-04 13:34:33 +01:00
|
|
|
watch(
|
|
|
|
|
() => stylesheetStore.content,
|
|
|
|
|
() => {
|
|
|
|
|
renderPreview();
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-12-03 15:20:49 +01:00
|
|
|
|
|
|
|
|
onMounted(() => renderPreview(true));
|
2025-11-24 14:01:48 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-11-24 16:51:55 +01:00
|
|
|
<div id="content-source" style="display: none">
|
2025-11-24 14:01:48 +01:00
|
|
|
<PagedJsWrapper />
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-24 18:18:27 +01:00
|
|
|
<EditorPanel />
|
2025-11-24 14:01:48 +01:00
|
|
|
|
2025-12-05 11:02:18 +01:00
|
|
|
<iframe
|
|
|
|
|
ref="previewFrame1"
|
|
|
|
|
class="preview-frame"
|
|
|
|
|
:class="{ shifted: activeTab.length > 0 }"
|
|
|
|
|
></iframe>
|
|
|
|
|
<iframe
|
|
|
|
|
ref="previewFrame2"
|
|
|
|
|
class="preview-frame"
|
|
|
|
|
:class="{ shifted: activeTab.length > 0 }"
|
|
|
|
|
></iframe>
|
2025-11-24 14:01:48 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
<ElementPopup ref="elementPopup" :iframeRef="activeFrame" />
|
2025-11-24 16:51:55 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style>
|
2025-12-04 15:27:23 +01:00
|
|
|
.preview-frame {
|
2025-11-24 16:51:55 +01:00
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
2025-12-04 13:34:33 +01:00
|
|
|
left: 0;
|
|
|
|
|
width: 100vw;
|
2025-11-24 16:51:55 +01:00
|
|
|
height: 100vh;
|
|
|
|
|
border: none;
|
2025-12-05 11:02:18 +01:00
|
|
|
transition: opacity 0.1s ease-in-out, margin-left ease-in-out var(--curve),
|
|
|
|
|
transform ease-in-out var(--curve), height ease-in-out var(--curve);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame.shifted {
|
|
|
|
|
margin-left: 19rem;
|
|
|
|
|
transform: scale(0.7) translateY(-30vh);
|
|
|
|
|
height: 142vh;
|
2025-12-04 15:27:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame:nth-of-type(1) {
|
|
|
|
|
z-index: 1;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame:nth-of-type(2) {
|
|
|
|
|
z-index: 0;
|
|
|
|
|
opacity: 0;
|
2025-11-24 14:01:48 +01:00
|
|
|
}
|
|
|
|
|
</style>
|