@qaiddev/thumbs-embed 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -21
- package/dist/chunks/{annotate-CMq95aXG.js → annotate-D-o8xCV9.js} +23 -17
- package/dist/chunks/annotate-D-o8xCV9.js.map +1 -0
- package/dist/chunks/dom-DvdtcUS8.js +17 -0
- package/dist/chunks/dom-DvdtcUS8.js.map +1 -0
- package/dist/chunks/{loader-CYM1rxDH.js → loader-DHtXHt1z.js} +172 -156
- package/dist/chunks/loader-DHtXHt1z.js.map +1 -0
- package/dist/chunks/modal-CTbqN-h9.js +213 -0
- package/dist/chunks/modal-CTbqN-h9.js.map +1 -0
- package/dist/chunks/{recording-BVFd_ryf.js → recording-Dx8AX-BC.js} +103 -98
- package/dist/chunks/recording-Dx8AX-BC.js.map +1 -0
- package/dist/chunks/{screenshot-BkOCLuS2.js → screenshot-DYOKg2bS.js} +2 -2
- package/dist/chunks/{screenshot-BkOCLuS2.js.map → screenshot-DYOKg2bS.js.map} +1 -1
- package/dist/chunks/{targeting-1pK6nJI9.js → targeting-Dz6z1BYl.js} +2 -2
- package/dist/chunks/{targeting-1pK6nJI9.js.map → targeting-Dz6z1BYl.js.map} +1 -1
- package/dist/chunks/{video-BOYqf2Im.js → video-Bd_29-rK.js} +76 -71
- package/dist/chunks/video-Bd_29-rK.js.map +1 -0
- package/dist/embed.d.ts +11 -0
- package/dist/icons.d.ts +2 -0
- package/dist/index.js +523 -418
- package/dist/index.js.map +1 -1
- package/dist/loader.js +1 -1
- package/dist/modal.d.ts +14 -4
- package/dist/qaid.umd.cjs +12 -36
- package/dist/qaid.umd.cjs.map +1 -1
- package/dist/recording.d.ts +6 -0
- package/dist/screenshot.d.ts +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/video-capture.d.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/annotate-CMq95aXG.js.map +0 -1
- package/dist/chunks/loader-CYM1rxDH.js.map +0 -1
- package/dist/chunks/modal-Br5x7m6f.js +0 -150
- package/dist/chunks/modal-Br5x7m6f.js.map +0 -1
- package/dist/chunks/recording-BVFd_ryf.js.map +0 -1
- package/dist/chunks/video-BOYqf2Im.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"targeting-1pK6nJI9.js","sources":["../../src/element-selector.ts","../../src/targeting.ts"],"sourcesContent":["/**\n * Element selector generation utilities\n */\n\n/** Data attributes to look for (in priority order) */\nexport const DATA_ATTRS = [\"data-comp\", \"data-qa\", \"data-testid\", \"data-id\"];\n\nexport interface ElementInfo {\n selector: string;\n text: string;\n dataAttr: string | null;\n}\n\n/**\n * Find the first matching data attribute on an element or its ancestors\n */\nexport function findDataAttribute(\n element: Element,\n stopAt: Element = document.body\n): string | null {\n let current: Element | null = element;\n\n while (current && current !== stopAt) {\n for (const attr of DATA_ATTRS) {\n const value = current.getAttribute(attr);\n if (value) {\n return `${attr}=\"${value}\"`;\n }\n }\n current = current.parentElement;\n }\n\n return null;\n}\n\n/**\n * Get truncated text content from an element\n */\nexport function getTruncatedText(element: Element, maxLength = 100): string {\n const textContent = element.textContent?.trim().slice(0, maxLength) || \"\";\n return textContent.length === maxLength ? textContent + \"...\" : textContent;\n}\n\n/**\n * Get the nth-child index of an element among its siblings\n */\nfunction getNthChildIndex(element: Element): number {\n let index = 1;\n let sibling = element.previousElementSibling;\n while (sibling) {\n index++;\n sibling = sibling.previousElementSibling;\n }\n return index;\n}\n\n/**\n * Generate an nth-child path from body to the element\n * This is always a valid CSS selector and uniquely identifies the element\n * Exported for testing\n */\nexport function generateNthChildPath(element: Element): string {\n const path: string[] = [];\n let current: Element | null = element;\n\n while (current && current !== document.body && current !== document.documentElement) {\n const tag = current.tagName.toLowerCase();\n const index = getNthChildIndex(current);\n path.unshift(`${tag}:nth-child(${index})`);\n current = current.parentElement;\n }\n\n return path.length > 0 ? `body > ${path.join(\" > \")}` : \"body\";\n}\n\n/**\n * Escape special CSS characters in a string (for IDs with special chars)\n */\nfunction escapeCSSIdentifier(str: string): string {\n return str.replace(/([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^`{|}~])/g, \"\\\\$1\");\n}\n\n/**\n * Generate a selector string for an element\n * Priority: data attribute > ID > nth-child path\n */\nexport function generateSelector(element: Element): string {\n // Try data attribute first (on element or ancestors) - most stable\n const dataAttr = findDataAttribute(element);\n if (dataAttr) {\n return `[${dataAttr}]`;\n }\n\n // Try ID (escaped for special characters)\n if (element.id) {\n return `#${escapeCSSIdentifier(element.id)}`;\n }\n\n // Fall back to nth-child path - always valid and unique\n return generateNthChildPath(element);\n}\n\n/**\n * Generate complete element info for feedback\n */\nexport function generateElementInfo(element: Element): ElementInfo {\n const text = getTruncatedText(element);\n const dataAttr = findDataAttribute(element);\n const selector = generateSelector(element);\n\n return { selector, text, dataAttr };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Keyboard element targeting */\n/* */\n/* The mouse targeting path in embed.ts resolves the target with */\n/* elementFromPoint(clientX, clientY). A keyboard-activated thumb produces a */\n/* synthetic click at (0, 0), so a keyboard-only user can never point at a */\n/* real element. This controller provides a coordinate-free alternative: */\n/* Tab / arrow keys move a highlight between candidate elements (seeded from */\n/* document.activeElement), Enter / Space selects, Escape cancels. It never */\n/* hides the cursor — it drives the *native* focus ring instead, so both */\n/* sighted keyboard users and screen readers can follow the highlight. */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Default set of \"targetable\" elements for keyboard navigation: focusable\n * controls, ARIA-role'd nodes, common semantic content, and anything already\n * carrying one of the stable data attributes we key selectors off of.\n */\nexport const TARGETABLE_SELECTOR: string = [\n \"a[href]\",\n \"button\",\n \"input:not([type=hidden])\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"[tabindex]\",\n \"[role]\",\n \"img\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"p\",\n \"li\",\n \"label\",\n ...DATA_ATTRS.map((attr) => `[${attr}]`),\n].join(\",\");\n\n/** Tag names that already take keyboard focus without an added tabindex. */\nconst NATIVELY_FOCUSABLE = /^(?:A|BUTTON|INPUT|SELECT|TEXTAREA|SUMMARY)$/;\n\n/**\n * Best-effort visibility test used to keep invisible nodes out of the keyboard\n * candidate list. Deliberately layout-free (no getBoundingClientRect) so it is\n * stable in headless/test environments: it only rejects nodes that are\n * explicitly hidden via the `hidden` attribute, `aria-hidden`, or a computed\n * `display:none` / `visibility:hidden`.\n */\nexport function isElementVisible(element: Element): boolean {\n if (element.hasAttribute(\"hidden\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n const view = element.ownerDocument?.defaultView;\n if (view && typeof view.getComputedStyle === \"function\") {\n const style = view.getComputedStyle(element);\n if (style.display === \"none\" || style.visibility === \"hidden\") return false;\n }\n return true;\n}\n\nexport interface CollectTargetableOptions {\n /** Where to search for candidates (default `document.body`). */\n root?: ParentNode;\n /** CSS selector for candidate elements (default {@link TARGETABLE_SELECTOR}). */\n selector?: string;\n /** Return true to drop an element (e.g. the embed's own shadow hosts). */\n isExcluded?: (element: Element) => boolean;\n /** Visibility predicate (default {@link isElementVisible}). */\n isVisible?: (element: Element) => boolean;\n}\n\n/**\n * Collect the ordered list of elements a keyboard user can target. Document\n * order matches the natural Tab order closely enough for feedback targeting.\n */\nexport function collectTargetableElements(\n options: CollectTargetableOptions = {}\n): Element[] {\n const root = options.root ?? document.body;\n const selector = options.selector ?? TARGETABLE_SELECTOR;\n const isExcluded = options.isExcluded ?? (() => false);\n const isVisible = options.isVisible ?? isElementVisible;\n\n return Array.from(root.querySelectorAll(selector)).filter(\n (element) => !isExcluded(element) && isVisible(element)\n );\n}\n\nexport interface KeyboardTargetingOptions {\n /**\n * Explicit candidate list. When omitted, candidates are collected via\n * {@link collectTargetableElements} using `root` / `selector` / `isExcluded`\n * / `isVisible`.\n */\n candidates?: Element[];\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n root?: ParentNode;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n selector?: string;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n isExcluded?: (element: Element) => boolean;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n isVisible?: (element: Element) => boolean;\n /**\n * Element to start the highlight on. Defaults to `document.activeElement`\n * when it is one of the candidates, otherwise the first candidate.\n */\n initial?: Element | null;\n /**\n * Move native focus to the highlighted element (default true). This is what\n * keeps a screen reader / focus ring on the highlight; non-focusable nodes\n * get a temporary `tabindex=\"-1\"` that is removed on {@link stop}. The cursor\n * is never hidden.\n */\n moveFocus?: boolean;\n /** Where the keydown listener is attached (default `document`). */\n eventTarget?: Document | HTMLElement;\n /** Fired whenever the highlighted candidate changes (and once on start). */\n onHighlight?: (element: Element, index: number) => void;\n /** Fired when the user commits a selection (Enter / Space). Auto-stops first. */\n onSelect: (element: Element) => void;\n /** Fired when the user cancels (Escape). Auto-stops first. */\n onCancel?: () => void;\n}\n\nexport interface KeyboardTargetingController {\n /** The (frozen) candidate list being navigated. */\n readonly candidates: readonly Element[];\n /** Index of the highlighted candidate, or -1 when there are none. */\n getIndex(): number;\n /** The highlighted candidate, or null when there are none. */\n getCurrent(): Element | null;\n /** Highlight the next candidate (wraps to the first). */\n next(): void;\n /** Highlight the previous candidate (wraps to the last). */\n prev(): void;\n /** Highlight a specific index (wraps out-of-range values). */\n moveTo(index: number): void;\n /** Commit the current candidate (fires onSelect) and stop. */\n select(): void;\n /** Cancel (fires onCancel) and stop. */\n cancel(): void;\n /**\n * Handle a keydown. Wired automatically to `eventTarget`; exposed so a host\n * can forward events from another surface (e.g. the embed's shadow root).\n */\n handleKey(event: KeyboardEvent): void;\n /** Remove the listener and restore any temporary tabindex. Idempotent. */\n stop(): void;\n}\n\n/**\n * Start a keyboard element-targeting session. The returned controller wires its\n * own capture-phase keydown listener on `eventTarget` and emits the initial\n * highlight synchronously before returning.\n */\nexport function startKeyboardTargeting(\n options: KeyboardTargetingOptions\n): KeyboardTargetingController {\n const candidates =\n options.candidates ??\n collectTargetableElements({\n root: options.root,\n selector: options.selector,\n isExcluded: options.isExcluded,\n isVisible: options.isVisible,\n });\n\n const moveFocus = options.moveFocus ?? true;\n const target: EventTarget = options.eventTarget ?? document;\n const addedTabindex = new Set<Element>();\n\n let index = -1;\n let stopped = false;\n\n // Seed the highlight from the currently focused element when it is a\n // candidate; this makes \"Tab to a control, then hit the thumb\" land on that\n // control rather than the top of the page.\n const seed =\n options.initial !== undefined\n ? options.initial\n : typeof document !== \"undefined\"\n ? document.activeElement\n : null;\n if (seed) {\n const seedIndex = candidates.indexOf(seed);\n if (seedIndex >= 0) index = seedIndex;\n }\n if (index < 0 && candidates.length > 0) index = 0;\n\n function focusCurrent(element: Element): void {\n if (!moveFocus) return;\n const focusable = element as HTMLElement;\n if (typeof focusable.focus !== \"function\") return;\n // Make non-focusable content (p, img, li, …) programmatically focusable so\n // the native focus ring and AT follow the highlight. Never hide the cursor.\n if (\n element.getAttribute(\"tabindex\") === null &&\n !NATIVELY_FOCUSABLE.test(element.tagName)\n ) {\n element.setAttribute(\"tabindex\", \"-1\");\n addedTabindex.add(element);\n }\n try {\n focusable.focus();\n } catch {\n /* focus can throw in detached/headless cases — highlight still works */\n }\n }\n\n function emitHighlight(): void {\n const element = candidates[index];\n if (!element) return;\n focusCurrent(element);\n options.onHighlight?.(element, index);\n }\n\n function moveTo(nextIndex: number): void {\n if (stopped || candidates.length === 0) return;\n const len = candidates.length;\n index = ((nextIndex % len) + len) % len; // wrap in both directions\n emitHighlight();\n }\n\n function next(): void {\n moveTo(index + 1);\n }\n\n function prev(): void {\n moveTo(index - 1);\n }\n\n function getCurrent(): Element | null {\n return candidates[index] ?? null;\n }\n\n function stop(): void {\n if (stopped) return;\n stopped = true;\n target.removeEventListener(\"keydown\", handleKey as EventListener, true);\n addedTabindex.forEach((element) => element.removeAttribute(\"tabindex\"));\n addedTabindex.clear();\n }\n\n function select(): void {\n if (stopped) return;\n const element = getCurrent();\n stop();\n if (element) options.onSelect(element);\n }\n\n function cancel(): void {\n if (stopped) return;\n stop();\n options.onCancel?.();\n }\n\n function handleKey(event: KeyboardEvent): void {\n if (stopped) return;\n switch (event.key) {\n case \"Tab\":\n event.preventDefault();\n event.stopPropagation();\n if (event.shiftKey) prev();\n else next();\n break;\n case \"ArrowDown\":\n case \"ArrowRight\":\n event.preventDefault();\n event.stopPropagation();\n next();\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n event.preventDefault();\n event.stopPropagation();\n prev();\n break;\n case \"Enter\":\n case \" \":\n case \"Spacebar\": // legacy key value\n event.preventDefault();\n event.stopPropagation();\n select();\n break;\n case \"Escape\":\n case \"Esc\": // legacy key value\n event.preventDefault();\n event.stopPropagation();\n cancel();\n break;\n default:\n break;\n }\n }\n\n target.addEventListener(\"keydown\", handleKey as EventListener, true);\n\n // Emit the initial highlight so the caller can position its highlight box.\n if (index >= 0) emitHighlight();\n\n return {\n candidates,\n getIndex: () => index,\n getCurrent,\n next,\n prev,\n moveTo,\n select,\n cancel,\n handleKey,\n stop,\n };\n}\n","/**\n * Element-targeting subsystem — extracted from embed.ts into its own lazily\n * loaded chunk (it also carries `element-selector`, ~14 KB). It's fetched the\n * first time the user starts targeting (and pre-warmed on thumb-button hover,\n * so the reticle appears without a perceptible wait).\n *\n * The controller owns the reticle/overlay, the mouse/touch/keyboard targeting\n * flows, and the selected-element marker; it talks back to the embed through\n * the narrow `TargetingHost` interface.\n */\n\nimport {\n generateElementInfo,\n startKeyboardTargeting,\n type KeyboardTargetingController,\n} from \"./element-selector\";\nimport {\n getElementAtPointUnderOverlay,\n isEmbedElement,\n getElementBounds,\n} from \"./dom-utils\";\nimport type {\n ResolvedFeedbackConfig,\n EmbedState,\n FeedbackData,\n FeedbackType,\n SelectedBounds,\n} from \"./types\";\n\n/** A touch that moves more than this (px) is a scroll, not a tap-to-select. */\nconst TOUCH_TAP_SLOP = 12;\n\n/** The slice of the embed the targeting subsystem needs. */\nexport interface TargetingHost {\n readonly config: ResolvedFeedbackConfig;\n readonly cssVars: Record<string, string>;\n readonly state: EmbedState;\n readonly shadowHost: HTMLDivElement | null;\n readonly overlayShadowHost: HTMLDivElement | null;\n readonly boundKeyDown: (e: KeyboardEvent) => void;\n /** Live references, mutated in place. */\n readonly feedbackData: FeedbackData;\n readonly selectedBounds: SelectedBounds;\n setState(state: EmbedState): void;\n ensureOverlayHost(): ShadowRoot;\n applyVars(el: HTMLElement): void;\n announceMsg(message: string, assertive?: boolean): void;\n clearActiveThumb(): void;\n submitFeedback(): Promise<void>;\n}\n\nexport class TargetingController {\n private mousePos = { x: 0, y: 0 };\n private touchStartPos: { x: number; y: number } | null = null;\n private overlayContainer: HTMLDivElement | null = null;\n private captureLayer: HTMLDivElement | null = null;\n private crosshairH: HTMLDivElement | null = null;\n private crosshairV: HTMLDivElement | null = null;\n private scope: HTMLDivElement | null = null;\n private highlightBox: HTMLDivElement | null = null;\n private marker: HTMLDivElement | null = null;\n private keyboardController: KeyboardTargetingController | null = null;\n\n private boundMouseMove = (e: MouseEvent): void => this.handleMouseMove(e);\n private boundClick = (e: MouseEvent): void => this.handleClick(e);\n private boundTouchStart = (e: TouchEvent): void => this.handleTouchStart(e);\n private boundTouchEnd = (e: TouchEvent): void => this.handleTouchEnd(e);\n\n constructor(private host: TargetingHost) {}\n\n startPointer(type: FeedbackType, e: MouseEvent): void {\n this.host.setState(\"TARGETING\");\n this.host.feedbackData.feedbackType = type;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n\n // Track initial mouse position from the click event\n this.mousePos.x = e.clientX;\n this.mousePos.y = e.clientY;\n\n // Add class to body (light DOM — for cursor override)\n document.body.classList.add(\"qaid-targeting\");\n if (type === \"up\") {\n document.body.classList.add(\"qaid-type-up\");\n } else {\n document.body.classList.remove(\"qaid-type-up\");\n }\n\n // Set targeting colors on body so .qaid-highlight rules can resolve them\n document.body.style.setProperty(\"--qaid-positive\", this.host.cssVars[\"--qaid-positive\"]);\n document.body.style.setProperty(\"--qaid-negative\", this.host.cssVars[\"--qaid-negative\"]);\n\n this.createTargetingOverlay();\n\n // Add event listeners on document — overlay is pointer-events:none so scroll works naturally\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n document.addEventListener(\"mousemove\", this.boundMouseMove);\n document.addEventListener(\"click\", this.boundClick, true);\n // Touch: tap to select (scrolling stays enabled). touchstart is passive\n // (we only read the point); touchend is non-passive so a tap can\n // preventDefault the synthesised click that would otherwise activate the\n // targeted element.\n document.addEventListener(\"touchstart\", this.boundTouchStart, { passive: true });\n document.addEventListener(\"touchend\", this.boundTouchEnd, { passive: false });\n }\n\n /**\n * Keyboard-driven targeting. Mirrors startPointer minus the mouse plumbing:\n * no `qaid-targeting` body class (keeps the cursor visible for keyboard\n * users), no mouse reticle, and no document mouse/click listeners. The\n * KeyboardTargetingController owns Tab/Arrow/Enter/Space/Escape.\n */\n startKeyboard(type: FeedbackType): void {\n this.host.setState(\"TARGETING\");\n this.host.feedbackData.feedbackType = type;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n\n document.body.style.setProperty(\"--qaid-positive\", this.host.cssVars[\"--qaid-positive\"]);\n document.body.style.setProperty(\"--qaid-negative\", this.host.cssVars[\"--qaid-negative\"]);\n\n // Build the highlight box, then hide the mouse reticle (meaningless here)\n this.createTargetingOverlay();\n if (this.crosshairH) this.crosshairH.style.display = \"none\";\n if (this.crosshairV) this.crosshairV.style.display = \"none\";\n if (this.scope) this.scope.style.display = \"none\";\n\n this.keyboardController = startKeyboardTargeting({\n isExcluded: (el) => isEmbedElement(el),\n onHighlight: (el) => {\n const rect = el.getBoundingClientRect();\n const b = this.highlightBox;\n if (b) {\n b.style.transform = `translate(${rect.left}px, ${rect.top}px)`;\n b.style.width = `${rect.width}px`;\n b.style.height = `${rect.height}px`;\n b.style.display = \"block\";\n }\n const { text } = generateElementInfo(el);\n this.host.announceMsg(`Targeting ${text || el.tagName.toLowerCase()}`);\n },\n onSelect: (el) => this.selectKeyboardTarget(el),\n onCancel: () => this.cancel(),\n });\n }\n\n private selectKeyboardTarget(el: Element): void {\n const bounds = getElementBounds(el, 8);\n Object.assign(this.host.selectedBounds, bounds, {\n clickX: bounds.x + bounds.width / 2,\n clickY: bounds.y + bounds.height / 2,\n visible: true,\n });\n\n const { selector, text } = generateElementInfo(el);\n this.host.feedbackData.elementSelector = selector;\n this.host.feedbackData.elementText = text;\n\n // Teardown (controller has already auto-stopped before onSelect fired)\n this.removeTargetingOverlay();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n this.keyboardController = null;\n this.host.clearActiveThumb();\n\n this.host.setState(\"SELECTED\");\n this.showSelectedMarker();\n void this.host.submitFeedback();\n }\n\n private createTargetingOverlay(): void {\n const root = this.host.ensureOverlayHost();\n\n this.overlayContainer = document.createElement(\"div\");\n this.overlayContainer.className = `qaid-targeting-overlay qaid-type-${this.host.feedbackData.feedbackType}`;\n\n // Capture layer (pointer-events:none — scroll passes through naturally)\n this.captureLayer = document.createElement(\"div\");\n this.captureLayer.className = \"qaid-capture-layer\";\n\n // Vignette\n const vignette = document.createElement(\"div\");\n vignette.className = \"qaid-vignette\";\n\n // Crosshairs\n this.crosshairH = document.createElement(\"div\");\n this.crosshairH.className = \"qaid-crosshair-h\";\n\n this.crosshairV = document.createElement(\"div\");\n this.crosshairV.className = \"qaid-crosshair-v\";\n\n // Scope\n this.scope = document.createElement(\"div\");\n this.scope.className = \"qaid-scope\";\n this.scope.innerHTML = `\n <div class=\"qaid-scope-ring\"></div>\n <div class=\"qaid-scope-ring-inner\"></div>\n <div class=\"qaid-scope-dot\"></div>\n `;\n\n // Highlight box overlay (positioned over hovered elements)\n this.highlightBox = document.createElement(\"div\");\n this.highlightBox.className = \"qaid-highlight-box\";\n\n this.overlayContainer.appendChild(this.captureLayer);\n this.overlayContainer.appendChild(vignette);\n this.overlayContainer.appendChild(this.highlightBox);\n this.overlayContainer.appendChild(this.crosshairH);\n this.overlayContainer.appendChild(this.crosshairV);\n this.overlayContainer.appendChild(this.scope);\n\n // Position crosshairs immediately at current mouse position\n this.crosshairH.style.top = `${this.mousePos.y}px`;\n this.crosshairV.style.left = `${this.mousePos.x}px`;\n this.scope.style.left = `${this.mousePos.x}px`;\n this.scope.style.top = `${this.mousePos.y}px`;\n\n this.host.applyVars(this.overlayContainer);\n root.appendChild(this.overlayContainer);\n }\n\n private handleMouseMove(e: MouseEvent): void {\n this.updateReticleAt(e.clientX, e.clientY);\n }\n\n /** Move the crosshair/scope reticle and highlight the element under (x, y). */\n private updateReticleAt(x: number, y: number): void {\n this.mousePos.x = x;\n this.mousePos.y = y;\n\n if (this.crosshairH) this.crosshairH.style.top = `${y}px`;\n if (this.crosshairV) this.crosshairV.style.left = `${x}px`;\n if (this.scope) {\n this.scope.style.left = `${x}px`;\n this.scope.style.top = `${y}px`;\n }\n\n // Find element underneath — hide both shadow hosts\n if (this.captureLayer && this.host.shadowHost) {\n const hosts = [this.host.shadowHost, this.host.overlayShadowHost].filter(Boolean) as HTMLElement[];\n const elementUnder = getElementAtPointUnderOverlay(x, y, hosts);\n\n if (elementUnder && !isEmbedElement(elementUnder)) {\n if (this.highlightBox) {\n const rect = elementUnder.getBoundingClientRect();\n this.highlightBox.style.transform = `translate(${rect.left}px, ${rect.top}px)`;\n this.highlightBox.style.width = `${rect.width}px`;\n this.highlightBox.style.height = `${rect.height}px`;\n this.highlightBox.style.display = \"block\";\n }\n } else if (this.highlightBox) {\n this.highlightBox.style.display = \"none\";\n }\n }\n }\n\n private handleClick(e: MouseEvent): void {\n e.preventDefault();\n e.stopPropagation();\n this.selectAt(e.clientX, e.clientY);\n }\n\n // ---- Touch targeting (iOS/iPadOS) ----\n private handleTouchStart(e: TouchEvent): void {\n const t = e.touches[0];\n if (!t) return;\n this.touchStartPos = { x: t.clientX, y: t.clientY };\n // Preview what's under the finger; don't preventDefault so scrolling works.\n this.updateReticleAt(t.clientX, t.clientY);\n }\n\n private handleTouchEnd(e: TouchEvent): void {\n // touches is empty on touchend; the lifted point is in changedTouches.\n const t = e.changedTouches[0];\n const start = this.touchStartPos;\n this.touchStartPos = null;\n if (!t || !start) return;\n // A drag beyond the slop threshold is a scroll, not a selection — ignore it.\n const moved = Math.hypot(t.clientX - start.x, t.clientY - start.y);\n if (moved > TOUCH_TAP_SLOP) return;\n e.preventDefault(); // suppress the tap's synthesised click\n this.selectAt(t.clientX, t.clientY);\n }\n\n /** Select the element under (x, y) and tear down targeting. */\n private selectAt(x: number, y: number): void {\n const hosts = [this.host.shadowHost, this.host.overlayShadowHost].filter(Boolean) as HTMLElement[];\n const target = getElementAtPointUnderOverlay(x, y, hosts);\n\n if (!target || isEmbedElement(target)) {\n return;\n }\n\n const bounds = getElementBounds(target, 8);\n Object.assign(this.host.selectedBounds, bounds, { clickX: x, clickY: y, visible: true });\n\n const { selector, text } = generateElementInfo(target);\n this.host.feedbackData.elementSelector = selector;\n this.host.feedbackData.elementText = text;\n\n // Remove targeting overlay and document listeners\n this.stopTargetingListeners();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n\n this.host.setState(\"SELECTED\");\n this.host.clearActiveThumb();\n\n this.showSelectedMarker();\n void this.host.submitFeedback();\n }\n\n private stopTargetingListeners(): void {\n this.removeTargetingOverlay();\n this.touchStartPos = null;\n document.removeEventListener(\"mousemove\", this.boundMouseMove);\n document.removeEventListener(\"click\", this.boundClick, true);\n document.removeEventListener(\"touchstart\", this.boundTouchStart);\n document.removeEventListener(\"touchend\", this.boundTouchEnd);\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n cancel(): void {\n this.keyboardController?.stop();\n this.keyboardController = null;\n this.stopTargetingListeners();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n this.host.clearActiveThumb();\n\n this.host.setState(\"IDLE\");\n this.host.feedbackData.feedbackType = null;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n }\n\n private removeTargetingOverlay(): void {\n if (this.overlayContainer) {\n this.overlayContainer.remove();\n this.overlayContainer = null;\n }\n this.captureLayer = null;\n this.crosshairH = null;\n this.crosshairV = null;\n this.scope = null;\n }\n\n private showSelectedMarker(): void {\n const root = this.host.ensureOverlayHost();\n\n this.marker = document.createElement(\"div\");\n this.marker.className = \"qaid-selected-marker\";\n this.marker.style.left = `${this.host.selectedBounds.x}px`;\n this.marker.style.top = `${this.host.selectedBounds.y}px`;\n this.marker.style.width = `${this.host.selectedBounds.width}px`;\n this.marker.style.height = `${this.host.selectedBounds.height}px`;\n this.marker.style.zIndex = String(this.host.config.zIndex + 1);\n\n this.host.applyVars(this.marker);\n root.appendChild(this.marker);\n }\n\n hideMarker(): void {\n if (this.marker) {\n this.marker.remove();\n this.marker = null;\n }\n }\n\n /** Full teardown for embed.destroy(). */\n destroy(): void {\n this.keyboardController?.stop();\n this.keyboardController = null;\n this.stopTargetingListeners();\n this.hideMarker();\n }\n}\n"],"names":["DATA_ATTRS","findDataAttribute","element","stopAt","current","attr","value","getTruncatedText","maxLength","textContent","getNthChildIndex","index","sibling","generateNthChildPath","path","tag","escapeCSSIdentifier","str","generateSelector","dataAttr","generateElementInfo","text","TARGETABLE_SELECTOR","NATIVELY_FOCUSABLE","isElementVisible","view","style","collectTargetableElements","options","root","selector","isExcluded","isVisible","startKeyboardTargeting","candidates","moveFocus","target","addedTabindex","stopped","seed","seedIndex","focusCurrent","focusable","emitHighlight","moveTo","nextIndex","len","next","prev","getCurrent","stop","handleKey","select","cancel","event","TOUCH_TAP_SLOP","TargetingController","host","e","type","el","isEmbedElement","rect","b","bounds","getElementBounds","vignette","x","y","hosts","elementUnder","getElementAtPointUnderOverlay","t","start"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"targeting-Dz6z1BYl.js","sources":["../../src/element-selector.ts","../../src/targeting.ts"],"sourcesContent":["/**\n * Element selector generation utilities\n */\n\n/** Data attributes to look for (in priority order) */\nexport const DATA_ATTRS = [\"data-comp\", \"data-qa\", \"data-testid\", \"data-id\"];\n\nexport interface ElementInfo {\n selector: string;\n text: string;\n dataAttr: string | null;\n}\n\n/**\n * Find the first matching data attribute on an element or its ancestors\n */\nexport function findDataAttribute(\n element: Element,\n stopAt: Element = document.body\n): string | null {\n let current: Element | null = element;\n\n while (current && current !== stopAt) {\n for (const attr of DATA_ATTRS) {\n const value = current.getAttribute(attr);\n if (value) {\n return `${attr}=\"${value}\"`;\n }\n }\n current = current.parentElement;\n }\n\n return null;\n}\n\n/**\n * Get truncated text content from an element\n */\nexport function getTruncatedText(element: Element, maxLength = 100): string {\n const textContent = element.textContent?.trim().slice(0, maxLength) || \"\";\n return textContent.length === maxLength ? textContent + \"...\" : textContent;\n}\n\n/**\n * Get the nth-child index of an element among its siblings\n */\nfunction getNthChildIndex(element: Element): number {\n let index = 1;\n let sibling = element.previousElementSibling;\n while (sibling) {\n index++;\n sibling = sibling.previousElementSibling;\n }\n return index;\n}\n\n/**\n * Generate an nth-child path from body to the element\n * This is always a valid CSS selector and uniquely identifies the element\n * Exported for testing\n */\nexport function generateNthChildPath(element: Element): string {\n const path: string[] = [];\n let current: Element | null = element;\n\n while (current && current !== document.body && current !== document.documentElement) {\n const tag = current.tagName.toLowerCase();\n const index = getNthChildIndex(current);\n path.unshift(`${tag}:nth-child(${index})`);\n current = current.parentElement;\n }\n\n return path.length > 0 ? `body > ${path.join(\" > \")}` : \"body\";\n}\n\n/**\n * Escape special CSS characters in a string (for IDs with special chars)\n */\nfunction escapeCSSIdentifier(str: string): string {\n return str.replace(/([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^`{|}~])/g, \"\\\\$1\");\n}\n\n/**\n * Generate a selector string for an element\n * Priority: data attribute > ID > nth-child path\n */\nexport function generateSelector(element: Element): string {\n // Try data attribute first (on element or ancestors) - most stable\n const dataAttr = findDataAttribute(element);\n if (dataAttr) {\n return `[${dataAttr}]`;\n }\n\n // Try ID (escaped for special characters)\n if (element.id) {\n return `#${escapeCSSIdentifier(element.id)}`;\n }\n\n // Fall back to nth-child path - always valid and unique\n return generateNthChildPath(element);\n}\n\n/**\n * Generate complete element info for feedback\n */\nexport function generateElementInfo(element: Element): ElementInfo {\n const text = getTruncatedText(element);\n const dataAttr = findDataAttribute(element);\n const selector = generateSelector(element);\n\n return { selector, text, dataAttr };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Keyboard element targeting */\n/* */\n/* The mouse targeting path in embed.ts resolves the target with */\n/* elementFromPoint(clientX, clientY). A keyboard-activated thumb produces a */\n/* synthetic click at (0, 0), so a keyboard-only user can never point at a */\n/* real element. This controller provides a coordinate-free alternative: */\n/* Tab / arrow keys move a highlight between candidate elements (seeded from */\n/* document.activeElement), Enter / Space selects, Escape cancels. It never */\n/* hides the cursor — it drives the *native* focus ring instead, so both */\n/* sighted keyboard users and screen readers can follow the highlight. */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Default set of \"targetable\" elements for keyboard navigation: focusable\n * controls, ARIA-role'd nodes, common semantic content, and anything already\n * carrying one of the stable data attributes we key selectors off of.\n */\nexport const TARGETABLE_SELECTOR: string = [\n \"a[href]\",\n \"button\",\n \"input:not([type=hidden])\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"[tabindex]\",\n \"[role]\",\n \"img\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"p\",\n \"li\",\n \"label\",\n ...DATA_ATTRS.map((attr) => `[${attr}]`),\n].join(\",\");\n\n/** Tag names that already take keyboard focus without an added tabindex. */\nconst NATIVELY_FOCUSABLE = /^(?:A|BUTTON|INPUT|SELECT|TEXTAREA|SUMMARY)$/;\n\n/**\n * Best-effort visibility test used to keep invisible nodes out of the keyboard\n * candidate list. Deliberately layout-free (no getBoundingClientRect) so it is\n * stable in headless/test environments: it only rejects nodes that are\n * explicitly hidden via the `hidden` attribute, `aria-hidden`, or a computed\n * `display:none` / `visibility:hidden`.\n */\nexport function isElementVisible(element: Element): boolean {\n if (element.hasAttribute(\"hidden\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n const view = element.ownerDocument?.defaultView;\n if (view && typeof view.getComputedStyle === \"function\") {\n const style = view.getComputedStyle(element);\n if (style.display === \"none\" || style.visibility === \"hidden\") return false;\n }\n return true;\n}\n\nexport interface CollectTargetableOptions {\n /** Where to search for candidates (default `document.body`). */\n root?: ParentNode;\n /** CSS selector for candidate elements (default {@link TARGETABLE_SELECTOR}). */\n selector?: string;\n /** Return true to drop an element (e.g. the embed's own shadow hosts). */\n isExcluded?: (element: Element) => boolean;\n /** Visibility predicate (default {@link isElementVisible}). */\n isVisible?: (element: Element) => boolean;\n}\n\n/**\n * Collect the ordered list of elements a keyboard user can target. Document\n * order matches the natural Tab order closely enough for feedback targeting.\n */\nexport function collectTargetableElements(\n options: CollectTargetableOptions = {}\n): Element[] {\n const root = options.root ?? document.body;\n const selector = options.selector ?? TARGETABLE_SELECTOR;\n const isExcluded = options.isExcluded ?? (() => false);\n const isVisible = options.isVisible ?? isElementVisible;\n\n return Array.from(root.querySelectorAll(selector)).filter(\n (element) => !isExcluded(element) && isVisible(element)\n );\n}\n\nexport interface KeyboardTargetingOptions {\n /**\n * Explicit candidate list. When omitted, candidates are collected via\n * {@link collectTargetableElements} using `root` / `selector` / `isExcluded`\n * / `isVisible`.\n */\n candidates?: Element[];\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n root?: ParentNode;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n selector?: string;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n isExcluded?: (element: Element) => boolean;\n /** Passed through to {@link collectTargetableElements} when `candidates` is omitted. */\n isVisible?: (element: Element) => boolean;\n /**\n * Element to start the highlight on. Defaults to `document.activeElement`\n * when it is one of the candidates, otherwise the first candidate.\n */\n initial?: Element | null;\n /**\n * Move native focus to the highlighted element (default true). This is what\n * keeps a screen reader / focus ring on the highlight; non-focusable nodes\n * get a temporary `tabindex=\"-1\"` that is removed on {@link stop}. The cursor\n * is never hidden.\n */\n moveFocus?: boolean;\n /** Where the keydown listener is attached (default `document`). */\n eventTarget?: Document | HTMLElement;\n /** Fired whenever the highlighted candidate changes (and once on start). */\n onHighlight?: (element: Element, index: number) => void;\n /** Fired when the user commits a selection (Enter / Space). Auto-stops first. */\n onSelect: (element: Element) => void;\n /** Fired when the user cancels (Escape). Auto-stops first. */\n onCancel?: () => void;\n}\n\nexport interface KeyboardTargetingController {\n /** The (frozen) candidate list being navigated. */\n readonly candidates: readonly Element[];\n /** Index of the highlighted candidate, or -1 when there are none. */\n getIndex(): number;\n /** The highlighted candidate, or null when there are none. */\n getCurrent(): Element | null;\n /** Highlight the next candidate (wraps to the first). */\n next(): void;\n /** Highlight the previous candidate (wraps to the last). */\n prev(): void;\n /** Highlight a specific index (wraps out-of-range values). */\n moveTo(index: number): void;\n /** Commit the current candidate (fires onSelect) and stop. */\n select(): void;\n /** Cancel (fires onCancel) and stop. */\n cancel(): void;\n /**\n * Handle a keydown. Wired automatically to `eventTarget`; exposed so a host\n * can forward events from another surface (e.g. the embed's shadow root).\n */\n handleKey(event: KeyboardEvent): void;\n /** Remove the listener and restore any temporary tabindex. Idempotent. */\n stop(): void;\n}\n\n/**\n * Start a keyboard element-targeting session. The returned controller wires its\n * own capture-phase keydown listener on `eventTarget` and emits the initial\n * highlight synchronously before returning.\n */\nexport function startKeyboardTargeting(\n options: KeyboardTargetingOptions\n): KeyboardTargetingController {\n const candidates =\n options.candidates ??\n collectTargetableElements({\n root: options.root,\n selector: options.selector,\n isExcluded: options.isExcluded,\n isVisible: options.isVisible,\n });\n\n const moveFocus = options.moveFocus ?? true;\n const target: EventTarget = options.eventTarget ?? document;\n const addedTabindex = new Set<Element>();\n\n let index = -1;\n let stopped = false;\n\n // Seed the highlight from the currently focused element when it is a\n // candidate; this makes \"Tab to a control, then hit the thumb\" land on that\n // control rather than the top of the page.\n const seed =\n options.initial !== undefined\n ? options.initial\n : typeof document !== \"undefined\"\n ? document.activeElement\n : null;\n if (seed) {\n const seedIndex = candidates.indexOf(seed);\n if (seedIndex >= 0) index = seedIndex;\n }\n if (index < 0 && candidates.length > 0) index = 0;\n\n function focusCurrent(element: Element): void {\n if (!moveFocus) return;\n const focusable = element as HTMLElement;\n if (typeof focusable.focus !== \"function\") return;\n // Make non-focusable content (p, img, li, …) programmatically focusable so\n // the native focus ring and AT follow the highlight. Never hide the cursor.\n if (\n element.getAttribute(\"tabindex\") === null &&\n !NATIVELY_FOCUSABLE.test(element.tagName)\n ) {\n element.setAttribute(\"tabindex\", \"-1\");\n addedTabindex.add(element);\n }\n try {\n focusable.focus();\n } catch {\n /* focus can throw in detached/headless cases — highlight still works */\n }\n }\n\n function emitHighlight(): void {\n const element = candidates[index];\n if (!element) return;\n focusCurrent(element);\n options.onHighlight?.(element, index);\n }\n\n function moveTo(nextIndex: number): void {\n if (stopped || candidates.length === 0) return;\n const len = candidates.length;\n index = ((nextIndex % len) + len) % len; // wrap in both directions\n emitHighlight();\n }\n\n function next(): void {\n moveTo(index + 1);\n }\n\n function prev(): void {\n moveTo(index - 1);\n }\n\n function getCurrent(): Element | null {\n return candidates[index] ?? null;\n }\n\n function stop(): void {\n if (stopped) return;\n stopped = true;\n target.removeEventListener(\"keydown\", handleKey as EventListener, true);\n addedTabindex.forEach((element) => element.removeAttribute(\"tabindex\"));\n addedTabindex.clear();\n }\n\n function select(): void {\n if (stopped) return;\n const element = getCurrent();\n stop();\n if (element) options.onSelect(element);\n }\n\n function cancel(): void {\n if (stopped) return;\n stop();\n options.onCancel?.();\n }\n\n function handleKey(event: KeyboardEvent): void {\n if (stopped) return;\n switch (event.key) {\n case \"Tab\":\n event.preventDefault();\n event.stopPropagation();\n if (event.shiftKey) prev();\n else next();\n break;\n case \"ArrowDown\":\n case \"ArrowRight\":\n event.preventDefault();\n event.stopPropagation();\n next();\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n event.preventDefault();\n event.stopPropagation();\n prev();\n break;\n case \"Enter\":\n case \" \":\n case \"Spacebar\": // legacy key value\n event.preventDefault();\n event.stopPropagation();\n select();\n break;\n case \"Escape\":\n case \"Esc\": // legacy key value\n event.preventDefault();\n event.stopPropagation();\n cancel();\n break;\n default:\n break;\n }\n }\n\n target.addEventListener(\"keydown\", handleKey as EventListener, true);\n\n // Emit the initial highlight so the caller can position its highlight box.\n if (index >= 0) emitHighlight();\n\n return {\n candidates,\n getIndex: () => index,\n getCurrent,\n next,\n prev,\n moveTo,\n select,\n cancel,\n handleKey,\n stop,\n };\n}\n","/**\n * Element-targeting subsystem — extracted from embed.ts into its own lazily\n * loaded chunk (it also carries `element-selector`, ~14 KB). It's fetched the\n * first time the user starts targeting (and pre-warmed on thumb-button hover,\n * so the reticle appears without a perceptible wait).\n *\n * The controller owns the reticle/overlay, the mouse/touch/keyboard targeting\n * flows, and the selected-element marker; it talks back to the embed through\n * the narrow `TargetingHost` interface.\n */\n\nimport {\n generateElementInfo,\n startKeyboardTargeting,\n type KeyboardTargetingController,\n} from \"./element-selector\";\nimport {\n getElementAtPointUnderOverlay,\n isEmbedElement,\n getElementBounds,\n} from \"./dom-utils\";\nimport type {\n ResolvedFeedbackConfig,\n EmbedState,\n FeedbackData,\n FeedbackType,\n SelectedBounds,\n} from \"./types\";\n\n/** A touch that moves more than this (px) is a scroll, not a tap-to-select. */\nconst TOUCH_TAP_SLOP = 12;\n\n/** The slice of the embed the targeting subsystem needs. */\nexport interface TargetingHost {\n readonly config: ResolvedFeedbackConfig;\n readonly cssVars: Record<string, string>;\n readonly state: EmbedState;\n readonly shadowHost: HTMLDivElement | null;\n readonly overlayShadowHost: HTMLDivElement | null;\n readonly boundKeyDown: (e: KeyboardEvent) => void;\n /** Live references, mutated in place. */\n readonly feedbackData: FeedbackData;\n readonly selectedBounds: SelectedBounds;\n setState(state: EmbedState): void;\n ensureOverlayHost(): ShadowRoot;\n applyVars(el: HTMLElement): void;\n announceMsg(message: string, assertive?: boolean): void;\n clearActiveThumb(): void;\n submitFeedback(): Promise<void>;\n}\n\nexport class TargetingController {\n private mousePos = { x: 0, y: 0 };\n private touchStartPos: { x: number; y: number } | null = null;\n private overlayContainer: HTMLDivElement | null = null;\n private captureLayer: HTMLDivElement | null = null;\n private crosshairH: HTMLDivElement | null = null;\n private crosshairV: HTMLDivElement | null = null;\n private scope: HTMLDivElement | null = null;\n private highlightBox: HTMLDivElement | null = null;\n private marker: HTMLDivElement | null = null;\n private keyboardController: KeyboardTargetingController | null = null;\n\n private boundMouseMove = (e: MouseEvent): void => this.handleMouseMove(e);\n private boundClick = (e: MouseEvent): void => this.handleClick(e);\n private boundTouchStart = (e: TouchEvent): void => this.handleTouchStart(e);\n private boundTouchEnd = (e: TouchEvent): void => this.handleTouchEnd(e);\n\n constructor(private host: TargetingHost) {}\n\n startPointer(type: FeedbackType, e: MouseEvent): void {\n this.host.setState(\"TARGETING\");\n this.host.feedbackData.feedbackType = type;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n\n // Track initial mouse position from the click event\n this.mousePos.x = e.clientX;\n this.mousePos.y = e.clientY;\n\n // Add class to body (light DOM — for cursor override)\n document.body.classList.add(\"qaid-targeting\");\n if (type === \"up\") {\n document.body.classList.add(\"qaid-type-up\");\n } else {\n document.body.classList.remove(\"qaid-type-up\");\n }\n\n // Set targeting colors on body so .qaid-highlight rules can resolve them\n document.body.style.setProperty(\"--qaid-positive\", this.host.cssVars[\"--qaid-positive\"]);\n document.body.style.setProperty(\"--qaid-negative\", this.host.cssVars[\"--qaid-negative\"]);\n\n this.createTargetingOverlay();\n\n // Add event listeners on document — overlay is pointer-events:none so scroll works naturally\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n document.addEventListener(\"mousemove\", this.boundMouseMove);\n document.addEventListener(\"click\", this.boundClick, true);\n // Touch: tap to select (scrolling stays enabled). touchstart is passive\n // (we only read the point); touchend is non-passive so a tap can\n // preventDefault the synthesised click that would otherwise activate the\n // targeted element.\n document.addEventListener(\"touchstart\", this.boundTouchStart, { passive: true });\n document.addEventListener(\"touchend\", this.boundTouchEnd, { passive: false });\n }\n\n /**\n * Keyboard-driven targeting. Mirrors startPointer minus the mouse plumbing:\n * no `qaid-targeting` body class (keeps the cursor visible for keyboard\n * users), no mouse reticle, and no document mouse/click listeners. The\n * KeyboardTargetingController owns Tab/Arrow/Enter/Space/Escape.\n */\n startKeyboard(type: FeedbackType): void {\n this.host.setState(\"TARGETING\");\n this.host.feedbackData.feedbackType = type;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n\n document.body.style.setProperty(\"--qaid-positive\", this.host.cssVars[\"--qaid-positive\"]);\n document.body.style.setProperty(\"--qaid-negative\", this.host.cssVars[\"--qaid-negative\"]);\n\n // Build the highlight box, then hide the mouse reticle (meaningless here)\n this.createTargetingOverlay();\n if (this.crosshairH) this.crosshairH.style.display = \"none\";\n if (this.crosshairV) this.crosshairV.style.display = \"none\";\n if (this.scope) this.scope.style.display = \"none\";\n\n this.keyboardController = startKeyboardTargeting({\n isExcluded: (el) => isEmbedElement(el),\n onHighlight: (el) => {\n const rect = el.getBoundingClientRect();\n const b = this.highlightBox;\n if (b) {\n b.style.transform = `translate(${rect.left}px, ${rect.top}px)`;\n b.style.width = `${rect.width}px`;\n b.style.height = `${rect.height}px`;\n b.style.display = \"block\";\n }\n const { text } = generateElementInfo(el);\n this.host.announceMsg(`Targeting ${text || el.tagName.toLowerCase()}`);\n },\n onSelect: (el) => this.selectKeyboardTarget(el),\n onCancel: () => this.cancel(),\n });\n }\n\n private selectKeyboardTarget(el: Element): void {\n const bounds = getElementBounds(el, 8);\n Object.assign(this.host.selectedBounds, bounds, {\n clickX: bounds.x + bounds.width / 2,\n clickY: bounds.y + bounds.height / 2,\n visible: true,\n });\n\n const { selector, text } = generateElementInfo(el);\n this.host.feedbackData.elementSelector = selector;\n this.host.feedbackData.elementText = text;\n\n // Teardown (controller has already auto-stopped before onSelect fired)\n this.removeTargetingOverlay();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n this.keyboardController = null;\n this.host.clearActiveThumb();\n\n this.host.setState(\"SELECTED\");\n this.showSelectedMarker();\n void this.host.submitFeedback();\n }\n\n private createTargetingOverlay(): void {\n const root = this.host.ensureOverlayHost();\n\n this.overlayContainer = document.createElement(\"div\");\n this.overlayContainer.className = `qaid-targeting-overlay qaid-type-${this.host.feedbackData.feedbackType}`;\n\n // Capture layer (pointer-events:none — scroll passes through naturally)\n this.captureLayer = document.createElement(\"div\");\n this.captureLayer.className = \"qaid-capture-layer\";\n\n // Vignette\n const vignette = document.createElement(\"div\");\n vignette.className = \"qaid-vignette\";\n\n // Crosshairs\n this.crosshairH = document.createElement(\"div\");\n this.crosshairH.className = \"qaid-crosshair-h\";\n\n this.crosshairV = document.createElement(\"div\");\n this.crosshairV.className = \"qaid-crosshair-v\";\n\n // Scope\n this.scope = document.createElement(\"div\");\n this.scope.className = \"qaid-scope\";\n this.scope.innerHTML = `\n <div class=\"qaid-scope-ring\"></div>\n <div class=\"qaid-scope-ring-inner\"></div>\n <div class=\"qaid-scope-dot\"></div>\n `;\n\n // Highlight box overlay (positioned over hovered elements)\n this.highlightBox = document.createElement(\"div\");\n this.highlightBox.className = \"qaid-highlight-box\";\n\n this.overlayContainer.appendChild(this.captureLayer);\n this.overlayContainer.appendChild(vignette);\n this.overlayContainer.appendChild(this.highlightBox);\n this.overlayContainer.appendChild(this.crosshairH);\n this.overlayContainer.appendChild(this.crosshairV);\n this.overlayContainer.appendChild(this.scope);\n\n // Position crosshairs immediately at current mouse position\n this.crosshairH.style.top = `${this.mousePos.y}px`;\n this.crosshairV.style.left = `${this.mousePos.x}px`;\n this.scope.style.left = `${this.mousePos.x}px`;\n this.scope.style.top = `${this.mousePos.y}px`;\n\n this.host.applyVars(this.overlayContainer);\n root.appendChild(this.overlayContainer);\n }\n\n private handleMouseMove(e: MouseEvent): void {\n this.updateReticleAt(e.clientX, e.clientY);\n }\n\n /** Move the crosshair/scope reticle and highlight the element under (x, y). */\n private updateReticleAt(x: number, y: number): void {\n this.mousePos.x = x;\n this.mousePos.y = y;\n\n if (this.crosshairH) this.crosshairH.style.top = `${y}px`;\n if (this.crosshairV) this.crosshairV.style.left = `${x}px`;\n if (this.scope) {\n this.scope.style.left = `${x}px`;\n this.scope.style.top = `${y}px`;\n }\n\n // Find element underneath — hide both shadow hosts\n if (this.captureLayer && this.host.shadowHost) {\n const hosts = [this.host.shadowHost, this.host.overlayShadowHost].filter(Boolean) as HTMLElement[];\n const elementUnder = getElementAtPointUnderOverlay(x, y, hosts);\n\n if (elementUnder && !isEmbedElement(elementUnder)) {\n if (this.highlightBox) {\n const rect = elementUnder.getBoundingClientRect();\n this.highlightBox.style.transform = `translate(${rect.left}px, ${rect.top}px)`;\n this.highlightBox.style.width = `${rect.width}px`;\n this.highlightBox.style.height = `${rect.height}px`;\n this.highlightBox.style.display = \"block\";\n }\n } else if (this.highlightBox) {\n this.highlightBox.style.display = \"none\";\n }\n }\n }\n\n private handleClick(e: MouseEvent): void {\n e.preventDefault();\n e.stopPropagation();\n this.selectAt(e.clientX, e.clientY);\n }\n\n // ---- Touch targeting (iOS/iPadOS) ----\n private handleTouchStart(e: TouchEvent): void {\n const t = e.touches[0];\n if (!t) return;\n this.touchStartPos = { x: t.clientX, y: t.clientY };\n // Preview what's under the finger; don't preventDefault so scrolling works.\n this.updateReticleAt(t.clientX, t.clientY);\n }\n\n private handleTouchEnd(e: TouchEvent): void {\n // touches is empty on touchend; the lifted point is in changedTouches.\n const t = e.changedTouches[0];\n const start = this.touchStartPos;\n this.touchStartPos = null;\n if (!t || !start) return;\n // A drag beyond the slop threshold is a scroll, not a selection — ignore it.\n const moved = Math.hypot(t.clientX - start.x, t.clientY - start.y);\n if (moved > TOUCH_TAP_SLOP) return;\n e.preventDefault(); // suppress the tap's synthesised click\n this.selectAt(t.clientX, t.clientY);\n }\n\n /** Select the element under (x, y) and tear down targeting. */\n private selectAt(x: number, y: number): void {\n const hosts = [this.host.shadowHost, this.host.overlayShadowHost].filter(Boolean) as HTMLElement[];\n const target = getElementAtPointUnderOverlay(x, y, hosts);\n\n if (!target || isEmbedElement(target)) {\n return;\n }\n\n const bounds = getElementBounds(target, 8);\n Object.assign(this.host.selectedBounds, bounds, { clickX: x, clickY: y, visible: true });\n\n const { selector, text } = generateElementInfo(target);\n this.host.feedbackData.elementSelector = selector;\n this.host.feedbackData.elementText = text;\n\n // Remove targeting overlay and document listeners\n this.stopTargetingListeners();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n\n this.host.setState(\"SELECTED\");\n this.host.clearActiveThumb();\n\n this.showSelectedMarker();\n void this.host.submitFeedback();\n }\n\n private stopTargetingListeners(): void {\n this.removeTargetingOverlay();\n this.touchStartPos = null;\n document.removeEventListener(\"mousemove\", this.boundMouseMove);\n document.removeEventListener(\"click\", this.boundClick, true);\n document.removeEventListener(\"touchstart\", this.boundTouchStart);\n document.removeEventListener(\"touchend\", this.boundTouchEnd);\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n cancel(): void {\n this.keyboardController?.stop();\n this.keyboardController = null;\n this.stopTargetingListeners();\n document.body.classList.remove(\"qaid-targeting\", \"qaid-type-up\");\n document.body.style.removeProperty(\"--qaid-positive\");\n document.body.style.removeProperty(\"--qaid-negative\");\n this.host.clearActiveThumb();\n\n this.host.setState(\"IDLE\");\n this.host.feedbackData.feedbackType = null;\n this.host.feedbackData.elementSelector = null;\n this.host.feedbackData.elementText = null;\n this.host.selectedBounds.visible = false;\n }\n\n private removeTargetingOverlay(): void {\n if (this.overlayContainer) {\n this.overlayContainer.remove();\n this.overlayContainer = null;\n }\n this.captureLayer = null;\n this.crosshairH = null;\n this.crosshairV = null;\n this.scope = null;\n }\n\n private showSelectedMarker(): void {\n const root = this.host.ensureOverlayHost();\n\n this.marker = document.createElement(\"div\");\n this.marker.className = \"qaid-selected-marker\";\n this.marker.style.left = `${this.host.selectedBounds.x}px`;\n this.marker.style.top = `${this.host.selectedBounds.y}px`;\n this.marker.style.width = `${this.host.selectedBounds.width}px`;\n this.marker.style.height = `${this.host.selectedBounds.height}px`;\n this.marker.style.zIndex = String(this.host.config.zIndex + 1);\n\n this.host.applyVars(this.marker);\n root.appendChild(this.marker);\n }\n\n hideMarker(): void {\n if (this.marker) {\n this.marker.remove();\n this.marker = null;\n }\n }\n\n /** Full teardown for embed.destroy(). */\n destroy(): void {\n this.keyboardController?.stop();\n this.keyboardController = null;\n this.stopTargetingListeners();\n this.hideMarker();\n }\n}\n"],"names":["DATA_ATTRS","findDataAttribute","element","stopAt","current","attr","value","getTruncatedText","maxLength","textContent","getNthChildIndex","index","sibling","generateNthChildPath","path","tag","escapeCSSIdentifier","str","generateSelector","dataAttr","generateElementInfo","text","TARGETABLE_SELECTOR","NATIVELY_FOCUSABLE","isElementVisible","view","style","collectTargetableElements","options","root","selector","isExcluded","isVisible","startKeyboardTargeting","candidates","moveFocus","target","addedTabindex","stopped","seed","seedIndex","focusCurrent","focusable","emitHighlight","moveTo","nextIndex","len","next","prev","getCurrent","stop","handleKey","select","cancel","event","TOUCH_TAP_SLOP","TargetingController","host","e","type","el","isEmbedElement","rect","b","bounds","getElementBounds","vignette","x","y","hosts","elementUnder","getElementAtPointUnderOverlay","t","start"],"mappings":"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"}
|
|
@@ -3,11 +3,11 @@ function D() {
|
|
|
3
3
|
return /iP(hone|ad|od)/.test(e) || // iPadOS 13+ masquerades as "MacIntel" but is a multi-touch device.
|
|
4
4
|
navigator.platform === "MacIntel" && (navigator.maxTouchPoints || 0) > 1;
|
|
5
5
|
}
|
|
6
|
-
function
|
|
7
|
-
const t = e >= a,
|
|
8
|
-
return t ===
|
|
6
|
+
function A(e, a, r, d, s) {
|
|
7
|
+
const t = e >= a, o = r >= d;
|
|
8
|
+
return t === o ? { width: e, height: a, rotate: 0 } : { width: a, height: e, rotate: s === 270 ? -90 : 90 };
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function x(e, a, r) {
|
|
11
11
|
if (r.rotate === 0) {
|
|
12
12
|
e.drawImage(a, 0, 0, r.width, r.height);
|
|
13
13
|
return;
|
|
@@ -18,7 +18,7 @@ function C() {
|
|
|
18
18
|
const e = typeof screen < "u" ? screen.orientation : void 0;
|
|
19
19
|
return e && typeof e.angle == "number" ? e.angle : 0;
|
|
20
20
|
}
|
|
21
|
-
function
|
|
21
|
+
function E(e) {
|
|
22
22
|
return new Promise((a) => {
|
|
23
23
|
if (e.videoWidth > 0) {
|
|
24
24
|
a();
|
|
@@ -27,78 +27,78 @@ function T(e) {
|
|
|
27
27
|
e.addEventListener("loadedmetadata", () => a(), { once: !0 });
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
async function
|
|
30
|
+
async function I(e) {
|
|
31
31
|
try {
|
|
32
32
|
await e.play();
|
|
33
33
|
} catch {
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
async function
|
|
36
|
+
async function F(e, a = 15) {
|
|
37
37
|
const r = document.createElement("canvas");
|
|
38
38
|
if (typeof r.captureStream != "function") return null;
|
|
39
39
|
const d = r.getContext("2d");
|
|
40
40
|
if (!d) return null;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const t =
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const s = document.createElement("video");
|
|
42
|
+
s.muted = !0, s.playsInline = !0, s.srcObject = e, await E(s);
|
|
43
|
+
const t = A(
|
|
44
|
+
s.videoWidth,
|
|
45
|
+
s.videoHeight,
|
|
46
46
|
window.innerWidth,
|
|
47
47
|
window.innerHeight,
|
|
48
48
|
C()
|
|
49
49
|
);
|
|
50
|
-
r.width = t.width, r.height = t.height, await
|
|
51
|
-
let
|
|
52
|
-
const
|
|
53
|
-
|
|
50
|
+
r.width = t.width, r.height = t.height, await I(s);
|
|
51
|
+
let o = 0;
|
|
52
|
+
const c = () => {
|
|
53
|
+
x(d, s, t), o = requestAnimationFrame(c);
|
|
54
54
|
};
|
|
55
|
-
|
|
55
|
+
c();
|
|
56
56
|
const n = r.captureStream(a);
|
|
57
57
|
return {
|
|
58
58
|
stream: n,
|
|
59
59
|
stop: () => {
|
|
60
|
-
|
|
60
|
+
o && cancelAnimationFrame(o), o = 0, n.getTracks().forEach((u) => u.stop()), s.pause(), s.srcObject = null;
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
function B(e, a, r, d,
|
|
65
|
-
const t = Math.max(0, Math.min(d, e.left * a)),
|
|
66
|
-
return u <= 0 ||
|
|
64
|
+
function B(e, a, r, d, s) {
|
|
65
|
+
const t = Math.max(0, Math.min(d, e.left * a)), o = Math.max(0, Math.min(s, e.top * r)), c = Math.max(0, Math.min(d, (e.left + e.width) * a)), n = Math.max(0, Math.min(s, (e.top + e.height) * r)), u = c - t, i = n - o;
|
|
66
|
+
return u <= 0 || i <= 0 ? null : { x: t, y: o, w: u, h: i };
|
|
67
67
|
}
|
|
68
|
-
function O(e, a, r, d,
|
|
69
|
-
e.drawImage(a, 0, 0, d,
|
|
70
|
-
const
|
|
68
|
+
function O(e, a, r, d, s, t, o) {
|
|
69
|
+
e.drawImage(a, 0, 0, d, s);
|
|
70
|
+
const c = d / Math.max(1, window.innerWidth), n = s / Math.max(1, window.innerHeight);
|
|
71
71
|
for (const u of r) {
|
|
72
|
-
const
|
|
72
|
+
const i = B(
|
|
73
73
|
u.getBoundingClientRect(),
|
|
74
|
-
|
|
74
|
+
c,
|
|
75
75
|
n,
|
|
76
76
|
d,
|
|
77
|
-
|
|
77
|
+
s
|
|
78
78
|
);
|
|
79
|
-
|
|
79
|
+
i && (e.save(), o ? (e.filter = `blur(${t}px)`, e.drawImage(a, i.x, i.y, i.w, i.h, i.x, i.y, i.w, i.h)) : (e.fillStyle = "#0b0b0b", e.fillRect(i.x, i.y, i.w, i.h)), e.restore());
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
async function
|
|
83
|
-
const d = r.frameRate ?? 15,
|
|
82
|
+
async function P(e, a, r = {}) {
|
|
83
|
+
const d = r.frameRate ?? 15, s = r.blurRadius ?? 12, t = document.createElement("canvas");
|
|
84
84
|
if (typeof t.captureStream != "function") return null;
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
const
|
|
88
|
-
n.muted = !0, n.playsInline = !0, n.srcObject = e, await
|
|
85
|
+
const o = t.getContext("2d");
|
|
86
|
+
if (!o) return null;
|
|
87
|
+
const c = "filter" in o, n = document.createElement("video");
|
|
88
|
+
n.muted = !0, n.playsInline = !0, n.srcObject = e, await E(n), t.width = n.videoWidth, t.height = n.videoHeight, await I(n);
|
|
89
89
|
let u = 0;
|
|
90
|
-
const
|
|
90
|
+
const i = () => {
|
|
91
91
|
O(
|
|
92
|
-
|
|
92
|
+
o,
|
|
93
93
|
n,
|
|
94
94
|
a,
|
|
95
95
|
t.width,
|
|
96
96
|
t.height,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
), u = requestAnimationFrame(
|
|
97
|
+
s,
|
|
98
|
+
c
|
|
99
|
+
), u = requestAnimationFrame(i);
|
|
100
100
|
};
|
|
101
|
-
|
|
101
|
+
i();
|
|
102
102
|
const h = t.captureStream(d);
|
|
103
103
|
return {
|
|
104
104
|
stream: h,
|
|
@@ -107,7 +107,8 @@ async function A(e, a, r = {}) {
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
const L = "This browser cannot blur the areas you picked, so the recording was not started.";
|
|
111
|
+
function V() {
|
|
111
112
|
return typeof navigator < "u" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia == "function" && typeof MediaRecorder < "u";
|
|
112
113
|
}
|
|
113
114
|
function j() {
|
|
@@ -123,18 +124,18 @@ function j() {
|
|
|
123
124
|
return a;
|
|
124
125
|
return "";
|
|
125
126
|
}
|
|
126
|
-
function
|
|
127
|
-
const a = e.maxDuration ?? 15, r = e.videoBitsPerSecond ?? 8e5, d = e.redactionElements ?? [],
|
|
128
|
-
let t = null,
|
|
129
|
-
function
|
|
130
|
-
p !== null && (clearInterval(p), p = null), v !== null && (clearTimeout(v), v = null),
|
|
127
|
+
function q(e = {}) {
|
|
128
|
+
const a = e.maxDuration ?? 15, r = e.videoBitsPerSecond ?? 8e5, d = e.redactionElements ?? [], s = e.redactionBlurRadius;
|
|
129
|
+
let t = null, o = null, c = null, n = null, u = [], i = null, h = null, p = null, R = 0, w = null, g = null, S = !1, v = null;
|
|
130
|
+
function k() {
|
|
131
|
+
p !== null && (clearInterval(p), p = null), v !== null && (clearTimeout(v), v = null), o && (o.stop(), o = null), c && (c.stop(), c = null), t && (t.getTracks().forEach((f) => f.stop()), t = null), n = null, u = [], i = null, h = null, w = null, g = null;
|
|
131
132
|
}
|
|
132
133
|
function y() {
|
|
133
|
-
|
|
134
|
+
S || (S = !0, n && n.state !== "inactive" && n.stop());
|
|
134
135
|
}
|
|
135
136
|
return {
|
|
136
137
|
async start() {
|
|
137
|
-
|
|
138
|
+
S = !1, u = [];
|
|
138
139
|
const f = j();
|
|
139
140
|
if (!f)
|
|
140
141
|
throw new Error("No supported video MIME type found");
|
|
@@ -150,37 +151,40 @@ function V(e = {}) {
|
|
|
150
151
|
monitorTypeSurfaces: "exclude",
|
|
151
152
|
surfaceSwitching: "exclude"
|
|
152
153
|
});
|
|
153
|
-
const m = t.getVideoTracks()[0],
|
|
154
|
-
if (
|
|
155
|
-
throw t.getTracks().forEach((
|
|
154
|
+
const m = t.getVideoTracks()[0], T = m?.getSettings?.()?.displaySurface;
|
|
155
|
+
if (T === "monitor" || T === "window")
|
|
156
|
+
throw t.getTracks().forEach((l) => l.stop()), t = null, new Error(
|
|
156
157
|
"qaid records only the current tab — please share this tab, not a window or your whole screen."
|
|
157
158
|
);
|
|
158
159
|
m && m.addEventListener("ended", () => {
|
|
159
160
|
y();
|
|
160
161
|
});
|
|
161
|
-
let
|
|
162
|
+
let b = t;
|
|
162
163
|
if (D()) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const l = await F(t);
|
|
165
|
+
l && (o = l, b = l.stream);
|
|
166
|
+
}
|
|
167
|
+
if (d.length > 0) {
|
|
168
|
+
const l = await P(b, d, {
|
|
169
|
+
blurRadius: s
|
|
168
170
|
});
|
|
169
|
-
|
|
171
|
+
if (!l)
|
|
172
|
+
throw o && (o.stop(), o = null), t.getTracks().forEach((M) => M.stop()), t = null, new Error(L);
|
|
173
|
+
c = l, b = l.stream;
|
|
170
174
|
}
|
|
171
|
-
n = new MediaRecorder(
|
|
175
|
+
n = new MediaRecorder(b, {
|
|
172
176
|
mimeType: f,
|
|
173
177
|
videoBitsPerSecond: r
|
|
174
|
-
}), n.ondataavailable = (
|
|
175
|
-
|
|
178
|
+
}), n.ondataavailable = (l) => {
|
|
179
|
+
l.data.size > 0 && u.push(l.data);
|
|
176
180
|
}, n.onstop = () => {
|
|
177
|
-
const
|
|
178
|
-
w && w(
|
|
181
|
+
const l = new Blob(u, { type: f });
|
|
182
|
+
w && w(l), h && h(l), o && (o.stop(), o = null), c && (c.stop(), c = null), t && t.getTracks().forEach((M) => M.stop());
|
|
179
183
|
}, n.onerror = () => {
|
|
180
184
|
g && g(new Error("MediaRecorder error"));
|
|
181
|
-
}, n.start(1e3),
|
|
182
|
-
const
|
|
183
|
-
|
|
185
|
+
}, n.start(1e3), R = Date.now(), p = setInterval(() => {
|
|
186
|
+
const l = Math.floor((Date.now() - R) / 1e3);
|
|
187
|
+
i && i(l);
|
|
184
188
|
}, 1e3), v = setTimeout(() => {
|
|
185
189
|
y();
|
|
186
190
|
}, a * 1e3);
|
|
@@ -191,19 +195,20 @@ function V(e = {}) {
|
|
|
191
195
|
});
|
|
192
196
|
},
|
|
193
197
|
onTick(f) {
|
|
194
|
-
|
|
198
|
+
i = f;
|
|
195
199
|
},
|
|
196
200
|
onStop(f) {
|
|
197
201
|
h = f;
|
|
198
202
|
},
|
|
199
203
|
destroy() {
|
|
200
|
-
y(),
|
|
204
|
+
y(), k();
|
|
201
205
|
}
|
|
202
206
|
};
|
|
203
207
|
}
|
|
204
208
|
export {
|
|
205
|
-
|
|
209
|
+
L as REDACTION_UNAVAILABLE,
|
|
210
|
+
q as createVideoRecorder,
|
|
206
211
|
j as getSupportedMimeType,
|
|
207
|
-
|
|
212
|
+
V as isVideoRecordingSupported
|
|
208
213
|
};
|
|
209
|
-
//# sourceMappingURL=video-
|
|
214
|
+
//# sourceMappingURL=video-Bd_29-rK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-Bd_29-rK.js","sources":["../../src/video-orientation.ts","../../src/video-redaction.ts","../../src/video-capture.ts"],"sourcesContent":["/**\n * iOS/iPadOS screen-recording orientation fix.\n *\n * On iOS/iPadOS, `getDisplayMedia` + `MediaRecorder` writes a device-orientation\n * rotation into the recorded file (even for screen captures). Players disagree\n * on whether to honour it — the widget's own preview `<video>` renders it\n * sideways, and so does the dashboard. Re-encoding the *live* decoded frames\n * (which carry no container rotation) through a canvas produces a file whose\n * pixels are already upright with no rotation metadata, so it renders the same\n * everywhere.\n *\n * If the live frames themselves come back with swapped dimensions (portrait\n * frame while the viewport is landscape, or vice versa), we additionally rotate\n * the canvas 90° to match the viewport. That branch only triggers on a clear\n * orientation mismatch, so a correctly-oriented recording is never rotated.\n */\n\nexport interface OrientationTransform {\n /** Output canvas width in px. */\n width: number;\n /** Output canvas height in px. */\n height: number;\n /** Clockwise rotation to apply while drawing: 0, 90, or -90. */\n rotate: 0 | 90 | -90;\n}\n\nexport interface OrientationCorrectedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\n/** True on iPhone/iPad/iPod, including iPadOS 13+ which reports as desktop Mac. */\nexport function isIOSDevice(): boolean {\n const ua = navigator.userAgent || \"\";\n return (\n /iP(hone|ad|od)/.test(ua) ||\n // iPadOS 13+ masquerades as \"MacIntel\" but is a multi-touch device.\n (navigator.platform === \"MacIntel\" && (navigator.maxTouchPoints || 0) > 1)\n );\n}\n\n/**\n * Decide the output canvas size and rotation needed to render a `frameW×frameH`\n * capture upright for a `viewportW×viewportH` viewport. When the frame and\n * viewport share an orientation, no rotation is applied (a straight re-encode,\n * which still strips the bad container rotation). Otherwise rotate 90°, its\n * direction chosen from the screen orientation angle.\n */\nexport function computeOrientationTransform(\n frameW: number,\n frameH: number,\n viewportW: number,\n viewportH: number,\n screenAngle: number\n): OrientationTransform {\n const frameLandscape = frameW >= frameH;\n const viewportLandscape = viewportW >= viewportH;\n\n if (frameLandscape === viewportLandscape) {\n return { width: frameW, height: frameH, rotate: 0 };\n }\n\n // Landscape-secondary (angle 270) is turned the opposite way to primary (90).\n const rotate: 90 | -90 = screenAngle === 270 ? -90 : 90;\n return { width: frameH, height: frameW, rotate };\n}\n\n/** Draw one video frame onto the canvas context, applying the transform. */\nexport function drawRotatedFrame(\n ctx: CanvasRenderingContext2D,\n source: CanvasImageSource,\n t: OrientationTransform\n): void {\n if (t.rotate === 0) {\n ctx.drawImage(source, 0, 0, t.width, t.height);\n return;\n }\n\n ctx.save();\n if (t.rotate === 90) {\n ctx.translate(t.width, 0);\n ctx.rotate(Math.PI / 2);\n } else {\n ctx.translate(0, t.height);\n ctx.rotate(-Math.PI / 2);\n }\n // The source is drawn at its natural (unrotated) extents, which are the\n // output dimensions swapped back.\n ctx.drawImage(source, 0, 0, t.height, t.width);\n ctx.restore();\n}\n\nfunction getScreenAngle(): number {\n const orientation =\n typeof screen !== \"undefined\" ? screen.orientation : undefined;\n return orientation && typeof orientation.angle === \"number\"\n ? orientation.angle\n : 0;\n}\n\nexport function waitForVideoDimensions(video: HTMLVideoElement): Promise<void> {\n return new Promise<void>((resolve) => {\n if (video.videoWidth > 0) {\n resolve();\n return;\n }\n video.addEventListener(\"loadedmetadata\", () => resolve(), { once: true });\n });\n}\n\nexport async function playSilently(video: HTMLVideoElement): Promise<void> {\n try {\n await video.play();\n } catch {\n // Autoplay can reject without a gesture; the element still produces frames\n // for the canvas, so this is non-fatal.\n }\n}\n\n/**\n * Build a canvas-backed, orientation-normalised copy of a display-capture\n * stream. Returns null when the browser can't support the canvas pipeline, in\n * which case the caller should record the original stream directly.\n */\nexport async function buildOrientationCorrectedStream(\n displayStream: MediaStream,\n frameRate = 15\n): Promise<OrientationCorrectedStream | null> {\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n\n const transform = computeOrientationTransform(\n video.videoWidth,\n video.videoHeight,\n window.innerWidth,\n window.innerHeight,\n getScreenAngle()\n );\n canvas.width = transform.width;\n canvas.height = transform.height;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRotatedFrame(ctx, video, transform);\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Live video redaction.\n *\n * Blurs the on-screen bounding boxes of chosen elements while the current tab\n * is recorded. Every frame re-reads each element's `getBoundingClientRect()`,\n * so the blur tracks the content as the page scrolls or reflows — no pixel\n * tracking and no post-recording re-encode. The captured frame is the tab's\n * viewport (video-capture restricts capture to the current tab), so viewport\n * CSS coordinates map onto the frame with a single scale factor.\n *\n * The display stream is drawn to a canvas, the tracked regions are blurred on\n * top, and MediaRecorder records the canvas stream instead of the raw capture —\n * mirroring the iOS orientation pipeline in `video-orientation.ts`.\n */\n\nimport { waitForVideoDimensions, playSilently } from \"./video-orientation\";\n\nexport interface RedactedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\nexport interface RedactionOptions {\n /** Output frame rate of the redacted stream. Default: 15. */\n frameRate?: number;\n /** Gaussian blur radius in px applied to each region. Default: 12. */\n blurRadius?: number;\n}\n\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Map a viewport rect (CSS px, from `getBoundingClientRect()`) onto the\n * captured frame, clamped to the frame bounds. Returns null when the rect is\n * fully outside the viewport (the element scrolled away) or has no area.\n */\nexport function mapRectToFrame(\n rect: { left: number; top: number; width: number; height: number },\n scaleX: number,\n scaleY: number,\n frameW: number,\n frameH: number\n): Box | null {\n const x = Math.max(0, Math.min(frameW, rect.left * scaleX));\n const y = Math.max(0, Math.min(frameH, rect.top * scaleY));\n const right = Math.max(0, Math.min(frameW, (rect.left + rect.width) * scaleX));\n const bottom = Math.max(0, Math.min(frameH, (rect.top + rect.height) * scaleY));\n const w = right - x;\n const h = bottom - y;\n if (w <= 0 || h <= 0) return null;\n return { x, y, w, h };\n}\n\n/**\n * Draw one frame: the full capture, then each element's live bounding box\n * blurred on top. Exported for unit testing the per-frame logic without a real\n * capture stream. When the 2D context can't apply a blur filter, the region is\n * filled solid instead so sensitive content is still hidden.\n */\nexport function drawRedactedFrame(\n ctx: CanvasRenderingContext2D,\n video: CanvasImageSource,\n elements: Element[],\n frameW: number,\n frameH: number,\n blurRadius: number,\n supportsFilter: boolean\n): void {\n ctx.drawImage(video, 0, 0, frameW, frameH);\n\n // The current-tab capture is the viewport, so this scale is ~devicePixelRatio;\n // deriving it live also absorbs viewport resizes mid-recording.\n const scaleX = frameW / Math.max(1, window.innerWidth);\n const scaleY = frameH / Math.max(1, window.innerHeight);\n\n for (const el of elements) {\n const box = mapRectToFrame(\n el.getBoundingClientRect(),\n scaleX,\n scaleY,\n frameW,\n frameH\n );\n if (!box) continue; // scrolled out of view\n\n ctx.save();\n if (supportsFilter) {\n ctx.filter = `blur(${blurRadius}px)`;\n // Re-sample just this region from the live frame so the blur source is\n // the region itself.\n ctx.drawImage(video, box.x, box.y, box.w, box.h, box.x, box.y, box.w, box.h);\n } else {\n // No canvas filter support — hide the region with a solid fill.\n ctx.fillStyle = \"#0b0b0b\";\n ctx.fillRect(box.x, box.y, box.w, box.h);\n }\n ctx.restore();\n }\n}\n\n/**\n * Build a canvas-backed copy of a display-capture stream that blurs the live\n * bounding boxes of `elements`. Returns null when the browser can't support the\n * canvas pipeline (no `captureStream` / 2D context), in which case the caller\n * should record the original stream unredacted.\n */\nexport async function buildRedactedStream(\n displayStream: MediaStream,\n elements: Element[],\n options: RedactionOptions = {}\n): Promise<RedactedStream | null> {\n const frameRate = options.frameRate ?? 15;\n const blurRadius = options.blurRadius ?? 12;\n\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Feature-detect canvas blur; fall back to solid fill where unsupported.\n const supportsFilter = \"filter\" in ctx;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRedactedFrame(\n ctx,\n video,\n elements,\n canvas.width,\n canvas.height,\n blurRadius,\n supportsFilter\n );\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Video capture utilities\n * Uses getDisplayMedia + MediaRecorder for screen recording\n */\n\nimport {\n isIOSDevice,\n buildOrientationCorrectedStream,\n type OrientationCorrectedStream,\n} from \"./video-orientation\";\nimport { buildRedactedStream, type RedactedStream } from \"./video-redaction\";\n\n/**\n * Thrown by `start()` when areas were picked for redaction but this browser\n * cannot run the canvas pipeline that blurs them. The recording is refused\n * rather than made without the blur.\n */\nexport const REDACTION_UNAVAILABLE =\n \"This browser cannot blur the areas you picked, so the recording was not started.\";\n\nexport interface VideoRecorderOptions {\n /** Max recording duration in seconds. Default: 15 */\n maxDuration?: number;\n /** Video bitrate in bps. Default: 800000 (800kbps) */\n videoBitsPerSecond?: number;\n /**\n * Elements whose live bounding boxes are blurred out of the recording. The\n * blur tracks each element as the page scrolls. Empty/omitted = no redaction.\n */\n redactionElements?: Element[];\n /** Blur radius in px for redacted regions. Default: 12. */\n redactionBlurRadius?: number;\n}\n\nexport interface VideoRecorder {\n /** Start recording. Requests getDisplayMedia if not already started. */\n start(): Promise<void>;\n /** Stop recording and return the video blob. */\n stop(): Promise<Blob>;\n /** Register a callback for each second tick (receives seconds elapsed). */\n onTick(callback: (elapsed: number) => void): void;\n /** Register a callback for when recording stops (e.g., browser stop button, max duration). */\n onStop(callback: (blob: Blob | null) => void): void;\n /** Clean up all resources. */\n destroy(): void;\n}\n\n/**\n * Check if video recording is supported in this browser\n */\nexport function isVideoRecordingSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n !!navigator.mediaDevices &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\" &&\n typeof MediaRecorder !== \"undefined\"\n );\n}\n\n/**\n * Detect the best supported MIME type for recording\n */\nexport function getSupportedMimeType(): string {\n if (typeof MediaRecorder === \"undefined\") return \"\";\n\n const candidates = [\n \"video/webm;codecs=vp9\",\n \"video/webm;codecs=vp8\",\n \"video/webm\",\n \"video/mp4\",\n ];\n\n for (const type of candidates) {\n if (MediaRecorder.isTypeSupported(type)) {\n return type;\n }\n }\n\n return \"\";\n}\n\n/**\n * Create a video recorder that captures the screen\n */\nexport function createVideoRecorder(options: VideoRecorderOptions = {}): VideoRecorder {\n const maxDuration = options.maxDuration ?? 15;\n const videoBitsPerSecond = options.videoBitsPerSecond ?? 800_000;\n const redactionElements = options.redactionElements ?? [];\n const redactionBlurRadius = options.redactionBlurRadius;\n\n let stream: MediaStream | null = null;\n let orientationCorrection: OrientationCorrectedStream | null = null;\n let redaction: RedactedStream | null = null;\n let recorder: MediaRecorder | null = null;\n let chunks: Blob[] = [];\n let tickCallback: ((elapsed: number) => void) | null = null;\n let stopCallback: ((blob: Blob | null) => void) | null = null;\n let tickInterval: ReturnType<typeof setInterval> | null = null;\n let startTime = 0;\n let resolveStop: ((blob: Blob) => void) | null = null;\n let rejectStop: ((err: Error) => void) | null = null;\n let stopped = false;\n let autoStopTimeout: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (tickInterval !== null) {\n clearInterval(tickInterval);\n tickInterval = null;\n }\n if (autoStopTimeout !== null) {\n clearTimeout(autoStopTimeout);\n autoStopTimeout = null;\n }\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n }\n recorder = null;\n chunks = [];\n tickCallback = null;\n stopCallback = null;\n resolveStop = null;\n rejectStop = null;\n }\n\n function finishRecording(): void {\n if (stopped) return;\n stopped = true;\n\n if (recorder && recorder.state !== \"inactive\") {\n recorder.stop();\n }\n }\n\n return {\n async start(): Promise<void> {\n stopped = false;\n chunks = [];\n\n const mimeType = getSupportedMimeType();\n if (!mimeType) {\n throw new Error(\"No supported video MIME type found\");\n }\n\n // Request screen capture, restricted to the current tab (the page the\n // embed is running on). `preferCurrentTab` collapses the Chromium picker\n // to a single \"share this tab\" choice; the other hints drop window and\n // whole-screen options where the browser honours them. These keys are\n // not in the DOM types yet.\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: {\n frameRate: 15,\n displaySurface: \"browser\",\n },\n audio: false,\n // @ts-expect-error preferCurrentTab is not in the TS types yet\n preferCurrentTab: true,\n selfBrowserSurface: \"include\",\n monitorTypeSurfaces: \"exclude\",\n surfaceSwitching: \"exclude\",\n });\n\n // Enforce tab-only capture. In browsers that ignore the hints above and\n // still let the user share a window or the whole screen, reject it — qaid\n // records only the tab it is embedded in, never other windows or the\n // desktop.\n const videoTrack = stream.getVideoTracks()[0];\n const surface = videoTrack?.getSettings?.()?.displaySurface;\n if (surface === \"monitor\" || surface === \"window\") {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n throw new Error(\n \"qaid records only the current tab — please share this tab, not a window or your whole screen.\",\n );\n }\n\n // Handle browser \"Stop sharing\" button\n if (videoTrack) {\n videoTrack.addEventListener(\"ended\", () => {\n finishRecording();\n });\n }\n\n // Route the raw capture through canvas pipelines when needed. On\n // iOS/iPadOS the captured file is rotated by the OS, so first re-encode\n // the live frames upright (falling back to the raw stream if the canvas\n // pipeline isn't available). Then, if the user marked regions to redact,\n // blur their live bounding boxes into whatever the first step produced.\n //\n // Redaction used to sit in an `else` of the iOS branch, so on an iPhone\n // or iPad the picker drew its outlines and the recording went out\n // unblurred. The upright frame matches the viewport, which is what the\n // redaction mapping assumes, so the two chain cleanly.\n let recordStream: MediaStream = stream;\n if (isIOSDevice()) {\n const corrected = await buildOrientationCorrectedStream(stream);\n if (corrected) {\n orientationCorrection = corrected;\n recordStream = corrected.stream;\n }\n }\n if (redactionElements.length > 0) {\n const redacted = await buildRedactedStream(recordStream, redactionElements, {\n blurRadius: redactionBlurRadius,\n });\n if (!redacted) {\n // The visitor was promised a blur. Recording the areas in the clear\n // would break that promise, so refuse to record instead.\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n throw new Error(REDACTION_UNAVAILABLE);\n }\n redaction = redacted;\n recordStream = redacted.stream;\n }\n\n recorder = new MediaRecorder(recordStream, {\n mimeType,\n videoBitsPerSecond,\n });\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data.size > 0) {\n chunks.push(e.data);\n }\n };\n\n recorder.onstop = () => {\n const blob = new Blob(chunks, { type: mimeType });\n if (resolveStop) {\n resolveStop(blob);\n }\n // Notify via callback (for unexpected stops like browser stop button)\n if (stopCallback) {\n stopCallback(blob);\n }\n // Stop the canvas pipeline (iOS orientation / redaction) and the\n // display stream tracks.\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n }\n };\n\n recorder.onerror = () => {\n if (rejectStop) {\n rejectStop(new Error(\"MediaRecorder error\"));\n }\n };\n\n recorder.start(1000); // collect data every second\n startTime = Date.now();\n\n // Start tick interval\n tickInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n if (tickCallback) {\n tickCallback(elapsed);\n }\n }, 1000);\n\n // Auto-stop at max duration\n autoStopTimeout = setTimeout(() => {\n finishRecording();\n }, maxDuration * 1000);\n },\n\n stop(): Promise<Blob> {\n return new Promise<Blob>((resolve, reject) => {\n resolveStop = resolve;\n rejectStop = reject;\n finishRecording();\n });\n },\n\n onTick(callback: (elapsed: number) => void): void {\n tickCallback = callback;\n },\n\n onStop(callback: (blob: Blob | null) => void): void {\n stopCallback = callback;\n },\n\n destroy(): void {\n finishRecording();\n cleanup();\n },\n };\n}\n"],"names":["isIOSDevice","ua","computeOrientationTransform","frameW","frameH","viewportW","viewportH","screenAngle","frameLandscape","viewportLandscape","drawRotatedFrame","ctx","source","t","getScreenAngle","orientation","waitForVideoDimensions","video","resolve","playSilently","buildOrientationCorrectedStream","displayStream","frameRate","canvas","transform","rafId","draw","stream","mapRectToFrame","rect","scaleX","scaleY","x","y","right","bottom","w","h","drawRedactedFrame","elements","blurRadius","supportsFilter","el","box","buildRedactedStream","options","REDACTION_UNAVAILABLE","isVideoRecordingSupported","getSupportedMimeType","candidates","type","createVideoRecorder","maxDuration","videoBitsPerSecond","redactionElements","redactionBlurRadius","orientationCorrection","redaction","recorder","chunks","tickCallback","stopCallback","tickInterval","startTime","resolveStop","rejectStop","stopped","autoStopTimeout","cleanup","finishRecording","mimeType","videoTrack","surface","recordStream","corrected","redacted","e","blob","elapsed","reject","callback"],"mappings":"AAkCO,SAASA,IAAuB;AACrC,QAAMC,IAAK,UAAU,aAAa;AAClC,SACE,iBAAiB,KAAKA,CAAE;AAAA,EAEvB,UAAU,aAAa,eAAe,UAAU,kBAAkB,KAAK;AAE5E;AASO,SAASC,EACdC,GACAC,GACAC,GACAC,GACAC,GACsB;AACtB,QAAMC,IAAiBL,KAAUC,GAC3BK,IAAoBJ,KAAaC;AAEvC,SAAIE,MAAmBC,IACd,EAAE,OAAON,GAAQ,QAAQC,GAAQ,QAAQ,EAAA,IAK3C,EAAE,OAAOA,GAAQ,QAAQD,GAAQ,QADfI,MAAgB,MAAM,MAAM,GACb;AAC1C;AAGO,SAASG,EACdC,GACAC,GACAC,GACM;AACN,MAAIA,EAAE,WAAW,GAAG;AAClB,IAAAF,EAAI,UAAUC,GAAQ,GAAG,GAAGC,EAAE,OAAOA,EAAE,MAAM;AAC7C;AAAA,EACF;AAEA,EAAAF,EAAI,KAAA,GACAE,EAAE,WAAW,MACfF,EAAI,UAAUE,EAAE,OAAO,CAAC,GACxBF,EAAI,OAAO,KAAK,KAAK,CAAC,MAEtBA,EAAI,UAAU,GAAGE,EAAE,MAAM,GACzBF,EAAI,OAAO,CAAC,KAAK,KAAK,CAAC,IAIzBA,EAAI,UAAUC,GAAQ,GAAG,GAAGC,EAAE,QAAQA,EAAE,KAAK,GAC7CF,EAAI,QAAA;AACN;AAEA,SAASG,IAAyB;AAChC,QAAMC,IACJ,OAAO,SAAW,MAAc,OAAO,cAAc;AACvD,SAAOA,KAAe,OAAOA,EAAY,SAAU,WAC/CA,EAAY,QACZ;AACN;AAEO,SAASC,EAAuBC,GAAwC;AAC7E,SAAO,IAAI,QAAc,CAACC,MAAY;AACpC,QAAID,EAAM,aAAa,GAAG;AACxB,MAAAC,EAAA;AACA;AAAA,IACF;AACA,IAAAD,EAAM,iBAAiB,kBAAkB,MAAMC,EAAA,GAAW,EAAE,MAAM,IAAM;AAAA,EAC1E,CAAC;AACH;AAEA,eAAsBC,EAAaF,GAAwC;AACzE,MAAI;AACF,UAAMA,EAAM,KAAA;AAAA,EACd,QAAQ;AAAA,EAGR;AACF;AAOA,eAAsBG,EACpBC,GACAC,IAAY,IACgC;AAC5C,QAAMC,IAAS,SAAS,cAAc,QAAQ;AAC9C,MAAI,OAAOA,EAAO,iBAAkB,WAAY,QAAO;AACvD,QAAMZ,IAAMY,EAAO,WAAW,IAAI;AAClC,MAAI,CAACZ,EAAK,QAAO;AAEjB,QAAMM,IAAQ,SAAS,cAAc,OAAO;AAC5C,EAAAA,EAAM,QAAQ,IACdA,EAAM,cAAc,IACpBA,EAAM,YAAYI,GAElB,MAAML,EAAuBC,CAAK;AAElC,QAAMO,IAAYtB;AAAA,IAChBe,EAAM;AAAA,IACNA,EAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,IACPH,EAAA;AAAA,EAAe;AAEjB,EAAAS,EAAO,QAAQC,EAAU,OACzBD,EAAO,SAASC,EAAU,QAE1B,MAAML,EAAaF,CAAK;AAExB,MAAIQ,IAAQ;AACZ,QAAMC,IAAO,MAAY;AACvB,IAAAhB,EAAiBC,GAAKM,GAAOO,CAAS,GACtCC,IAAQ,sBAAsBC,CAAI;AAAA,EACpC;AACA,EAAAA,EAAA;AAEA,QAAMC,IAASJ,EAAO,cAAcD,CAAS;AAE7C,SAAO;AAAA,IACL,QAAAK;AAAA,IACA,MAAM,MAAY;AAChB,MAAIF,0BAA4BA,CAAK,GACrCA,IAAQ,GACRE,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM,GAC1CI,EAAM,MAAA,GACNA,EAAM,YAAY;AAAA,IACpB;AAAA,EAAA;AAEJ;AClIO,SAASW,EACdC,GACAC,GACAC,GACA5B,GACAC,GACY;AACZ,QAAM4B,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI7B,GAAQ0B,EAAK,OAAOC,CAAM,CAAC,GACpDG,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI7B,GAAQyB,EAAK,MAAME,CAAM,CAAC,GACnDG,IAAQ,KAAK,IAAI,GAAG,KAAK,IAAI/B,IAAS0B,EAAK,OAAOA,EAAK,SAASC,CAAM,CAAC,GACvEK,IAAS,KAAK,IAAI,GAAG,KAAK,IAAI/B,IAASyB,EAAK,MAAMA,EAAK,UAAUE,CAAM,CAAC,GACxEK,IAAIF,IAAQF,GACZK,IAAIF,IAASF;AACnB,SAAIG,KAAK,KAAKC,KAAK,IAAU,OACtB,EAAE,GAAAL,GAAG,GAAAC,GAAG,GAAAG,GAAG,GAAAC,EAAA;AACpB;AAQO,SAASC,EACd3B,GACAM,GACAsB,GACApC,GACAC,GACAoC,GACAC,GACM;AACN,EAAA9B,EAAI,UAAUM,GAAO,GAAG,GAAGd,GAAQC,CAAM;AAIzC,QAAM0B,IAAS3B,IAAS,KAAK,IAAI,GAAG,OAAO,UAAU,GAC/C4B,IAAS3B,IAAS,KAAK,IAAI,GAAG,OAAO,WAAW;AAEtD,aAAWsC,KAAMH,GAAU;AACzB,UAAMI,IAAMf;AAAA,MACVc,EAAG,sBAAA;AAAA,MACHZ;AAAA,MACAC;AAAA,MACA5B;AAAA,MACAC;AAAA,IAAA;AAEF,IAAKuC,MAELhC,EAAI,KAAA,GACA8B,KACF9B,EAAI,SAAS,QAAQ6B,CAAU,OAG/B7B,EAAI,UAAUM,GAAO0B,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,CAAC,MAG3EhC,EAAI,YAAY,WAChBA,EAAI,SAASgC,EAAI,GAAGA,EAAI,GAAGA,EAAI,GAAGA,EAAI,CAAC,IAEzChC,EAAI,QAAA;AAAA,EACN;AACF;AAQA,eAAsBiC,EACpBvB,GACAkB,GACAM,IAA4B,CAAA,GACI;AAChC,QAAMvB,IAAYuB,EAAQ,aAAa,IACjCL,IAAaK,EAAQ,cAAc,IAEnCtB,IAAS,SAAS,cAAc,QAAQ;AAC9C,MAAI,OAAOA,EAAO,iBAAkB,WAAY,QAAO;AACvD,QAAMZ,IAAMY,EAAO,WAAW,IAAI;AAClC,MAAI,CAACZ,EAAK,QAAO;AAGjB,QAAM8B,IAAiB,YAAY9B,GAE7BM,IAAQ,SAAS,cAAc,OAAO;AAC5C,EAAAA,EAAM,QAAQ,IACdA,EAAM,cAAc,IACpBA,EAAM,YAAYI,GAElB,MAAML,EAAuBC,CAAK,GAClCM,EAAO,QAAQN,EAAM,YACrBM,EAAO,SAASN,EAAM,aAEtB,MAAME,EAAaF,CAAK;AAExB,MAAIQ,IAAQ;AACZ,QAAMC,IAAO,MAAY;AACvB,IAAAY;AAAA,MACE3B;AAAA,MACAM;AAAA,MACAsB;AAAA,MACAhB,EAAO;AAAA,MACPA,EAAO;AAAA,MACPiB;AAAA,MACAC;AAAA,IAAA,GAEFhB,IAAQ,sBAAsBC,CAAI;AAAA,EACpC;AACA,EAAAA,EAAA;AAEA,QAAMC,IAASJ,EAAO,cAAcD,CAAS;AAE7C,SAAO;AAAA,IACL,QAAAK;AAAA,IACA,MAAM,MAAY;AAChB,MAAIF,0BAA4BA,CAAK,GACrCA,IAAQ,GACRE,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM,GAC1CI,EAAM,MAAA,GACNA,EAAM,YAAY;AAAA,IACpB;AAAA,EAAA;AAEJ;ACtJO,MAAM6B,IACX;AAgCK,SAASC,IAAqC;AACnD,SACE,OAAO,YAAc,OACrB,CAAC,CAAC,UAAU,gBACZ,OAAO,UAAU,aAAa,mBAAoB,cAClD,OAAO,gBAAkB;AAE7B;AAKO,SAASC,IAA+B;AAC7C,MAAI,OAAO,gBAAkB,IAAa,QAAO;AAEjD,QAAMC,IAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,aAAWC,KAAQD;AACjB,QAAI,cAAc,gBAAgBC,CAAI;AACpC,aAAOA;AAIX,SAAO;AACT;AAKO,SAASC,EAAoBN,IAAgC,IAAmB;AACrF,QAAMO,IAAcP,EAAQ,eAAe,IACrCQ,IAAqBR,EAAQ,sBAAsB,KACnDS,IAAoBT,EAAQ,qBAAqB,CAAA,GACjDU,IAAsBV,EAAQ;AAEpC,MAAIlB,IAA6B,MAC7B6B,IAA2D,MAC3DC,IAAmC,MACnCC,IAAiC,MACjCC,IAAiB,CAAA,GACjBC,IAAmD,MACnDC,IAAqD,MACrDC,IAAsD,MACtDC,IAAY,GACZC,IAA6C,MAC7CC,IAA4C,MAC5CC,IAAU,IACVC,IAAwD;AAE5D,WAASC,IAAgB;AACvB,IAAIN,MAAiB,SACnB,cAAcA,CAAY,GAC1BA,IAAe,OAEbK,MAAoB,SACtB,aAAaA,CAAe,GAC5BA,IAAkB,OAEhBX,MACFA,EAAsB,KAAA,GACtBA,IAAwB,OAEtBC,MACFA,EAAU,KAAA,GACVA,IAAY,OAEV9B,MACFA,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM,GAC1Cc,IAAS,OAEX+B,IAAW,MACXC,IAAS,CAAA,GACTC,IAAe,MACfC,IAAe,MACfG,IAAc,MACdC,IAAa;AAAA,EACf;AAEA,WAASI,IAAwB;AAC/B,IAAIH,MACJA,IAAU,IAENR,KAAYA,EAAS,UAAU,cACjCA,EAAS,KAAA;AAAA,EAEb;AAEA,SAAO;AAAA,IACL,MAAM,QAAuB;AAC3B,MAAAQ,IAAU,IACVP,IAAS,CAAA;AAET,YAAMW,IAAWtB,EAAA;AACjB,UAAI,CAACsB;AACH,cAAM,IAAI,MAAM,oCAAoC;AAQtD,MAAA3C,IAAS,MAAM,UAAU,aAAa,gBAAgB;AAAA,QACpD,OAAO;AAAA,UACL,WAAW;AAAA,UACX,gBAAgB;AAAA,QAAA;AAAA,QAElB,OAAO;AAAA;AAAA,QAEP,kBAAkB;AAAA,QAClB,oBAAoB;AAAA,QACpB,qBAAqB;AAAA,QACrB,kBAAkB;AAAA,MAAA,CACnB;AAMD,YAAM4C,IAAa5C,EAAO,eAAA,EAAiB,CAAC,GACtC6C,IAAUD,GAAY,cAAA,GAAiB;AAC7C,UAAIC,MAAY,aAAaA,MAAY;AACvC,cAAA7C,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM,GAC1Cc,IAAS,MACH,IAAI;AAAA,UACR;AAAA,QAAA;AAKJ,MAAI4C,KACFA,EAAW,iBAAiB,SAAS,MAAM;AACzC,QAAAF,EAAA;AAAA,MACF,CAAC;AAaH,UAAII,IAA4B9C;AAChC,UAAI3B,KAAe;AACjB,cAAM0E,IAAY,MAAMtD,EAAgCO,CAAM;AAC9D,QAAI+C,MACFlB,IAAwBkB,GACxBD,IAAeC,EAAU;AAAA,MAE7B;AACA,UAAIpB,EAAkB,SAAS,GAAG;AAChC,cAAMqB,IAAW,MAAM/B,EAAoB6B,GAAcnB,GAAmB;AAAA,UAC1E,YAAYC;AAAA,QAAA,CACb;AACD,YAAI,CAACoB;AAGH,gBAAInB,MACFA,EAAsB,KAAA,GACtBA,IAAwB,OAE1B7B,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM,GAC1Cc,IAAS,MACH,IAAI,MAAMmB,CAAqB;AAEvC,QAAAW,IAAYkB,GACZF,IAAeE,EAAS;AAAA,MAC1B;AAEA,MAAAjB,IAAW,IAAI,cAAce,GAAc;AAAA,QACzC,UAAAH;AAAA,QACA,oBAAAjB;AAAA,MAAA,CACD,GAEDK,EAAS,kBAAkB,CAACkB,MAAiB;AAC3C,QAAIA,EAAE,KAAK,OAAO,KAChBjB,EAAO,KAAKiB,EAAE,IAAI;AAAA,MAEtB,GAEAlB,EAAS,SAAS,MAAM;AACtB,cAAMmB,IAAO,IAAI,KAAKlB,GAAQ,EAAE,MAAMW,GAAU;AAChD,QAAIN,KACFA,EAAYa,CAAI,GAGdhB,KACFA,EAAagB,CAAI,GAIfrB,MACFA,EAAsB,KAAA,GACtBA,IAAwB,OAEtBC,MACFA,EAAU,KAAA,GACVA,IAAY,OAEV9B,KACFA,EAAO,YAAY,QAAQ,CAACd,MAAMA,EAAE,MAAM;AAAA,MAE9C,GAEA6C,EAAS,UAAU,MAAM;AACvB,QAAIO,KACFA,EAAW,IAAI,MAAM,qBAAqB,CAAC;AAAA,MAE/C,GAEAP,EAAS,MAAM,GAAI,GACnBK,IAAY,KAAK,IAAA,GAGjBD,IAAe,YAAY,MAAM;AAC/B,cAAMgB,IAAU,KAAK,OAAO,KAAK,IAAA,IAAQf,KAAa,GAAI;AAC1D,QAAIH,KACFA,EAAakB,CAAO;AAAA,MAExB,GAAG,GAAI,GAGPX,IAAkB,WAAW,MAAM;AACjC,QAAAE,EAAA;AAAA,MACF,GAAGjB,IAAc,GAAI;AAAA,IACvB;AAAA,IAEA,OAAsB;AACpB,aAAO,IAAI,QAAc,CAAClC,GAAS6D,MAAW;AAC5C,QAAAf,IAAc9C,GACd+C,IAAac,GACbV,EAAA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,OAAOW,GAA2C;AAChD,MAAApB,IAAeoB;AAAA,IACjB;AAAA,IAEA,OAAOA,GAA6C;AAClD,MAAAnB,IAAemB;AAAA,IACjB;AAAA,IAEA,UAAgB;AACd,MAAAX,EAAA,GACAD,EAAA;AAAA,IACF;AAAA,EAAA;AAEJ;"}
|
package/dist/embed.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { FeedbackConfig } from "./types";
|
|
2
2
|
export declare function isHiddenByUser(apiKey?: string): boolean;
|
|
3
3
|
export declare function setHiddenByUser(apiKey?: string, hidden?: boolean): void;
|
|
4
|
+
/**
|
|
5
|
+
* A random v4 UUID.
|
|
6
|
+
*
|
|
7
|
+
* `crypto.randomUUID` exists only in secure contexts (HTTPS and localhost). On
|
|
8
|
+
* a plain http:// page it is undefined, and calling it threw inside the
|
|
9
|
+
* constructor, so the widget never appeared. `crypto.getRandomValues` has no
|
|
10
|
+
* such restriction; `Math.random` covers a runtime with no `crypto` at all.
|
|
11
|
+
* The id only groups one browser's feedback, so it needs to be unique, not
|
|
12
|
+
* secret.
|
|
13
|
+
*/
|
|
14
|
+
export declare function randomId(): string;
|
|
4
15
|
/**
|
|
5
16
|
* Get or create a visitor ID stored in localStorage
|
|
6
17
|
*/
|
package/dist/icons.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export declare const PENCIL_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" cl
|
|
|
12
12
|
export declare const BLUR_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"qaid-icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 3s6 6.4 6 10a6 6 0 01-12 0c0-3.6 6-10 6-10z\"/>\n</svg>";
|
|
13
13
|
export declare const UNDO_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"qaid-icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 14L4 9l5-5M4 9h11a5 5 0 010 10h-4\"/>\n</svg>";
|
|
14
14
|
export declare const DONE_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"qaid-icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M20 6L9 17l-5-5\"/>\n</svg>";
|
|
15
|
+
/** Exclamation mark for the "message not sent" screen. */
|
|
16
|
+
export declare const ERROR_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"qaid-icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 6v8m0 4h.01\"/>\n</svg>";
|
|
15
17
|
export declare const SKIP_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"qaid-icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 6l12 12M18 6L6 18\"/>\n</svg>";
|