- New extension fe_comment (Evomedien, framework-agnostic): floating page
feedback widget served from /api/fe-comment/widget.js, note pad in localStorage, submissions mailed via /api/fe-comment/submit with recipients from the extension configuration; classic sites get the script injected via AssetCollector when the jsonOutput setting is off. An enabled master switch serves the widget EMPTY when off (no 404, no console errors) and rejects submissions - the go-live kill switch, no front-end build change needed - URL swap finished: /_json prefix middleware, SPA fallback live with a dedicated root rule (REQUEST_FILENAME of / is a directory) and a query-string exception for the TYPO3 v14 install tool entry (?__typo3_install on the site root - the backend Settings/Environment modules depend on it); /api/ as a whole stays with TYPO3 - MailMessage::send() no longer exists in v14: fe_comment and the form EmailDelivery (latently broken since the core upgrade) now send through the Mailer service. Mail transport moved to SMTP against the local exim so delivery errors surface instead of bouncing into the blackhole - exim verifies the envelope sender, which therefore must be a routable existing address (transport config lives in the untracked settings.php) - Related-product card texts editable on the product record (table rebuilt with uid/pid, TCA + inline field, one-time migration script); shared RteCompare for the module badge and the CLI comparison - batch dry-run now reports 0 writes instead of flagging every richtext field - Workbook clarification cases executed: VisionIO-SD14, VisionSC-SD14 and both Diamond OG records imported (40 fields, live-verified); ArtioSign and ArtioView created hidden (uid 109/110) via create_artio_records.php - the product table has no sorting column; WallControl_10 mapping REVERTED and parked: the dry run revealed aetria-wallcontrol already carries its own agency workbook, which WallControl_10 would have overwritten. Manifest now 75 product / 24 family / 1 open
This commit is contained in:
1 parent
6db2234851
commit
f0b0694842
28 files changed
+1272
-54
No files matched your search
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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: <script src="/api/fe-comment/widget.js" defer></script>
|
||||
* (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 =
|
||||
'<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">' +
|
||||
'<path fill="currentColor" d="M4 3h16a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H8.4L4 21.6A1 1 0 0 1 2.4 20.8V5a2 2 0 0 1 2-2Zm2.6 5.2h11m-11 4h7"' +
|
||||
' stroke="currentColor" stroke-width="1.6" stroke-linecap="round" fill="none"/></svg>';
|
||||
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); });
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user