- migliorata la grafica dell'aggiungi elemento.

This commit is contained in:
Surya Paolo
2025-09-08 20:42:36 +02:00
parent ac84755dbb
commit cb3baf3dbb
13 changed files with 871 additions and 470 deletions

View File

@@ -125,6 +125,7 @@ export const shared_consts = {
IMAGEUPLOAD: 35,
SEPARATOR: 40,
VIDEO: 50,
VIDEO_YOUTUBE: 52,
PAGE: 55,
PAGEINTRO: 58,
CALENDAR: 70,
@@ -1816,66 +1817,82 @@ export const shared_consts = {
{
value: 5,
label: 'Titolo',
icon: 'fas fa-heading',
},
{
value: 8,
label: 'ImgTitolo',
icon: 'fas fa-image',
},
{
value: 10,
label: 'Testo semplice',
icon: 'fas fa-file-alt',
},
{
value: 30,
label: 'Immagine (nomefile)',
icon: 'fas fa-image',
},
{
value: 130,
label: 'MainView',
icon: 'fas fa-eye',
},
{
value: 140,
label: 'Dashboard',
icon: 'fas fa-tachometer-alt',
},
{
value: 145,
label: 'DashGroup',
icon: 'fas fa-users',
},
{
value: 148,
label: 'Lista Movimenti',
icon: 'fas fa-list',
},
{
value: 150,
label: 'SendCoinTo',
icon: 'fas fa-wallet',
},
{
value: 280,
label: 'Tutorial',
icon: 'fas fa-book',
},
{
value: 400,
label: 'Visualizzatore Tabelle',
icon: 'fas fa-table',
},
{
value: 410,
label: 'Qr Code',
icon: 'fas fa-qrcode',
},
{
value: 420,
label: 'Lista Cataloghi',
icon: 'fas fa-list',
},
{
value: 450,
label: 'Raccolte Cataloghi',
icon: 'fas fa-list',
},
{
value: 460,
label: 'Statistiche Pagine',
icon: 'fas fa-chart-pie',
},
{
value: 430, // SEARCHPRODUCT
label: 'Cerca Prodotto',
icon: 'fas fa-search',
},
],
@@ -1883,130 +1900,162 @@ export const shared_consts = {
{
value: 100,
label: 'Check Email',
icon: 'fas fa-envelope',
},
{
value: 120,
label: 'OpenStreetMap',
icon: 'fas fa-globe',
},
{
value: 160,
label: 'Stato Registrati',
icon: 'fas fa-user',
},
{
value: 170,
label: 'CheckIfIsLogged',
icon: 'fas fa-user-lock',
},
{
value: 180,
label: 'Info Versione',
icon: 'fas fa-info-circle',
},
{
value: 190,
label: 'Bottone Condividi',
icon: 'fas fa-share-alt',
},
{
value: 192,
label: 'Bottone Chat Territoriale',
icon: 'fas fa-comments',
},
{
value: 200,
label: 'Presentazione',
icon: 'fas fa-presentation',
},
{
value: 205,
label: 'Attività',
icon: 'fas fa-briefcase',
},
{
value: 210,
label: 'Notifiche in Top',
icon: 'fas fa-bell',
},
{
value: 135,
label: 'Check App Running',
icon: 'fas fa-spinner',
},
{
value: 258,
label: 'Registration',
icon: 'fas fa-user-plus',
},
{
value: 220,
label: 'CHART',
icon: 'fas fa-chart-pie',
},
{
value: 230,
label: 'Check New Version',
icon: 'fas fa-sync-alt',
},
{
value: 240,
label: 'Check Test Version',
icon: 'fas fa-flask',
},
{
value: 250,
label: 'Butt Registrati',
icon: 'fas fa-user-plus',
},
{
value: 255,
label: 'Butt Registrati col Bot',
icon: 'fas fa-user-plus',
},
{
value: 260,
label: 'Butt Login',
icon: 'fas fa-sign-in-alt',
},
{
value: 270,
label: 'Footer',
icon: 'fas fa-copyright',
},
{
value: 280,
label: 'Visu Promo and PDF',
icon: 'fas fa-file-pdf',
},
{
value: 40,
label: 'Separatore',
icon: 'fas fa-minus',
},
{
value: 70,
label: 'Calendario',
icon: 'fas fa-calendar',
},
{
value: 300,
label: 'E-COMMERCE',
icon: 'fas fa-shopping-cart',
},
{
value: 310,
label: 'CATALOGO',
icon: 'fas fa-list',
},
{
value: 315,
label: 'RACCOLTA CATALOGHI',
icon: 'fas fa-list',
},
{
value: 320,
label: 'TOOLS AI',
icon: 'fas fa-robot',
},
{
value: 325,
label: 'CHATBOT',
icon: 'fas fa-robot',
},
{
value: 350,
label: 'MAPPA',
icon: 'fas fa-globe',
},
{
value: 360,
label: 'MAPPAUTENTI',
icon: 'fas fa-globe',
},
{
value: 370,
label: 'MAPPACOMUNI',
icon: 'fas fa-globe',
},
{
value: 380,
label: 'MAPPA GET COORD',
icon: 'fas fa-globe',
},
{
value: 390,
label: 'EDIT ADDRESS BY COORD',
icon: 'fas fa-globe',
},
],
@@ -2024,11 +2073,12 @@ export const shared_consts = {
{
value: 35,
label: 'Immagine',
icon: '',
icon: 'fas fa-image',
},
{
value: 7,
label: 'Scheda (IMG + Testo)',
icon: 'fas fa-id-card',
},
/*
Disattivato perchè attualmente non funziona bene
@@ -2039,26 +2089,37 @@ export const shared_consts = {
{
value: 195,
label: 'Bottone',
icon: 'fas fa-hand-point-right',
},
{
value: 50,
label: 'Video',
icon: 'fas fa-video',
},
{
value: 52,
label: 'Video Youtube',
icon: 'fab fa-youtube',
},
{
value: 55,
label: 'Pagina',
icon: 'fas fa-file-alt',
},
{
value: 58,
label: 'Pagina (solo Intro)',
icon: 'fas fa-file-alt',
},
{
value: 110,
label: "Galleria d'Immagini",
icon: 'fas fa-images',
},
{
value: 6,
label: 'Margine',
icon: 'fas fa-arrows-alt-h',
},
],

View File

@@ -1097,6 +1097,53 @@
</q-input>
</div>
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.VIDEO_YOUTUBE">
<div
v-if="enableEdit"
class="row q-col-gutter-sm"
>
<!-- Link YouTube -->
<div class="col-12">
<q-input
dense
filled
label="Link YouTube"
v-model="myel.container"
@update:model-value="modifElem"
v-on:keyup.enter="saveElem"
:rules="[(v) => !!v || 'Inserisci un link YouTube']"
/>
</div>
<!-- Titolo accessibile (facoltativo) -->
<div class="col-12">
<q-input
dense
filled
label="Titolo (facoltativo)"
v-model="myel.container2"
@update:model-value="modifElem"
v-on:keyup.enter="saveElem"
/>
</div>
<!-- Ratio -->
<div class="col-12 col-sm-6">
<q-input
dense
filled
type="number"
step="any"
min="0.2"
label="Ratio (es. 1.777 per 16/9)"
v-model.number="myel.ratio"
@update:model-value="modifElem"
v-on:keyup.enter="saveElem"
/>
</div>
</div>
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.FOOTER"></div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.PAGE">
<div

View File

@@ -20,6 +20,7 @@ import { shared_consts } from '@src/common/shared_vuejs';
import { LandingFooter } from '@src/components/LandingFooter';
import { CMyActivities } from '@src/components/CMyActivities';
import { CECommerce } from '@src/components/CECommerce';
import { CMyVideoYoutube } from '@src/components/CMyVideoYoutube';
import { CStatMacro } from '@src/components/CStatMacro';
import { CSearchProduct } from '@src/components/CSearchProduct';
import { CPageViewStats } from '@src/components/CPageViewStats';
@@ -117,6 +118,7 @@ export default defineComponent({
CSection,
CRow,
CColumn,
CMyVideoYoutube,
// , //CMapMarker,
},
emits: ['selElemClick'],

View File

@@ -312,7 +312,7 @@
:height="myel.heightimg ? myel.heightimg : undefined"
></q-img>
<q-img
v-else
v-else
src="images/noimg.png"
:fit="myel.fit ? myel.fit : 'contain'"
class="img"
@@ -335,6 +335,26 @@
</q-video>
</div>
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.VIDEO_YOUTUBE">
<CMyVideoYoutube
:url="myelem.container"
:title="myelem.container2 || ''"
:ratio="myelem.ratio || 16 / 9"
:privacyMode="myelem.privacyMode ?? true"
:thumbnailClickToPlay="myelem.thumbnailClickToPlay ?? true"
:autoplay="myelem.autoplay ?? false"
:controls="myelem.controls ?? true"
:mute="myelem.mute ?? false"
:loop="myelem.loop ?? false"
:start="myelem.start || 0"
:end="myelem.end || 0"
:rel="myelem.rel ?? false"
:modestBranding="myelem.modestBranding ?? true"
:playsinline="myelem.playsinline ?? true"
:ccLang="myelem.ccLang || ''"
:ccLoad="myelem.ccLoad ?? false"
/>
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.PAGE">
<div
:class="myel.class + (editOn ? ` clEdit` : ``) + getClass()"

View File

@@ -459,4 +459,43 @@ h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.elementor-btn {
border-radius: 12px;
transition: all 0.2s ease;
&:hover {
background-color: rgba($primary, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
// Migliora il look delle espansioni
.q-expansion-item {
&__content {
padding: 0;
}
}
// Risposta mobile
@media (max-width: 600px) {
.elementor-btn {
q-icon {
size: 32px;
}
.text-caption {
font-size: 0.8em;
}
}
}
.my-stacked-btn .q-btn__label {
white-space: normal;
line-height: 1.4;
font-size: 0.85em;
text-align: center;
}

View File

@@ -31,11 +31,8 @@ import { useProducts } from '@src/store/Products';
export default defineComponent({
name: 'CMyElemAdd',
components: {
},
emits: [
'AddedNewElem',
],
components: {},
emits: ['AddedNewElem'],
props: {
myelem: {
type: Object as PropType<IMyElem>,
@@ -76,6 +73,16 @@ export default defineComponent({
const catalogStore = useCatalogStore();
const router = useRouter();
const sections = computed(() => [
{ label: 'Principali', icon: 'fas fa-eye', items: shared_consts.TypesElem },
{ label: 'Gestione', icon: 'fas fa-cog', items: shared_consts.TypesElemAdmin },
{
label: 'Avanzati',
icon: 'fas fa-star',
items: shared_consts.TypesElemAdminTools,
},
]);
const { setmeta, getsrcbyimg } = MixinMetaTags();
const { setValDb, getValDb } = MixinBase();
@@ -107,7 +114,6 @@ export default defineComponent({
const selectedClasses = ref(<any>[]);
async function addNewElem(elemsel: any, direz: number) {
// Nascondi la visualizzazione di aggiunta (presumo sia una variabile reattiva)
visuadd.value = false;
@@ -148,7 +154,7 @@ export default defineComponent({
let sectionId = '';
let rowId = '';
console.log('sectionId', sectionId, 'rowId', rowId)
console.log('sectionId', sectionId, 'rowId', rowId);
// Aggiungi un nuovo elemento alla sezione o riga usando il metodo preparato
const newrec = await globalStore.prepareAddNewElem(
@@ -157,7 +163,7 @@ export default defineComponent({
t,
myelem,
props.myElemParent,
newtype.value,
newtype.value
);
// Emitti l'evento per la selezione del nuovo elemento
@@ -167,7 +173,6 @@ export default defineComponent({
// emit('updateAll', newrec);
}
return {
tools,
shared_consts,
@@ -191,6 +196,7 @@ export default defineComponent({
Products,
globalStore,
myel,
sections,
};
},
});

View File

@@ -1,107 +1,46 @@
<template>
<div>
<q-card class="">
<q-bar
dense
class="bg-primary text-white"
>
<q-card class="shadow-6 rounded-lg" style="overflow: hidden">
<!-- Barra superiore -->
<q-bar class="bg-primary text-white">
Aggiungi Elemento:
<q-space />
<q-btn
flat
round
color="white"
icon="close"
v-close-popup
></q-btn>
<q-btn flat round icon="close" v-close-popup />
</q-bar>
<div class="q-pa-md row justify-center">
<div style="width: 100%; max-width: 600px">
<q-list
padding
bordered
class="rounded-borders"
>
<!-- Contenuto principale -->
<div class="q-pa-sm row justify-center">
<div style="width: 100%; max-width: 350px">
<q-list padding bordered class="rounded-borders shadow-sm">
<!-- Sezioni generate dinamicamente -->
<q-expansion-item
label="Principali"
icon="fas fa-eye"
dense
dense-toggle
v-for="(sec, i) in sections"
:key="sec.label"
:label="sec.label"
:icon="sec.icon"
:default-opened="i === 0"
expand-separator
default-opened
header-class="text-subtitle1 text-weight-bold"
>
<div class="row q-pa-sm">
<div class="row q-pa-xs" v-if="enableAdd">
<div
v-for="(rec, index) in shared_consts.TypesElem"
:key="index"
class="col-6 q-pa-xs"
v-for="(rec, idx) in sec.items"
:key="idx"
class="col-6"
>
<q-btn
v-if="enableAdd"
flat
no-caps
stack
class="elementor-btn full-width q-py-sm q-px-sm my-stacked-btn"
:icon="rec.icon"
:label="rec.label"
color="primary"
class="full-width uniform-button q-px-sm"
text-color="primary"
@click="
newtype = rec.value;
addNewElem(myel, direzadd);
"
>
</q-btn>
</div>
</div>
</q-expansion-item>
<q-expansion-item
dense
dense-toggle
expand-separator
label="Gestione"
icon="fas fa-cog"
>
<div class="row q-pa-sm">
<div
v-for="(rec, index) in shared_consts.TypesElemAdmin"
:key="index"
class="col-6 q-pa-xs"
>
<q-btn
v-if="enableAdd"
:label="rec.label"
color="primary"
class="full-width uniform-button q-px-sm"
@click="
newtype = rec.value;
addNewElem(myel, direzadd);
"
>
</q-btn>
</div>
</div>
</q-expansion-item>
<q-expansion-item
dense
dense-toggle
expand-separator
label="Avanzati"
icon="fas fa-star"
>
<div class="row q-pa-sm">
<div
v-for="(rec, index) in shared_consts.TypesElemAdminTools"
:key="index"
class="col-6 q-pa-sm"
>
<q-btn
v-if="enableAdd"
:label="rec.label"
color="primary"
class="full-width uniform-button q-px-sm"
@click="
newtype = rec.value;
addNewElem(myel, direzadd);
"
>
</q-btn>
/>
</div>
</div>
</q-expansion-item>

View File

@@ -115,6 +115,66 @@ export default defineComponent({
const onloading = ref(false);
// Blocchi media DRY (img/content/video x 1..3) + fix img3
const mediaBlocks = computed(() => {
if (!rec.value) return [];
const r = rec.value;
return [
{
img: r.img1 || null,
html: r.content || null,
video: r.video1 || null,
ratio: r.ratio1 || null,
},
{
img: r.img2 || null,
html: r.content2 || null,
video: r.video2 || null,
ratio: r.ratio2 || null,
},
{
img: r.img3 || null,
html: r.content3 || null,
video: r.video3 || null,
ratio: r.ratio3 || null,
},
];
});
// Larghezza drawer responsiva (usa mobile/tablet/desktop)
const drawerWidth = computed(() => {
// se hai $q disponibile: const s = $q.screen;
// fallback semplice:
return tools.isMobile() ? 340 : Math.min(mywidthEditor.value || 420, 560);
});
const containerStyle = computed(() => ({
maxWidth: '980px', // comodo per lettura
marginLeft: 0,
marginRight: 0,
}));
const closeEditor = () => {
visuEditor.value = false;
selElem.value = {};
};
const openAdd = (col: any, parent: any) => {
visuadd.value = true;
myElemSel.value = col;
myElemParent.value = parent;
};
const addAtEnd = () => {
visuadd.value = true;
const last =
myelems.value.length > 0 ? myelems.value[myelems.value.length - 1] : null;
myElemSel.value = last;
myElemParent.value = last;
};
const showOrder = ref(false)
const myelems = computed(() => {
if (myidPage.value) return globalStore.getMyElemsByIdPage(myidPage.value);
else if (mypathin.value) return globalStore.getMyElems(mypathin.value);
@@ -376,11 +436,9 @@ export default defineComponent({
idRowToAddDown?: string,
neword?: number
) {
const section = newElem
const section = newElem;
// trova la section “vera” nello store (per sicurezza)
const newRow =
newElem.rows[newElem.rows.length - 1]
const newRow = newElem.rows[newElem.rows.length - 1];
if (!section) return;
if (!Array.isArray(section.rows)) section.rows = [];
@@ -515,6 +573,13 @@ export default defineComponent({
myElemSel,
myElemParent,
getColClasses,
mediaBlocks,
drawerWidth,
containerStyle,
closeEditor,
openAdd,
addAtEnd,
showOrder,
};
},
});

View File

@@ -8,48 +8,45 @@
<q-spinner-tail
color="primary"
size="4em"
>
</q-spinner-tail>
/>
</q-inner-loading>
<div v-if="!onloading">
<!-- Toggle edit solo manager -->
<q-toggle
v-if="tools.isManager()"
v-model="editOn"
dense
color="green"
size="sm"
@update:model-value="changeVisuDrawer(mypathin, editOn)"
icon="fas fa-pencil-alt"
>
</q-toggle>
@update:model-value="changeVisuDrawer(mypathin, editOn)"
/>
<!-- Drawer Editor -->
<q-drawer
v-model="visuEditor"
v-if="selElem && editOn && !tools.isObjectEmpty(selElem)"
show-if-above
:breakpoint="350"
side="right"
:width="tools.isMobile() ? 350 : mywidthEditor"
:breakpoint="420"
:width="drawerWidth"
elevated
style="transition: 'width 0.3s ease'"
:style="{ transition: 'width 0.3s ease' }"
>
<q-bar
dense
class="q-ma-xs bg-primary text-white"
>
<q-toolbar-title> Editor </q-toolbar-title>
<q-toolbar-title>Editor</q-toolbar-title>
<q-btn
flat
round
size="md"
color="white"
icon="close"
@click="
visuEditor = false;
selElem = {};
"
></q-btn>
@click="closeEditor"
/>
</q-bar>
<CMyEditElem
@@ -61,365 +58,340 @@
@deleteElem="deleteElem"
@toggleSize="toggleSize"
@dupPage="duplicatePage"
@expPage="showexportPage = !showexportPage"
@impPage="showimportPage = !showimportPage"
>
</CMyEditElem>
@expPage="showexportPage = true"
@impPage="showimportPage = true"
/>
</q-drawer>
<!-- Contenuto pagina -->
<div
:class="{ 'q-gutter-xs': !hideHeader }"
:style="[
{
'margin-left': hideHeader ? 0 : 1 + 'px',
'margin-right': hideHeader ? 0 : 1 + 'px',
},
]"
:class="[{ 'q-gutter-xs': !hideHeader }, 'q-mx-auto', 'q-px-sm', 'q-pb-lg']"
:style="containerStyle"
>
<div
v-if="!!rec.img1"
class="text-center"
<!-- Media/Content blocks (1..3) -->
<section
v-for="(blk, i) in mediaBlocks"
:key="`mblk-${i}`"
class="q-mb-md"
>
<q-img
:src="`` + rec.img1"
class="img"
></q-img>
</div>
<div
v-if="blk.img"
class="text-center q-mb-sm"
>
<q-img
:src="blk.img"
class="page-img"
:ratio="16 / 9"
loading="lazy"
/>
</div>
<div
v-if="blk.html"
v-html="blk.html"
class="q-mb-sm content-html"
></div>
<q-video
v-if="blk.video"
:src="blk.video"
:ratio="blk.ratio || 16 / 9"
class="q-mb-md"
/>
</section>
<!-- Contenuti extra HTML -->
<div
v-if="!!rec.content"
v-html="rec.content"
></div>
<q-video
v-if="!!rec.video1"
:src="rec.video1"
:ratio="rec.ratio1"
>
</q-video>
<div
v-if="!!rec.img2"
class="text-center"
>
<q-img
:src="`` + rec.img2"
class="img"
></q-img>
</div>
<div
v-if="!!rec.content2"
v-html="rec.content2"
></div>
<q-video
v-if="!!rec.video2"
:src="rec.video2"
:ratio="rec.ratio2"
></q-video>
<div
v-if="!!rec.img3"
class="text-center"
>
<q-img
:src="`` + rec.img2"
class="img"
></q-img>
</div>
<div
v-if="!!rec.content3"
v-html="rec.content3"
></div>
<q-video
v-if="!!rec.video3"
:src="rec.video3"
:ratio="rec.ratio3"
></q-video>
<div
v-if="!!rec.content4"
v-if="rec.content4"
v-html="rec.content4"
class="q-mb-md content-html"
></div>
<!-- Lista elementi -->
<div
v-for="myelem in myelems"
:key="myelem._id"
class="q-mb-lg"
>
<div>
<transition
:duration="1000"
appear
>
<div>
<CTitleBanner
v-if="(myelem.active || editOn) && !!rec.path && myelem.titleBanner"
:class="`q-pa-xs`"
:title="myelem.titleBanner"
bgcolor="bg-primary"
:clcolor="myelem.color ? `` : `text-white`"
:mystyle="myelem.color ? `color: ${myelem.color} !important;` : ``"
:myclass="myelem.classBanner"
:canopen="true"
>
</CTitleBanner>
<transition
appear
:duration="300"
enter-active-class="animated fadeInUp"
>
<div>
<CTitleBanner
v-if="(myelem.active || editOn) && !!rec.path && myelem.titleBanner"
class="q-pa-xs"
:title="myelem.titleBanner"
bgcolor="bg-primary"
:clcolor="myelem.color ? '' : 'text-white'"
:mystyle="myelem.color ? `color: ${myelem.color} !important;` : ''"
:myclass="myelem.classBanner"
:canopen="true"
/>
<div
v-if="showOrder"
class="text-caption text-grey q-mb-xs"
>
order: {{ myelem.order }}
</div>
<!-- Sezione -->
<div v-if="myelem.type === shared_consts.ELEMTYPE.SECTION">
<!-- Sezione -->
<div v-if="myelem.type === shared_consts.ELEMTYPE.SECTION">
<div
v-if="editOn"
class="text-center text-caption q-mb-sm"
>
SEZIONE
</div>
<CMyElem
:myelem="myelem"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="rec.path || ''"
:selElem="selElem"
@selElemClick="selElemClick"
>
<!-- Righe della sezione -->
<div
v-if="editOn"
class="text-center"
v-for="(row, indriga) in myelem.rows"
:key="row._id"
class="q-mb-md"
>
<div v-if="editOn">SEZIONE:</div>
</div>
<CMyElem
:myelem="myelem"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="!!rec.path ? rec.path : ''"
:selElem="selElem"
@selElemClick="selElemClick"
>
<!-- Rendering righe dentro la sezione -->
<div
v-for="(row, indriga) in myelem.rows"
:key="row._id"
class="row-container"
>
<div
v-if="editOn"
class="text-center q-mb-md"
>
<q-btn
v-if="editOn"
dense
rounded
label="Riga"
size="sm"
color="positive"
icon="add"
@click="
addNewElemSectRow(
myelem.order + 1,
myelem,
shared_consts.ELEMTYPE.ROW,
row._id
)
"
>
<q-tooltip> Aggiungi Riga </q-tooltip>
</q-btn>
</div>
<div v-if="row.type === shared_consts.ELEMTYPE.ROW">
<div
v-if="editOn"
class="text-center"
>
<div v-if="editOn">RIGA {{ indriga + 1 }}:</div>
</div>
<CMyElem
:myelem="row"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="!!rec.path ? rec.path : ''"
:selElem="selElem"
@selElemClick="selElemClick"
>
<!-- Rendering colonne dentro la riga -->
<div class="row q-col-gutter-md items-stretch">
<template
v-for="(col, index) in row.columns"
:key="col._id"
>
<div
v-if="col.type === shared_consts.ELEMTYPE.COLUMN"
:class="getColClasses(col, row, index)"
>
<div
:style="editOn ? `border: 2px dashed #1976d2` : ``"
>
<div
v-if="editOn"
class="text-center"
>
Colonna {{ index + 1 }}:
</div>
<div
v-for="el in col.elems"
:key="el._id"
>
<CMyElem
:myelem="el"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="!!rec.path ? rec.path : ''"
:selElem="selElem"
@selElemClick="selElemClick"
/>
<div class="text-center q-mb-md">
<q-btn
v-if="editOn"
dense
rounded
size="sm"
color="negative"
icon="delete"
@click="deleteElemento(el)"
>
<q-tooltip> Elimina Elemento </q-tooltip>
</q-btn>
</div>
</div>
</div>
<div class="text-center q-mb-md">
<q-btn
v-if="editOn"
dense
rounded
size="sm"
color="positive"
icon="add"
@click="
visuadd = true;
myElemSel = col;
myElemParent = myelem;
"
>
<q-tooltip> Aggiungi Elemento </q-tooltip>
</q-btn>
</div>
<div class="text-center q-mb-md">
<q-btn
v-if="editOn"
dense
rounded
size="sm"
label="Colonna"
color="negative"
icon="delete"
@click="deleteCol(col)"
>
<q-tooltip> Elimina Colonna </q-tooltip>
</q-btn>
</div>
</div>
</template>
</div>
</CMyElem>
<div
v-if="editOn"
class="text-center q-mb-md"
>
<q-btn
v-if="editOn"
dense
rounded
size="sm"
label="Colonna"
color="primary"
icon="add"
@click="
addNewElemSectRow(
row.order + 1,
row,
shared_consts.ELEMTYPE.COLUMN
)
"
>
<q-tooltip> Aggiungi Colonna </q-tooltip>
</q-btn>
</div>
</div>
<div class="text-center q-mb-md">
<q-btn
v-if="editOn"
dense
rounded
size="sm"
label="Riga"
color="negative"
icon="delete"
@click="deleteRow(row)"
>
<q-tooltip> Elimina Riga </q-tooltip>
</q-btn>
</div>
</div>
<div
v-if="editOn"
class="text-center q-mb-md"
class="text-center q-mb-sm"
>
<q-btn
v-if="editOn"
dense
rounded
label="Riga"
size="sm"
color="positive"
icon="add"
label="Riga"
@click="
addNewElemSectRow(
myelem.order + 1,
myelem,
shared_consts.ELEMTYPE.ROW
shared_consts.ELEMTYPE.ROW,
row._id
)
"
>
<q-tooltip> Aggiungi Riga </q-tooltip>
<q-tooltip>Aggiungi Riga</q-tooltip>
</q-btn>
</div>
</CMyElem>
</div>
<!-- Elementi senza Sezione (retrocompatibilità) -->
<div v-if="myelem.type !== shared_consts.ELEMTYPE.SECTION">
<CMyElem
:myelem="myelem"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="!!rec.path ? rec.path : ''"
:selElem="selElem"
@selElemClick="selElemClick"
/>
</div>
<div v-if="row.type === shared_consts.ELEMTYPE.ROW">
<div
v-if="editOn"
class="text-center text-caption q-mb-xs"
>
RIGA {{ indriga + 1 }}
</div>
<div class="text-center q-mb-md">
<q-btn
<CMyElem
:myelem="row"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="rec.path || ''"
:selElem="selElem"
@selElemClick="selElemClick"
>
<!-- Colonne della riga -->
<div class="row q-col-gutter-md items-stretch">
<template
v-for="(col, index) in row.columns"
:key="col._id"
>
<div
v-if="col.type === shared_consts.ELEMTYPE.COLUMN"
:class="getColClasses(col, row, index)"
>
<div
:class="[
{ 'editor-border': editOn },
'q-pa-xs q-mb-sm',
]"
>
<div
v-if="editOn"
class="text-center text-caption q-mb-xs"
>
Colonna {{ index + 1 }}
</div>
<div
v-for="el in col.elems"
:key="el._id"
class="q-mb-sm"
>
<CMyElem
:myelem="el"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="rec.path || ''"
:selElem="selElem"
@selElemClick="selElemClick"
/>
<div
v-if="editOn"
class="text-center q-mt-xs"
>
<q-btn
dense
rounded
size="sm"
color="negative"
icon="delete"
@click="deleteElemento(el)"
>
<q-tooltip>Elimina Elemento</q-tooltip>
</q-btn>
</div>
</div>
</div>
<!-- Azioni colonna -->
<div
v-if="editOn"
class="row justify-center q-gutter-sm q-mb-sm"
>
<q-btn
dense
rounded
size="sm"
color="positive"
icon="add"
@click="openAdd(col, myelem)"
>
<q-tooltip>Aggiungi Elemento</q-tooltip>
</q-btn>
<q-btn
dense
rounded
size="sm"
color="negative"
icon="delete"
label="Colonna"
@click="deleteCol(col)"
>
<q-tooltip>Elimina Colonna</q-tooltip>
</q-btn>
</div>
</div>
</template>
</div>
</CMyElem>
<!-- Aggiungi colonna -->
<div
v-if="editOn"
class="text-center q-mb-sm"
>
<q-btn
dense
rounded
size="sm"
color="primary"
icon="add"
label="Colonna"
@click="
addNewElemSectRow(
row.order + 1,
row,
shared_consts.ELEMTYPE.COLUMN
)
"
>
<q-tooltip>Aggiungi Colonna</q-tooltip>
</q-btn>
</div>
</div>
<!-- Elimina riga -->
<div
v-if="editOn"
class="text-center"
>
<q-btn
dense
rounded
size="sm"
color="negative"
icon="delete"
label="Riga"
@click="deleteRow(row)"
>
<q-tooltip>Elimina Riga</q-tooltip>
</q-btn>
</div>
</div>
<!-- Aggiungi riga -->
<div
v-if="editOn"
dense
rounded
size="sm"
color="positive"
icon="add"
@click="
visuadd = true;
myElemSel =
myelems.length > 0 ? myelems[myelems.length - 1] : null;
myElemParent =
myelems.length > 0 ? myelems[myelems.length - 1] : null;
"
class="text-center q-mt-sm"
>
<q-tooltip> Aggiungi Elemento </q-tooltip>
</q-btn>
</div>
<q-btn
dense
rounded
size="sm"
color="positive"
icon="add"
label="Riga"
@click="
addNewElemSectRow(
myelem.order + 1,
myelem,
shared_consts.ELEMTYPE.ROW
)
"
>
<q-tooltip>Aggiungi Riga</q-tooltip>
</q-btn>
</div>
</CMyElem>
</div>
</transition>
</div>
<!-- Elementi fuori sezione (retrocompatibilità) -->
<div v-else>
<CMyElem
:myelem="myelem"
:idPage="rec._id"
:editOn="editOn"
:addOn="addOn"
:path="rec.path || ''"
:selElem="selElem"
@selElemClick="selElemClick"
/>
</div>
<!-- Aggiungi elemento al fondo lista -->
<div
v-if="editOn"
class="text-center q-mt-sm"
>
<q-btn
dense
rounded
size="sm"
color="positive"
icon="add"
@click="addAtEnd()"
>
<q-tooltip>Aggiungi Elemento</q-tooltip>
</q-btn>
</div>
</div>
</transition>
</div>
<div v-if="myelems.length === 0">
<!-- Stato vuoto -->
<div v-if="myelems.length === 0 && editOn">
<CMyElem
v-if="editOn && !!rec.path"
:myelem="myelemVoid"
:editOn="editOn"
:addOn="addOn"
@@ -427,13 +399,15 @@
:selElem="selElem"
:path="rec.path"
@selElemClick="selElemClick"
>
</CMyElem>
/>
</div>
</div>
<LandingFooter v-if="rec.showFooter"></LandingFooter>
<LandingFooter v-if="rec.showFooter" />
</div>
</div>
<!-- Header di fallback -->
<div v-else>
<div v-if="!!title">
<CTitle
@@ -442,81 +416,69 @@
:headtitle="title"
:sizes="sizes"
:styleadd="styleadd"
></CTitle>
<div v-if="!imgbackground">
/>
<div v-else-if="img">
<CImgTitle
v-if="img"
:src="img"
:title="title"
>
</CImgTitle>
/>
</div>
<slot></slot>
<slot />
<div v-if="!nofooter"></div>
</div>
</div>
<!-- Dialog Export/Import -->
<q-dialog v-model="showexportPage">
<q-card class="dialog_card">
<q-toolbar class="bg-primary text-white">
<q-toolbar-title> Esporta Pagina </q-toolbar-title>
<q-toolbar-title>Esporta Pagina</q-toolbar-title>
<q-btn
flat
round
color="white"
icon="close"
v-close-popup
></q-btn>
/>
</q-toolbar>
<q-card-section class="q-pa-xs inset-shadow">
<br />
<CExportImportPage
:idPage="rec._id"
:nomefileprop="`esporta_${rec.path}.json`"
:esporta="true"
>
</CExportImportPage>
<br />
/>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="showimportPage">
<q-card class="dialog_card">
<q-toolbar class="bg-primary text-white">
<q-toolbar-title> Esporta Pagina </q-toolbar-title>
<q-toolbar-title>Importa Pagina</q-toolbar-title>
<q-btn
flat
round
color="white"
icon="close"
v-close-popup
></q-btn>
/>
</q-toolbar>
<q-card-section class="q-pa-xs inset-shadow">
<br />
<CExportImportPage
:idPage="rec._id"
:nomefileprop="`esporta_${rec.path}.json`"
>
</CExportImportPage>
<br />
/>
</q-card-section>
</q-card>
</q-dialog>
<div>
<q-dialog
v-model="visuadd"
style="
width: 600px;
max-width: 100%;
position: fixed;
left: 0;
top: 0;
height: 100%;
"
transition-show="slide-up"
transition-hide="slide-down"
>
<!-- Dialog Add Element -->
<q-dialog
v-model="visuadd"
transition-show="slide-up"
transition-hide="slide-down"
>
<div class="full-height-dialog">
<CMyElemAdd
v-if="visuadd"
:myelem="myElemSel"
@@ -527,10 +489,9 @@
:addonlyinMem="true"
@AddedNewElem="AddedNewElem"
@close="visuadd = false"
>
</CMyElemAdd>
</q-dialog>
</div>
/>
</div>
</q-dialog>
</div>
</template>

View File

@@ -0,0 +1,72 @@
.cmy-yt {
width: 100%;
}
.cmy-yt__error {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 8px;
background: rgba(255, 171, 0, 0.12);
color: #7a4f01;
}
.cmy-yt__frame-wrapper {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
}
.cmy-yt__iframe {
width: 100%;
height: 100%;
display: block;
}
.cmy-yt__thumb-wrapper {
position: relative;
}
.cmy-yt__thumb-img {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
}
.cmy-yt__overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: linear-gradient(0deg, rgba(0,0,0,0.35), rgba(0,0,0,0.15));
}
.cmy-yt__play-btn {
appearance: none;
border: none;
border-radius: 999px;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.92);
color: #111;
cursor: pointer;
transform: scale(1);
transition: transform .15s ease, box-shadow .15s ease, background .2s ease;
box-shadow: 0 6px 20px rgba(0,0,0,.18);
display: inline-flex;
align-items: center;
justify-content: center;
}
.cmy-yt__play-btn:hover,
.cmy-yt__play-btn:focus-visible {
transform: scale(1.05);
box-shadow: 0 10px 28px rgba(0,0,0,.22);
background: #fff;
}
@media (max-width: 599px) {
.cmy-yt__frame-wrapper,
.cmy-yt__thumb-img {
border-radius: 10px;
}
}

View File

@@ -0,0 +1,134 @@
import { defineComponent, computed, ref, watch } from 'vue';
type Nullable<T> = T | null | undefined;
function extractYouTubeId(url: string): string | null {
if (!url) return null;
// URL normalizzati, rimuovi spazi
const u = url.trim();
// youtu.be/<id>
const short = u.match(/^https?:\/\/(?:www\.)?youtu\.be\/([A-Za-z0-9_-]{11})/i);
if (short?.[1]) return short[1];
// youtube.com/watch?v=<id> (&…)
const watchMatch = u.match(/[?&]v=([A-Za-z0-9_-]{11})/i);
if (watchMatch?.[1]) return watchMatch[1];
// youtube.com/embed/<id>
const embed = u.match(/\/embed\/([A-Za-z0-9_-]{11})/i);
if (embed?.[1]) return embed[1];
// shorts
const shorts = u.match(/\/shorts\/([A-Za-z0-9_-]{11})/i);
if (shorts?.[1]) return shorts[1];
// fallback debole: sequenza di 11 char nel path o query
const loose = u.match(/([A-Za-z0-9_-]{11})/);
return loose?.[1] ?? null;
}
export default defineComponent({
name: 'CMyVideoYoutube',
props: {
/** Link completo YouTube (watch, youtu.be, embed, shorts) */
url: { type: String, required: true },
/** Rapporto d'aspetto (es. 16/9) */
ratio: { type: Number, default: 16 / 9 },
/** Modalità privacy (usa youtube-nocookie.com) */
privacyMode: { type: Boolean, default: true },
/** Caricamento lazy dell'iframe */
lazy: { type: Boolean, default: true },
/** Mostra thumbnail e avvia iframe solo al click */
thumbnailClickToPlay: { type: Boolean, default: true },
/** Titolo accessibile (fallback al titolo standard) */
title: { type: String, default: '' },
// ---- Parametri player utili ----
autoplay: { type: Boolean, default: false },
controls: { type: Boolean, default: true },
mute: { type: Boolean, default: false },
loop: { type: Boolean, default: false },
start: { type: Number, default: 0 },
end: { type: Number, default: 0 }, // 0 = nessun end
rel: { type: Boolean, default: false }, // video correlati (false = solo stesso canale)
modestBranding: { type: Boolean, default: true },
playsinline: { type: Boolean, default: true },
/** Lingua sottotitoli ("it", "en", ...) */
ccLang: { type: String, default: '' },
/** Forza sottotitoli abilitati */
ccLoad: { type: Boolean, default: false }
},
emits: ['started'],
setup(props, { emit }) {
const isPlaying = ref(false);
const videoId = computed(() => extractYouTubeId(props.url));
const computedTitle = computed(() => {
if (props.title) return props.title;
return 'Video YouTube';
});
const baseDomain = computed(() =>
props.privacyMode ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com'
);
const thumbUrl = computed(() => {
if (!videoId.value) return '';
// hqdefault.jpg è un buon compromesso tra qualità e peso
return `https://i.ytimg.com/vi/${videoId.value}/hqdefault.jpg`;
});
const embedUrl = computed(() => {
if (!videoId.value) return '';
const params = new URLSearchParams();
params.set('autoplay', (isPlaying.value || props.autoplay) ? '1' : '0');
params.set('controls', props.controls ? '1' : '0');
params.set('mute', props.mute ? '1' : '0');
params.set('modestbranding', props.modestBranding ? '1' : '0');
params.set('playsinline', props.playsinline ? '1' : '0');
params.set('rel', props.rel ? '1' : '0');
if (props.start > 0) params.set('start', String(props.start));
if (props.end > 0) params.set('end', String(props.end));
if (props.ccLang) params.set('cc_lang_pref', props.ccLang);
if (props.ccLoad) params.set('cc_load_policy', '1');
// Loop su singolo video: serve anche playlist=id
if (props.loop) {
params.set('loop', '1');
params.set('playlist', videoId.value);
}
const path = `/embed/${videoId.value}`;
return `${baseDomain.value}${path}?${params.toString()}`;
});
function startPlayback() {
isPlaying.value = true;
emit('started');
}
// Se cambia URL, resetta stato play
watch(() => props.url, () => {
isPlaying.value = false;
});
return {
videoId,
computedTitle,
embedUrl,
thumbUrl,
isPlaying,
startPlayback
};
}
});

View File

@@ -0,0 +1,54 @@
<template>
<div class="cmy-yt">
<!-- Stato errore URL non valido -->
<div v-if="!videoId" class="cmy-yt__error">
<q-icon name="warning" class="q-mr-sm" />
Link YouTube non valido.
<div class="text-caption text-grey-7 q-mt-xs">
Esempi accettati: https://youtu.be/ID, https://www.youtube.com/watch?v=ID
</div>
</div>
<!-- Modalità thumbnail -> click per avviare -->
<div v-else-if="thumbnailClickToPlay && !isPlaying" class="cmy-yt__thumb-wrapper">
<q-responsive :ratio="ratio">
<q-img
:src="thumbUrl"
:alt="computedTitle"
class="cmy-yt__thumb-img"
spinner-color="primary"
loading="lazy"
>
<div class="cmy-yt__overlay">
<button
class="cmy-yt__play-btn"
type="button"
:aria-label="`Riproduci video: ${computedTitle}`"
@click="startPlayback"
>
<q-icon name="play_arrow" size="40px" />
</button>
</div>
</q-img>
</q-responsive>
</div>
<!-- Iframe diretto -->
<q-responsive v-else :ratio="ratio" class="cmy-yt__frame-wrapper">
<iframe
class="cmy-yt__iframe"
:title="computedTitle"
:src="embedUrl"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
:loading="lazy ? 'lazy' : 'eager'"
></iframe>
</q-responsive>
</div>
</template>
<script lang="ts" src="./CMyVideoYoutube.ts"></script>
<style lang="scss" scoped>
@import './CMyVideoYoutube.scss';
</style>

View File

@@ -0,0 +1 @@
export {default as CMyVideoYoutube} from './CMyVideoYoutube.vue'