refactor: make EditorPanel and ElementPopup fully autonomous

- Move all popup logic into ElementPopup component (state, positioning, click handling)
- Make EditorPanel autonomous with direct store access
- Simplify App.vue by removing prop drilling and intermediary logic
- Update EditorPanel to control paragraph font-size instead of .about
- Fix CSS parsing: escape selectors in extractCssValue and updateCssValue
- Remove hardcoded .about references from PagedJsWrapper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
isUnknown 2025-11-24 18:18:27 +01:00
parent 7bc0dad32b
commit ae8136a48e
6 changed files with 82 additions and 116 deletions

View file

@ -47,18 +47,14 @@ const injectStylesToIframe = () => {
styleElement.textContent = stylesheetStore.content;
};
const elementPopup = ref(null);
const renderPreview = async () => {
const iframe = previewFrame.value;
if (!iframe) return;
await stylesheetStore.loadStylesheet();
const initialFontSize = stylesheetStore.extractValue('.about', 'font-size');
if (initialFontSize) {
aboutFontSize.value = initialFontSize.value;
aboutFontSizeUnit.value = initialFontSize.unit;
}
iframe.srcdoc = `
<!DOCTYPE html>
<html>
@ -73,68 +69,10 @@ const renderPreview = async () => {
`;
iframe.onload = () => {
iframe.contentDocument.addEventListener('click', handleIframeClick);
iframe.contentDocument.addEventListener('click', elementPopup.value.handleIframeClick);
};
};
// ============================================================================
// Editor panel (temporary hardcoded .about selector)
// ============================================================================
const aboutFontSize = ref(2);
const aboutFontSizeUnit = ref('rem');
watch(aboutFontSize, (newVal) => {
stylesheetStore.updateProperty(
'.about',
'font-size',
newVal,
aboutFontSizeUnit.value
);
});
// ============================================================================
// Element popup
// ============================================================================
const popupVisible = ref(false);
const popupPosition = ref({ x: 0, y: 0 });
const popupSelector = ref('');
const getSelectorFromElement = (element) => {
return element.id
? `#${element.id}`
: `.${element.className.split(' ')[0]}`;
};
const calculatePopupPosition = (element) => {
const rect = element.getBoundingClientRect();
const iframeRect = previewFrame.value.getBoundingClientRect();
return {
x: iframeRect.left + rect.left,
y: iframeRect.top + rect.bottom + 5,
};
};
const openPopup = (element) => {
popupSelector.value = getSelectorFromElement(element);
popupPosition.value = calculatePopupPosition(element);
popupVisible.value = true;
};
const closePopup = () => {
popupVisible.value = false;
};
const handleIframeClick = (event) => {
const element = event.target;
if (element.tagName === 'BODY' || element.tagName === 'HTML') {
closePopup();
return;
}
openPopup(element);
};
// ============================================================================
// Lifecycle
// ============================================================================
@ -147,22 +85,13 @@ onMounted(renderPreview);
<PagedJsWrapper />
</div>
<EditorPanel
:fontSize="aboutFontSize"
:unit="aboutFontSizeUnit"
@update:fontSize="aboutFontSize = $event"
/>
<EditorPanel />
<iframe ref="previewFrame" id="preview-frame"></iframe>
<StylesheetViewer :stylesheet="stylesheetStore.content" />
<ElementPopup
:visible="popupVisible"
:position="popupPosition"
:selector="popupSelector"
@close="closePopup"
/>
<ElementPopup ref="elementPopup" :iframeRef="previewFrame" />
</template>
<style>

View file

@ -2,39 +2,36 @@
<aside id="editor-panel">
<h3>Éditeur</h3>
<div class="control">
<label>Font-size .about</label>
<label>Taille de police des paragraphes</label>
<input
type="number"
step="0.1"
v-model.number="localFontSize"
@input="updateFontSize"
:value="fontSizeData?.value ?? 1"
@input="updateFontSize(parseFloat($event.target.value))"
/>
<span>{{ unit }}</span>
<span>{{ fontSizeData?.unit ?? 'rem' }}</span>
</div>
</aside>
</template>
<script setup>
import { ref, watch } from 'vue';
import { computed } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet';
const props = defineProps({
fontSize: Number,
unit: String,
const stylesheetStore = useStylesheetStore();
const fontSizeData = computed(() => {
return stylesheetStore.extractValue('p', 'font-size');
});
const emit = defineEmits(['update:fontSize']);
const localFontSize = ref(props.fontSize);
watch(
() => props.fontSize,
(newVal) => {
localFontSize.value = newVal;
}
);
const updateFontSize = () => {
emit('update:fontSize', localFontSize.value);
const updateFontSize = (value) => {
if (!fontSizeData.value) return;
stylesheetStore.updateProperty(
'p',
'font-size',
value,
fontSizeData.value.unit
);
};
</script>

View file

@ -6,7 +6,7 @@
>
<div class="popup-header">
<span>{{ selector }}</span>
<button class="close-btn" @click="$emit('close')">×</button>
<button class="close-btn" @click="close">×</button>
</div>
<div class="popup-body">
<div class="popup-controls">
@ -30,7 +30,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { ref, computed } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet';
import hljs from 'highlight.js/lib/core';
import css from 'highlight.js/lib/languages/css';
@ -41,21 +41,57 @@ hljs.registerLanguage('css', css);
const stylesheetStore = useStylesheetStore();
const props = defineProps({
visible: Boolean,
position: Object,
selector: String
iframeRef: Object
});
defineEmits(['close']);
const visible = ref(false);
const position = ref({ x: 0, y: 0 });
const selector = ref('');
const getSelectorFromElement = (element) => {
return element.id
? `#${element.id}`
: `.${element.className.split(' ')[0]}`;
};
const calculatePosition = (element) => {
const rect = element.getBoundingClientRect();
const iframeRect = props.iframeRef.getBoundingClientRect();
return {
x: iframeRect.left + rect.left,
y: iframeRect.top + rect.bottom + 5,
};
};
const open = (element) => {
selector.value = getSelectorFromElement(element);
position.value = calculatePosition(element);
visible.value = true;
};
const close = () => {
visible.value = false;
};
const handleIframeClick = (event) => {
const element = event.target;
if (element.tagName === 'BODY' || element.tagName === 'HTML') {
close();
return;
}
open(element);
};
const elementCss = computed(() => {
if (!props.selector) return '';
return stylesheetStore.extractBlock(props.selector);
if (!selector.value) return '';
return stylesheetStore.extractBlock(selector.value);
});
const fontSizeData = computed(() => {
if (!props.selector) return null;
return stylesheetStore.extractValue(props.selector, 'font-size');
if (!selector.value) return null;
return stylesheetStore.extractValue(selector.value, 'font-size');
});
const highlightedCss = computed(() => {
@ -66,12 +102,14 @@ const highlightedCss = computed(() => {
const updateFontSize = (value) => {
if (!fontSizeData.value) return;
stylesheetStore.updateProperty(
props.selector,
selector.value,
'font-size',
value,
fontSizeData.value.unit
);
};
defineExpose({ handleIframeClick });
</script>
<style scoped>

View file

@ -1,6 +1,6 @@
<template>
<section class="chapter">
<p class="about">
<p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex
@ -14,7 +14,7 @@
condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
tristique arcu id maximus nunc fusce suspendisse.
</p>
<p class="about">
<p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex

View file

@ -29,8 +29,9 @@ const extractCssBlock = (css, selector) => {
* extractCssValue(css, '@page', 'margin'); // { value: 20, unit: 'mm' }
*/
const extractCssValue = (css, selector, property) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(
`${selector}\\s*{[^}]*${property}:\\s*([\\d.]+)(px|rem|em|mm|cm|in)`,
`${escaped}\\s*{[^}]*${property}:\\s*([\\d.]+)(px|rem|em|mm|cm|in)`,
'i'
);
const match = css.match(regex);
@ -56,8 +57,9 @@ const extractCssValue = (css, selector, property) => {
* }); // '@page { margin: 30mm; }'
*/
const updateCssValue = ({ css, selector, property, value, unit }) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(
`(${selector}\\s*{[^}]*${property}:\\s*)[\\d.]+(px|rem|em|mm|cm|in)`,
`(${escaped}\\s*{[^}]*${property}:\\s*)[\\d.]+(px|rem|em|mm|cm|in)`,
'gi'
);
return css.replace(regex, `$1${value}${unit}`);