) !== 0) continue; var fiber = el[key]; while (fiber) { var src = fiber._debugSource; if (src && src.fileName) return { file: String(src.fileName), line: Number(src.lineNumber) || null }; fiber = fiber.return; } } } catch (e) {} return null; } function describe(el) { var r = el.getBoundingClientRect(); var out = { tag: tagOf(el), id: el.id ? clip(String(el.id), 100) : null, classes: clip(squash(attr(el, 'class')), 200), selector: selectorFor(el), text: textOf(el), html: htmlOf(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } }; if (MULTI) out.viewport = { w: W.innerWidth, h: W.innerHeight }; if (REACT_SOURCE) out.source = reactSourceOf(el); return out; } function kindOf(el) { var t = tagOf(el), role = attr(el, 'role'), type = attr(el, 'type'); if (t === 'button' || role === 'button' || (t === 'input' && /^(button|submit|reset)$/.test(type))) return 'button'; if (t === 'a' || role === 'link') return 'link'; if (t === 'input' || t === 'textarea' || t === 'select') return 'field'; if (t === 'img' || t === 'svg' || t === 'picture' || t === 'canvas') return 'image'; if (/^h[1-6]$/.test(t)) return 'heading'; return t; } function label(el) { var text = squash(textOf(el)); var kind = kindOf(el); return text ? '\u201c' + clip(text, 28) + '\u201d ' + kind : kind; } function cursor() { var svg = ""; return 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '") 16 16, crosshair'; } function canHole() { try { return !!(W.CSS && W.CSS.supports && W.CSS.supports('clip-path', 'path(evenodd, "M0 0H1V1Z")')); } catch (e) { return false; } } // The veil covers the app except a rounded window around the element. function holePath(r) { var w = Math.round(W.innerWidth), h = Math.round(W.innerHeight), p = 6; var x = Math.round(r.left - p), y = Math.round(r.top - p); var rw = Math.round(r.width + 2 * p), rh = Math.round(r.height + 2 * p); var k = Math.max(0, Math.min(12, Math.floor(rw / 2), Math.floor(rh / 2))); var arc = 'A' + k + ' ' + k + ' 0 0 1 '; return 'path(evenodd, "M0 0H' + w + 'V' + h + 'H0Z' + 'M' + (x + k) + ' ' + y + 'H' + (x + rw - k) + arc + (x + rw) + ' ' + (y + k) + 'V' + (y + rh - k) + arc + (x + rw - k) + ' ' + (y + rh) + 'H' + (x + k) + arc + x + ' ' + (y + rh - k) + 'V' + (y + k) + arc + (x + k) + ' ' + y + 'Z")'; } // The Remix ring and chip, shared by the pointer, the marks and the agent. function ringCss(color) { return 'box-sizing:border-box;border:2px solid ' + color + ';border-radius:12px;box-shadow:0 0 0 5px ' + color + '29'; } function chipCss(color) { return 'white-space:nowrap;padding:4px 10px;border-radius:999px;background:#141416;color:#f6f3ee;font:' + CHIP_FONT + ';box-shadow:0 0 0 1px ' + color; } // A host for our nodes: a custom element, max z-index, its own closed // shadow root so the page's CSS can't restyle (or hide) what we draw. Last // child of , so at an equal z-index it still paints on top. function makeHost(role, position) { var el = D.createElement('remix-point'); if (role === 'point') el.setAttribute('data-remix-point', ''); el.setAttribute('data-codeam', role); el.style.cssText = 'all:initial;position:' + position + ';top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647'; return el; } function build() { if (leaving && leaving.parentNode) leaving.parentNode.removeChild(leaving); leaving = null; var motion = reduced() ? 'none' : 'transform .16s ' + EASE + ', width .16s ' + EASE + ', height .16s ' + EASE + ', opacity .16s'; host = makeHost('point', 'fixed'); var root = shadowOf(host); veil = D.createElement('div'); veil.setAttribute('data-codeam', 'veil'); veil.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;opacity:0;' + 'background:rgba(246,243,238,.46) ' + GRAIN + ';-webkit-backdrop-filter:grayscale(.7);backdrop-filter:grayscale(.7);' + 'transition:' + (reduced() ? 'none' : 'opacity .2s'); ring = D.createElement('div'); ring.setAttribute('data-codeam', 'ring'); ring.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;opacity:0;' + ringCss(accent) + ';transition:' + motion; chip = D.createElement('div'); chip.setAttribute('data-codeam', 'chip'); chip.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;opacity:0;' + chipCss(accent) + ';transition:' + motion; root.appendChild(veil); root.appendChild(ring); root.appendChild(chip); D.documentElement.appendChild(host); style = D.createElement('style'); style.textContent = 'html[data-remix-pointing],html[data-remix-pointing] *{cursor:' + cursor() + ' !important;' + 'user-select:none !important;-webkit-user-select:none !important;-webkit-touch-callout:none !important}'; (D.head || D.documentElement).appendChild(style); D.documentElement.setAttribute('data-remix-pointing', ''); } function paint(el) { current = el; var r = el.getBoundingClientRect(); ring.style.transform = 'translate(' + Math.round(r.left - 4) + 'px,' + Math.round(r.top - 4) + 'px)'; ring.style.width = Math.round(r.width + 8) + 'px'; ring.style.height = Math.round(r.height + 8) + 'px'; ring.style.opacity = '1'; if (canHole()) { veil.style.clipPath = holePath(r); veil.style.opacity = '1'; } chip.textContent = label(el); var top = r.top >= 34 ? r.top - 32 : r.top + r.height + 8; var left = Math.max(8, Math.min(r.left, W.innerWidth - 200)); chip.style.transform = 'translate(' + Math.round(left) + 'px,' + Math.round(top) + 'px)'; chip.style.opacity = '1'; } // Paint, and in multi mode tell the parent what is under the finger. function hoverTo(el) { var changed = el !== current; paint(el); if (changed && MULTI) send('hover', { element: describe(el) }); } // While pointing, the app never sees the gesture: no click, no focus, no // drag. Touch scrolling still works (its default is left alone). function block(e) { if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (e.cancelable && e.type !== 'touchstart' && e.type !== 'touchmove') e.preventDefault(); } // What is under the pointer. Hit-testing first (a touch keeps targeting // the element it went down on); the event target where there is none. function at(e) { var el = null; if (D.elementFromPoint) { try { el = D.elementFromPoint(e.clientX, e.clientY); } catch (x) { el = null; } } if (!el && e.target && e.target.nodeType === 1) el = e.target; return pickable(el) ? target(el) : null; } function onDown(e) { block(e); down = { id: e.pointerId }; tapped = true; var el = at(e); if (el) hoverTo(el); } function onMove(e) { block(e); var el = at(e); if (el && el !== current) hoverTo(el); } // Browsers without pointer events (old WebViews) only send mouse events. // Never blocked: the app keeps its own mousemove, as it always did. function onMouseMove(e) { var el = at(e); if (el && el !== current) hoverTo(el); } function onUp(e) { block(e); var d = down; down = null; if (!d || d.id !== e.pointerId || e.button > 0) return; var el = at(e) || current; if (el) { pick(el); armClickSwallow(); } } // The click that follows a tap was already picked on pointerup; a click // with no pointer gesture before it (an old WebView) is the pick itself. function onClick(e) { block(e); if (tapped) { tapped = false; return; } var el = at(e); if (el) pick(el); } // The browser fires a click after the pointerup that picked. Once pointing // is off (single mode, or the parent turned it off in between) nothing of // ours would catch it, and a pointed-at link would navigate. One shot: // gone on the next click, or after 400 ms when no click comes. function armClickSwallow() { disarmClickSwallow(); var fn = function (e) { block(e); disarmClickSwallow(); }; swallow = fn; W.addEventListener('click', fn, { capture: true, passive: false }); W.setTimeout(function () { if (swallow === fn) disarmClickSwallow(); }, 400); } function disarmClickSwallow() { if (!swallow) return; W.removeEventListener('click', swallow, { capture: true }); swallow = null; } // The browser took the touch over to scroll: that was not a tap. function onCancel(e) { block(e); down = null; tapped = false; } function onKey(e) { block(e); // In multi mode the parent owns the toggle: Escape ends nothing it can't see. if (!MULTI && e.type === 'keydown' && (e.key === 'Escape' || e.key === 'Esc')) { send('cancel'); off(false); } } function onScroll() { if (current) paint(current); if (picked.length) drawMarks(); } var HANDLERS = [ ['pointerdown', onDown], ['pointermove', onMove], ['pointerup', onUp], ['pointercancel', onCancel], ['mousedown', block], ['mouseup', block], ['mousemove', onMouseMove], ['click', onClick], ['dblclick', block], ['auxclick', block], ['contextmenu', block], ['touchstart', block], ['touchmove', block], ['touchend', block], ['submit', block], ['keydown', onKey], ['keyup', block], ['keypress', block] ]; function pick(el) { var element = describe(el); if (!MULTI) { off(true); send('pick', { element: element }); return; } // Past the cap the pick still goes up (the app says "4 marks max") but // leaves nothing in the page the app would never be able to remove. if (picked.length < MAX_MARKS) { var id = 'p' + LOAD + '-' + (++seq); picked.push({ id: id, el: el }); element.markId = id; drawMarks(id); } send('pick', { element: element }); } // The pinned marks: Remix rings with a numbered chip, anchored to the // DOCUMENT (absolute + scroll) so they travel with the content. function drawMarks(fresh) { if (!picked.length) { drop(marksHost); marksHost = marksRoot = null; return; } if (!marksHost || !marksHost.parentNode) { marksHost = makeHost('marks', 'absolute'); marksRoot = shadowOf(marksHost); D.documentElement.appendChild(marksHost); } clear(marksRoot); var calm = reduced(); if (!calm) { var keys = D.createElement('style'); keys.textContent = '@keyframes pp-pin{from{opacity:0;transform:scale(1.04)}to{opacity:1;transform:scale(1)}}'; marksRoot.appendChild(keys); } var sx = W.scrollX || W.pageXOffset || 0, sy = W.scrollY || W.pageYOffset || 0; for (var i = 0; i < picked.length; i++) { var r = picked[i].el.getBoundingClientRect(); var box = D.createElement('div'); box.setAttribute('data-codeam', 'mark'); box.style.cssText = 'position:absolute;pointer-events:none;' + ringCss(accent) + ';left:' + Math.round(r.left + sx - 4) + 'px;top:' + Math.round(r.top + sy - 4) + 'px' + ';width:' + Math.round(r.width + 8) + 'px;height:' + Math.round(r.height + 8) + 'px' + (!calm && picked[i].id === fresh ? ';animation:pp-pin .32s ' + EASE : ''); var badge = D.createElement('div'); badge.setAttribute('data-codeam', 'mark-badge'); badge.style.cssText = 'position:absolute;left:-11px;top:-11px;width:22px;height:22px;border-radius:11px;' + 'background:#141416;color:#f6f3ee;font:italic 600 12px/22px Georgia,"Times New Roman",serif;text-align:center;' + 'box-shadow:0 0 0 1px ' + accent + ';pointer-events:none'; badge.textContent = String(i + 1); box.appendChild(badge); marksRoot.appendChild(box); } } // The parent sends the ids still alive, in order: a removed chip removes // its mark, and the rest renumber (what the person writes, "1 is off", // refers to these numbers). function syncMarks(ids) { var byId = {}; for (var i = 0; i < picked.length; i++) byId[picked[i].id] = picked[i]; var next = []; for (var j = 0; j < ids.length; j++) if (byId[ids[j]]) next.push(byId[ids[j]]); picked = next; drawMarks(); } // The agent pointing (highlight_element): a cursor travels to the element, // centres it, and leaves a cyan Remix ring with its label. Independent of // pointing mode, anchored to the document like the marks. function ensureAgent() { if (agentHost && agentHost.parentNode) return; agentHost = makeHost('agent', 'absolute'); agentRoot = shadowOf(agentHost); var keys = D.createElement('style'); keys.textContent = '@keyframes pp-agent-pulse{0%{box-shadow:0 0 0 0 rgba(34,211,238,.55)}70%{box-shadow:0 0 0 10px rgba(34,211,238,0)}100%{box-shadow:0 0 0 5px rgba(34,211,238,.16)}}' + '@keyframes pp-agent-in{from{opacity:0;transform:scale(1.04)}to{opacity:1;transform:scale(1)}}'; agentRoot.appendChild(keys); agentLayer = D.createElement('div'); agentRoot.appendChild(agentLayer); D.documentElement.appendChild(agentHost); } function ensureAgentCursor() { if (agentCursor && agentCursor.parentNode) return agentCursor; agentCursor = D.createElement('div'); agentCursor.setAttribute('data-codeam', 'agent-cursor'); agentCursor.style.cssText = 'position:fixed;pointer-events:none;opacity:0;' + 'left:' + Math.round(W.innerWidth / 2) + 'px;top:' + Math.round(W.innerHeight - 40) + 'px;' + 'transition:' + (reduced() ? 'none' : 'left .6s cubic-bezier(.22,1,.36,1),top .6s cubic-bezier(.22,1,.36,1),opacity .3s'); agentCursor.innerHTML = '' + '' + '
Agent
'; agentRoot.appendChild(agentCursor); return agentCursor; } function drawAgentMarks() { if (!agentLayer) return; clear(agentLayer); var calm = reduced(); var sx = W.scrollX || W.pageXOffset || 0, sy = W.scrollY || W.pageYOffset || 0; for (var i = 0; i < agentMarks.length; i++) { var m = agentMarks[i]; if (m.el.isConnected === false) continue; var r = m.el.getBoundingClientRect(); var box = D.createElement('div'); box.setAttribute('data-codeam', 'agent-mark'); box.style.cssText = 'position:absolute;pointer-events:none;' + ringCss(AGENT_ACCENT) + ';left:' + Math.round(r.left + sx - 4) + 'px;top:' + Math.round(r.top + sy - 4) + 'px' + ';width:' + Math.round(r.width + 8) + 'px;height:' + Math.round(r.height + 8) + 'px' + (calm ? '' : ';animation:pp-agent-in .25s ' + EASE + ',pp-agent-pulse 1.6s ease-out 3'); if (m.label) { var tag = D.createElement('div'); tag.setAttribute('data-codeam', 'agent-mark-label'); // Above the element when it fits, else below — same rule as the chip. tag.style.cssText = 'position:absolute;left:0;' + (r.top >= 34 ? 'top:-30px' : 'top:' + Math.round(r.height + 12) + 'px') + ';pointer-events:none;' + chipCss(AGENT_ACCENT) + ';max-width:260px;overflow:hidden;text-overflow:ellipsis'; tag.textContent = m.label; box.appendChild(tag); } agentLayer.appendChild(box); } } function listenAgentLayout() { if (agentListening) return; agentListening = true; W.addEventListener('scroll', drawAgentMarks, true); W.addEventListener('resize', drawAgentMarks); } function agentHighlight(selector, text) { var el = null; try { el = D.querySelector(String(selector)); } catch (e) { el = null; } if (!el || !el.getBoundingClientRect) return false; ensureAgent(); listenAgentLayout(); var calm = reduced(); try { el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: calm ? 'auto' : 'smooth' }); } catch (e) {} var pointer = ensureAgentCursor(); var owner = agentHost; // After the smooth scroll the cursor travels to the element and, on // arrival, the ring appears: read as "the agent is pointing at this". W.setTimeout(function () { if (agentHost !== owner) return; var r = el.getBoundingClientRect(); pointer.style.opacity = '1'; pointer.style.left = Math.round(r.left + Math.min(r.width / 2, 40)) + 'px'; pointer.style.top = Math.round(r.top + Math.min(r.height / 2, 24)) + 'px'; W.setTimeout(function () { if (agentHost !== owner) return; agentMarks.push({ el: el, label: text ? String(text).slice(0, 80) : '' }); drawAgentMarks(); }, 620); }, 380); return true; } function agentClear() { agentMarks = []; drop(agentHost); agentHost = agentRoot = agentLayer = agentCursor = null; if (agentListening) { W.removeEventListener('scroll', drawAgentMarks, true); W.removeEventListener('resize', drawAgentMarks); agentListening = false; } } function enable(origin, a) { driver = origin; accent = typeof a === 'string' && ACCENT_RE.test(a) ? a : DEFAULT_ACCENT; if (!on) { on = true; build(); for (var i = 0; i < HANDLERS.length; i++) { W.addEventListener(HANDLERS[i][0], HANDLERS[i][1], { capture: true, passive: false }); bound.push(HANDLERS[i]); } W.addEventListener('scroll', onScroll, true); W.addEventListener('resize', onScroll, true); } send(MULTI ? 'ready' : 'on'); } function off(pinned) { if (!on) return; on = false; while (bound.length) { var h = bound.pop(); W.removeEventListener(h[0], h[1], { capture: true }); } W.removeEventListener('scroll', onScroll, true); W.removeEventListener('resize', onScroll, true); D.documentElement.removeAttribute('data-remix-pointing'); if (style && style.parentNode) style.parentNode.removeChild(style); var node = host; if (pinned && node && !reduced()) { // A short "pinned" beat before the ring goes, so the tap feels taken. ring.style.transition = 'transform .32s ' + EASE + ', opacity .32s'; ring.style.transform += ' scale(1.04)'; ring.style.opacity = '0'; veil.style.opacity = '0'; chip.style.opacity = '0'; leaving = node; W.setTimeout(function () { if (node.parentNode) node.parentNode.removeChild(node); if (leaving === node) leaving = null; }, 320); } else if (node && node.parentNode) { node.parentNode.removeChild(node); } host = veil = ring = chip = style = null; current = null; down = null; tapped = false; } W.addEventListener('message', function (e) { if (ALLOWED.indexOf(e.origin) === -1 || e.source !== W.parent) return; var d = e.data; if (!d || typeof d !== 'object' || d[FIELD] !== CH) return; if (d.type === 'enable') enable(e.origin, d.accent); else if (d.type === 'disable' && e.origin === driver) off(false); else if (MULTI && d.type === 'marks' && d.ids && d.ids.length !== undefined) syncMarks(d.ids); else if (AGENT && d.type === 'agent-highlight' && d.selector) agentHighlight(d.selector, d.label); else if (AGENT && d.type === 'agent-clear') agentClear(); }); if (!MULTI) { try { Object.defineProperty(W, '__remixPoint', { value: Object.freeze({ version: 1, active: function () { return on; } }) }); } catch (e) {} } // The React Native door. Only where the bridge exists: on a normal page it // would be a global that turns the picker on with nobody's permission. if (GLOBAL && rnBridge()) { W[GLOBAL] = { enable: function () { enable('react-native'); }, disable: function () { off(false); }, marks: function (ids) { if (MULTI && ids && ids.length !== undefined) syncMarks(ids); }, highlight: function (selector, text) { return AGENT ? agentHighlight(selector, text) : false; }, clear: function () { if (AGENT) agentClear(); } }; } } catch (e) { // Whatever happens, the person's app keeps working. } })();