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

@ -1,7 +1,7 @@
.about {
font-size: 1rem;
}
#chapter-2 { #chapter-2 {
font-size: 2rem; font-size: 2rem;
} }
p {
font-size: 1rem;
}

View file

@ -47,18 +47,14 @@ const injectStylesToIframe = () => {
styleElement.textContent = stylesheetStore.content; styleElement.textContent = stylesheetStore.content;
}; };
const elementPopup = ref(null);
const renderPreview = async () => { const renderPreview = async () => {
const iframe = previewFrame.value; const iframe = previewFrame.value;
if (!iframe) return; if (!iframe) return;
await stylesheetStore.loadStylesheet(); await stylesheetStore.loadStylesheet();
const initialFontSize = stylesheetStore.extractValue('.about', 'font-size');
if (initialFontSize) {
aboutFontSize.value = initialFontSize.value;
aboutFontSizeUnit.value = initialFontSize.unit;
}
iframe.srcdoc = ` iframe.srcdoc = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -73,68 +69,10 @@ const renderPreview = async () => {
`; `;
iframe.onload = () => { 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 // Lifecycle
// ============================================================================ // ============================================================================
@ -147,22 +85,13 @@ onMounted(renderPreview);
<PagedJsWrapper /> <PagedJsWrapper />
</div> </div>
<EditorPanel <EditorPanel />
:fontSize="aboutFontSize"
:unit="aboutFontSizeUnit"
@update:fontSize="aboutFontSize = $event"
/>
<iframe ref="previewFrame" id="preview-frame"></iframe> <iframe ref="previewFrame" id="preview-frame"></iframe>
<StylesheetViewer :stylesheet="stylesheetStore.content" /> <StylesheetViewer :stylesheet="stylesheetStore.content" />
<ElementPopup <ElementPopup ref="elementPopup" :iframeRef="previewFrame" />
:visible="popupVisible"
:position="popupPosition"
:selector="popupSelector"
@close="closePopup"
/>
</template> </template>
<style> <style>

View file

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

View file

@ -6,7 +6,7 @@
> >
<div class="popup-header"> <div class="popup-header">
<span>{{ selector }}</span> <span>{{ selector }}</span>
<button class="close-btn" @click="$emit('close')">×</button> <button class="close-btn" @click="close">×</button>
</div> </div>
<div class="popup-body"> <div class="popup-body">
<div class="popup-controls"> <div class="popup-controls">
@ -30,7 +30,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet'; import { useStylesheetStore } from '../stores/stylesheet';
import hljs from 'highlight.js/lib/core'; import hljs from 'highlight.js/lib/core';
import css from 'highlight.js/lib/languages/css'; import css from 'highlight.js/lib/languages/css';
@ -41,21 +41,57 @@ hljs.registerLanguage('css', css);
const stylesheetStore = useStylesheetStore(); const stylesheetStore = useStylesheetStore();
const props = defineProps({ const props = defineProps({
visible: Boolean, iframeRef: Object
position: Object,
selector: String
}); });
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(() => { const elementCss = computed(() => {
if (!props.selector) return ''; if (!selector.value) return '';
return stylesheetStore.extractBlock(props.selector); return stylesheetStore.extractBlock(selector.value);
}); });
const fontSizeData = computed(() => { const fontSizeData = computed(() => {
if (!props.selector) return null; if (!selector.value) return null;
return stylesheetStore.extractValue(props.selector, 'font-size'); return stylesheetStore.extractValue(selector.value, 'font-size');
}); });
const highlightedCss = computed(() => { const highlightedCss = computed(() => {
@ -66,12 +102,14 @@ const highlightedCss = computed(() => {
const updateFontSize = (value) => { const updateFontSize = (value) => {
if (!fontSizeData.value) return; if (!fontSizeData.value) return;
stylesheetStore.updateProperty( stylesheetStore.updateProperty(
props.selector, selector.value,
'font-size', 'font-size',
value, value,
fontSizeData.value.unit fontSizeData.value.unit
); );
}; };
defineExpose({ handleIframeClick });
</script> </script>
<style scoped> <style scoped>

View file

@ -1,6 +1,6 @@
<template> <template>
<section class="chapter"> <section class="chapter">
<p class="about"> <p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex 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 condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
tristique arcu id maximus nunc fusce suspendisse. tristique arcu id maximus nunc fusce suspendisse.
</p> </p>
<p class="about"> <p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex 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' } * extractCssValue(css, '@page', 'margin'); // { value: 20, unit: 'mm' }
*/ */
const extractCssValue = (css, selector, property) => { const extractCssValue = (css, selector, property) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp( 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' 'i'
); );
const match = css.match(regex); const match = css.match(regex);
@ -56,8 +57,9 @@ const extractCssValue = (css, selector, property) => {
* }); // '@page { margin: 30mm; }' * }); // '@page { margin: 30mm; }'
*/ */
const updateCssValue = ({ css, selector, property, value, unit }) => { const updateCssValue = ({ css, selector, property, value, unit }) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp( 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' 'gi'
); );
return css.replace(regex, `$1${value}${unit}`); return css.replace(regex, `$1${value}${unit}`);