diff --git a/public/newsletter.php b/public/newsletter.php index c059e45..ed13771 100644 --- a/public/newsletter.php +++ b/public/newsletter.php @@ -61,23 +61,105 @@ $statusLabel = [ @@ -101,6 +183,16 @@ $statusLabel = [ +
+ +
+
+ +
+
+
@@ -136,7 +228,9 @@ $statusLabel = [ - Nessuna newsletter. Creane una nuova o importane una da The Newsletter. + + Nessuna newsletter. Creane una nuova o importane una da The Newsletter. + body.append(k, v)); - const r = await fetch(API, { method: 'POST', body, credentials: 'same-origin' }); + const r = await fetch(API, { + method: 'POST', + body, + credentials: 'same-origin' + }); let j = null; - try { j = await r.json(); } catch (e) { } + try { + j = await r.json(); + } catch (e) {} if (!j || !j.success) throw new Error((j && j.message) || 'Errore del server.'); return j; } function esc(s) { - return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + return String(s ?? '').replace(/[&<>"']/g, c => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } [c])); } document.getElementById('btnNew').addEventListener('click', async () => { @@ -212,16 +318,24 @@ $statusLabel = [ }); if (!r.isConfirmed) return; try { - const j = await api('create', { title: r.value }); + const j = await api('create', { + title: r.value + }); location.href = 'newsletter_editor.php?id=' + j.id; - } catch (e) { Swal.fire('Errore', e.message, 'error'); } + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } }); document.querySelectorAll('.js-dup').forEach(b => b.addEventListener('click', async () => { try { - const j = await api('duplicate', { id: b.dataset.id }); + const j = await api('duplicate', { + id: b.dataset.id + }); location.href = 'newsletter_editor.php?id=' + j.id; - } catch (e) { Swal.fire('Errore', e.message, 'error'); } + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } })); document.querySelectorAll('.js-del').forEach(b => b.addEventListener('click', async () => { @@ -235,25 +349,45 @@ $statusLabel = [ cancelButtonText: 'Annulla' }); if (!r.isConfirmed) return; - try { await api('delete', { id: b.dataset.id }); location.reload(); } catch (e) { Swal.fire('Errore', e.message, 'error'); } + try { + await api('delete', { + id: b.dataset.id + }); + location.reload(); + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } })); document.getElementById('btnImport').addEventListener('click', async () => { - Swal.fire({ title: 'Carico le newsletter…', didOpen: () => Swal.showLoading() }); + Swal.fire({ + title: 'Carico le newsletter…', + didOpen: () => Swal.showLoading() + }); let list; try { - const r = await fetch(API + '?action=list_wp', { credentials: 'same-origin' }); + const r = await fetch(API + '?action=list_wp', { + credentials: 'same-origin' + }); const j = await r.json(); if (!j.success) throw new Error(j.message); list = j.emails; - } catch (e) { return Swal.fire('Errore', e.message || 'Impossibile leggere The Newsletter.', 'error'); } + } catch (e) { + return Swal.fire('Errore', e.message || 'Impossibile leggere The Newsletter.', 'error'); + } - const labels = { new: 'bozza', sending: 'in invio', paused: 'in pausa', sent: 'inviata', error: 'errore' }; + const labels = { + new: 'bozza', + sending: 'in invio', + paused: 'in pausa', + sent: 'inviata', + error: 'errore' + }; const html = '
' + list.map(m => - '
' + esc(m.subject || '(senza oggetto)') + '
' + - 'n. ' + m.id + ' · ' + esc(labels[m.status] || m.status) + ' · ' + esc((m.created || '').slice(0, 10)) + '
' + - '
' - ).join('') + '
' + + '
' + esc(m.subject || '(senza oggetto)') + '
' + + 'n. ' + m.id + ' · ' + esc(labels[m.status] || m.status) + ' · ' + esc((m.created || '').slice(0, 10)) + '
' + + '
' + ).join('') + '
' + '

Le bozze non ancora inviate restano collegate: "Pubblica" aggiornerà quella stessa newsletter.

'; Swal.fire({ @@ -267,9 +401,13 @@ $statusLabel = [ b.disabled = true; b.textContent = '…'; try { - const j = await api('import_wp', { wp_id: b.dataset.id }); + const j = await api('import_wp', { + wp_id: b.dataset.id + }); location.href = 'newsletter_editor.php?id=' + j.id; - } catch (e) { Swal.fire('Errore', e.message, 'error'); } + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } })); } }); @@ -277,4 +415,4 @@ $statusLabel = [ - + \ No newline at end of file diff --git a/public/newsletter_editor.php b/public/newsletter_editor.php index f2bb2b9..fad3c2f 100644 --- a/public/newsletter_editor.php +++ b/public/newsletter_editor.php @@ -55,77 +55,389 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ @@ -150,6 +462,16 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_
+
+ +
+
+ +
+
+
@@ -268,130 +590,477 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ const LS_KEY = 'yb_nl_draft_' + DRAFT_ID; const TYPES = { - header: { label: 'Logo', icon: 'fa-crown' }, - heading: { label: 'Titolo', icon: 'fa-heading' }, - text: { label: 'Testo', icon: 'fa-align-left' }, - image: { label: 'Immagine', icon: 'fa-image' }, - button: { label: 'Pulsante', icon: 'fa-hand-pointer' }, - columns: { label: 'Immagine + testo', icon: 'fa-table-columns' }, - separator: { label: 'Separatore', icon: 'fa-minus' }, - social: { label: 'Social', icon: 'fa-share-nodes' }, - footer: { label: 'Piè di pagina', icon: 'fa-shoe-prints' }, - raw: { label: 'Blocco WordPress', icon: 'fa-lock' } + header: { + label: 'Logo', + icon: 'fa-crown' + }, + heading: { + label: 'Titolo', + icon: 'fa-heading' + }, + text: { + label: 'Testo', + icon: 'fa-align-left' + }, + image: { + label: 'Immagine', + icon: 'fa-image' + }, + button: { + label: 'Pulsante', + icon: 'fa-hand-pointer' + }, + columns: { + label: 'Immagine + testo', + icon: 'fa-table-columns' + }, + separator: { + label: 'Separatore', + icon: 'fa-minus' + }, + social: { + label: 'Social', + icon: 'fa-share-nodes' + }, + footer: { + label: 'Piè di pagina', + icon: 'fa-shoe-prints' + }, + raw: { + label: 'Blocco WordPress', + icon: 'fa-lock' + } }; - const PAD = [ - { k: 'pad_top', label: 'Spazio sopra', type: 'number', min: 0, max: 120 }, - { k: 'pad_bottom', label: 'Spazio sotto', type: 'number', min: 0, max: 120 } + const PAD = [{ + k: 'pad_top', + label: 'Spazio sopra', + type: 'number', + min: 0, + max: 120 + }, + { + k: 'pad_bottom', + label: 'Spazio sotto', + type: 'number', + min: 0, + max: 120 + } ]; const FIELDS = { - header: [ - { k: 'logo_url', label: 'Logo', type: 'image' }, - { row: [{ k: 'logo_width', label: 'Larghezza (px)', type: 'number', min: 20, max: 600 }, { k: 'link', label: 'Link al clic', type: 'url' }] }, - { row: PAD } + header: [{ + k: 'logo_url', + label: 'Logo', + type: 'image' + }, + { + row: [{ + k: 'logo_width', + label: 'Larghezza (px)', + type: 'number', + min: 20, + max: 600 + }, { + k: 'link', + label: 'Link al clic', + type: 'url' + }] + }, + { + row: PAD + } ], - heading: [ - { k: 'text', label: 'Titolo', type: 'textarea', rows: 2 }, - { row: [{ k: 'align', label: 'Allineamento', type: 'select', options: [['left', 'Sinistra'], ['center', 'Centro'], ['right', 'Destra']] }, { k: 'font_size', label: 'Dimensione (vuoto = stile)', type: 'number', min: 14, max: 60 }] }, - { row: [{ k: 'color', label: 'Colore', type: 'color' }, { k: 'background', label: 'Sfondo blocco', type: 'color' }] }, - { row: PAD } + heading: [{ + k: 'text', + label: 'Titolo', + type: 'textarea', + rows: 2 + }, + { + row: [{ + k: 'align', + label: 'Allineamento', + type: 'select', + options: [ + ['left', 'Sinistra'], + ['center', 'Centro'], + ['right', 'Destra'] + ] + }, { + k: 'font_size', + label: 'Dimensione (vuoto = stile)', + type: 'number', + min: 14, + max: 60 + }] + }, + { + row: [{ + k: 'color', + label: 'Colore', + type: 'color' + }, { + k: 'background', + label: 'Sfondo blocco', + type: 'color' + }] + }, + { + row: PAD + } ], - text: [ - { k: 'html', type: 'rich' }, - { row: [{ k: 'background', label: 'Sfondo', type: 'color' }, { k: 'background_2', label: 'Sfumatura verso', type: 'color' }] }, - { row: PAD } + text: [{ + k: 'html', + type: 'rich' + }, + { + row: [{ + k: 'background', + label: 'Sfondo', + type: 'color' + }, { + k: 'background_2', + label: 'Sfumatura verso', + type: 'color' + }] + }, + { + row: PAD + } ], - image: [ - { k: 'url', label: 'Immagine', type: 'image' }, - { k: 'alt', label: 'Testo alternativo (se le immagini sono bloccate)', type: 'text' }, - { row: [{ k: 'link', label: 'Link al clic', type: 'url' }, { k: 'width', label: 'Larghezza (max 600)', type: 'number', min: 50, max: 600 }] }, - { row: PAD } + image: [{ + k: 'url', + label: 'Immagine', + type: 'image' + }, + { + k: 'alt', + label: 'Testo alternativo (se le immagini sono bloccate)', + type: 'text' + }, + { + row: [{ + k: 'link', + label: 'Link al clic', + type: 'url' + }, { + k: 'width', + label: 'Larghezza (max 600)', + type: 'number', + min: 50, + max: 600 + }] + }, + { + row: PAD + } ], - button: [ - { k: 'label', label: 'Testo del pulsante', type: 'text', hint: 'Per andare a capo scrivi
' }, - { k: 'url', label: 'Link', type: 'url' }, - { row: [{ k: 'button_bg', label: 'Colore pulsante', type: 'color' }, { k: 'color', label: 'Colore testo', type: 'color' }] }, - { row: [{ k: 'font_size', label: 'Testo (px)', type: 'number', min: 10, max: 60 }, { k: 'width', label: 'Larghezza (px)', type: 'number', min: 80, max: 600 }, { k: 'radius', label: 'Angoli', type: 'number', min: 0, max: 50 }] }, - { row: PAD } + button: [{ + k: 'label', + label: 'Testo del pulsante', + type: 'text', + hint: 'Per andare a capo scrivi
' + }, + { + k: 'url', + label: 'Link', + type: 'url' + }, + { + row: [{ + k: 'button_bg', + label: 'Colore pulsante', + type: 'color' + }, { + k: 'color', + label: 'Colore testo', + type: 'color' + }] + }, + { + row: [{ + k: 'font_size', + label: 'Testo (px)', + type: 'number', + min: 10, + max: 60 + }, { + k: 'width', + label: 'Larghezza (px)', + type: 'number', + min: 80, + max: 600 + }, { + k: 'radius', + label: 'Angoli', + type: 'number', + min: 0, + max: 50 + }] + }, + { + row: PAD + } ], - columns: [ - { k: 'image_url', label: 'Immagine', type: 'image' }, - { row: [{ k: 'image_alt', label: 'Testo alternativo', type: 'text' }, { k: 'link', label: 'Link immagine', type: 'url' }] }, - { k: 'image_side', label: 'Posizione immagine', type: 'select', options: [['left', 'A sinistra'], ['right', 'A destra']] }, - { k: 'html', type: 'rich' }, - { row: PAD } + columns: [{ + k: 'image_url', + label: 'Immagine', + type: 'image' + }, + { + row: [{ + k: 'image_alt', + label: 'Testo alternativo', + type: 'text' + }, { + k: 'link', + label: 'Link immagine', + type: 'url' + }] + }, + { + k: 'image_side', + label: 'Posizione immagine', + type: 'select', + options: [ + ['left', 'A sinistra'], + ['right', 'A destra'] + ] + }, + { + k: 'html', + type: 'rich' + }, + { + row: PAD + } ], - separator: [ - { row: [{ k: 'color', label: 'Colore', type: 'color' }, { k: 'height', label: 'Spessore', type: 'number', min: 1, max: 10 }] }, - { row: PAD } + separator: [{ + row: [{ + k: 'color', + label: 'Colore', + type: 'color' + }, { + k: 'height', + label: 'Spessore', + type: 'number', + min: 1, + max: 10 + }] + }, + { + row: PAD + } ], - social: [ - { k: 'icon_size', label: 'Dimensione icone', type: 'number', min: 16, max: 64 }, - { note: 'I link dei social si cambiano da Stile.' }, - { row: PAD } + social: [{ + k: 'icon_size', + label: 'Dimensione icone', + type: 'number', + min: 16, + max: 64 + }, + { + note: 'I link dei social si cambiano da Stile.' + }, + { + row: PAD + } ], - footer: [ - { k: 'profile_label', label: 'Testo "gestisci iscrizione"', type: 'text' }, - { k: 'view_label', label: 'Testo "vedi online"', type: 'text' }, - { k: 'unsub_label', label: 'Testo "disiscriviti" (vuoto = non mostrarlo)', type: 'text' }, - { row: PAD } + footer: [{ + k: 'profile_label', + label: 'Testo "gestisci iscrizione"', + type: 'text' + }, + { + k: 'view_label', + label: 'Testo "vedi online"', + type: 'text' + }, + { + k: 'unsub_label', + label: 'Testo "disiscriviti" (vuoto = non mostrarlo)', + type: 'text' + }, + { + row: PAD + } ], - raw: [ - { note: 'Blocco importato da WordPress che qui non si può modificare: viene pubblicato identico. Puoi spostarlo o eliminarlo.' } - ] + raw: [{ + note: 'Blocco importato da WordPress che qui non si può modificare: viene pubblicato identico. Puoi spostarlo o eliminarlo.' + }] }; - const STYLE_FIELDS = [ - { row: [{ k: 'background', label: 'Sfondo esterno', type: 'color' }, { k: 'block_background', label: 'Sfondo contenuto', type: 'color' }] }, - { row: [{ k: 'font_family', label: 'Carattere', type: 'select', options: [ - ['Verdana, Geneva, sans-serif', 'Verdana'], ['Helvetica, Arial, sans-serif', 'Helvetica / Arial'], - ['Georgia, serif', 'Georgia'], ["'Trebuchet MS', Tahoma, sans-serif", 'Trebuchet'], ["Tahoma, Geneva, sans-serif", 'Tahoma']] }, - { k: 'font_size', label: 'Dimensione testo', type: 'number', min: 12, max: 22 }] }, - { row: [{ k: 'text_color', label: 'Colore testo', type: 'color' }, { k: 'link_color', label: 'Colore link', type: 'color' }] }, - { row: [{ k: 'title_font_size', label: 'Dimensione titoli', type: 'number', min: 16, max: 60 }, { k: 'title_color', label: 'Colore titoli', type: 'color' }] }, - { row: [{ k: 'button_background', label: 'Colore pulsanti', type: 'color' }, { k: 'button_color', label: 'Testo pulsanti', type: 'color' }] }, - { heading: 'Link social' }, - { row: [{ k: 'social.facebook', label: 'Facebook', type: 'url' }, { k: 'social.instagram', label: 'Instagram', type: 'url' }] }, - { row: [{ k: 'social.youtube', label: 'YouTube', type: 'url' }, { k: 'social.whatsapp', label: 'WhatsApp (https://wa.me/39…)', type: 'url' }] } + const STYLE_FIELDS = [{ + row: [{ + k: 'background', + label: 'Sfondo esterno', + type: 'color' + }, { + k: 'block_background', + label: 'Sfondo contenuto', + type: 'color' + }] + }, + { + row: [{ + k: 'font_family', + label: 'Carattere', + type: 'select', + options: [ + ['Verdana, Geneva, sans-serif', 'Verdana'], + ['Helvetica, Arial, sans-serif', 'Helvetica / Arial'], + ['Georgia, serif', 'Georgia'], + ["'Trebuchet MS', Tahoma, sans-serif", 'Trebuchet'], + ["Tahoma, Geneva, sans-serif", 'Tahoma'] + ] + }, + { + k: 'font_size', + label: 'Dimensione testo', + type: 'number', + min: 12, + max: 22 + } + ] + }, + { + row: [{ + k: 'text_color', + label: 'Colore testo', + type: 'color' + }, { + k: 'link_color', + label: 'Colore link', + type: 'color' + }] + }, + { + row: [{ + k: 'title_font_size', + label: 'Dimensione titoli', + type: 'number', + min: 16, + max: 60 + }, { + k: 'title_color', + label: 'Colore titoli', + type: 'color' + }] + }, + { + row: [{ + k: 'button_background', + label: 'Colore pulsanti', + type: 'color' + }, { + k: 'button_color', + label: 'Testo pulsanti', + type: 'color' + }] + }, + { + heading: 'Link social' + }, + { + row: [{ + k: 'social.facebook', + label: 'Facebook', + type: 'url' + }, { + k: 'social.instagram', + label: 'Instagram', + type: 'url' + }] + }, + { + row: [{ + k: 'social.youtube', + label: 'YouTube', + type: 'url' + }, { + k: 'social.whatsapp', + label: 'WhatsApp (https://wa.me/39…)', + type: 'url' + }] + } ]; /* ================= Stato ================= */ - let D = null; // bozza corrente - let openId = null; // blocco aperto - let editSeq = 0; // contatore modifiche - let savedSeq = 0; // ultima modifica salvata sul server + let D = null; // bozza corrente + let openId = null; // blocco aperto + let editSeq = 0; // contatore modifiche + let savedSeq = 0; // ultima modifica salvata sul server let saving = false; - let saveTimer = null, firstDirtyAt = 0, retryDelay = 3000; + let saveTimer = null, + firstDirtyAt = 0, + retryDelay = 3000; let conflict = false; - let previewTimer = null, previewReq = 0; + let previewTimer = null, + previewReq = 0; let lastChecks = []; const $ = (s, el = document) => el.querySelector(s); const $$ = (s, el = document) => Array.from(el.querySelectorAll(s)); - const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } [c])); const uid = () => Math.random().toString(16).slice(2, 14); const isDirty = () => editSeq !== savedSeq; - const lsGet = () => { try { return JSON.parse(localStorage.getItem(LS_KEY) || 'null'); } catch (e) { return null; } }; - const lsSet = v => { try { localStorage.setItem(LS_KEY, JSON.stringify(v)); } catch (e) { } }; - const lsDel = () => { try { localStorage.removeItem(LS_KEY); } catch (e) { } }; + const lsGet = () => { + try { + return JSON.parse(localStorage.getItem(LS_KEY) || 'null'); + } catch (e) { + return null; + } + }; + const lsSet = v => { + try { + localStorage.setItem(LS_KEY, JSON.stringify(v)); + } catch (e) {} + }; + const lsDel = () => { + try { + localStorage.removeItem(LS_KEY); + } catch (e) {} + }; function content() { - return { title: D.title, subject: D.subject, preheader: D.preheader, blocks: D.blocks, settings: D.settings }; + return { + title: D.title, + subject: D.subject, + preheader: D.preheader, + blocks: D.blocks, + settings: D.settings + }; } async function apiPost(action, data) { const body = new FormData(); body.append('action', action); Object.entries(data || {}).forEach(([k, v]) => body.append(k, v)); - const r = await fetch(API, { method: 'POST', body, credentials: 'same-origin' }); + const r = await fetch(API, { + method: 'POST', + body, + credentials: 'same-origin' + }); let j = null; - try { j = await r.json(); } catch (e) { } - if (!j) throw Object.assign(new Error('Il server non risponde correttamente.'), { network: true }); + try { + j = await r.json(); + } catch (e) {} + if (!j) throw Object.assign(new Error('Il server non risponde correttamente.'), { + network: true + }); j.httpStatus = r.status; return j; } async function apiGet(params) { - const r = await fetch(API + '?' + new URLSearchParams(params), { credentials: 'same-origin' }); + const r = await fetch(API + '?' + new URLSearchParams(params), { + credentials: 'same-origin' + }); return r.json(); } @@ -401,11 +1070,19 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ el.className = 'nl-status ' + (kind || ''); $('.t', el).textContent = text; } - const hhmm = d => (d ? new Date(d.replace(' ', 'T')) : new Date()).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); + const hhmm = d => (d ? new Date(d.replace(' ', 'T')) : new Date()).toLocaleTimeString('it-IT', { + hour: '2-digit', + minute: '2-digit' + }); function changed(opts = {}) { editSeq++; - lsSet({ seq: editSeq, base: D.revision, at: Date.now(), data: content() }); + lsSet({ + seq: editSeq, + base: D.revision, + at: Date.now(), + data: content() + }); if (!conflict) setStatus('busy', 'Modifiche non salvate…'); if (!firstDirtyAt) firstDirtyAt = Date.now(); clearTimeout(saveTimer); @@ -426,17 +1103,29 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ const c = content(); try { const j = await apiPost('save', { - id: DRAFT_ID, revision: D.revision, force: force ? '1' : '', - title: c.title, subject: c.subject, preheader: c.preheader, - blocks: JSON.stringify(c.blocks), settings: JSON.stringify(c.settings) + id: DRAFT_ID, + revision: D.revision, + force: force ? '1' : '', + title: c.title, + subject: c.subject, + preheader: c.preheader, + blocks: JSON.stringify(c.blocks), + settings: JSON.stringify(c.settings) }); if (j.success) { D.revision = j.revision; savedSeq = seq; retryDelay = 3000; - if (conflict) { conflict = false; $('#bnConflict').classList.remove('show'); layout(); } - if (!isDirty()) { lsDel(); firstDirtyAt = 0; setStatus('ok', 'Salvato alle ' + hhmm(j.updated_at)); } - else retryIn = 800; // altre modifiche arrivate durante il salvataggio + if (conflict) { + conflict = false; + $('#bnConflict').classList.remove('show'); + layout(); + } + if (!isDirty()) { + lsDel(); + firstDirtyAt = 0; + setStatus('ok', 'Salvato alle ' + hhmm(j.updated_at)); + } else retryIn = 800; // altre modifiche arrivate durante il salvataggio } else if (j.conflict) { showConflict(j); } else { @@ -450,7 +1139,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ retryDelay = Math.min(retryDelay * 2, 60000); } finally { saving = false; - if (retryIn && !conflict) { clearTimeout(saveTimer); saveTimer = setTimeout(save, retryIn); } + if (retryIn && !conflict) { + clearTimeout(saveTimer); + saveTimer = setTimeout(save, retryIn); + } } } @@ -474,15 +1166,24 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ '
'; b.classList.add('show'); layout(); - $('#cfReload').onclick = () => { lsDel(); location.reload(); }; + $('#cfReload').onclick = () => { + lsDel(); + location.reload(); + }; $('#cfForce').onclick = () => save(true); } window.addEventListener('beforeunload', e => { - if (isDirty() || saving) { e.preventDefault(); e.returnValue = ''; } + if (isDirty() || saving) { + e.preventDefault(); + e.returnValue = ''; + } }); document.addEventListener('keydown', e => { - if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault(); save(); } + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { + e.preventDefault(); + save(); + } }); /* ================= Anteprima ================= */ @@ -496,20 +1197,29 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ const c = content(); try { const j = await apiPost('preview', { - subject: c.subject, preheader: c.preheader, - blocks: JSON.stringify(c.blocks), settings: JSON.stringify(c.settings) + subject: c.subject, + preheader: c.preheader, + blocks: JSON.stringify(c.blocks), + settings: JSON.stringify(c.settings) }); if (req !== previewReq || !j.success) return; const f = $('#nlFrame'); let y = 0; - try { y = f.contentWindow.scrollY; } catch (e) { } - f.onload = () => { try { f.contentWindow.scrollTo(0, y); } catch (e) { } }; + try { + y = f.contentWindow.scrollY; + } catch (e) {} + f.onload = () => { + try { + f.contentWindow.scrollTo(0, y); + } catch (e) {} + }; f.srcdoc = j.html; $('#nlKb').textContent = j.kb + ' KB' + (j.kb > 100 ? ' – oltre il limite di Gmail!' : ''); lastChecks = j.checks || []; $('#nlChecks').innerHTML = lastChecks.map(c => '
' + esc(c[1]) + '
').join(''); - } catch (e) { /* l'anteprima riprova alla prossima modifica */ } + } catch (e) { + /* l'anteprima riprova alla prossima modifica */ } } $$('.nl-device button').forEach(b => b.addEventListener('click', () => { @@ -521,10 +1231,14 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ function getVal(obj, k) { return k.split('.').reduce((o, p) => (o == null ? undefined : o[p]), obj); } + function setVal(obj, k, v) { const parts = k.split('.'); let o = obj; - parts.slice(0, -1).forEach(p => { if (typeof o[p] !== 'object' || o[p] === null) o[p] = {}; o = o[p]; }); + parts.slice(0, -1).forEach(p => { + if (typeof o[p] !== 'object' || o[p] === null) o[p] = {}; + o = o[p]; + }); o[parts[parts.length - 1]] = v; } @@ -609,7 +1323,11 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ body.append('action', 'upload_image'); body.append('image', file); try { - const r = await fetch(API, { method: 'POST', body, credentials: 'same-origin' }); + const r = await fetch(API, { + method: 'POST', + body, + credentials: 'same-origin' + }); const j = await r.json(); if (!j.success) throw new Error(j.message); const t = $('[data-k="' + fileInput.dataset.upload + '"]', root); @@ -631,7 +1349,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ // Se l'editor ricco non si carica, resta un campo HTML semplice ta.classList.add('form-control'); ta.rows = 10; - ta.addEventListener('input', () => { block[key] = ta.value; changed(); }); + ta.addEventListener('input', () => { + block[key] = ta.value; + changed(); + }); return; } tinymce.init({ @@ -662,7 +1383,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ }); const sync = () => { const html = ed.getContent(); - if (html !== block[key]) { block[key] = html; changed(); } + if (html !== block[key]) { + block[key] = html; + changed(); + } }; ed.on('input change undo redo keyup ExecCommand', sync); ed.on('PastePostProcess', () => setTimeout(sync, 0)); @@ -679,12 +1403,19 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ function summary(b) { const strip = h => String(h || '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim(); switch (b.type) { - case 'heading': return b.text; - case 'text': case 'columns': return strip(b.html); - case 'image': return b.url ? (b.alt || b.url.split('/').pop()) : 'Nessuna immagine'; - case 'button': return strip(b.label) + (b.url ? ' → ' + b.url : ''); - case 'raw': return 'tipo: ' + (b.wp_id || '?'); - default: return ''; + case 'heading': + return b.text; + case 'text': + case 'columns': + return strip(b.html); + case 'image': + return b.url ? (b.alt || b.url.split('/').pop()) : 'Nessuna immagine'; + case 'button': + return strip(b.label) + (b.url ? ' → ' + b.url : ''); + case 'raw': + return 'tipo: ' + (b.wp_id || '?'); + default: + return ''; } } @@ -699,7 +1430,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ destroyRich(); const box = $('#nlBlocks'); box.innerHTML = D.blocks.map((b, i) => { - const t = TYPES[b.type] || { label: b.type, icon: 'fa-square' }; + const t = TYPES[b.type] || { + label: b.type, + icon: 'fa-square' + }; return '
' + '
' + '
' + esc(t.label) + '
' + esc(summary(b)) + '
' + @@ -718,10 +1452,17 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ const b = D.blocks.find(x => x.id === card.dataset.id); $('.nl-card-head', card).addEventListener('click', ev => { const act = ev.target.closest('[data-act]'); - if (act) { ev.stopPropagation(); blockAction(act.dataset.act, b.id); return; } + if (act) { + ev.stopPropagation(); + blockAction(act.dataset.act, b.id); + return; + } openId = openId === b.id ? null : b.id; renderBlocks(); - if (openId) card.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + if (openId) card.scrollIntoView({ + block: 'nearest', + behavior: 'smooth' + }); }); if (b.id === openId) { bindFields($('.nl-card-body', card), b, () => changed()); @@ -733,14 +1474,26 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ function blockAction(act, id) { const i = D.blocks.findIndex(x => x.id === id); if (i < 0) return; - if (act === 'up' && i > 0) { [D.blocks[i - 1], D.blocks[i]] = [D.blocks[i], D.blocks[i - 1]]; } - else if (act === 'down' && i < D.blocks.length - 1) { [D.blocks[i + 1], D.blocks[i]] = [D.blocks[i], D.blocks[i + 1]]; } - else if (act === 'dup') { const c = JSON.parse(JSON.stringify(D.blocks[i])); c.id = uid(); D.blocks.splice(i + 1, 0, c); openId = c.id; } - else if (act === 'add') { return chooseBlock(i + 1); } - else if (act === 'del') { + if (act === 'up' && i > 0) { + [D.blocks[i - 1], D.blocks[i]] = [D.blocks[i], D.blocks[i - 1]]; + } else if (act === 'down' && i < D.blocks.length - 1) { + [D.blocks[i + 1], D.blocks[i]] = [D.blocks[i], D.blocks[i + 1]]; + } else if (act === 'dup') { + const c = JSON.parse(JSON.stringify(D.blocks[i])); + c.id = uid(); + D.blocks.splice(i + 1, 0, c); + openId = c.id; + } else if (act === 'add') { + return chooseBlock(i + 1); + } else if (act === 'del') { return Swal.fire({ - title: 'Eliminare il blocco?', text: (TYPES[D.blocks[i].type] || {}).label || '', icon: 'warning', - showCancelButton: true, confirmButtonColor: '#d33', confirmButtonText: 'Elimina', cancelButtonText: 'Annulla' + title: 'Eliminare il blocco?', + text: (TYPES[D.blocks[i].type] || {}).label || '', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + confirmButtonText: 'Elimina', + cancelButtonText: 'Annulla' }).then(r => { if (!r.isConfirmed) return; D.blocks.splice(i, 1); @@ -769,7 +1522,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ renderBlocks(); changed(); const c = $('.nl-card[data-id="' + b.id + '"]'); - if (c) c.scrollIntoView({ block: 'center', behavior: 'smooth' }); + if (c) c.scrollIntoView({ + block: 'center', + behavior: 'smooth' + }); })) }); } @@ -781,8 +1537,13 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ out.textContent = n ? n + ' caratteri' + (n > max ? ' – ' + label : '') : ''; out.classList.toggle('warn', n > max); } + function bindTop() { - const f = { fTitle: 'title', fSubject: 'subject', fPreheader: 'preheader' }; + const f = { + fTitle: 'title', + fSubject: 'subject', + fPreheader: 'preheader' + }; Object.entries(f).forEach(([elId, key]) => { const el = $('#' + elId); el.value = D[key] || ''; @@ -790,7 +1551,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ D[key] = el.value; if (key === 'subject') counter(el, $('#cSubject'), 60, 'sul telefono si vede solo l\'inizio'); if (key === 'preheader') counter(el, $('#cPreheader'), 110, 'troppo lungo'); - changed({ summary: false, preview: key !== 'title' }); + changed({ + summary: false, + preview: key !== 'title' + }); }); }); counter($('#fSubject'), $('#cSubject'), 60, 'sul telefono si vede solo l\'inizio'); @@ -800,8 +1564,20 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ function renderWpBadge() { const el = $('#nlWp'); const btn = $('#btnPublish'); - const labels = { new: 'bozza da inviare', sending: 'in invio', paused: 'invio in pausa', sent: 'già inviata', error: 'errore di invio', deleted: 'eliminata in WordPress' }; - if (!D.wp_email_id) { el.innerHTML = ''; btn.innerHTML = ' Pubblica su The Newsletter'; btn.disabled = false; return; } + const labels = { + new: 'bozza da inviare', + sending: 'in invio', + paused: 'invio in pausa', + sent: 'già inviata', + error: 'errore di invio', + deleted: 'eliminata in WordPress' + }; + if (!D.wp_email_id) { + el.innerHTML = ''; + btn.innerHTML = ' Pubblica su The Newsletter'; + btn.disabled = false; + return; + } el.innerHTML = 'The Newsletter n. ' + D.wp_email_id + (D.wp_status ? ' · ' + esc(labels[D.wp_status] || D.wp_status) : ''); const locked = ['sending', 'paused', 'sent'].includes(D.wp_status); btn.disabled = locked; @@ -813,21 +1589,37 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ $('#btnStyle').addEventListener('click', () => { const body = $('#styleBody'); body.innerHTML = STYLE_FIELDS.map(f => fieldHtml(f, D.settings)).join(''); - bindFields(body, D.settings, () => changed({ summary: false })); + bindFields(body, D.settings, () => changed({ + summary: false + })); bootstrap.Modal.getOrCreateInstance($('#mStyle')).show(); }); - $('#mStyle').addEventListener('hidden.bs.modal', () => { if (openId) renderBlocks(); }); + $('#mStyle').addEventListener('hidden.bs.modal', () => { + if (openId) renderBlocks(); + }); /* ================= Versioni ================= */ async function loadVersions() { const list = $('#versList'); list.innerHTML = '
Caricamento…
'; - const j = await apiGet({ action: 'versions', id: DRAFT_ID }); - if (!j.success) { list.innerHTML = '
' + esc(j.message) + '
'; return; } - if (!j.versions.length) { list.innerHTML = '
Nessuna versione ancora: se ne crea una ogni 10 minuti di lavoro.
'; return; } + const j = await apiGet({ + action: 'versions', + id: DRAFT_ID + }); + if (!j.success) { + list.innerHTML = '
' + esc(j.message) + '
'; + return; + } + if (!j.versions.length) { + list.innerHTML = '
Nessuna versione ancora: se ne crea una ogni 10 minuti di lavoro.
'; + return; + } list.innerHTML = j.versions.map(v => '
' + esc(v.note) + '
' + - esc(new Date(v.created_at.replace(' ', 'T')).toLocaleString('it-IT', { dateStyle: 'short', timeStyle: 'short' })) + + esc(new Date(v.created_at.replace(' ', 'T')).toLocaleString('it-IT', { + dateStyle: 'short', + timeStyle: 'short' + })) + (v.author ? ' · ' + esc(v.author) : '') + '
' + '
' + '
').join(''); @@ -841,39 +1633,61 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ const r = await Swal.fire({ title: 'Ripristinare questa versione?', text: 'La versione attuale viene salvata nello storico prima del ripristino, quindi puoi sempre tornare indietro.', - icon: 'question', showCancelButton: true, confirmButtonText: 'Ripristina', cancelButtonText: 'Annulla' + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Ripristina', + cancelButtonText: 'Annulla' }); if (!r.isConfirmed) return; try { await flush(); - const j = await apiPost('restore', { id: DRAFT_ID, idversion: idv }); + const j = await apiPost('restore', { + id: DRAFT_ID, + idversion: idv + }); if (!j.success) throw new Error(j.message); applyDraft(j.draft); bootstrap.Modal.getOrCreateInstance($('#mVersions')).hide(); - Swal.fire({ icon: 'success', title: 'Versione ripristinata', timer: 1500, showConfirmButton: false }); - } catch (e) { Swal.fire('Errore', e.message, 'error'); } + Swal.fire({ + icon: 'success', + title: 'Versione ripristinata', + timer: 1500, + showConfirmButton: false + }); + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } } $('#btnVersions').addEventListener('click', async () => { $('#versFrame').removeAttribute('src'); bootstrap.Modal.getOrCreateInstance($('#mVersions')).show(); - try { await flush(); } catch (e) { } + try { + await flush(); + } catch (e) {} loadVersions(); }); $('#btnSnapshot').addEventListener('click', async () => { try { await flush(); - const j = await apiPost('snapshot', { id: DRAFT_ID, note: $('#versNote').value }); + const j = await apiPost('snapshot', { + id: DRAFT_ID, + note: $('#versNote').value + }); if (!j.success) throw new Error(j.message); $('#versNote').value = ''; loadVersions(); - } catch (e) { Swal.fire('Errore', e.message, 'error'); } + } catch (e) { + Swal.fire('Errore', e.message, 'error'); + } }); /* ================= Prova e pubblicazione ================= */ $('#btnTest').addEventListener('click', async () => { let last = MY_EMAIL; - try { last = localStorage.getItem('yb_nl_test_to') || MY_EMAIL; } catch (e) { } + try { + last = localStorage.getItem('yb_nl_test_to') || MY_EMAIL; + } catch (e) {} const r = await Swal.fire({ title: 'Invia una prova', input: 'email', @@ -886,29 +1700,51 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ preConfirm: async to => { try { await flush(); - const j = await apiPost('send_test', { id: DRAFT_ID, to }); + const j = await apiPost('send_test', { + id: DRAFT_ID, + to + }); if (!j.success) throw new Error(j.message); - try { localStorage.setItem('yb_nl_test_to', to); } catch (e) { } + try { + localStorage.setItem('yb_nl_test_to', to); + } catch (e) {} return j; - } catch (e) { Swal.showValidationMessage(e.message); } + } catch (e) { + Swal.showValidationMessage(e.message); + } } }); - if (r.isConfirmed && r.value) Swal.fire({ icon: 'success', title: 'Prova inviata', text: r.value.message }); + if (r.isConfirmed && r.value) Swal.fire({ + icon: 'success', + title: 'Prova inviata', + text: r.value.message + }); }); async function publish(confirmed) { try { await flush(); - Swal.fire({ title: 'Pubblico su The Newsletter…', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); - const j = await apiPost('publish', { id: DRAFT_ID, confirm: confirmed ? '1' : '' }); + Swal.fire({ + title: 'Pubblico su The Newsletter…', + allowOutsideClick: false, + didOpen: () => Swal.showLoading() + }); + const j = await apiPost('publish', { + id: DRAFT_ID, + confirm: confirmed ? '1' : '' + }); if (j.needs_confirm) { const html = '
' + j.checks.map(c => '
' + esc(c[1]) + '
').join('') + '
'; const r = await Swal.fire({ - title: 'Controlla prima di pubblicare', html, icon: 'warning', - showCancelButton: true, confirmButtonText: 'Pubblica comunque', cancelButtonText: 'Torno a sistemare' + title: 'Controlla prima di pubblicare', + html, + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Pubblica comunque', + cancelButtonText: 'Torno a sistemare' }); if (r.isConfirmed) return publish(true); return; @@ -923,9 +1759,12 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ title: j.created ? 'Creata in The Newsletter' : 'Aggiornata in The Newsletter', html: esc(j.message) + '

Ora apri WordPress, scegli le liste e invia come sempre.' + '
Apri The Newsletter', - showConfirmButton: false, showCloseButton: true + showConfirmButton: false, + showCloseButton: true }); - } catch (e) { Swal.fire('Non pubblicata', e.message, 'error'); } + } catch (e) { + Swal.fire('Non pubblicata', e.message, 'error'); + } } $('#btnPublish').addEventListener('click', () => publish(false)); @@ -933,7 +1772,11 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ function layout() { const top = $('.nl-top'); const wrap = $('.nl-preview-wrap'); - if (window.innerWidth < 992) { wrap.style.top = ''; $('#nlFrame').style.height = ''; return; } + if (window.innerWidth < 992) { + wrap.style.top = ''; + $('#nlFrame').style.height = ''; + return; + } const offset = parseFloat(getComputedStyle(top).top) || 70; const t = offset + top.getBoundingClientRect().height + 12; wrap.style.top = t + 'px'; @@ -948,7 +1791,14 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ if (!D.settings.social) D.settings.social = {}; editSeq = savedSeq = 0; lsDel(); - ['fTitle', 'fSubject', 'fPreheader'].forEach(i => { const el = $('#' + i); el.value = D[{ fTitle: 'title', fSubject: 'subject', fPreheader: 'preheader' }[i]] || ''; }); + ['fTitle', 'fSubject', 'fPreheader'].forEach(i => { + const el = $('#' + i); + el.value = D[{ + fTitle: 'title', + fSubject: 'subject', + fPreheader: 'preheader' + } [i]] || ''; + }); renderBlocks(); renderPreview(); renderWpBadge(); @@ -956,8 +1806,14 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ } async function start() { - const j = await apiGet({ action: 'load', id: DRAFT_ID }); - if (!j.success) { setStatus('err', j.message || 'Impossibile aprire la newsletter'); return; } + const j = await apiGet({ + action: 'load', + id: DRAFT_ID + }); + if (!j.success) { + setStatus('err', j.message || 'Impossibile aprire la newsletter'); + return; + } D = j.draft; if (!D.settings.social) D.settings.social = {}; bindTop(); @@ -970,7 +1826,10 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ // Modifiche rimaste solo nel browser (connessione caduta, scheda chiusa…) const bk = lsGet(); if (bk && bk.data && JSON.stringify(bk.data) !== JSON.stringify(content())) { - const when = new Date(bk.at).toLocaleString('it-IT', { dateStyle: 'short', timeStyle: 'short' }); + const when = new Date(bk.at).toLocaleString('it-IT', { + dateStyle: 'short', + timeStyle: 'short' + }); const b = $('#bnRestore'); b.innerHTML = ' In questo browser ci sono modifiche del ' + esc(when) + ' che non erano arrivate al server.' + @@ -980,13 +1839,24 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ layout(); $('#bkYes').onclick = () => { Object.assign(D, bk.data); - ['fTitle', 'fSubject', 'fPreheader'].forEach(i => { const el = $('#' + i); el.value = D[{ fTitle: 'title', fSubject: 'subject', fPreheader: 'preheader' }[i]] || ''; }); + ['fTitle', 'fSubject', 'fPreheader'].forEach(i => { + const el = $('#' + i); + el.value = D[{ + fTitle: 'title', + fSubject: 'subject', + fPreheader: 'preheader' + } [i]] || ''; + }); b.classList.remove('show'); layout(); renderBlocks(); changed(); }; - $('#bkNo').onclick = () => { lsDel(); b.classList.remove('show'); layout(); }; + $('#bkNo').onclick = () => { + lsDel(); + b.classList.remove('show'); + layout(); + }; } } @@ -995,4 +1865,4 @@ $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_ - + \ No newline at end of file