/** * EXT:fe_comment — page feedback widget (framework-agnostic, vanilla JS). * * One floating button at the right edge opens a small form: current page * URL + a comment field. Saved notes land on a local note pad * (localStorage, survives navigation — also SPA route changes, the URL is * read at save time). The pad lists every note (URL + editable text) and * submits all of them to POST /api/fe-comment/submit, which mails them. * * Integration: * (classic TYPO3 sites get this injected by the extension automatically * when the "JSON output" setting is off). */ (function () { 'use strict'; if (window.__evofcLoaded) { return; } window.__evofcLoaded = true; var ENDPOINT = '/api/fe-comment/submit'; var CSS_URL = '/api/fe-comment/widget.css'; var STORAGE_KEY = 'evoFeCommentNotes'; var T = { open: 'Seite kommentieren', formTitle: 'Anmerkung zu dieser Seite', urlLabel: 'Seite', commentLabel: 'Anmerkung', placeholder: 'Was ist dir aufgefallen?', save: 'Auf den Merkzettel', showPad: 'Merkzettel', padTitle: 'Merkzettel', padEmpty: 'Noch keine Anmerkungen.', remove: 'Entfernen', submit: 'Absenden', submitting: 'Sende …', back: 'Zurück', savedHint: 'Gespeichert ✓', sentOk: 'Danke! Die Anmerkungen wurden gesendet.', sentFail: 'Senden fehlgeschlagen: ' }; function loadNotes() { try { var raw = localStorage.getItem(STORAGE_KEY); var list = raw ? JSON.parse(raw) : []; return Array.isArray(list) ? list : []; } catch (e) { return []; } } function saveNotes(notes) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); } catch (e) { /* private mode */ } } function currentUrl() { return location.pathname + location.search; } function el(tag, className, text) { var node = document.createElement(tag); if (className) { node.className = className; } if (text) { node.textContent = text; } return node; } // ---- styles ------------------------------------------------------------ var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = CSS_URL; document.head.appendChild(link); // ---- structure --------------------------------------------------------- var root = el('div', 'evofc'); root.setAttribute('data-evofc', ''); var toggle = el('button', 'evofc-toggle'); toggle.type = 'button'; toggle.title = T.open; toggle.setAttribute('aria-label', T.open); toggle.innerHTML = ''; var badge = el('span', 'evofc-badge'); badge.hidden = true; toggle.appendChild(badge); var panel = el('div', 'evofc-panel'); panel.hidden = true; root.appendChild(toggle); root.appendChild(panel); // ---- views ------------------------------------------------------------- var view = 'form'; // 'form' | 'pad' function refreshBadge() { var count = loadNotes().length; badge.textContent = String(count); badge.hidden = count === 0; } function renderForm() { view = 'form'; panel.innerHTML = ''; panel.appendChild(el('div', 'evofc-title', T.formTitle)); var urlLabel = el('label', 'evofc-label', T.urlLabel); var urlInput = el('input', 'evofc-url'); urlInput.type = 'text'; urlInput.readOnly = true; urlInput.value = currentUrl(); var commentLabel = el('label', 'evofc-label', T.commentLabel); var textarea = el('textarea', 'evofc-text'); textarea.rows = 4; textarea.placeholder = T.placeholder; var saveButton = el('button', 'evofc-button evofc-primary', T.save); saveButton.type = 'button'; var hint = el('span', 'evofc-hint'); saveButton.addEventListener('click', function () { var comment = textarea.value.trim(); if (comment === '') { textarea.focus(); return; } var notes = loadNotes(); notes.push({ url: urlInput.value, comment: comment, added: Date.now() }); saveNotes(notes); textarea.value = ''; refreshBadge(); renderFooter(footer); hint.textContent = T.savedHint; setTimeout(function () { hint.textContent = ''; }, 1800); }); var footer = el('div', 'evofc-footer'); renderFooter(footer); panel.appendChild(urlLabel); panel.appendChild(urlInput); panel.appendChild(commentLabel); panel.appendChild(textarea); var row = el('div', 'evofc-row'); row.appendChild(saveButton); row.appendChild(hint); panel.appendChild(row); panel.appendChild(footer); } function renderFooter(footer) { footer.innerHTML = ''; var count = loadNotes().length; if (count > 0) { var padButton = el('button', 'evofc-button', T.showPad + ' (' + count + ')'); padButton.type = 'button'; padButton.addEventListener('click', renderPad); footer.appendChild(padButton); } } function renderPad() { view = 'pad'; panel.innerHTML = ''; panel.appendChild(el('div', 'evofc-title', T.padTitle)); var notes = loadNotes(); if (notes.length === 0) { panel.appendChild(el('p', 'evofc-empty', T.padEmpty)); } var list = el('div', 'evofc-list'); notes.forEach(function (note, index) { var item = el('div', 'evofc-item'); var head = el('div', 'evofc-item-head'); head.appendChild(el('span', 'evofc-item-url', note.url || '(ohne URL)')); var removeButton = el('button', 'evofc-remove', '×'); removeButton.type = 'button'; removeButton.title = T.remove; removeButton.addEventListener('click', function () { var current = loadNotes(); current.splice(index, 1); saveNotes(current); refreshBadge(); renderPad(); }); head.appendChild(removeButton); var textarea = el('textarea', 'evofc-text'); textarea.rows = 3; textarea.value = note.comment; textarea.addEventListener('input', function () { var current = loadNotes(); if (current[index]) { current[index].comment = textarea.value; saveNotes(current); } }); item.appendChild(head); item.appendChild(textarea); list.appendChild(item); }); panel.appendChild(list); var status = el('p', 'evofc-status'); var row = el('div', 'evofc-row'); var backButton = el('button', 'evofc-button', T.back); backButton.type = 'button'; backButton.addEventListener('click', renderForm); row.appendChild(backButton); if (notes.length > 0) { var submitButton = el('button', 'evofc-button evofc-primary', T.submit); submitButton.type = 'button'; submitButton.addEventListener('click', function () { submitButton.disabled = true; submitButton.textContent = T.submitting; var entries = loadNotes().map(function (note) { return { url: note.url, comment: note.comment }; }); fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entries: entries }) }).then(function (response) { return response.json(); }).then(function (result) { if (result && result.success) { saveNotes([]); refreshBadge(); renderPad(); status.textContent = T.sentOk; panel.appendChild(status); } else { throw new Error((result && result.error) || 'unknown'); } }).catch(function (error) { submitButton.disabled = false; submitButton.textContent = T.submit; status.textContent = T.sentFail + error.message; panel.appendChild(status); }); }); row.appendChild(submitButton); } panel.appendChild(row); } toggle.addEventListener('click', function () { if (panel.hidden) { renderForm(); panel.hidden = false; } else { panel.hidden = true; } }); refreshBadge(); if (document.body) { document.body.appendChild(root); } else { document.addEventListener('DOMContentLoaded', function () { document.body.appendChild(root); }); } })();