img popup base
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 26s
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 26s
This commit is contained in:
parent
87c08bc529
commit
75e7f375b3
20 changed files with 532 additions and 23 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1833,7 +1833,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2012,7 +2011,6 @@
|
|||
"integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.5.0",
|
||||
"buffer-builder": "^0.2.0",
|
||||
|
|
@ -2491,7 +2489,6 @@
|
|||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -2566,7 +2563,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
|
|
|
|||
|
|
@ -192,6 +192,29 @@
|
|||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.block-image.element-hovered {
|
||||
outline: 2px solid #0d996050 !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.block-image.element-selected {
|
||||
outline: 2px dashed #0d9960 !important;
|
||||
}
|
||||
|
||||
.image-hover-label {
|
||||
position: absolute;
|
||||
background: #0d9960;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* Marks (to delete when merge in paged.js) */
|
||||
|
||||
.pagedjs_marks-crop {
|
||||
|
|
|
|||
|
|
@ -102,4 +102,18 @@
|
|||
--color-700: #6930f5;
|
||||
--color-800: #5d28e6;
|
||||
--color-900: #5223d6;
|
||||
}
|
||||
|
||||
|
||||
[data-color-type="image"]{
|
||||
--color-050: #f0faf5;
|
||||
--color-100: #d9f2e6;
|
||||
--color-200: #b3e5cd;
|
||||
--color-300: #7dd4b0;
|
||||
--color-400: #43c08e;
|
||||
--color-500: #1aad74;
|
||||
--color-600: #0d9960;
|
||||
--color-700: #0a8452;
|
||||
--color-800: #087043;
|
||||
--color-900: #065c35;
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
|
||||
.toggle-setting:checked + label{
|
||||
background: var(--color-purple);
|
||||
background: var(--color-700);
|
||||
|
||||
&::after {
|
||||
transform: translateX(10px);
|
||||
|
|
|
|||
|
|
@ -189,6 +189,19 @@
|
|||
--color-900: #5223d6;
|
||||
}
|
||||
|
||||
[data-color-type=image] {
|
||||
--color-050: #f0faf5;
|
||||
--color-100: #d9f2e6;
|
||||
--color-200: #b3e5cd;
|
||||
--color-300: #7dd4b0;
|
||||
--color-400: #43c08e;
|
||||
--color-500: #1aad74;
|
||||
--color-600: #0d9960;
|
||||
--color-700: #0a8452;
|
||||
--color-800: #087043;
|
||||
--color-900: #065c35;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
padding: 0;
|
||||
|
|
@ -647,7 +660,7 @@ input[type=number] {
|
|||
}
|
||||
|
||||
.toggle-setting:checked + label {
|
||||
background: var(--color-purple);
|
||||
background: var(--color-700);
|
||||
}
|
||||
.toggle-setting:checked + label::after {
|
||||
transform: translateX(10px);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
1
public/assets/svg/image.svg
Normal file
1
public/assets/svg/image.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2.9918 21C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918ZM20 15V5H4V19L14 9L20 15ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"/></svg>
|
||||
|
After Width: | Height: | Size: 441 B |
10
src/App.vue
10
src/App.vue
|
|
@ -2,6 +2,7 @@
|
|||
import PagedJsWrapper from './components/PagedJsWrapper.vue';
|
||||
import EditorPanel from './components/editor/EditorPanel.vue';
|
||||
import ElementPopup from './components/ElementPopup.vue';
|
||||
import ImagePopup from './components/ImagePopup.vue';
|
||||
// import PagePopup from './components/PagePopup.vue'; // DISABLED: page template styling feature
|
||||
import PreviewLoader from './components/PreviewLoader.vue';
|
||||
import SaveButton from './components/SaveButton.vue';
|
||||
|
|
@ -23,6 +24,7 @@ const { loadFontsFromCss } = useProjectFonts();
|
|||
const previewFrame1 = ref(null);
|
||||
const previewFrame2 = ref(null);
|
||||
const elementPopup = ref(null);
|
||||
const imagePopup = ref(null);
|
||||
// const pagePopup = ref(null); // DISABLED: page template styling feature
|
||||
const activeTab = ref('');
|
||||
|
||||
|
|
@ -38,7 +40,8 @@ const {
|
|||
handleIframeClick,
|
||||
// handlePagePopupClose, // DISABLED: page template styling feature
|
||||
handleElementPopupClose,
|
||||
} = useIframeInteractions({ elementPopup });
|
||||
handleImagePopupClose,
|
||||
} = useIframeInteractions({ elementPopup, imagePopup });
|
||||
|
||||
// Setup preview renderer with double buffering
|
||||
const {
|
||||
|
|
@ -126,6 +129,11 @@ onMounted(async () => {
|
|||
:iframeRef="activeFrame"
|
||||
@close="handleElementPopupClose"
|
||||
/>
|
||||
|
||||
<ImagePopup
|
||||
ref="imagePopup"
|
||||
@close="handleImagePopupClose"
|
||||
/>
|
||||
<!-- DISABLED: page template styling feature
|
||||
<PagePopup
|
||||
ref="pagePopup"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<BasePopup
|
||||
ref="basePopup"
|
||||
id="element-popup"
|
||||
color-type="text"
|
||||
:display-css="displayedCss"
|
||||
:editable-css="editableFullCss"
|
||||
:popup-width="800"
|
||||
|
|
|
|||
255
src/components/ImagePopup.vue
Normal file
255
src/components/ImagePopup.vue
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
<template>
|
||||
<BasePopup
|
||||
ref="basePopup"
|
||||
id="image-popup"
|
||||
color-type="image"
|
||||
:display-css="displayedCss"
|
||||
:editable-css="editableCss"
|
||||
:popup-width="440"
|
||||
:popup-height="200"
|
||||
:show-inheritance="false"
|
||||
@close="handleClose"
|
||||
@css-input="handleCssInput"
|
||||
>
|
||||
<template #header-left>
|
||||
<span class="image-label">{{ selector || '' }}</span>
|
||||
</template>
|
||||
|
||||
<template #controls>
|
||||
|
||||
<!-- Largeur -->
|
||||
<div
|
||||
class="setting__section"
|
||||
data-setting="width"
|
||||
:class="{ 'setting-disabled': !widthEnabled }"
|
||||
@click="onSectionClick"
|
||||
>
|
||||
<div class="setting__header">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle-width"
|
||||
class="toggle-setting"
|
||||
:checked="widthEnabled"
|
||||
@change="onToggleWidth($event.target.checked)"
|
||||
/><label for="toggle-width" aria-label="Activer le réglage de largeur" @click.stop></label>
|
||||
<label class="label-with-tooltip" data-css="width">Largeur</label>
|
||||
</div>
|
||||
<div class="setting__body">
|
||||
<InputWithUnit
|
||||
v-model="widthModel"
|
||||
:units="['%', 'px']"
|
||||
:min="1"
|
||||
:max="width.unit === 'px' ? 2000 : 200"
|
||||
showRange
|
||||
/>
|
||||
</div>
|
||||
<p class="info-default">Valeur par défaut : {{ imageDefaults.width.value }}{{ imageDefaults.width.unit }}</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</BasePopup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import InputWithUnit from './ui/InputWithUnit.vue';
|
||||
import BasePopup from './ui/BasePopup.vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
import { useImageDefaults } from '../composables/useImageDefaults';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
const imageDefaults = useImageDefaults();
|
||||
|
||||
const basePopup = ref(null);
|
||||
const visible = computed(() => basePopup.value?.visible ?? false);
|
||||
|
||||
const selector = ref('');
|
||||
const width = reactive({ value: 100, unit: '%' });
|
||||
const widthEnabled = ref(false);
|
||||
const widthCache = ref(null); // { value, unit } | null — persists user value when toggle is OFF
|
||||
|
||||
// Per-selector persistent state
|
||||
const elementStates = new Map();
|
||||
|
||||
let isUpdatingFromStore = false;
|
||||
|
||||
// InputWithUnit model
|
||||
const widthModel = computed({
|
||||
get: () => ({ value: width.value, unit: width.unit }),
|
||||
set: (v) => { width.value = v.value; width.unit = v.unit; },
|
||||
});
|
||||
|
||||
// Watch width changes → update CSS when toggle is ON
|
||||
watch(() => [width.value, width.unit], () => {
|
||||
if (isUpdatingFromStore || !widthEnabled.value || !selector.value) return;
|
||||
stylesheetStore.updateProperty(selector.value, 'width', width.value, width.unit);
|
||||
saveState();
|
||||
});
|
||||
|
||||
// Sync greyed field with imageDefaults when toggle is OFF and no cache
|
||||
watch(() => imageDefaults.width, (val) => {
|
||||
if (!widthEnabled.value && widthCache.value === null) {
|
||||
isUpdatingFromStore = true;
|
||||
width.value = val.value;
|
||||
width.unit = val.unit;
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// --- Toggle ---
|
||||
|
||||
const onSectionClick = () => {
|
||||
if (!widthEnabled.value) onToggleWidth(true);
|
||||
};
|
||||
|
||||
const onToggleWidth = (enabled) => {
|
||||
isUpdatingFromStore = true;
|
||||
widthEnabled.value = enabled;
|
||||
|
||||
if (enabled) {
|
||||
if (widthCache.value) {
|
||||
width.value = widthCache.value.value;
|
||||
width.unit = widthCache.value.unit;
|
||||
widthCache.value = null;
|
||||
}
|
||||
stylesheetStore.updateProperty(selector.value, 'width', width.value, width.unit);
|
||||
} else {
|
||||
widthCache.value = { value: width.value, unit: width.unit };
|
||||
removeWidthProp();
|
||||
width.value = imageDefaults.width.value;
|
||||
width.unit = imageDefaults.width.unit;
|
||||
}
|
||||
|
||||
saveState();
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// --- CSS helpers ---
|
||||
|
||||
const removeWidthProp = () => {
|
||||
if (!selector.value) return;
|
||||
const block = stylesheetStore.extractBlock(selector.value);
|
||||
if (!block || !stylesheetStore.customCss.includes(block)) return;
|
||||
|
||||
const newBlock = block.replace(/[ \t]*width\s*:[^;]*;[ \t]*\n?/g, '');
|
||||
const inner = newBlock.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, '');
|
||||
if (!inner.trim()) {
|
||||
stylesheetStore.replaceInCustomCss(block, '');
|
||||
} else {
|
||||
stylesheetStore.replaceBlock(block, newBlock);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Displayed CSS ---
|
||||
|
||||
const displayedCss = computed(() => {
|
||||
if (!selector.value) return '';
|
||||
const widthVal = `${width.value}${width.unit}`;
|
||||
const defaultVal = `${imageDefaults.width.value}${imageDefaults.width.unit}`;
|
||||
const comment = (!widthEnabled.value && widthVal === defaultVal) ? ' /* hérité de .block-image */' : '';
|
||||
return `${selector.value} {\n width: ${widthVal};${comment}\n}`;
|
||||
});
|
||||
|
||||
const editableCss = computed(() => {
|
||||
return displayedCss.value.replace(/ \/\* hérité de \.block-image \*\//g, '');
|
||||
});
|
||||
|
||||
// --- CSS input (edit mode) ---
|
||||
|
||||
const handleCssInput = (newCss) => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
const match = newCss.match(/width\s*:\s*([\d.]+)(%|px)/i);
|
||||
if (match) {
|
||||
width.value = parseFloat(match[1]);
|
||||
width.unit = match[2];
|
||||
}
|
||||
|
||||
const newBlock = `${selector.value} {\n width: ${width.value}${width.unit};\n}\n`;
|
||||
const oldBlock = stylesheetStore.extractBlock(selector.value) || '';
|
||||
if (oldBlock) {
|
||||
stylesheetStore.replaceInCustomCss(oldBlock, newBlock);
|
||||
} else {
|
||||
stylesheetStore.setCustomCss((stylesheetStore.customCss || '') + '\n' + newBlock);
|
||||
}
|
||||
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// --- State persistence ---
|
||||
|
||||
const saveState = () => {
|
||||
if (!selector.value) return;
|
||||
elementStates.set(selector.value, {
|
||||
toggle: widthEnabled.value,
|
||||
cache: widthCache.value ? { ...widthCache.value } : null,
|
||||
value: { value: width.value, unit: width.unit },
|
||||
});
|
||||
};
|
||||
|
||||
// --- Open ---
|
||||
|
||||
const handleImageClick = (figure, event) => {
|
||||
const uniqueClass = Array.from(figure.classList).find(cls => cls.startsWith('block-image--'));
|
||||
if (!uniqueClass) return;
|
||||
|
||||
isUpdatingFromStore = true;
|
||||
selector.value = `.${uniqueClass}`;
|
||||
|
||||
const stored = elementStates.get(selector.value);
|
||||
if (stored) {
|
||||
widthEnabled.value = stored.toggle;
|
||||
widthCache.value = stored.cache ? { ...stored.cache } : null;
|
||||
if (stored.toggle) {
|
||||
width.value = stored.value.value;
|
||||
width.unit = stored.value.unit;
|
||||
} else if (stored.cache) {
|
||||
width.value = stored.cache.value;
|
||||
width.unit = stored.cache.unit;
|
||||
} else {
|
||||
width.value = imageDefaults.width.value;
|
||||
width.unit = imageDefaults.width.unit;
|
||||
}
|
||||
} else {
|
||||
widthEnabled.value = false;
|
||||
widthCache.value = null;
|
||||
|
||||
const cssVal = stylesheetStore.extractValue(selector.value, 'width');
|
||||
if (cssVal && cssVal.value !== undefined) {
|
||||
width.value = cssVal.value;
|
||||
width.unit = cssVal.unit;
|
||||
widthEnabled.value = true;
|
||||
} else {
|
||||
width.value = imageDefaults.width.value;
|
||||
width.unit = imageDefaults.width.unit;
|
||||
}
|
||||
}
|
||||
|
||||
basePopup.value.open(event);
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// --- Close ---
|
||||
|
||||
const handleClose = () => {
|
||||
saveState();
|
||||
selector.value = '';
|
||||
if (basePopup.value?.visible) basePopup.value.close();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
defineExpose({ handleImageClick, close: () => basePopup.value?.close(), visible });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-label {
|
||||
background: var(--color-600);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
:key="block.id"
|
||||
:is="getBlockComponent(block.type)"
|
||||
:content="block.content"
|
||||
v-bind="block.type === 'image' ? { blockId: block.id } : {}"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
|
@ -97,6 +98,7 @@
|
|||
:key="block.id"
|
||||
:is="getBlockComponent(block.type)"
|
||||
:content="block.content"
|
||||
v-bind="block.type === 'image' ? { blockId: block.id } : {}"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<figure
|
||||
class="block-image"
|
||||
:class="['block-image', blockId ? `block-image--${blockId.slice(0, 8)}` : '']"
|
||||
:style="figureStyle"
|
||||
>
|
||||
<img
|
||||
|
|
@ -19,16 +19,16 @@ const props = defineProps({
|
|||
content: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
blockId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const figureStyle = computed(() => {
|
||||
const styles = {};
|
||||
|
||||
if (props.content.width && props.content.width !== '100%') {
|
||||
styles.width = props.content.width;
|
||||
}
|
||||
|
||||
if (props.content.position === 'center') {
|
||||
styles.margin = '0 auto';
|
||||
} else if (props.content.position === 'right') {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
<div v-if="activeTab === 'document'" class="tab-panel">
|
||||
<PageSettings />
|
||||
<TextSettings />
|
||||
<ImageSettings />
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'code'" class="tab-panel">
|
||||
|
|
@ -70,6 +71,7 @@
|
|||
import { inject } from 'vue';
|
||||
import PageSettings from './PageSettings.vue';
|
||||
import TextSettings from './TextSettings.vue';
|
||||
import ImageSettings from './ImageSettings.vue';
|
||||
import StylesheetViewer from '../StylesheetViewer.vue';
|
||||
|
||||
const activeTab = inject('activeTab');
|
||||
|
|
|
|||
79
src/components/editor/ImageSettings.vue
Normal file
79
src/components/editor/ImageSettings.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<section class="panel-settings__container" id="settings__container_image" data-color-type="image">
|
||||
|
||||
<div class="settings__header">
|
||||
<div class="icon" v-html="imageIcon"></div>
|
||||
<h2 class="title">Réglage des images par défaut</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Largeur -->
|
||||
<div class="setting__section" data-setting="width">
|
||||
<div class="setting__header">
|
||||
<label class="label-with-tooltip" data-css="width">Largeur</label>
|
||||
</div>
|
||||
<div class="setting__body">
|
||||
<InputWithUnit
|
||||
v-model="width"
|
||||
:units="['%', 'px']"
|
||||
:min="1"
|
||||
:max="width.unit === 'px' ? 2000 : 200"
|
||||
showRange
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||
import imageIcon from '/assets/svg/image.svg?raw';
|
||||
import { IMAGE_DEFAULTS } from '../../utils/defaults';
|
||||
import InputWithUnit from '../ui/InputWithUnit.vue';
|
||||
import { useCssUpdater } from '../../composables/useCssUpdater';
|
||||
import { useCssSync } from '../../composables/useCssSync';
|
||||
import { useDebounce } from '../../composables/useDebounce';
|
||||
import { useImageDefaults } from '../../composables/useImageDefaults';
|
||||
|
||||
const { updateStyle } = useCssUpdater();
|
||||
const { extractNumericValue } = useCssSync();
|
||||
const { debouncedUpdate } = useDebounce(500);
|
||||
const imageDefaults = useImageDefaults();
|
||||
|
||||
// State — initial value from defaults.js (overwritten by syncFromStore)
|
||||
const width = ref({ ...IMAGE_DEFAULTS.width });
|
||||
|
||||
// Start true to block immediate watchers during setup
|
||||
let isUpdatingFromStore = true;
|
||||
|
||||
watch(width, (val) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
imageDefaults.width = { value: val.value, unit: val.unit };
|
||||
debouncedUpdate(() => {
|
||||
updateStyle('.block-image', 'width', `${val.value}${val.unit}`);
|
||||
});
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
const syncFromStore = () => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
if (imageDefaults._initialized) {
|
||||
width.value = { value: imageDefaults.width.value, unit: imageDefaults.width.unit };
|
||||
} else {
|
||||
const widthVal = extractNumericValue('.block-image', 'width', ['%', 'px']);
|
||||
if (widthVal) width.value = widthVal;
|
||||
|
||||
imageDefaults.width = { value: width.value.value, unit: width.value.unit };
|
||||
imageDefaults._initialized = true;
|
||||
}
|
||||
|
||||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncFromStore();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
v-if="visible"
|
||||
class="settings-popup"
|
||||
:style="{ top: position.y + 'px', left: position.x + 'px' }"
|
||||
:data-color-type="colorType || undefined"
|
||||
>
|
||||
<div class="popup-header" @mousedown="startDrag">
|
||||
<div class="header-left">
|
||||
|
|
@ -100,6 +101,7 @@ const props = defineProps({
|
|||
popupWidth: { type: Number, default: 800 },
|
||||
popupHeight: { type: Number, default: 600 },
|
||||
showInheritance: { type: Boolean, default: true },
|
||||
colorType: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'css-input', 'toggle-inheritance']);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { ref } from 'vue';
|
|||
* Composable for managing interactions with pages and elements in the iframe
|
||||
* Handles hover effects, labels, and click events for both pages and content elements
|
||||
*/
|
||||
export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED: page template styling feature */ }) {
|
||||
export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup // DISABLED: page template styling feature */ }) {
|
||||
// DISABLED: page template styling feature
|
||||
// const hoveredPage = ref(null);
|
||||
// const selectedPages = ref([]); // Pages with active border (when popup is open)
|
||||
const hoveredElement = ref(null); // Currently hovered content element
|
||||
const hoveredFigure = ref(null); // Currently hovered block-image figure
|
||||
const selectedElement = ref(null); // Selected element (when popup is open)
|
||||
const selectedFigure = ref(null); // Selected figure (when image popup is open)
|
||||
// const EDGE_THRESHOLD = 30; // px from edge to trigger hover // DISABLED: page template styling feature
|
||||
|
||||
// Text elements that can trigger ElementPopup (excluding containers, images, etc.)
|
||||
|
|
@ -119,6 +121,34 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
}
|
||||
};
|
||||
|
||||
// Create and position image label on hover
|
||||
const createImageLabel = (figure) => {
|
||||
const doc = figure.ownerDocument;
|
||||
const existing = doc.querySelector('.image-hover-label');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const uniqueClass = Array.from(figure.classList).find(cls => cls.startsWith('block-image--'));
|
||||
if (!uniqueClass) return;
|
||||
|
||||
const rect = figure.getBoundingClientRect();
|
||||
const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
|
||||
const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
||||
|
||||
const label = doc.createElement('div');
|
||||
label.className = 'image-hover-label';
|
||||
label.textContent = `.${uniqueClass}`;
|
||||
label.style.top = `${rect.top + scrollTop - 32}px`;
|
||||
label.style.left = `${rect.left + scrollLeft}px`;
|
||||
|
||||
doc.body.appendChild(label);
|
||||
};
|
||||
|
||||
// Remove image label
|
||||
const removeImageLabel = (doc) => {
|
||||
const label = doc.querySelector('.image-hover-label');
|
||||
if (label) label.remove();
|
||||
};
|
||||
|
||||
/* DISABLED: page template styling feature
|
||||
// Create and position page label on hover
|
||||
const createPageLabel = (page) => {
|
||||
|
|
@ -153,6 +183,18 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
};
|
||||
*/
|
||||
|
||||
// Find closest block-image figure ancestor (including self)
|
||||
const getImageFigure = (element) => {
|
||||
let current = element;
|
||||
while (current && current.tagName !== 'BODY') {
|
||||
if (current.tagName === 'FIGURE' && current.classList.contains('block-image')) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Check if element is a content element (or find closest content parent)
|
||||
const getContentElement = (element) => {
|
||||
let current = element;
|
||||
|
|
@ -226,9 +268,23 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
}
|
||||
*/
|
||||
|
||||
// Check for content element hover
|
||||
const contentElement = getContentElement(event.target);
|
||||
// Check for block-image figure hover
|
||||
const imageFigure = getImageFigure(event.target);
|
||||
const doc = event.target.ownerDocument;
|
||||
if (imageFigure !== hoveredFigure.value) {
|
||||
if (hoveredFigure.value && hoveredFigure.value !== selectedFigure.value) {
|
||||
hoveredFigure.value.classList.remove('element-hovered');
|
||||
}
|
||||
removeImageLabel(doc);
|
||||
if (imageFigure && imageFigure !== selectedFigure.value) {
|
||||
imageFigure.classList.add('element-hovered');
|
||||
createImageLabel(imageFigure);
|
||||
}
|
||||
hoveredFigure.value = imageFigure;
|
||||
}
|
||||
|
||||
// Check for content element hover
|
||||
const contentElement = imageFigure ? null : getContentElement(event.target);
|
||||
|
||||
if (contentElement !== hoveredElement.value) {
|
||||
// Remove highlight from previous element (only if not selected)
|
||||
|
|
@ -287,15 +343,45 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
const isInsidePage = element.closest('.pagedjs_page');
|
||||
if (!isInsidePage) {
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
if (elementPopup.value.visible) elementPopup.value.close();
|
||||
imagePopup.value?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicking a block-image figure
|
||||
const imageFigure = getImageFigure(element);
|
||||
if (imageFigure) {
|
||||
clearSelectedElement();
|
||||
if (elementPopup.value.visible) elementPopup.value.close();
|
||||
const doc = event.target.ownerDocument;
|
||||
removeElementLabel(doc);
|
||||
removeImageLabel(doc);
|
||||
if (imagePopup.value?.visible) {
|
||||
// Deselect current figure
|
||||
if (selectedFigure.value) {
|
||||
selectedFigure.value.classList.remove('element-selected');
|
||||
selectedFigure.value = null;
|
||||
}
|
||||
imagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
// Select the figure
|
||||
imageFigure.classList.remove('element-hovered');
|
||||
imageFigure.classList.add('element-selected');
|
||||
hoveredFigure.value = null;
|
||||
selectedFigure.value = imageFigure;
|
||||
imagePopup.value?.handleImageClick(imageFigure, event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close image popup if open when clicking a text element
|
||||
imagePopup.value?.close();
|
||||
|
||||
// Only show ElementPopup for content elements, not divs
|
||||
const contentElement = getContentElement(element);
|
||||
if (!contentElement) {
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
if (elementPopup.value.visible) elementPopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +428,13 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
clearSelectedElement();
|
||||
};
|
||||
|
||||
const handleImagePopupClose = () => {
|
||||
if (selectedFigure.value) {
|
||||
selectedFigure.value.classList.remove('element-selected');
|
||||
selectedFigure.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
// hoveredPage, // DISABLED: page template styling feature
|
||||
|
|
@ -353,6 +446,7 @@ export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED:
|
|||
handleIframeClick,
|
||||
// handlePagePopupClose, // DISABLED: page template styling feature
|
||||
handleElementPopupClose,
|
||||
handleImagePopupClose,
|
||||
// Utilities
|
||||
// clearSelectedPages, // DISABLED: page template styling feature
|
||||
clearSelectedElement,
|
||||
|
|
|
|||
12
src/composables/useImageDefaults.js
Normal file
12
src/composables/useImageDefaults.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { reactive } from 'vue';
|
||||
import { IMAGE_DEFAULTS } from '../utils/defaults';
|
||||
|
||||
// Singleton reactive — ImageSettings writes here, ImagePopup reads when toggle is disabled
|
||||
const defaults = reactive({
|
||||
width: { ...IMAGE_DEFAULTS.width },
|
||||
_initialized: false,
|
||||
});
|
||||
|
||||
export function useImageDefaults() {
|
||||
return defaults;
|
||||
}
|
||||
|
|
@ -39,12 +39,12 @@ export function usePopupPosition(popupWidth, popupHeight) {
|
|||
else {
|
||||
x = cursorX - OFFSET - popupWidth;
|
||||
y = cursorY - OFFSET - popupHeight;
|
||||
|
||||
// Ensure it doesn't go off-screen
|
||||
x = Math.max(10, x);
|
||||
y = Math.max(10, y);
|
||||
}
|
||||
|
||||
// Final clamp — ensure popup is always fully visible
|
||||
x = Math.max(10, Math.min(viewportWidth - popupWidth - 10, x));
|
||||
y = Math.max(10, Math.min(viewportHeight - popupHeight - 10, y));
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as cssComments from '../utils/css-comments';
|
|||
import prettier from 'prettier/standalone';
|
||||
import parserPostcss from 'prettier/plugins/postcss';
|
||||
import { getCsrfToken } from '../utils/kirby-auth';
|
||||
import { PAGE_DEFAULTS, TEXT_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults';
|
||||
import { PAGE_DEFAULTS, TEXT_DEFAULTS, IMAGE_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults';
|
||||
|
||||
export const useStylesheetStore = defineStore('stylesheet', () => {
|
||||
// Base state
|
||||
|
|
@ -259,6 +259,9 @@ export const useStylesheetStore = defineStore('stylesheet', () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Image defaults
|
||||
set('.block-image', 'width', IMAGE_DEFAULTS.width.value, IMAGE_DEFAULTS.width.unit);
|
||||
|
||||
// Inline element defaults (em, i, strong, b, a)
|
||||
for (const [tag, props] of Object.entries(INLINE_DEFAULTS)) {
|
||||
if (props.fontStyle) set(tag, 'font-style', props.fontStyle);
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ export const PARAGRAPH_CLASS_DEFAULTS = Object.freeze({
|
|||
}),
|
||||
});
|
||||
|
||||
export const IMAGE_DEFAULTS = Object.freeze({
|
||||
width: Object.freeze({ value: 100, unit: '%' }),
|
||||
});
|
||||
|
||||
export const INLINE_DEFAULTS = Object.freeze({
|
||||
em: Object.freeze({ fontStyle: 'italic' }),
|
||||
i: Object.freeze({ fontStyle: 'italic' }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue