better rum todo
This commit is contained in:
1 parent
cb3266bf33
commit
e81b8c2fcd
6 files changed
+199
-19
No files matched your search
@@ -39,6 +39,8 @@ type todoPayload struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
UserAgent string `json:"ua"`
|
UserAgent string `json:"ua"`
|
||||||
Session string `json:"session"`
|
Session string `json:"session"`
|
||||||
|
ElementSelector string `json:"elementSelector"`
|
||||||
|
ElementLabel string `json:"elementLabel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleTodoCreate ingests a todo posted by otel-todo.js. It is guarded by
|
// HandleTodoCreate ingests a todo posted by otel-todo.js. It is guarded by
|
||||||
@@ -81,6 +83,8 @@ func (c *Collector) HandleTodoCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
URL: clipTo(p.URL, maxTodoMeta),
|
URL: clipTo(p.URL, maxTodoMeta),
|
||||||
UserAgent: clipTo(p.UserAgent, maxTodoMeta),
|
UserAgent: clipTo(p.UserAgent, maxTodoMeta),
|
||||||
Session: clipTo(p.Session, maxTodoMeta),
|
Session: clipTo(p.Session, maxTodoMeta),
|
||||||
|
ElementSelector: clipTo(p.ElementSelector, maxTodoMeta),
|
||||||
|
ElementLabel: clipTo(p.ElementLabel, maxTodoMeta),
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
if err := c.todoStore.Add(r.Context(), t); err != nil {
|
if err := c.todoStore.Add(r.Context(), t); err != nil {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type Todo struct {
|
|||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
UserAgent string `json:"userAgent,omitempty"`
|
UserAgent string `json:"userAgent,omitempty"`
|
||||||
Session string `json:"session,omitempty"`
|
Session string `json:"session,omitempty"`
|
||||||
|
ElementSelector string `json:"elementSelector,omitempty"` // CSS selector of the referenced element, if picked
|
||||||
|
ElementLabel string `json:"elementLabel,omitempty"` // human-readable tag/text summary of that element
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
DoneAt time.Time `json:"doneAt,omitzero"`
|
DoneAt time.Time `json:"doneAt,omitzero"`
|
||||||
@@ -154,7 +156,7 @@ func NewClickHouseStore(ctx context.Context, conn driver.Conn, db string) (*Clic
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, col := range []string{"done UInt8", "done_at DateTime64(3, 'UTC')"} {
|
for _, col := range []string{"done UInt8", "done_at DateTime64(3, 'UTC')", "element_selector String", "element_label String"} {
|
||||||
if err := conn.Exec(ctx, fmt.Sprintf(`ALTER TABLE %s.todos ADD COLUMN IF NOT EXISTS %s`, db, col)); err != nil {
|
if err := conn.Exec(ctx, fmt.Sprintf(`ALTER TABLE %s.todos ADD COLUMN IF NOT EXISTS %s`, db, col)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -163,12 +165,12 @@ func NewClickHouseStore(ctx context.Context, conn driver.Conn, db string) (*Clic
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClickHouseStore) Add(ctx context.Context, t Todo) error {
|
func (s *ClickHouseStore) Add(ctx context.Context, t Todo) error {
|
||||||
return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.todos (id, project, text, component, path, hash, url, user_agent, session, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, s.db),
|
return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.todos (id, project, text, component, path, hash, url, user_agent, session, element_selector, element_label, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, s.db),
|
||||||
t.ID, t.Project, t.Text, t.Component, t.Path, t.Hash, t.URL, t.UserAgent, t.Session, t.CreatedAt.UTC())
|
t.ID, t.Project, t.Text, t.Component, t.Path, t.Hash, t.URL, t.UserAgent, t.Session, t.ElementSelector, t.ElementLabel, t.CreatedAt.UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) ([]Todo, error) {
|
func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) ([]Todo, error) {
|
||||||
q := fmt.Sprintf(`SELECT id, project, text, component, path, hash, url, user_agent, session, created_at, done, done_at FROM %s.todos`, s.db)
|
q := fmt.Sprintf(`SELECT id, project, text, component, path, hash, url, user_agent, session, element_selector, element_label, created_at, done, done_at FROM %s.todos`, s.db)
|
||||||
var args []any
|
var args []any
|
||||||
if project != "" {
|
if project != "" {
|
||||||
q += ` WHERE project = ?`
|
q += ` WHERE project = ?`
|
||||||
@@ -185,7 +187,7 @@ func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) (
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var t Todo
|
var t Todo
|
||||||
var done uint8
|
var done uint8
|
||||||
if err := rows.Scan(&t.ID, &t.Project, &t.Text, &t.Component, &t.Path, &t.Hash, &t.URL, &t.UserAgent, &t.Session, &t.CreatedAt, &done, &t.DoneAt); err != nil {
|
if err := rows.Scan(&t.ID, &t.Project, &t.Text, &t.Component, &t.Path, &t.Hash, &t.URL, &t.UserAgent, &t.Session, &t.ElementSelector, &t.ElementLabel, &t.CreatedAt, &done, &t.DoneAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.Done = done != 0
|
t.Done = done != 0
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import type { LucideIcon } from "lucide-react"
|
import type { LucideIcon } from "lucide-react"
|
||||||
import { MetricsVisualizer } from "./MetricsVisualizer"
|
import { MetricsVisualizer } from "./MetricsVisualizer"
|
||||||
@@ -398,11 +399,27 @@ export const TabsComponent = () => {
|
|||||||
{statusNotice && (
|
{statusNotice && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
className={`fixed bottom-6 left-1/2 z-50 flex max-w-[min(560px,calc(100vw-120px))] -translate-x-1/2 items-center gap-3 rounded-[10px] border bg-ds-surface px-4 py-2.5 text-[13px] ${
|
className={`fixed bottom-10 left-1/2 z-50 flex max-w-[min(560px,calc(100vw-120px))] -translate-x-1/2 items-center gap-3 overflow-hidden rounded-2xl border bg-ds-surface/90 py-2.5 pl-2.5 pr-4 text-[13px] shadow-[0_8px_30px_-8px_rgba(0,0,0,0.45)] backdrop-blur-md animate-in fade-in slide-in-from-bottom-3 duration-300 ${
|
||||||
statusNotice.error ? "border-ds-critical/40 text-ds-critical" : "border-ds-line-strong text-ds-text"
|
statusNotice.error ? "border-ds-critical/30" : "border-ds-ok/25"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{statusNotice.text}</span>
|
<span
|
||||||
|
className={`pointer-events-none absolute inset-x-0 top-0 h-[2px] ${
|
||||||
|
statusNotice.error ? "bg-ds-critical/70" : "bg-ds-ok/70"
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full ${
|
||||||
|
statusNotice.error ? "bg-ds-critical-bg text-ds-critical" : "bg-ds-ok/15 text-ds-ok"
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{statusNotice.error ? <AlertTriangle className="h-3.5 w-3.5" /> : <CheckCircle2 className="h-3.5 w-3.5" />}
|
||||||
|
</span>
|
||||||
|
<span className={`truncate ${statusNotice.error ? "text-ds-critical" : "text-ds-text"}`}>
|
||||||
|
{statusNotice.text}
|
||||||
|
</span>
|
||||||
{statusNotice.undo && (
|
{statusNotice.undo && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -410,11 +427,19 @@ export const TabsComponent = () => {
|
|||||||
statusNotice.undo?.()
|
statusNotice.undo?.()
|
||||||
setStatusNotice(null)
|
setStatusNotice(null)
|
||||||
}}
|
}}
|
||||||
className="shrink-0 font-semibold text-ds-accent hover:underline"
|
className="shrink-0 rounded-full border border-ds-accent/30 bg-ds-accent-bg px-2.5 py-1 font-semibold text-ds-accent transition-colors hover:bg-ds-accent hover:text-ds-on-accent"
|
||||||
>
|
>
|
||||||
Undo
|
Undo
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStatusNotice(null)}
|
||||||
|
aria-label="Dismiss"
|
||||||
|
className="shrink-0 rounded-full p-1 text-ds-text-3 transition-colors hover:bg-ds-fill hover:text-ds-text"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { CheckCircle2, ExternalLink, ListTodo, RefreshCw, RotateCcw } from "lucide-react"
|
import { CheckCircle2, Crosshair, ExternalLink, ListTodo, RefreshCw, RotateCcw } from "lucide-react"
|
||||||
import { useTodos } from "../hooks/useTodos"
|
import { useTodos } from "../hooks/useTodos"
|
||||||
import { formatRelativeTime } from "../lib/timeUtils"
|
import { formatRelativeTime } from "../lib/timeUtils"
|
||||||
import type { Todo } from "../lib/types"
|
import type { Todo } from "../lib/types"
|
||||||
@@ -16,9 +16,20 @@ const TodoCard = ({ todo, onDone }: { todo: Todo; onDone: (todo: Todo, done: boo
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<p className={`min-w-0 flex-1 whitespace-pre-wrap break-words text-[14px] ${todo.done ? "text-ds-text-3 line-through" : "text-ds-text"}`}>
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className={`whitespace-pre-wrap break-words text-[14px] ${todo.done ? "text-ds-text-3 line-through" : "text-ds-text"}`}>
|
||||||
{todo.text}
|
{todo.text}
|
||||||
</p>
|
</p>
|
||||||
|
{todo.elementSelector && (
|
||||||
|
<div
|
||||||
|
title={todo.elementSelector}
|
||||||
|
className="mt-2 inline-flex max-w-full items-center gap-1.5 rounded-[7px] border border-ds-accent/25 bg-ds-accent-bg px-2 py-1 text-[11px] text-ds-accent"
|
||||||
|
>
|
||||||
|
<Crosshair className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||||
|
<span className="truncate font-[family-name:var(--font-data)]">{todo.elementLabel || todo.elementSelector}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
{todo.url && (
|
{todo.url && (
|
||||||
<a
|
<a
|
||||||
@@ -141,7 +152,7 @@ export const TodosVisualizer = () => {
|
|||||||
</p>
|
</p>
|
||||||
{todos.length === 0 && !loading && (
|
{todos.length === 0 && !loading && (
|
||||||
<p className="max-w-sm text-[12.5px] text-ds-text-3">
|
<p className="max-w-sm text-[12.5px] text-ds-text-3">
|
||||||
Press Alt+Shift+T on a page running otel-rum.js to file one.
|
Press Alt+Shift+T on a page running otel-rum.js to file one — click "Reference an element" in that dialog to point at what needs to change.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -347,6 +347,8 @@ export interface Todo {
|
|||||||
url?: string;
|
url?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
session?: string;
|
session?: string;
|
||||||
|
elementSelector?: string;
|
||||||
|
elementLabel?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
doneAt?: string;
|
doneAt?: string;
|
||||||
|
|||||||
+143
-7
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* otel-todo.js - file todos / issues straight from the page.
|
* otel-todo.js - file todos / issues straight from the page.
|
||||||
*
|
*
|
||||||
* Shows a small floating symbol and listens for a keyboard shortcut. Either
|
* Shows a small floating symbol and listens for a keyboard shortcut. Opens a
|
||||||
* opens a native <dialog> in the bottom-right corner with a text box and an
|
* native <dialog> in the bottom-right corner with a text box, an optional
|
||||||
* optional component picker, and POSTs the note (with the current path and
|
* component picker, and an "Reference an element" toggle that lets you click
|
||||||
* hash) to the collector's /v1/todos endpoint.
|
* 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"
|
* <script src="https://otel.example.com/static/otel-todo.js"
|
||||||
* data-service-name="checkout-web"
|
* data-service-name="checkout-web"
|
||||||
@@ -65,7 +67,22 @@
|
|||||||
'dialog.otel-todo textarea{min-height:90px;resize:vertical}' +
|
'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 .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 .meta{opacity:.65;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
|
||||||
'dialog.otel-todo .status{margin-right:auto;font-size:12px}';
|
'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);
|
document.head.appendChild(css);
|
||||||
|
|
||||||
var dlg = document.createElement('dialog');
|
var dlg = document.createElement('dialog');
|
||||||
@@ -74,6 +91,8 @@
|
|||||||
dlg.innerHTML =
|
dlg.innerHTML =
|
||||||
'<form method="dialog">' +
|
'<form method="dialog">' +
|
||||||
'<div class="meta"></div>' +
|
'<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>' +
|
'<textarea name="text" placeholder="What needs doing?" required maxlength="4000"></textarea>' +
|
||||||
'<select name="component" aria-label="Component (optional)"></select>' +
|
'<select name="component" aria-label="Component (optional)"></select>' +
|
||||||
'<div class="row"><span class="status" role="status"></span>' +
|
'<div class="row"><span class="status" role="status"></span>' +
|
||||||
@@ -84,8 +103,118 @@
|
|||||||
var selectEl = form.elements.component;
|
var selectEl = form.elements.component;
|
||||||
var statusEl = form.querySelector('.status');
|
var statusEl = form.querySelector('.status');
|
||||||
var metaEl = form.querySelector('.meta');
|
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);
|
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() {
|
function components() {
|
||||||
var seen = {};
|
var seen = {};
|
||||||
var list = [];
|
var list = [];
|
||||||
@@ -113,12 +242,16 @@
|
|||||||
selectEl.hidden = list.length === 0;
|
selectEl.hidden = list.length === 0;
|
||||||
metaEl.textContent = project + ' · ' + location.pathname + location.hash;
|
metaEl.textContent = project + ' · ' + location.pathname + location.hash;
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
|
setRef(null);
|
||||||
// Non-modal so the page stays usable while the note is being written.
|
// Non-modal so the page stays usable while the note is being written.
|
||||||
dlg.show();
|
dlg.show();
|
||||||
textEl.focus();
|
textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() { if (dlg.open) dlg.close(); }
|
function close() {
|
||||||
|
stopPicking();
|
||||||
|
if (dlg.open) dlg.close();
|
||||||
|
}
|
||||||
|
|
||||||
form.querySelector('[data-cancel]').addEventListener('click', close);
|
form.querySelector('[data-cancel]').addEventListener('click', close);
|
||||||
dlg.addEventListener('keydown', function (e) {
|
dlg.addEventListener('keydown', function (e) {
|
||||||
@@ -145,11 +278,14 @@
|
|||||||
hash: location.hash,
|
hash: location.hash,
|
||||||
url: location.href,
|
url: location.href,
|
||||||
ua: navigator.userAgent,
|
ua: navigator.userAgent,
|
||||||
session: sessionId
|
session: sessionId,
|
||||||
|
elementSelector: picked ? picked.selector : '',
|
||||||
|
elementLabel: picked ? picked.label : ''
|
||||||
})
|
})
|
||||||
}).then(function (res) {
|
}).then(function (res) {
|
||||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
textEl.value = '';
|
textEl.value = '';
|
||||||
|
setRef(null);
|
||||||
close();
|
close();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusEl.textContent = 'Failed: ' + err.message;
|
statusEl.textContent = 'Failed: ' + err.message;
|
||||||
|
|||||||
Reference in new issue
Block a user