';
@@ -297,6 +321,9 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
}
$mail = GeneralUtility::makeInstance(MailMessage::class);
+ foreach ($embeds as $cid => $absolutePath) {
+ $mail->embedFromPath($absolutePath, $cid);
+ }
$mail->to(...$to)
->subject($this->settings->subject() . ' — ' . $user['name'] . ' (' . count($payload['items']) . ' item' . (count($payload['items']) === 1 ? '' : 's') . ')')
->html($html);
@@ -318,6 +345,33 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
// ---- helpers ------------------------------------------------------------
+ /**
+ * Rewrites in a note to cid: references and
+ * collects the embeds (cid => absolute path) for the mail. Images whose
+ * file is gone keep their URL made absolute, so at least the link works.
+ *
+ * @param array $embeds
+ */
+ private function embedNoteImages(string $noteHtml, array &$embeds): string
+ {
+ return (string)preg_replace_callback(
+ '/src="(\/fileadmin\/[^"]+)"/',
+ function (array $match) use (&$embeds): string {
+ $absolute = \TYPO3\CMS\Core\Core\Environment::getPublicPath() . rawurldecode($match[1]);
+ if (!is_file($absolute)) {
+ return 'src="https://' . ($_SERVER['HTTP_HOST'] ?? 'dev.vitec.com') . $match[1] . '"';
+ }
+ $cid = array_search($absolute, $embeds, true);
+ if ($cid === false) {
+ $cid = 'fecimg' . (count($embeds) + 1);
+ $embeds[$cid] = $absolute;
+ }
+ return 'src="cid:' . $cid . '"';
+ },
+ $noteHtml
+ );
+ }
+
/** @return array|null */
private function openSummary(int $userUid, bool $createIfMissing): ?array
{
diff --git a/packages/fe_comment/Classes/Service/ExtensionSettings.php b/packages/fe_comment/Classes/Service/ExtensionSettings.php
index 7a9223d..43509ed 100644
--- a/packages/fe_comment/Classes/Service/ExtensionSettings.php
+++ b/packages/fe_comment/Classes/Service/ExtensionSettings.php
@@ -68,6 +68,12 @@ final class ExtensionSettings
return (int)($this->settings['storagePid'] ?? 0);
}
+ /** Page uid of the feedback overview page; 0 = no overview link. */
+ public function overviewPid(): int
+ {
+ return (int)($this->settings['overviewPid'] ?? 0);
+ }
+
/**
* Selectable recipients, parsed from "Label ; Label2 ".
* The index doubles as the id the widget sends back.
diff --git a/packages/fe_comment/Resources/Public/Css/fe-comment.css b/packages/fe_comment/Resources/Public/Css/fe-comment.css
index 5c1c3cc..77079f9 100644
--- a/packages/fe_comment/Resources/Public/Css/fe-comment.css
+++ b/packages/fe_comment/Resources/Public/Css/fe-comment.css
@@ -1,4 +1,4 @@
-/* EXT:fe_comment widget — isolated styles, .evofc-prefixed, high z-index. */
+/* EXT:fe_comment v2 widget — isolated styles, .evofc-prefixed, high z-index. */
.evofc {
position: fixed;
@@ -50,7 +50,7 @@
right: 52px;
top: 50%;
transform: translateY(-50%);
- width: 320px;
+ width: 340px;
max-width: calc(100vw - 70px);
max-height: 80vh;
overflow: auto;
@@ -64,21 +64,74 @@
.evofc-title { font-weight: 700; margin-bottom: 10px; }
.evofc-label { display: block; font-size: 12px; color: #5a6572; margin: 8px 0 2px; }
-.evofc-url,
-.evofc-text {
+.evofc-url {
width: 100%;
box-sizing: border-box;
border: 1px solid #c9d1da;
border-radius: 6px;
padding: 6px 8px;
font: inherit;
- color: inherit;
- background: #fff;
+ background: #f3f5f7;
+ color: #5a6572;
}
-.evofc-url { background: #f3f5f7; color: #5a6572; }
-.evofc-text { resize: vertical; }
-.evofc-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
+/* Mini toolbar + rich-text area */
+.evofc-toolbar { display: flex; gap: 4px; margin-bottom: 4px; }
+.evofc-tool {
+ min-width: 28px;
+ height: 26px;
+ border: 1px solid #c9d1da;
+ border-radius: 5px;
+ background: #f6f8fa;
+ color: #1c2430;
+ font-size: 13px;
+ cursor: pointer;
+ padding: 0 6px;
+}
+.evofc-tool:hover { background: #eef1f4; }
+
+.evofc-rte {
+ width: 100%;
+ box-sizing: border-box;
+ min-height: 96px;
+ max-height: 260px;
+ overflow: auto;
+ border: 1px solid #c9d1da;
+ border-radius: 6px;
+ padding: 6px 8px;
+ background: #fff;
+ outline: none;
+}
+.evofc-rte:focus { border-color: #1f6feb; }
+.evofc-rte:empty::before {
+ content: attr(data-placeholder);
+ color: #9aa4af;
+ pointer-events: none;
+}
+.evofc-rte img { max-width: 100%; height: auto; border-radius: 4px; }
+.evofc-rte ul { margin: 4px 0; padding-left: 20px; }
+
+/* Attachment chips */
+.evofc-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
+.evofc-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ max-width: 100%;
+ border: 1px solid #e2e7ec;
+ border-radius: 12px;
+ background: #f6f8fa;
+ padding: 2px 6px 2px 10px;
+ font-size: 12px;
+}
+.evofc-chip-name {
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.evofc-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
.evofc-button {
border: 1px solid #c9d1da;
@@ -91,25 +144,16 @@
cursor: pointer;
}
.evofc-button:hover { background: #eef1f4; }
+.evofc-button:disabled { opacity: .6; cursor: default; }
.evofc-primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
.evofc-primary:hover { background: #1a5fd0; }
.evofc-primary:disabled { opacity: .6; cursor: default; }
.evofc-hint { color: #2f9e44; font-size: 12px; }
.evofc-footer { margin-top: 10px; }
-.evofc-empty, .evofc-status { margin: 8px 0 0; font-size: 13px; }
-.evofc-status { font-weight: 600; }
+.evofc-status { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: #d9463e; }
+.evofc-status:empty { display: none; }
-.evofc-list { display: flex; flex-direction: column; gap: 10px; }
-.evofc-item { border: 1px solid #e2e7ec; border-radius: 8px; padding: 8px; }
-.evofc-item-head { display: flex; justify-content: space-between; align-items: center; gap: 6px; margin-bottom: 6px; }
-.evofc-item-url {
- font-size: 12px;
- color: #5a6572;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
.evofc-remove {
border: 0;
background: none;
diff --git a/packages/fe_comment/Resources/Public/JavaScript/fe-comment.js b/packages/fe_comment/Resources/Public/JavaScript/fe-comment.js
index b90b1a8..3db0d11 100644
--- a/packages/fe_comment/Resources/Public/JavaScript/fe-comment.js
+++ b/packages/fe_comment/Resources/Public/JavaScript/fe-comment.js
@@ -1,11 +1,15 @@
/**
- * EXT:fe_comment — page feedback widget (framework-agnostic, vanilla JS).
+ * EXT:fe_comment v2 — 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.
+ * Server-backed: everything lives in the fe_comment API (/api/fe-comment/),
+ * bound to the logged-in FE user. The widget only renders for users the API
+ * accepts (GET state -> allowed) — for everyone else it stays completely
+ * invisible, so the script tag is safe on public pages.
+ *
+ * Form: current page URL + a small rich-text area (bold/italic/list) with
+ * file attachments — including Ctrl+V image paste straight into the text.
+ * Saving adds the note to the user's open summary ("note pad"); the badge
+ * mirrors the server count and the pad button links to the overview page.
*
* Integration:
* (classic TYPO3 sites get this injected by the extension automatically
@@ -16,42 +20,69 @@
if (window.__evofcLoaded) { return; }
window.__evofcLoaded = true;
- var ENDPOINT = '/api/fe-comment/submit';
+ var API = '/api/fe-comment/';
var CSS_URL = '/api/fe-comment/widget.css';
- var STORAGE_KEY = 'evoFeCommentNotes';
+ var MAX_UPLOAD = 20 * 1024 * 1024;
- 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: '
+ // ---- i18n (English is the default, German when the page says so) -------
+ var LANG = (document.documentElement.lang || 'en').toLowerCase().indexOf('de') === 0 ? 'de' : 'en';
+ var TEXTS = {
+ en: {
+ open: 'Comment on this page',
+ formTitle: 'Feedback on this page',
+ urlLabel: 'Page',
+ noteLabel: 'Note',
+ placeholder: 'What did you notice? Paste screenshots right here (Ctrl+V).',
+ bold: 'Bold', italic: 'Italic', list: 'List',
+ attach: 'Attach file',
+ save: 'Add to note pad',
+ saving: 'Saving …',
+ savedHint: 'Saved ✓',
+ pad: 'Note pad',
+ uploadTooBig: 'File is larger than 20 MB.',
+ uploadFail: 'Upload failed: ',
+ saveFail: 'Saving failed: ',
+ removeAttachment: 'Remove attachment'
+ },
+ de: {
+ open: 'Seite kommentieren',
+ formTitle: 'Anmerkung zu dieser Seite',
+ urlLabel: 'Seite',
+ noteLabel: 'Anmerkung',
+ placeholder: 'Was ist dir aufgefallen? Screenshots einfach hier einfügen (Strg+V).',
+ bold: 'Fett', italic: 'Kursiv', list: 'Liste',
+ attach: 'Datei anhängen',
+ save: 'Auf den Merkzettel',
+ saving: 'Speichere …',
+ savedHint: 'Gespeichert ✓',
+ pad: 'Merkzettel',
+ uploadTooBig: 'Datei ist größer als 20 MB.',
+ uploadFail: 'Upload fehlgeschlagen: ',
+ saveFail: 'Speichern fehlgeschlagen: ',
+ removeAttachment: 'Anhang entfernen'
+ }
};
+ var T = TEXTS[LANG];
- function loadNotes() {
- try {
- var raw = localStorage.getItem(STORAGE_KEY);
- var list = raw ? JSON.parse(raw) : [];
- return Array.isArray(list) ? list : [];
- } catch (e) { return []; }
+ // ---- API helpers --------------------------------------------------------
+ function api(path, options) {
+ options = options || {};
+ options.credentials = 'same-origin';
+ return fetch(API + path, options).then(function (response) {
+ return response.json();
+ }).then(function (result) {
+ if (result && result.error) { throw new Error(result.error); }
+ return result;
+ });
}
- function saveNotes(notes) {
- try { localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); } catch (e) { /* private mode */ }
- }
- function currentUrl() {
- return location.pathname + location.search;
+ function apiJson(path, payload) {
+ return api(path, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
}
+
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) { node.className = className; }
@@ -59,200 +90,265 @@
return node;
}
- // ---- styles ------------------------------------------------------------
- var link = document.createElement('link');
- link.rel = 'stylesheet';
- link.href = CSS_URL;
- document.head.appendChild(link);
+ // ---- state --------------------------------------------------------------
+ var serverState = null; // last GET state payload
+ var draftUid = 0; // feedback uid of the form's unsaved draft (0 = none)
+ var draftAttachments = []; // [{refUid, name}] of the draft
- // ---- structure ---------------------------------------------------------
- var root = el('div', 'evofc');
- root.setAttribute('data-evofc', '');
+ function itemCount() {
+ return serverState && serverState.summary ? serverState.summary.items.length : 0;
+ }
- 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);
+ function refreshState() {
+ return api('state').then(function (state) {
+ serverState = state;
+ refreshBadge();
+ return state;
+ });
+ }
- var panel = el('div', 'evofc-panel');
- panel.hidden = true;
+ // ---- bootstrap: only build the UI for allowed users ----------------------
+ api('state').then(function (state) {
+ if (!state || !state.allowed) { return; }
+ serverState = state;
+ injectCss();
+ buildUi();
+ }).catch(function () { /* API unreachable - stay invisible */ });
- root.appendChild(toggle);
- root.appendChild(panel);
+ function injectCss() {
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = CSS_URL;
+ document.head.appendChild(link);
+ }
- // ---- views -------------------------------------------------------------
- var view = 'form'; // 'form' | 'pad'
+ // ---- UI -------------------------------------------------------------------
+ var root, toggle, badge, panel;
+
+ function buildUi() {
+ root = el('div', 'evofc');
+ root.setAttribute('data-evofc', '');
+
+ toggle = el('button', 'evofc-toggle');
+ toggle.type = 'button';
+ toggle.title = T.open;
+ toggle.setAttribute('aria-label', T.open);
+ toggle.innerHTML =
+ '';
+ badge = el('span', 'evofc-badge');
+ badge.hidden = true;
+ toggle.appendChild(badge);
+
+ panel = el('div', 'evofc-panel');
+ panel.hidden = true;
+
+ toggle.addEventListener('click', function () {
+ if (panel.hidden) {
+ renderForm();
+ panel.hidden = false;
+ } else {
+ panel.hidden = true;
+ }
+ });
+
+ root.appendChild(toggle);
+ root.appendChild(panel);
+ refreshBadge();
+ if (document.body) {
+ document.body.appendChild(root);
+ } else {
+ document.addEventListener('DOMContentLoaded', function () { document.body.appendChild(root); });
+ }
+ }
function refreshBadge() {
- var count = loadNotes().length;
+ if (!badge) { return; }
+ var count = itemCount();
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);
+ panel.appendChild(el('label', 'evofc-label', T.urlLabel));
var urlInput = el('input', 'evofc-url');
urlInput.type = 'text';
urlInput.readOnly = true;
- urlInput.value = currentUrl();
+ urlInput.value = location.href;
+ panel.appendChild(urlInput);
- var commentLabel = el('label', 'evofc-label', T.commentLabel);
- var textarea = el('textarea', 'evofc-text');
- textarea.rows = 4;
- textarea.placeholder = T.placeholder;
+ panel.appendChild(el('label', 'evofc-label', T.noteLabel));
+
+ // Mini toolbar + contenteditable rich-text area
+ var toolbar = el('div', 'evofc-toolbar');
+ var editor = el('div', 'evofc-rte');
+ editor.contentEditable = 'true';
+ editor.setAttribute('data-placeholder', T.placeholder);
+ [['bold', 'B', T.bold],
+ ['italic', 'I', T.italic],
+ ['insertUnorderedList', '☰', T.list]].forEach(function (def) {
+ var button = el('button', 'evofc-tool');
+ button.type = 'button';
+ button.innerHTML = def[1];
+ button.title = def[2];
+ // mousedown + preventDefault keeps the editor selection alive
+ button.addEventListener('mousedown', function (event) {
+ event.preventDefault();
+ editor.focus();
+ document.execCommand(def[0], false, null);
+ });
+ toolbar.appendChild(button);
+ });
+ panel.appendChild(toolbar);
+ panel.appendChild(editor);
+
+ var status = el('div', 'evofc-status');
+ var chips = el('div', 'evofc-chips');
+ panel.appendChild(chips);
+ renderChips(chips, status);
+
+ // Ctrl+V: image in the clipboard -> upload + inline
+ editor.addEventListener('paste', function (event) {
+ var items = (event.clipboardData || {}).items || [];
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].kind === 'file' && items[i].type.indexOf('image/') === 0) {
+ event.preventDefault();
+ var file = items[i].getAsFile();
+ uploadFile(file, status, chips, function (attachment) {
+ editor.focus();
+ document.execCommand('insertHTML', false,
+ '');
+ });
+ return;
+ }
+ }
+ });
+
+ // Attach button + hidden file input
+ var fileInput = el('input');
+ fileInput.type = 'file';
+ fileInput.hidden = true;
+ fileInput.addEventListener('change', function () {
+ if (fileInput.files && fileInput.files[0]) {
+ uploadFile(fileInput.files[0], status, chips, null);
+ fileInput.value = '';
+ }
+ });
+ var attachButton = el('button', 'evofc-button', T.attach);
+ attachButton.type = 'button';
+ attachButton.addEventListener('click', function () { fileInput.click(); });
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 note = editor.innerHTML;
+ var isEmpty = editor.textContent.trim() === '' && editor.querySelector('img') === null;
+ if (isEmpty && draftUid === 0) { editor.focus(); return; }
+ saveButton.disabled = true;
+ saveButton.textContent = T.saving;
+ var request = draftUid > 0
+ ? apiJson('feedback/' + draftUid, { note: note, url: urlInput.value })
+ : apiJson('feedback', { note: note, url: urlInput.value });
+ request.then(function () {
+ draftUid = 0;
+ draftAttachments = [];
+ editor.innerHTML = '';
+ renderChips(chips, status);
+ return refreshState();
+ }).then(function () {
+ renderFooter(footer);
+ hint.textContent = T.savedHint;
+ setTimeout(function () { hint.textContent = ''; }, 1800);
+ }).catch(function (error) {
+ status.textContent = T.saveFail + error.message;
+ }).finally(function () {
+ saveButton.disabled = false;
+ saveButton.textContent = T.save;
+ });
});
- 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(attachButton);
row.appendChild(saveButton);
row.appendChild(hint);
panel.appendChild(row);
+ panel.appendChild(fileInput);
+ panel.appendChild(status);
+
+ var footer = el('div', 'evofc-footer');
+ renderFooter(footer);
panel.appendChild(footer);
}
+ /** The draft row must exist before the first upload (uploads attach to it). */
+ function ensureDraft(urlValue) {
+ if (draftUid > 0) { return Promise.resolve(draftUid); }
+ return apiJson('feedback', { url: urlValue, note: '' }).then(function (result) {
+ draftUid = result.uid;
+ return refreshState().then(function () { return draftUid; });
+ });
+ }
+
+ function uploadFile(file, status, chips, onDone) {
+ if (!file) { return; }
+ if (file.size > MAX_UPLOAD) {
+ status.textContent = T.uploadTooBig;
+ return;
+ }
+ status.textContent = '';
+ ensureDraft(location.href).then(function (uid) {
+ var body = new FormData();
+ body.append('file', file, file.name || 'pasted-image.png');
+ body.append('feedback', String(uid));
+ return api('upload', { method: 'POST', body: body });
+ }).then(function (result) {
+ draftAttachments.push({ refUid: result.attachment.refUid, name: result.attachment.name });
+ renderChips(chips, status);
+ if (onDone) { onDone(result.attachment); }
+ }).catch(function (error) {
+ status.textContent = T.uploadFail + error.message;
+ });
+ }
+
+ function renderChips(chips, status) {
+ chips.innerHTML = '';
+ draftAttachments.forEach(function (attachment, index) {
+ var chip = el('span', 'evofc-chip');
+ chip.appendChild(el('span', 'evofc-chip-name', attachment.name));
+ var removeButton = el('button', 'evofc-remove', '×');
+ removeButton.type = 'button';
+ removeButton.title = T.removeAttachment;
+ removeButton.addEventListener('click', function () {
+ apiJson('attachment/' + attachment.refUid + '/delete', {}).then(function () {
+ draftAttachments.splice(index, 1);
+ renderChips(chips, status);
+ }).catch(function (error) {
+ status.textContent = error.message;
+ });
+ });
+ chip.appendChild(removeButton);
+ chips.appendChild(chip);
+ });
+ }
+
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();
+ var count = itemCount();
+ if (count === 0) { return; }
+ var padButton = el('button', 'evofc-button', T.pad + ' (' + count + ')');
+ padButton.type = 'button';
+ if (serverState.overviewUrl) {
+ padButton.addEventListener('click', function () {
+ location.href = serverState.overviewUrl;
});
- 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;
+ padButton.disabled = true;
}
- });
-
- refreshBadge();
- if (document.body) {
- document.body.appendChild(root);
- } else {
- document.addEventListener('DOMContentLoaded', function () { document.body.appendChild(root); });
+ footer.appendChild(padButton);
}
})();
diff --git a/packages/fe_comment/ext_conf_template.txt b/packages/fe_comment/ext_conf_template.txt
index deff794..768f176 100644
--- a/packages/fe_comment/ext_conf_template.txt
+++ b/packages/fe_comment/ext_conf_template.txt
@@ -24,3 +24,6 @@ recipients =
# cat=basic//20; type=string; label=Storage pid: page/folder uid the feedback records are stored on (backend visibility). 0 = root.
storagePid = 0
+
+# cat=basic//30; type=string; label=Overview page (uid): page showing the feedback overview; its URL is delivered to the widget as the note pad link. 0 = no link.
+overviewPid = 0
diff --git a/public/widget-test.html b/public/widget-test.html
new file mode 100644
index 0000000..58474db
--- /dev/null
+++ b/public/widget-test.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+fe_comment widget test
+
+
+
+
fe_comment v2 — Widget-Testseite
+
Diese Seite tut, was spaeter die React-App tut: felogin-Formular
+per JSON holen (inkl. CSRF-Token), Login-POST, dann erscheint rechts das
+Feedback-Widget (nur fuer berechtigte FE-User).