Newer
Older
bugtrail / packages / extension / src / content / overlay / PickerLayer.vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import type { ElementContext } from "@ltt/shared";
import { buildElementContext } from "../../lib/selector";

const emit = defineEmits<{
  (e: "pick", element: ElementContext): void;
  (e: "cancel"): void;
}>();

const highlight = ref<{ x: number; y: number; w: number; h: number } | null>(null);
const label = ref("");
const layerRef = ref<HTMLDivElement>();

function elementAtPoint(x: number, y: number): Element | null {
  const layer = layerRef.value;
  if (!layer) return null;
  // momentarily drop the layer from hit-testing so elementFromPoint
  // resolves to the page element underneath
  layer.style.pointerEvents = "none";
  const el = document.elementFromPoint(x, y);
  layer.style.pointerEvents = "";
  return el;
}

function describe(el: Element): string {
  const tag = el.tagName.toLowerCase();
  const id = el.id ? `#${el.id}` : "";
  const classes = Array.from(el.classList).slice(0, 2).map((c) => `.${c}`).join("");
  return `${tag}${id}${classes}`.slice(0, 80);
}

function onMouseMove(event: MouseEvent) {
  const el = elementAtPoint(event.clientX, event.clientY);
  if (!el || el === document.documentElement || el === document.body) {
    highlight.value = null;
    label.value = "";
    return;
  }
  const rect = el.getBoundingClientRect();
  highlight.value = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
  label.value = describe(el);
}

function onClick(event: MouseEvent) {
  event.preventDefault();
  event.stopPropagation();
  const el = elementAtPoint(event.clientX, event.clientY);
  if (!el || el === document.documentElement || el === document.body) return;
  emit("pick", buildElementContext(el));
}

function onKeyDown(event: KeyboardEvent) {
  if (event.key === "Escape") {
    event.preventDefault();
    event.stopPropagation();
    emit("cancel");
  }
}

onMounted(() => {
  window.addEventListener("mousemove", onMouseMove, true);
  window.addEventListener("keydown", onKeyDown, true);
});
onUnmounted(() => {
  window.removeEventListener("mousemove", onMouseMove, true);
  window.removeEventListener("keydown", onKeyDown, true);
});
</script>

<template>
  <div ref="layerRef" class="picker-layer" @click.capture="onClick">
    <div
      v-if="highlight"
      class="picker-highlight"
      :style="{
        left: `${highlight.x}px`,
        top: `${highlight.y}px`,
        width: `${highlight.w}px`,
        height: `${highlight.h}px`,
      }"
    />
    <div
      v-if="label"
      class="picker-tooltip"
      :style="highlight ? { left: `${highlight.x}px`, top: `${Math.max(highlight.y - 26, 4)}px` } : {}"
    >
      {{ label }}
    </div>
  </div>
</template>

<style scoped>
.picker-layer {
  position: absolute;
  inset: 0;
  cursor: crosshair;
}
.picker-highlight {
  position: fixed;
  box-sizing: border-box;
  border: 2px solid var(--color-accent, #7aa2f7);
  background: rgba(122, 162, 247, 0.12);
  pointer-events: none;
}
.picker-tooltip {
  position: fixed;
  transform: translateY(-100%);
  padding: 2px 8px;
  background: var(--color-surface, #1a1c2b);
  border: 1px solid var(--color-accent, #7aa2f7);
  color: var(--color-text, #c0caf5);
  font-size: 11px;
  white-space: nowrap;
  pointer-events: none;
}
</style>