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 {
|
#chapter-2 {
|
||||||
font-size: 2rem;
|
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;
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue