/*!
* Golem15.Feedback embed widget.
*
* One script tag, self-configuring from its own
*
* html2canvas is lazy-loaded from cdnjs (pinned version + SRI) only when the
* widget is actually opened — this file stays hand-written and small rather
* than vendoring a ~500KB third-party library inline with no build step to
* verify it against.
*/
(function () {
'use strict';
var CURRENT_SCRIPT = document.currentScript;
var WIDGET_KEY = CURRENT_SCRIPT && CURRENT_SCRIPT.getAttribute('data-key');
var API_BASE = (function () {
var src = CURRENT_SCRIPT ? CURRENT_SCRIPT.src : '';
try {
return new URL(src).origin;
} catch (e) {
return '';
}
})();
if (!WIDGET_KEY || !API_BASE) {
return; // missing/misconfigured snippet — stay silent, never break the host page
}
// data-lang lets the host page sync the widget to its own current locale
// (e.g. Nuxt's i18n locale at page-load time — not reactive to an
// in-app language switch without a reload, since the script only reads
// this once). Anything other than 'en' falls back to 'pl'.
var LANG = (CURRENT_SCRIPT.getAttribute('data-lang') === 'en') ? 'en' : 'pl';
// Same idea as data-lang, for the per-user "hide this widget" preference:
// covers the state at page-load time (the host page already knows it —
// it rides along on the user's own /fetch response); window.G15Feedback
// .hide()/.show() handle every later change without a reload.
var pendingHidden = CURRENT_SCRIPT.getAttribute('data-hidden') === 'true';
var I18N = {
pl: {
title: 'Zgłoś opinię',
placeholder: 'Co poszło nie tak?',
emailPlaceholder: 'Email (opcjonalnie)',
typeLabel: 'Rodzaj zgłoszenia',
typeBug: 'Błąd',
typeFeature: 'Propozycja funkcji',
typeOther: 'Inne',
send: 'Wyślij',
cancel: 'Anuluj',
capturing: 'Przechwytywanie zrzutu ekranu…',
sending: 'Wysyłanie…',
pleaseDescribe: 'Opisz, co poszło nie tak.',
success: 'Dzięki za zgłoszenie!',
error: 'Nie udało się wysłać — spróbuj ponownie.',
hideWidget: 'Ukryj widget',
},
en: {
title: 'Send feedback',
placeholder: 'What went wrong?',
emailPlaceholder: 'Email (optional)',
typeLabel: 'Type',
typeBug: 'Bug',
typeFeature: 'Feature request',
typeOther: 'Other',
send: 'Send',
cancel: 'Cancel',
capturing: 'Capturing screenshot…',
sending: 'Sending…',
pleaseDescribe: 'Please describe the issue.',
success: 'Thanks for the feedback!',
error: 'Could not send feedback — please try again.',
hideWidget: 'Hide widget',
},
};
function t(key) {
return I18N[LANG][key];
}
var TYPE_KEY_MAP = { bug: 'typeBug', feature: 'typeFeature', other: 'typeOther' };
var API_ROOT = API_BASE + '/_feedback/api/v1/' + encodeURIComponent(WIDGET_KEY);
// html2canvas 1.4.1 (the upstream, unmaintained since 2022) throws on any
// element whose computed style uses a modern CSS color function —
// oklch(), oklab(), lab(), color-mix() — which is Tailwind v4's default
// palette encoding. That made every single capture on this app fail,
// regardless of page content: "Attempting to parse an unsupported color
// function 'oklch'". html2canvas-pro is an actively maintained,
// API-compatible fork that adds support for those functions; it still
// exposes itself as window.html2canvas.
var HTML2CANVAS_URL = 'https://cdn.jsdelivr.net/npm/html2canvas-pro@2.3.9/dist/html2canvas-pro.min.js';
var HTML2CANVAS_INTEGRITY = 'sha512-KGyDjROZQZxrvNCSjDB9/3H3nv6/Zl64VAmW4Dnb3oykEDApl27U+Md5h0p656d04uAnoGxB8KGUWsUI0u3sTA==';
// --- console log buffer -------------------------------------------------
// Wrapped as early as possible (top of this file, before any UI exists) so
// errors logged before the widget is ever opened are still captured.
var LOG_LIMIT = 50;
var consoleBuffer = [];
function pushLog(level, text) {
consoleBuffer.push('[' + level + '] ' + text);
if (consoleBuffer.length > LOG_LIMIT) {
consoleBuffer.shift();
}
}
['log', 'warn', 'error'].forEach(function (level) {
var original = console[level];
console[level] = function () {
try {
var text = Array.prototype.slice.call(arguments).map(function (a) {
if (typeof a === 'string') {
return a;
}
try {
return JSON.stringify(a);
} catch (e) {
return String(a);
}
}).join(' ');
pushLog(level, text);
} catch (e) {
// never let logging itself throw
}
return original.apply(console, arguments);
};
});
window.addEventListener('error', function (event) {
pushLog('error', event.message + ' (' + event.filename + ':' + event.lineno + ')');
});
// --- html2canvas lazy loader --------------------------------------------
var html2canvasPromise = null;
function loadHtml2Canvas() {
if (window.html2canvas) {
return Promise.resolve(window.html2canvas);
}
if (html2canvasPromise) {
return html2canvasPromise;
}
html2canvasPromise = new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = HTML2CANVAS_URL;
script.integrity = HTML2CANVAS_INTEGRITY;
script.crossOrigin = 'anonymous';
script.onload = function () {
resolve(window.html2canvas);
};
script.onerror = function () {
html2canvasPromise = null;
reject(new Error('Failed to load html2canvas'));
};
document.head.appendChild(script);
});
return html2canvasPromise;
}
// --- API calls -----------------------------------------------------------
function fetchConfig() {
return fetch(API_ROOT + '/config?lang=' + LANG, { headers: { Accept: 'application/json' } })
.then(function (res) {
if (!res.ok) {
throw new Error('config fetch failed');
}
return res.json();
})
.then(function (body) {
return body.data;
});
}
// --- widget UI -------------------------------------------------------------
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str == null ? '' : str;
return div.innerHTML;
}
// Bottom corners cover an app's own bottom nav/action bar on mobile (the
// reported problem), so a Marker.io-style vertical edge tab is offered
// as an alternative — pinned to the middle of the left/right viewport
// edge instead of a corner, out of the way of bottom chrome.
var POSITION_CSS = {
'bottom-right': {
btn: 'bottom: 20px; right: 20px; border-radius: 999px; padding: 12px 18px;',
panel: 'bottom: 76px; right: 20px;',
panelMaxWidth: 'calc(100vw - 40px)',
},
'bottom-left': {
btn: 'bottom: 20px; left: 20px; border-radius: 999px; padding: 12px 18px;',
panel: 'bottom: 76px; left: 20px;',
panelMaxWidth: 'calc(100vw - 40px)',
},
// The panel intentionally has NO `top`/`transform` here for vertical
// centering — Chromium mispositions a native