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:
parent
7bc0dad32b
commit
ae8136a48e
6 changed files with 82 additions and 116 deletions
|
|
@ -1,7 +1,7 @@
|
|||
.about {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#chapter-2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
|
|||
81
src/App.vue
81
src/App.vue
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = (value) => {
|
||||
if (!fontSizeData.value) return;
|
||||
stylesheetStore.updateProperty(
|
||||
'p',
|
||||
'font-size',
|
||||
value,
|
||||
fontSizeData.value.unit
|
||||
);
|
||||
|
||||
const updateFontSize = () => {
|
||||
emit('update:fontSize', localFontSize.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue