Files
go-otel/static/otel-todo.js
mats e81b8c2fcd
Build and Publish / Metadata (push) Successful in 10s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m53s
better rum todo
2026-09-22 09:15:00 +02:00

328 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* otel-todo.js - file todos / issues straight from the page.
*
* Shows a small floating symbol and listens for a keyboard shortcut. Opens a
* native <dialog> in the bottom-right corner with a text box, an optional
* component picker, and an "Reference an element" toggle that lets you click
* any node on the page (with a hover outline) to attach its CSS selector and
* a short label to the note. POSTs the note (with the current path, hash and
* any picked element) to the collector's /v1/todos endpoint.
*
* <script src="https://otel.example.com/static/otel-todo.js"
* data-service-name="checkout-web"
* data-ingest-key="..."
* data-components="Header,Cart,Payment"></script>
*
* Attributes (or OtelTodo.init({...}) keys): data-base-url, data-service-name
* (the project), data-ingest-key, data-components (comma list, also
* discovered from [data-component] elements on the page), data-shortcut
* (default "alt+shift+t"), data-todo-endpoint, data-hide-button="true".
*/
(function (global) {
'use strict';
var DEFAULT_BASE_URL = 'https://otel.k6n.net';
var scriptEl = document.currentScript;
function attr(name) {
return (scriptEl && scriptEl.getAttribute(name)) || '';
}
function parseShortcut(s) {
var parts = String(s).toLowerCase().split('+');
var sc = { key: parts.pop(), alt: false, shift: false, ctrl: false, meta: false };
parts.forEach(function (p) {
if (p === 'alt' || p === 'option') sc.alt = true;
else if (p === 'shift') sc.shift = true;
else if (p === 'ctrl' || p === 'control') sc.ctrl = true;
else if (p === 'meta' || p === 'cmd') sc.meta = true;
});
return sc;
}
function init(config) {
config = config || {};
if (global.OtelTodo && global.OtelTodo.instance) return global.OtelTodo.instance;
var baseUrl = (config.baseUrl || attr('data-base-url') || DEFAULT_BASE_URL).replace(/\/+$/, '');
var endpoint = config.endpoint || attr('data-todo-endpoint') || baseUrl + '/v1/todos';
var project = config.serviceName || attr('data-service-name') || attr('data-service') || 'frontend';
var ingestKey = config.ingestKey || attr('data-ingest-key');
var shortcut = parseShortcut(config.shortcut || attr('data-shortcut') || 'alt+shift+t');
var hideButton = config.hideButton || attr('data-hide-button') === 'true';
var staticComponents = config.components || attr('data-components').split(',');
staticComponents = staticComponents.map(function (c) { return c.trim(); }).filter(Boolean);
var sessionId = '';
try { sessionId = global.sessionStorage.getItem('otel-rum-session') || ''; } catch (e) {}
var css = document.createElement('style');
css.textContent =
'.otel-todo-btn{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:32px;height:32px;border-radius:50%;' +
'border:1px solid #8886;background:#fffd;color:#333;font:16px/1 system-ui,sans-serif;cursor:pointer;opacity:.55;padding:0}' +
'.otel-todo-btn:hover,.otel-todo-btn:focus-visible{opacity:1}' +
'dialog.otel-todo{position:fixed;inset:auto 16px 60px auto;margin:0;width:min(340px,calc(100vw - 32px));padding:12px;' +
'border:1px solid #8886;border-radius:8px;background:Canvas;color:CanvasText;font:13px system-ui,sans-serif}' +
'dialog.otel-todo *{box-sizing:border-box;font:inherit}' +
'dialog.otel-todo textarea,dialog.otel-todo select{display:block;width:100%;margin:6px 0;padding:6px}' +
'dialog.otel-todo textarea{min-height:90px;resize:vertical}' +
'dialog.otel-todo .row{display:flex;gap:8px;justify-content:flex-end;align-items:center}' +
'dialog.otel-todo .meta{opacity:.65;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'dialog.otel-todo .status{margin-right:auto;font-size:12px}' +
'.otel-todo-pick{display:inline-flex;align-items:center;gap:5px;border:1px solid #8886;border-radius:6px;' +
'background:transparent;color:inherit;padding:5px 8px;margin:0 0 6px;cursor:pointer;font-size:12px}' +
'.otel-todo-pick:hover,.otel-todo-pick:focus-visible{background:#8882}' +
'.otel-todo-pick[aria-pressed="true"]{background:#3b82f633;border-color:#3b82f6aa;color:#3b82f6}' +
'.otel-todo-ref{display:none;align-items:center;gap:6px;margin:0 0 6px;padding:5px 7px;border:1px solid #8886;' +
'border-radius:6px;background:#8881;font:11px/1.3 ui-monospace,monospace}' +
'.otel-todo-ref.show{display:flex}' +
'.otel-todo-ref span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.otel-todo-ref button{border:0;background:none;cursor:pointer;color:inherit;opacity:.6;font:12px/1 inherit;padding:0 2px}' +
'.otel-todo-ref button:hover{opacity:1}' +
'.otel-todo-hover-outline{position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #3b82f6;' +
'background:#3b82f626;border-radius:3px;transition:all .05s linear}' +
'.otel-todo-pick-hint{position:fixed;left:50%;top:16px;transform:translateX(-50%);z-index:2147483647;' +
'background:#3b82f6;color:#fff;font:12px/1 system-ui,sans-serif;padding:7px 12px;border-radius:20px;' +
'box-shadow:0 4px 14px #0004}';
document.head.appendChild(css);
var dlg = document.createElement('dialog');
dlg.className = 'otel-todo';
dlg.setAttribute('aria-label', 'Add todo');
dlg.innerHTML =
'<form method="dialog">' +
'<div class="meta"></div>' +
'<button type="button" class="otel-todo-pick" aria-pressed="false">⌖ Reference an element</button>' +
'<div class="otel-todo-ref"><span></span><button type="button" data-clear-ref aria-label="Remove reference">✕</button></div>' +
'<textarea name="text" placeholder="What needs doing?" required maxlength="4000"></textarea>' +
'<select name="component" aria-label="Component (optional)"></select>' +
'<div class="row"><span class="status" role="status"></span>' +
'<button type="button" data-cancel>Cancel</button><button type="submit">Add</button></div>' +
'</form>';
var form = dlg.firstChild;
var textEl = form.elements.text;
var selectEl = form.elements.component;
var statusEl = form.querySelector('.status');
var metaEl = form.querySelector('.meta');
var pickBtn = form.querySelector('.otel-todo-pick');
var refEl = form.querySelector('.otel-todo-ref');
var refLabelEl = refEl.querySelector('span');
document.body.appendChild(dlg);
// --- element picker: click a node on the page to attach its selector ---
var picked = null; // { selector, label }
var hoverBox = null;
var picking = false;
var hint = null;
function cssPath(el) {
if (!(el instanceof Element)) return '';
var parts = [];
var node = el;
while (node && node.nodeType === 1 && parts.length < 6) {
if (node.id) { parts.unshift('#' + CSS.escape(node.id)); break; }
var part = node.tagName.toLowerCase();
var cls = (node.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean).slice(0, 2);
if (cls.length) part += '.' + cls.map(function (c) { return CSS.escape(c); }).join('.');
var parent = node.parentElement;
if (parent) {
var siblings = [].filter.call(parent.children, function (c) { return c.tagName === node.tagName; });
if (siblings.length > 1) part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
}
parts.unshift(part);
node = parent;
}
return parts.join(' > ');
}
function describeElement(el) {
var tag = el.tagName.toLowerCase();
var text = (el.getAttribute('aria-label') || el.textContent || '').replace(/\s+/g, ' ').trim();
if (text.length > 60) text = text.slice(0, 60) + '…';
return { selector: cssPath(el), label: text ? tag + ' – "' + text + '"' : tag };
}
function setRef(ref) {
picked = ref;
if (ref) {
refLabelEl.textContent = ref.label;
refLabelEl.title = ref.selector;
refEl.classList.add('show');
} else {
refEl.classList.remove('show');
}
}
function ensureHoverBox() {
if (!hoverBox) {
hoverBox = document.createElement('div');
hoverBox.className = 'otel-todo-hover-outline';
document.body.appendChild(hoverBox);
}
return hoverBox;
}
function onPickMove(e) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) { ensureHoverBox().style.display = 'none'; return; }
var r = el.getBoundingClientRect();
var box = ensureHoverBox();
box.style.display = 'block';
box.style.left = r.left + 'px';
box.style.top = r.top + 'px';
box.style.width = r.width + 'px';
box.style.height = r.height + 'px';
}
function onPickClick(e) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) return; // let dialog controls work normally
e.preventDefault();
e.stopPropagation();
setRef(describeElement(el));
stopPicking();
}
function onPickKey(e) {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); stopPicking(); }
}
function startPicking() {
if (picking) return;
picking = true;
pickBtn.setAttribute('aria-pressed', 'true');
pickBtn.textContent = '⌖ Click an element… (Esc to cancel)';
hint = document.createElement('div');
hint.className = 'otel-todo-pick-hint';
hint.textContent = 'Click the element you mean — Esc to cancel';
document.body.appendChild(hint);
document.addEventListener('mousemove', onPickMove, true);
document.addEventListener('click', onPickClick, true);
document.addEventListener('keydown', onPickKey, true);
}
function stopPicking() {
if (!picking) return;
picking = false;
pickBtn.setAttribute('aria-pressed', 'false');
pickBtn.textContent = '⌖ Reference an element';
document.removeEventListener('mousemove', onPickMove, true);
document.removeEventListener('click', onPickClick, true);
document.removeEventListener('keydown', onPickKey, true);
if (hoverBox) { hoverBox.style.display = 'none'; }
if (hint) { hint.remove(); hint = null; }
}
pickBtn.addEventListener('click', function () { picking ? stopPicking() : startPicking(); });
refEl.querySelector('[data-clear-ref]').addEventListener('click', function () { setRef(null); });
function components() {
var seen = {};
var list = [];
staticComponents.concat([].map.call(document.querySelectorAll('[data-component]'), function (el) {
return el.getAttribute('data-component');
})).forEach(function (c) {
if (c && !seen[c]) { seen[c] = true; list.push(c); }
});
return list;
}
function open() {
if (dlg.open) { textEl.focus(); return; }
selectEl.innerHTML = '';
var none = document.createElement('option');
none.value = '';
none.textContent = 'No component';
selectEl.appendChild(none);
var list = components();
list.forEach(function (c) {
var o = document.createElement('option');
o.value = o.textContent = c;
selectEl.appendChild(o);
});
selectEl.hidden = list.length === 0;
metaEl.textContent = project + ' · ' + location.pathname + location.hash;
statusEl.textContent = '';
setRef(null);
// Non-modal so the page stays usable while the note is being written.
dlg.show();
textEl.focus();
}
function close() {
stopPicking();
if (dlg.open) dlg.close();
}
form.querySelector('[data-cancel]').addEventListener('click', close);
dlg.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); form.requestSubmit(); }
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = textEl.value.trim();
if (!text) return;
var headers = { 'Content-Type': 'application/json' };
var url = endpoint;
if (ingestKey) headers['X-Otel-Ingest-Key'] = ingestKey;
statusEl.textContent = 'Sending…';
global.fetch(url, {
method: 'POST',
mode: 'cors',
headers: headers,
body: JSON.stringify({
project: project,
text: text,
component: selectEl.value,
path: location.pathname,
hash: location.hash,
url: location.href,
ua: navigator.userAgent,
session: sessionId,
elementSelector: picked ? picked.selector : '',
elementLabel: picked ? picked.label : ''
})
}).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
textEl.value = '';
setRef(null);
close();
}).catch(function (err) {
statusEl.textContent = 'Failed: ' + err.message;
});
});
global.addEventListener('keydown', function (e) {
var k = e.key.toLowerCase();
var code = (e.code || '').replace(/^(Key|Digit)/, '').toLowerCase();
if ((k === shortcut.key || code === shortcut.key) && e.altKey === shortcut.alt && e.shiftKey === shortcut.shift &&
e.ctrlKey === shortcut.ctrl && e.metaKey === shortcut.meta) {
e.preventDefault();
dlg.open ? close() : open();
}
});
if (!hideButton) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'otel-todo-btn';
btn.textContent = '✎';
btn.title = 'Add todo (' + (config.shortcut || attr('data-shortcut') || 'alt+shift+t') + ')';
btn.setAttribute('aria-label', 'Add todo');
btn.addEventListener('click', function () { dlg.open ? close() : open(); });
document.body.appendChild(btn);
}
var instance = { open: open, close: close };
global.OtelTodo.instance = instance;
return instance;
}
global.OtelTodo = { init: init, instance: undefined };
if (scriptEl && (attr('data-auto-init') === 'true' || attr('data-service-name') || attr('data-service'))) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(); });
else init();
}
})(window);