瀏覽代碼

refactor: automa element selector

Ahmad Kholid 3 年之前
父節點
當前提交
2383c38825

+ 1 - 9
src/components/content/selector/SelectorElementList.vue

@@ -6,15 +6,7 @@
       @mouseenter="$emit('highlight', { highlight: true, index, element })"
       @mouseleave="$emit('highlight', { highlight: false, index, element })"
     >
-      <p
-        class="mb-1 cursor-pointer"
-        title="Scroll into view"
-        @click="
-          element.element.scrollIntoView({ block: 'center', inline: 'center' })
-        "
-      >
-        #{{ index + 1 }} {{ elementName }}
-      </p>
+      <p class="mb-1">#{{ index + 1 }} {{ elementName }}</p>
       <slot name="item" v-bind="{ element }" />
     </li>
   </ul>

+ 1 - 0
src/components/content/selector/SelectorElementsDetail.vue

@@ -33,6 +33,7 @@
             </p>
             <input
               :value="value"
+              :placeholder="!value ? 'EMPTY' : null"
               readonly
               title="Attribute value"
               class="bg-transparent w-full"

+ 3 - 9
src/components/content/selector/SelectorQuery.vue

@@ -24,9 +24,9 @@
     <div class="mt-2 flex items-center">
       <ui-input
         :model-value="selector"
+        readonly
         placeholder="Element selector"
         class="leading-normal flex-1 h-full element-selector"
-        @change="updateSelector"
       >
         <template #prepend>
           <button class="absolute ml-2 left-0" @click="copySelector">
@@ -34,7 +34,7 @@
           </button>
         </template>
       </ui-input>
-      <template v-if="selectedCount === 1">
+      <template v-if="selectedCount === 1 && !selector.includes('|>')">
         <button
           class="mr-1 ml-2"
           title="Parent element"
@@ -51,7 +51,6 @@
 </template>
 <script setup>
 import { inject } from 'vue';
-import { debounce } from '@/utils/helper';
 import UiInput from '@/components/ui/UiInput.vue';
 
 const props = defineProps({
@@ -72,7 +71,7 @@ const props = defineProps({
     default: false,
   },
 });
-const emit = defineEmits([
+defineEmits([
   'change',
   'list',
   'parent',
@@ -83,11 +82,6 @@ const emit = defineEmits([
 
 const rootElement = inject('rootElement');
 
-const updateSelector = debounce((value) => {
-  if (value === props.selector) return;
-
-  emit('change', value);
-}, 250);
 function copySelector() {
   rootElement.shadowRoot.querySelector('input')?.select();
 

+ 53 - 29
src/components/content/shared/SharedElementSelector.vue

@@ -42,7 +42,8 @@
 <script setup>
 import { reactive, watch, onMounted, onBeforeUnmount } from 'vue';
 import { finder } from '@medv/finder';
-import { generateXPath } from '@/content/utils';
+import { debounce } from '@/utils/helper';
+import { generateXPath, getElementPath, getElementRect } from '@/content/utils';
 import findElementList from '@/content/elementSelector/listSelector';
 import generateElementsSelector from '@/content/elementSelector/generateElementsSelector';
 import SharedElementHighlighter from './SharedElementHighlighter.vue';
@@ -52,12 +53,19 @@ const props = defineProps({
     type: String,
     default: 'css',
   },
+  selectedEls: {
+    type: Array,
+    default: () => [],
+  },
   list: Boolean,
   disabled: Boolean,
+  withAttributes: Boolean,
 });
 const emit = defineEmits(['selected']);
 
 let frameElement = null;
+let lastScrollPosY = window.scrollY;
+let lastScrollPosX = window.scrollX;
 
 let hoveredElements = [];
 const elementsState = reactive({
@@ -65,10 +73,23 @@ const elementsState = reactive({
   selected: [],
 });
 
-function onScroll() {
+const onScroll = debounce(() => {
+  if (state.hide) return;
+
   hoveredElements = [];
-  elementsState.hoveredElements = [];
-}
+  elementsState.selected = [];
+
+  const yPos = window.scrollY - lastScrollPosY;
+  const xPos = window.scrollX - lastScrollPosX;
+
+  state.selected.forEach((_, index) => {
+    state.selected[index].x -= xPos;
+    state.selected[index].y -= yPos;
+  });
+
+  lastScrollPosX = window.scrollX;
+  lastScrollPosY = window.scrollY;
+}, 100);
 function resetFramesElements(options = {}) {
   const elements = document.querySelectorAll('iframe, frame');
 
@@ -82,21 +103,6 @@ function resetFramesElements(options = {}) {
     );
   });
 }
-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'
@@ -107,10 +113,12 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
 
   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 (type === 'selected') {
+      const prevSelectedList = document.querySelectorAll('[automa-el-list]');
+      prevSelectedList.forEach((el) => {
+        el.removeAttribute('automa-el-list');
+      });
+    }
 
     if (target.contentDocument) {
       target = target.contentDocument.elementsFromPoint(clientX, clientY);
@@ -123,6 +131,7 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
         clientY,
         list: isSelectList,
         type: 'automa:get-element-rect',
+        withAttributes: props.withAttributes,
       };
 
       if (type === 'selected')
@@ -140,18 +149,20 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
   }
 
   frameElement = null;
+
   let elementsRect = [];
+  const withAttribute = props.withAttributes && type === 'selected';
 
   if (isSelectList) {
     const elements = findElementList(target) || [];
 
     if (type === 'hovered') hoveredElements = elements;
 
-    elementsRect = elements.map((el) => getElementRect(el));
+    elementsRect = elements.map((el) => getElementRect(el, withAttribute));
   } else {
     if (type === 'hovered') hoveredElements = [target];
 
-    elementsRect = [getElementRect(target)];
+    elementsRect = [getElementRect(target, withAttribute)];
   }
 
   elementsState[type] = elementsRect;
@@ -165,7 +176,11 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
       list: isSelectList,
       selectorType: props.selectorType,
     });
-    console.log(selector);
+    emit('selected', {
+      selector,
+      elements: elementsRect,
+      path: getElementPath(target),
+    });
   }
 }
 function onMousemove(event) {
@@ -183,7 +198,10 @@ function onMessage({ data }) {
         ? finder(frameElement, { tagName: () => true })
         : generateXPath(frameElement);
 
-    emit('selected', `${frameSelector} |> ${data.selector}`);
+    emit('selected', {
+      elements: data.elements,
+      selector: `${frameSelector} |> ${data.selector}`,
+    });
   }
 
   const key = data.click ? 'selected' : 'hovered';
@@ -191,15 +209,15 @@ function onMessage({ data }) {
 }
 function attachListeners() {
   window.addEventListener('scroll', onScroll);
+  document.addEventListener('click', onClick);
   window.addEventListener('message', onMessage);
   window.addEventListener('mousemove', onMousemove);
-  document.addEventListener('click', onClick, true);
 }
 function detachListeners() {
   window.removeEventListener('scroll', onScroll);
+  document.removeEventListener('click', onClick);
   window.removeEventListener('message', onMessage);
   window.removeEventListener('mousemove', onMousemove);
-  document.removeEventListener('click', onClick, true);
 }
 
 watch(
@@ -208,6 +226,12 @@ watch(
     resetFramesElements({ clearCache: true });
   }
 );
+watch(
+  () => props.selectedEls,
+  () => {
+    elementsState.selected = props.selectedEls;
+  }
+);
 
 onMounted(attachListeners);
 onBeforeUnmount(detachListeners);

+ 42 - 39
src/content/elementSelector/App.vue

@@ -50,9 +50,8 @@
           v-model:selectList="state.selectList"
           :selector="state.elSelector"
           :selected-count="state.selectedElements.length"
-          @child="selectChildElement"
-          @parent="selectParentElement"
-          @change="updateSelectedElements"
+          @parent="selectElementPath('up')"
+          @child="selectElementPath('down')"
         />
         <selector-elements-detail
           v-if="!state.hide && state.selectedElements.length > 0"
@@ -72,22 +71,26 @@
     :disabled="state.hide"
     :list="state.selectList"
     :selector-type="state.selectorType"
-    @selected="state.elSelector = $event"
+    :selected-els="state.selectedElements"
+    with-attributes
+    @selected="onElementsSelected"
   />
 </template>
 <script setup>
 import { reactive, ref, watch, inject, onMounted, onBeforeUnmount } from 'vue';
+import { finder } from '@medv/finder';
 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 { getElementRect } from '../utils';
 
+const originalFontSize = document.documentElement.style.fontSize;
 const selectedElement = {
   path: [],
   pathIndex: 0,
+  cache: new WeakMap(),
 };
 
-const originalFontSize = document.documentElement.style.fontSize;
-
 const rootElement = inject('rootElement');
 
 const cardEl = ref('cardEl');
@@ -95,7 +98,6 @@ const mainActiveTab = ref('selector');
 const state = reactive({
   hide: false,
   elSelector: '',
-  listSelector: '',
   isDragging: false,
   selectList: false,
   isExecuting: false,
@@ -120,8 +122,15 @@ const cardElementObserver = new ResizeObserver(([entry]) => {
 function toggleHighlightElement({ index, highlight }) {
   state.selectedElements[index].highlight = highlight;
 }
+function onElementsSelected({ selector, elements, path }) {
+  if (path) {
+    selectedElement.path = path;
+  }
 
-function handleMouseMove({ clientX, clientY }) {
+  state.elSelector = selector;
+  state.selectedElements = elements || [];
+}
+function onMousemove({ clientX, clientY }) {
   if (!state.isDragging) return;
 
   const height = window.innerHeight;
@@ -137,42 +146,36 @@ function handleMouseMove({ clientX, clientY }) {
   cardRect.x = clientX;
   cardRect.y = clientY;
 }
-
-function selectChildElement() {
-  if (selectedElement.path.length === 0 || state.hide) return;
-
-  const currentEl = selectedElement.path[selectedElement.pathIndex];
-  let childElement = currentEl;
-
-  if (selectedElement.pathIndex <= 0) {
-    const childEl = Array.from(currentEl.children).find(
+function selectElementPath(type) {
+  let pathIndex =
+    type === 'up'
+      ? selectedElement.pathIndex + 1
+      : selectedElement.pathIndex - 1;
+  let element = selectedElement.path[pathIndex];
+
+  if ((type === 'up' && !element) || element?.tagName === 'BODY') return;
+
+  if (type === 'down' && !element) {
+    const previousElement = selectedElement.path[selectedElement.pathIndex];
+    const childEl = Array.from(previousElement.children).find(
       (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
     );
 
-    if (currentEl.childElementCount === 0 || currentEl === childEl) return;
+    if (!childEl) return;
 
-    childElement = childEl;
+    element = childEl;
     selectedElement.path.unshift(childEl);
-    selectedElement.pathIndex = 0;
-  } else {
-    selectedElement.pathIndex -= 1;
-    childElement = selectedElement.path[selectedElement.pathIndex];
+    pathIndex = 0;
   }
 
-  updateSelectedElements(getElementSelector(childElement));
-}
-function selectParentElement() {
-  if (selectedElement.path.length === 0 || state.hide) return;
-
-  const parentElement = selectedElement.path[selectedElement.pathIndex];
-
-  if (parentElement.tagName === 'HTML') return;
-
-  selectedElement.pathIndex += 1;
+  selectedElement.pathIndex = pathIndex;
 
-  updateSelectedElements(getElementSelector(parentElement));
+  state.selectedElements = [getElementRect(element, true)];
+  state.elSelector = selectedElement.cache.has(element)
+    ? selectedElement.cache.get(element)
+    : finder(element);
 }
-function handleMouseUp() {
+function onMouseup() {
   if (state.isDragging) state.isDragging = false;
 }
 function destroy() {
@@ -198,14 +201,14 @@ function destroy() {
 function attachListeners() {
   cardElementObserver.observe(cardEl.value);
 
-  window.addEventListener('mouseup', handleMouseUp);
-  window.addEventListener('mousemove', handleMouseMove);
+  window.addEventListener('mouseup', onMouseup);
+  window.addEventListener('mousemove', onMousemove);
 }
 function detachListeners() {
   cardElementObserver.disconnect();
 
-  window.removeEventListener('mouseup', handleMouseUp);
-  window.removeEventListener('mousemove', handleMouseMove);
+  window.removeEventListener('mouseup', onMouseup);
+  window.removeEventListener('mousemove', onMousemove);
 }
 
 watch(

+ 13 - 12
src/content/elementSelector/selectorFrameContext.js

@@ -1,19 +1,18 @@
+import { getElementRect } from '../utils';
 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,
-  };
+function getElementRectWithOffset(element, data) {
+  const withAttributes = data.withAttributes && data.click;
+  const elementRect = getElementRect(element, withAttributes);
+
+  elementRect.y += data.top;
+  elementRect.x += data.left;
 
-  return rect;
+  return elementRect;
 }
 function getElementsRect(data) {
   const [element] = document.elementsFromPoint(
@@ -41,7 +40,9 @@ function getElementsRect(data) {
     });
 
     payload.selector = selector;
-    payload.elements = hoveredElements.map((el) => elementRect(el, data));
+    payload.elements = hoveredElements.map((el) =>
+      getElementRectWithOffset(el, data)
+    );
   } else {
     prevSelectedElement = element;
     let elementsRect = [];
@@ -50,10 +51,10 @@ function getElementsRect(data) {
       const elements = findElementList(element) || [];
 
       hoveredElements = elements;
-      elementsRect = elements.map((el) => elementRect(el, data));
+      elementsRect = elements.map((el) => getElementRectWithOffset(el, data));
     } else {
       hoveredElements = [element];
-      elementsRect = [elementRect(element, data)];
+      elementsRect = [getElementRectWithOffset(element, data)];
     }
 
     payload.elements = elementsRect;

+ 1 - 10
src/content/services/recordWorkflow/App.vue

@@ -207,6 +207,7 @@ import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
 import { elementsHighlightData, tasks } from '@/utils/shared';
 import SharedElementHighlighter from '@/components/content/shared/SharedElementHighlighter.vue';
+import { getElementPath } from '../../utils';
 import findElementList from '../../elementSelector/listSelector';
 import addBlock from './addBlock';
 
@@ -467,16 +468,6 @@ function onMousemove({ clientX, clientY, target: eventTarget }) {
     selectState.hoveredElements = elementsRect;
   }
 }
-function getElementPath(el, root = document.documentElement) {
-  const path = [el];
-
-  /* eslint-disable-next-line */
-  while ((el = el.parentNode) && !el.isEqualNode(root)) {
-    path.push(el);
-  }
-
-  return path;
-}
 function onClick(event) {
   if (!selectState.isSelecting) return;
 

+ 37 - 0
src/content/utils.js

@@ -1,3 +1,40 @@
+export function getElementRect(target, withAttributes) {
+  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 (withAttributes) {
+    const attributes = {};
+
+    Array.from(target.attributes).forEach(({ name, value }) => {
+      if (name === 'automa-el-list') return;
+
+      attributes[name] = value;
+    });
+
+    result.attributes = attributes;
+  }
+
+  return result;
+}
+
+export function getElementPath(el, root = document.documentElement) {
+  const path = [el];
+
+  /* eslint-disable-next-line */
+  while ((el = el.parentNode) && !el.isEqualNode(root)) {
+    path.push(el);
+  }
+
+  return path;
+}
+
 export function generateXPath(element) {
   if (!element) return null;
   if (element.id !== '') return `id("${element.id}")`;