Browse Source

feat: select element inside `iframe` in element selector

Ahmad Kholid 3 years ago
parent
commit
a47b33eda3

+ 31 - 80
src/components/content/shared/SharedElementHighlighter.vue

@@ -1,95 +1,46 @@
 <template>
-  <svg
-    class="automa-element-highlighter"
-    style="
-      height: 100%;
-      width: 100%;
-      top: 0;
-      left: 0;
-      pointer-events: none;
-      position: fixed;
-      z-index: 10;
-    "
-  >
-    <g v-for="(colors, key) in data" :key="key">
-      <rect
-        v-for="(item, index) in items[key]"
-        v-bind="{
-          x: item.x,
-          y: item.y,
-          width: item.width,
-          height: item.height,
-          'stroke-dasharray': item.outline ? '5,5' : null,
-          fill: getFillColor(item, colors),
-          stroke: getStrokeColor(item, colors),
-        }"
-        :key="key + index"
-        stroke-width="2"
-      ></rect>
-    </g>
-  </svg>
+  <rect
+    v-for="(item, index) in items"
+    v-bind="{
+      ...item,
+      'stroke-dasharray': item.outline ? '5,5' : null,
+      fill: getFillColor(item),
+      stroke: getStrokeColor(item),
+    }"
+    :key="index"
+    stroke-width="2"
+  ></rect>
 </template>
 <script setup>
-import { onMounted, onBeforeUnmount } from 'vue';
-import { debounce } from '@/utils/helper';
-
 const props = defineProps({
-  disabled: {
-    type: Boolean,
-    default: false,
-  },
-  data: {
-    type: Object,
-    default: () => ({}),
-  },
   items: {
     type: Object,
     default: () => ({}),
   },
+  stroke: {
+    type: String,
+    default: null,
+  },
+  activeStroke: {
+    type: String,
+    default: null,
+  },
+  fill: {
+    type: String,
+    default: null,
+  },
+  activeFill: {
+    type: String,
+    default: null,
+  },
 });
-const emit = defineEmits(['update']);
-
-let lastScrollPosY = window.scrollY;
-let lastScrollPosX = window.scrollX;
 
-const handleScroll = debounce(() => {
-  if (props.hide) return;
-
-  const yPos = window.scrollY - lastScrollPosY;
-  const xPos = window.scrollX - lastScrollPosX;
-
-  const updatePositions = (key) =>
-    props.items[key]?.map((item) => {
-      const copyItem = { ...item };
-
-      copyItem.x -= xPos;
-      copyItem.y -= yPos;
-
-      return copyItem;
-    }) || [];
-
-  Object.keys(props.data).forEach((key) => {
-    const newPositions = updatePositions(key);
-    emit('update', { key, items: newPositions });
-  });
-
-  lastScrollPosX = window.scrollX;
-  lastScrollPosY = window.scrollY;
-}, 100);
-
-function getFillColor(item, colors) {
+function getFillColor(item) {
   if (item.outline) return null;
 
-  return item.highlight ? colors.fill : colors.activeFill || colors.fill;
+  return item.highlight ? props.fill : props.activeFill || props.fill;
 }
-function getStrokeColor(item, colors) {
-  return item.highlight ? colors.stroke : colors.activeStroke || colors.stroke;
+function getStrokeColor(item) {
+  return item.highlight ? props.stroke : props.activeStroke || props.stroke;
 }
-
-onMounted(() => {
-  window.addEventListener('scroll', handleScroll);
-});
-onBeforeUnmount(() => {
-  window.removeEventListener('scroll', handleScroll);
-});
 </script>

+ 214 - 0
src/components/content/shared/SharedElementSelector.vue

@@ -0,0 +1,214 @@
+<template>
+  <svg
+    v-if="!disabled"
+    class="automa-element-highlighter"
+    style="
+      height: 100%;
+      width: 100%;
+      top: 0;
+      left: 0;
+      pointer-events: none;
+      position: fixed;
+      z-index: 10;
+    "
+  >
+    <shared-element-highlighter
+      :items="elementsState.hovered"
+      stroke="#fbbf24"
+      fill="rgba(251, 191, 36, 0.1)"
+    />
+    <shared-element-highlighter
+      :items="elementsState.selected"
+      stroke="#2563EB"
+      active-stroke="#f87171"
+      fill="rgba(37, 99, 235, 0.1)"
+      active-fill="rgba(248, 113, 113, 0.1)"
+    />
+  </svg>
+  <teleport to="body">
+    <div
+      v-if="!disabled"
+      style="
+        z-index: 9999999;
+        position: fixed;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+      "
+    ></div>
+  </teleport>
+</template>
+<script setup>
+import { reactive, watch, onMounted, onBeforeUnmount } from 'vue';
+import { finder } from '@medv/finder';
+import { generateXPath } from '@/content/utils';
+import findElementList from '@/content/elementSelector/listSelector';
+import generateElementsSelector from '@/content/elementSelector/generateElementsSelector';
+import SharedElementHighlighter from './SharedElementHighlighter.vue';
+
+const props = defineProps({
+  selectorType: {
+    type: String,
+    default: 'css',
+  },
+  list: Boolean,
+  disabled: Boolean,
+});
+const emit = defineEmits(['selected']);
+
+let frameElement = null;
+
+let hoveredElements = [];
+const elementsState = reactive({
+  hovered: [],
+  selected: [],
+});
+
+function onScroll() {
+  hoveredElements = [];
+  elementsState.hoveredElements = [];
+}
+function resetFramesElements(options = {}) {
+  const elements = document.querySelectorAll('iframe, frame');
+
+  elements.forEach((element) => {
+    element.contentWindow.postMessage(
+      {
+        ...options,
+        type: 'automa:reset-element-selector',
+      },
+      '*'
+    );
+  });
+}
+function getElementRect(target, withElement = false) {
+  if (!target) return {};
+
+  const { x, y, height, width } = target.getBoundingClientRect();
+  const result = {
+    width: width + 4,
+    height: height + 4,
+    x: x - 2,
+    y: y - 2,
+  };
+
+  if (withElement) result.element = target;
+
+  return result;
+}
+function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
+  const isAutomaContainer = eventTarget.classList.contains(
+    'automa-element-selector'
+  );
+  if (props.disabled || isAutomaContainer) return;
+
+  const isSelectList = props.list && props.selectorType === 'css';
+
+  let { 1: target } = document.elementsFromPoint(clientX, clientY);
+  if (target.tagName === 'IFRAME' || target.tagName === 'FRAME') {
+    const prevSelectedList = document.querySelectorAll('[automa-el-list]');
+    prevSelectedList.forEach((el) => {
+      el.removeAttribute('automa-el-list');
+    });
+
+    if (target.contentDocument) {
+      target = target.contentDocument.elementsFromPoint(clientX, clientY);
+    } else {
+      const { top, left } = target.getBoundingClientRect();
+      const payload = {
+        top,
+        left,
+        clientX,
+        clientY,
+        list: isSelectList,
+        type: 'automa:get-element-rect',
+      };
+
+      if (type === 'selected')
+        Object.assign(payload, {
+          click: true,
+          selectorType: props.selectorType,
+        });
+
+      target.contentWindow.postMessage(payload, '*');
+    }
+
+    frameElement = target;
+
+    return;
+  }
+
+  frameElement = null;
+  let elementsRect = [];
+
+  if (isSelectList) {
+    const elements = findElementList(target) || [];
+
+    if (type === 'hovered') hoveredElements = elements;
+
+    elementsRect = elements.map((el) => getElementRect(el));
+  } else {
+    if (type === 'hovered') hoveredElements = [target];
+
+    elementsRect = [getElementRect(target)];
+  }
+
+  elementsState[type] = elementsRect;
+
+  if (type === 'selected') {
+    resetFramesElements();
+
+    const selector = generateElementsSelector({
+      target,
+      hoveredElements,
+      list: isSelectList,
+      selectorType: props.selectorType,
+    });
+    console.log(selector);
+  }
+}
+function onMousemove(event) {
+  retrieveElementsRect(event, 'hovered');
+}
+function onClick(event) {
+  retrieveElementsRect(event, 'selected');
+}
+function onMessage({ data }) {
+  if (data.type !== 'automa:iframe-element-rect') return;
+
+  if (data.click) {
+    const frameSelector =
+      props.selectorType === 'css'
+        ? finder(frameElement, { tagName: () => true })
+        : generateXPath(frameElement);
+
+    emit('selected', `${frameSelector} |> ${data.selector}`);
+  }
+
+  const key = data.click ? 'selected' : 'hovered';
+  elementsState[key] = data.elements;
+}
+function attachListeners() {
+  window.addEventListener('scroll', onScroll);
+  window.addEventListener('message', onMessage);
+  window.addEventListener('mousemove', onMousemove);
+  document.addEventListener('click', onClick, true);
+}
+function detachListeners() {
+  window.removeEventListener('scroll', onScroll);
+  window.removeEventListener('message', onMessage);
+  window.removeEventListener('mousemove', onMousemove);
+  document.removeEventListener('click', onClick, true);
+}
+
+watch(
+  () => [props.list, props.disabled],
+  () => {
+    resetFramesElements({ clearCache: true });
+  }
+);
+
+onMounted(attachListeners);
+onBeforeUnmount(detachListeners);
+</script>

+ 19 - 306
src/content/elementSelector/App.vue

@@ -67,41 +67,19 @@
         />
       </div>
     </div>
-    <shared-element-highlighter
-      v-if="!state.hide"
-      :disabled="state.hide"
-      :data="elementsHighlightData"
-      :items="{
-        hoveredElements: state.hoveredElements,
-        selectedElements: state.selectedElements,
-      }"
-      @update="state[$event.key] = $event.items"
-    />
   </div>
-  <teleport to="body">
-    <div
-      v-if="!state.hide"
-      style="
-        z-index: 9999999;
-        position: fixed;
-        left: 0;
-        top: 0;
-        width: 100%;
-        height: 100%;
-      "
-    ></div>
-  </teleport>
+  <shared-element-selector
+    :disabled="state.hide"
+    :list="state.selectList"
+    :selector-type="state.selectorType"
+    @selected="state.elSelector = $event"
+  />
 </template>
 <script setup>
 import { reactive, ref, watch, inject, onMounted, onBeforeUnmount } from 'vue';
-import { getCssSelector } from 'css-selector-generator';
-import { finder } from '@medv/finder';
-import { elementsHighlightData } from '@/utils/shared';
-import findElement from '@/utils/FindElement';
 import SelectorQuery from '@/components/content/selector/SelectorQuery.vue';
+import SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';
 import SelectorElementsDetail from '@/components/content/selector/SelectorElementsDetail.vue';
-import SharedElementHighlighter from '@/components/content/shared/SharedElementHighlighter.vue';
-import findElementList from './listSelector';
 
 const selectedElement = {
   path: [],
@@ -115,17 +93,15 @@ const rootElement = inject('rootElement');
 const cardEl = ref('cardEl');
 const mainActiveTab = ref('selector');
 const state = reactive({
+  hide: false,
   elSelector: '',
   listSelector: '',
   isDragging: false,
   selectList: false,
   isExecuting: false,
-  selectElements: [],
-  hoveredElements: [],
   selectorType: 'css',
   selectedElements: [],
   activeTab: 'attributes',
-  hide: window.self !== window.top,
 });
 const cardRect = reactive({
   x: 0,
@@ -141,275 +117,27 @@ const cardElementObserver = new ResizeObserver(([entry]) => {
   cardRect.height = height;
 });
 
-/* eslint-disable  no-use-before-define */
-const getElementSelector = (element, options = {}) => {
-  if (state.selectorType === 'css') {
-    if (Array.isArray(element)) {
-      return getCssSelector(element, {
-        root: document.body,
-        blacklist: [
-          '[focused]',
-          /focus/,
-          '[src=*]',
-          '[data-*]',
-          '[href=*]',
-          '[style=*]',
-          '[value=*]',
-          '[automa-*]',
-        ],
-        includeTag: true,
-        ...options,
-      });
-    }
-
-    return finder(element);
-  }
-
-  return generateXPath(element);
-};
-
-function generateXPath(element) {
-  if (!element) return null;
-  if (element.id !== '') return `id("${element.id}")`;
-  if (element === document.body) return `//${element.tagName}`;
-
-  let ix = 0;
-  const siblings = element.parentNode.childNodes;
-
-  for (let index = 0; index < siblings.length; index += 1) {
-    const sibling = siblings[index];
-
-    if (sibling === element) {
-      return `${generateXPath(element.parentNode)}/${element.tagName}[${
-        ix + 1
-      }]`;
-    }
-
-    if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
-      ix += 1;
-    }
-  }
-
-  return null;
-}
 function toggleHighlightElement({ index, highlight }) {
   state.selectedElements[index].highlight = highlight;
 }
-function getElementRect(target, withElement = false) {
-  if (!target) return {};
 
-  const { x, y, height, width } = target.getBoundingClientRect();
-  const result = {
-    width: width + 4,
-    height: height + 4,
-    x: x - 2,
-    y: y - 2,
-  };
+function handleMouseMove({ clientX, clientY }) {
+  if (!state.isDragging) return;
 
-  if (withElement) result.element = target;
+  const height = window.innerHeight;
+  const width = document.documentElement.clientWidth;
 
-  return result;
-}
-function updateSelectedElements(selector) {
-  state.elSelector = selector;
-
-  try {
-    const selectorType = state.selectorType === 'css' ? 'cssSelector' : 'xpath';
-    let elements = findElement[selectorType]({ selector, multiple: true });
-    const selectElements = [];
-
-    if (selectorType === 'xpath') {
-      elements = elements ? [elements] : [];
-    }
-
-    const elementsDetail = Array.from(elements).map((element, index) => {
-      const attributes = Array.from(element.attributes).reduce(
-        (acc, { name, value }) => {
-          if (name === 'automa-el-list') return acc;
-
-          acc[name] = value;
-
-          return acc;
-        },
-        {}
-      );
-
-      const elementProps = {
-        element,
-        attributes,
-        highlight: false,
-        ...getElementRect(element),
-      };
-
-      if (element.tagName === 'SELECT') {
-        const options = Array.from(element.querySelectorAll('option')).map(
-          (option) => ({
-            name: option.innerText,
-            value: option.value,
-          })
-        );
-
-        selectElements.push({ ...elementProps, options, index });
-      }
-
-      return elementProps;
-    });
-
-    state.selectElements = selectElements;
-    state.selectedElements = elementsDetail;
-  } catch (error) {
-    state.selectElements = [];
-    state.selectedElements = [];
-  }
-}
-function getElementList(target) {
-  const automaListEl = target.closest('[automa-el-list]');
-
-  if (automaListEl) {
-    if (target.hasAttribute('automa-el-list')) return [];
-
-    const childSelector = finder(target, {
-      root: automaListEl,
-      idName: () => false,
-    });
-    const elements = document.querySelectorAll(
-      `${state.listSelector} ${childSelector}`
-    );
+  if (clientY < 10) clientY = 10;
+  else if (cardRect.height + clientY > height)
+    clientY = height - cardRect.height;
 
-    return Array.from(elements);
-  }
+  if (clientX < 10) clientX = 10;
+  else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
 
-  return findElementList(target) || [target];
+  cardRect.x = clientX;
+  cardRect.y = clientY;
 }
-let prevHoverElement = null;
-function handleMouseMove({ clientX, clientY, target }) {
-  if (state.isDragging) {
-    const height = window.innerHeight;
-    const width = document.documentElement.clientWidth;
-
-    if (clientY < 10) clientY = 10;
-    else if (cardRect.height + clientY > height)
-      clientY = height - cardRect.height;
-
-    if (clientX < 10) clientX = 10;
-    else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
-
-    cardRect.x = clientX;
-    cardRect.y = clientY;
-
-    return;
-  }
-
-  const { 1: realTarget } = document.elementsFromPoint(clientX, clientY);
-
-  if (prevHoverElement === realTarget) return;
-  prevHoverElement = realTarget;
-
-  if (state.hide || rootElement === target) return;
-
-  let elementsRect = [];
-
-  if (state.selectList) {
-    const elements = getElementList(realTarget) || [];
 
-    elementsRect = elements.map((el) => getElementRect(el, true));
-  } else {
-    elementsRect = [getElementRect(realTarget)];
-  }
-
-  state.hoveredElements = elementsRect;
-}
-function handleClick(event) {
-  const { target: eventTarget, path, ctrlKey, clientY, clientX } = event;
-
-  if (eventTarget === rootElement || state.hide || state.isExecuting) return;
-  event.stopPropagation();
-  event.preventDefault();
-
-  const { 1: target } = document.elementsFromPoint(clientX, clientY);
-
-  if (state.selectList) {
-    const firstElement = state.hoveredElements[0].element;
-
-    if (!firstElement) return;
-
-    const isInList = target.closest('[automa-el-list]');
-    if (isInList) {
-      const childSelector = finder(target, {
-        root: isInList,
-        idName: () => false,
-      });
-      updateSelectedElements(`${state.listSelector} ${childSelector}`, true);
-
-      return;
-    }
-
-    const prevSelectedList = document.querySelectorAll('[automa-el-list]');
-    prevSelectedList.forEach((element) => {
-      element.removeAttribute('automa-el-list');
-    });
-
-    state.hoveredElements.forEach(({ element }) => {
-      element.setAttribute('automa-el-list', '');
-    });
-
-    const parentSelector = finder(firstElement.parentElement);
-    const elementSelector = `${parentSelector} > ${firstElement.tagName.toLowerCase()}`;
-
-    state.listSelector = elementSelector;
-    updateSelectedElements(elementSelector);
-
-    return;
-  }
-
-  const getElementDetail = (element) => {
-    const attributes = {};
-
-    Array.from(element.attributes).forEach(({ name, value }) => {
-      if (name === 'automa-el-list') return;
-
-      attributes[name] = value;
-    });
-
-    return {
-      ...getElementRect(element),
-      element,
-      attributes,
-      highlight: false,
-      outline: state.selectList && state.selectedElements.length,
-    };
-  };
-
-  let targetElement = target;
-  const targetElementDetail = getElementDetail(target);
-
-  if (state.selectorType === 'css' && ctrlKey) {
-    let elementIndex = -1;
-
-    const elements = state.selectedElements.map(({ element }, index) => {
-      if (element === targetElement) {
-        elementIndex = index;
-      }
-
-      return element;
-    });
-
-    if (elementIndex === -1) {
-      targetElement = [...elements, target];
-      state.selectedElements.push(targetElementDetail);
-    } else {
-      targetElement = elements.splice(elementIndex, 1);
-      state.selectedElements.splice(elementIndex, 1);
-    }
-  } else {
-    state.selectedElements = [targetElementDetail];
-  }
-
-  state.elSelector = getElementSelector(targetElement);
-
-  selectedElement.index = 0;
-  selectedElement.path = path;
-}
 function selectChildElement() {
   if (selectedElement.path.length === 0 || state.hide) return;
 
@@ -472,14 +200,12 @@ function attachListeners() {
 
   window.addEventListener('mouseup', handleMouseUp);
   window.addEventListener('mousemove', handleMouseMove);
-  document.addEventListener('click', handleClick, true);
 }
 function detachListeners() {
   cardElementObserver.disconnect();
 
   window.removeEventListener('mouseup', handleMouseUp);
   window.removeEventListener('mousemove', handleMouseMove);
-  document.removeEventListener('click', handleClick, true);
 }
 
 watch(
@@ -488,19 +214,6 @@ watch(
     document.body.toggleAttribute('automa-isDragging', value);
   }
 );
-watch(
-  () => state.selectList,
-  (value) => {
-    if (value) {
-      state.selectedElements = [];
-    } else {
-      const prevSelectedList = document.querySelectorAll('[automa-el-list]');
-      prevSelectedList.forEach((element) => {
-        element.removeAttribute('automa-el-list');
-      });
-    }
-  }
-);
 
 onMounted(() => {
   setTimeout(() => {

+ 41 - 0
src/content/elementSelector/generateElementsSelector.js

@@ -0,0 +1,41 @@
+import { finder } from '@medv/finder';
+import { generateXPath } from '../utils';
+
+export default function ({ list, target, selectorType, hoveredElements }) {
+  let selector = '';
+  const [selectedElement] = hoveredElements;
+
+  if (list) {
+    const isInList = target.closest('[automa-el-list]');
+
+    if (isInList) {
+      const childSelector = finder(target, {
+        root: isInList,
+        idName: () => false,
+      });
+      const listSelector = isInList.getAttribute('automa-el-list');
+
+      selector = `${listSelector} ${childSelector}`;
+    } else {
+      selector = `${finder(
+        selectedElement.parentElement
+      )} > ${selectedElement.tagName.toLowerCase()}`;
+
+      const prevSelectedList = document.querySelectorAll('[automa-el-list]');
+      prevSelectedList.forEach((el) => {
+        el.removeAttribute('automa-el-list');
+      });
+
+      hoveredElements.forEach((el) => {
+        el.setAttribute('automa-el-list', selector);
+      });
+    }
+  } else {
+    selector =
+      selectorType === 'css'
+        ? finder(selectedElement)
+        : generateXPath(selectedElement);
+  }
+
+  return selector;
+}

+ 19 - 9
src/content/elementSelector/index.js

@@ -1,6 +1,7 @@
 import browser from 'webextension-polyfill';
 import initElementSelector from './main';
 import injectAppStyles from '../injectAppStyles';
+import selectorFrameContext from './selectorFrameContext';
 
 function elementSelectorInstance() {
   const rootElementExist = document.querySelector(
@@ -28,19 +29,28 @@ function elementSelectorInstance() {
   });
 
   try {
-    const isAppExists = elementSelectorInstance();
+    if (window.self === window.top) {
+      const isAppExists = elementSelectorInstance();
 
-    if (isAppExists) return;
+      if (isAppExists) return;
 
-    const rootElement = document.createElement('div');
-    rootElement.setAttribute('id', 'app-container');
-    rootElement.classList.add('automa-element-selector');
-    rootElement.attachShadow({ mode: 'open' });
+      const rootElement = document.createElement('div');
+      rootElement.setAttribute('id', 'app-container');
+      rootElement.classList.add('automa-element-selector');
+      rootElement.attachShadow({ mode: 'open' });
 
-    initElementSelector(rootElement);
-    await injectAppStyles(rootElement.shadowRoot);
+      initElementSelector(rootElement);
+      await injectAppStyles(rootElement.shadowRoot);
 
-    document.documentElement.appendChild(rootElement);
+      document.documentElement.appendChild(rootElement);
+    } else {
+      const style = document.createElement('style');
+      style.textContent = '[automa-el-list] {outline: 2px dashed #6366f1;}';
+
+      document.body.appendChild(style);
+
+      selectorFrameContext();
+    }
   } catch (error) {
     console.error(error);
   }

+ 21 - 1
src/content/elementSelector/listSelector.js

@@ -1,3 +1,5 @@
+import { finder } from '@medv/finder';
+
 /* eslint-disable  no-cond-assign */
 export function getAllSiblings(el, selector) {
   const siblings = [el];
@@ -81,4 +83,22 @@ export function getElementList(el, maxDepth = 50, paths = []) {
   return siblings;
 }
 
-export default getElementList;
+export default function (target) {
+  const automaListEl = target.closest('[automa-el-list]');
+
+  if (automaListEl) {
+    if (target.hasAttribute('automa-el-list')) return [];
+
+    const childSelector = finder(target, {
+      root: automaListEl,
+      idName: () => false,
+    });
+    const elements = document.querySelectorAll(
+      `[automa-el-list] ${childSelector}`
+    );
+
+    return Array.from(elements);
+  }
+
+  return getElementList(target) || [target];
+}

+ 83 - 0
src/content/elementSelector/selectorFrameContext.js

@@ -0,0 +1,83 @@
+import findElementList from './listSelector';
+import generateElementsSelector from './generateElementsSelector';
+
+let hoveredElements = [];
+let prevSelectedElement = null;
+
+function elementRect(element, offset) {
+  const { x, y, height, width } = element.getBoundingClientRect();
+  const rect = {
+    width: width + 4,
+    height: height + 4,
+    y: y + offset.top - 2,
+    x: x + offset.left - 2,
+  };
+
+  return rect;
+}
+function getElementsRect(data) {
+  const [element] = document.elementsFromPoint(
+    data.clientX - data.left,
+    data.clientY - data.top
+  );
+  if ((!element || element === prevSelectedElement) && !data.click) return;
+
+  const payload = {
+    elements: [],
+    type: 'automa:iframe-element-rect',
+  };
+
+  if (data.click) {
+    if (hoveredElements.length === 0) return;
+
+    payload.click = true;
+
+    const [selectedElement] = hoveredElements;
+    const selector = generateElementsSelector({
+      hoveredElements,
+      list: data.list,
+      target: selectedElement,
+      selectorType: data.selectorType,
+    });
+
+    payload.selector = selector;
+    payload.elements = hoveredElements.map((el) => elementRect(el, data));
+  } else {
+    prevSelectedElement = element;
+    let elementsRect = [];
+
+    if (data.list) {
+      const elements = findElementList(element) || [];
+
+      hoveredElements = elements;
+      elementsRect = elements.map((el) => elementRect(el, data));
+    } else {
+      hoveredElements = [element];
+      elementsRect = [elementRect(element, data)];
+    }
+
+    payload.elements = elementsRect;
+  }
+
+  window.top.postMessage(payload, '*');
+}
+function resetElementSelector(data) {
+  const prevSelectedList = document.querySelectorAll('[automa-el-list]');
+  prevSelectedList.forEach((el) => {
+    el.removeAttribute('automa-el-list');
+  });
+
+  if (data.clearCache) {
+    hoveredElements = [];
+    prevSelectedElement = null;
+  }
+}
+function onMessage({ data }) {
+  if (data.type === 'automa:get-element-rect') getElementsRect(data);
+  else if (data.type === 'automa:reset-element-selector')
+    resetElementSelector(data);
+}
+
+export default function () {
+  window.addEventListener('message', onMessage);
+}

+ 25 - 0
src/content/utils.js

@@ -1,3 +1,28 @@
+export function generateXPath(element) {
+  if (!element) return null;
+  if (element.id !== '') return `id("${element.id}")`;
+  if (element === document.body) return `//${element.tagName}`;
+
+  let ix = 0;
+  const siblings = element.parentNode.childNodes;
+
+  for (let index = 0; index < siblings.length; index += 1) {
+    const sibling = siblings[index];
+
+    if (sibling === element) {
+      return `${generateXPath(element.parentNode)}/${element.tagName}[${
+        ix + 1
+      }]`;
+    }
+
+    if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
+      ix += 1;
+    }
+  }
+
+  return null;
+}
+
 export function automaRefDataStr(varName) {
   return `
 function findData(obj, path) {