/**
* 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); });
}
})();