Browse Source

feat: add list element selector

Ahmad Kholid 3 years ago
parent
commit
09fe31e1e0

+ 80 - 19
src/content/element-selector/App.vue

@@ -33,10 +33,10 @@
         </ui-button>
       </div>
       <app-selector
+        v-model:selectorType="state.selectorType"
+        v-model:selectList="state.selectList"
         :selector="state.elSelector"
         :selected-count="state.selectedElements.length"
-        :selector-type="state.selectorType"
-        @selector="state.selectorType = $event"
         @child="selectChildElement"
         @parent="selectParentElement"
         @change="updateSelectedElements"
@@ -159,6 +159,7 @@ import findElement from '@/utils/find-element';
 import AppBlocks from './AppBlocks.vue';
 import AppSelector from './AppSelector.vue';
 import AppElementList from './AppElementList.vue';
+import { getAllSiblings } from './list-selector';
 
 const selectedElement = {
   path: [],
@@ -175,6 +176,7 @@ const state = reactive({
   activeTab: '',
   elSelector: '',
   isDragging: false,
+  selectList: false,
   isExecuting: false,
   selectElements: [],
   selectorType: 'css',
@@ -197,10 +199,11 @@ const getElementSelector = (element, options = {}) =>
         blacklist: [
           '[focused]',
           /focus/,
+          '[src=*]',
           '[data-*]',
           '[href=*]',
-          '[src=*]',
           '[value=*]',
+          '[automa-*]',
         ],
         selectors: ['id', 'class', 'tag', 'attribute'],
         includeTag: true,
@@ -235,17 +238,20 @@ function generateXPath(element) {
 function toggleHighlightElement({ index, highlight }) {
   state.selectedElements[index].highlight = highlight;
 }
-function getElementRect(target) {
+function getElementRect(target, withElement = false) {
   if (!target) return {};
 
   const { x, y, height, width } = target.getBoundingClientRect();
-
-  return {
+  const result = {
     width: width + 4,
     height: height + 4,
     x: x - 2,
     y: y - 2,
   };
+
+  if (withElement) result.element = target;
+
+  return result;
 }
 function updateSelectedElements(selector) {
   state.elSelector = selector;
@@ -289,7 +295,37 @@ function updateSelectedElements(selector) {
     state.selectedElements = [];
   }
 }
-function handleMouseMove({ clientX, clientY, target }) {
+function elementList(target, path) {
+  // if (target.closest('[automa-el-list]')) return;
+
+  const hasMostChildren = path.reduce((el, acc) => {
+    if (el.childElementCount < acc.childElementCount) return acc;
+
+    return el;
+  }, path[0]);
+
+  if (target.parentElement === hasMostChildren) {
+    return getAllSiblings(target).elements;
+  }
+
+  const findElementList = (el) => {
+    let { elements } = getAllSiblings(el);
+
+    if (elements.length <= 1 && el?.parentElement) {
+      elements = findElementList(el.parentElement);
+    }
+
+    return elements;
+  };
+
+  return findElementList(target);
+}
+let prevHoverElement = null;
+function handleMouseMove({ clientX, clientY, target, path }) {
+  if (prevHoverElement === target) return;
+
+  prevHoverElement = target;
+
   if (state.isDragging) {
     const height = window.innerHeight;
     const width = document.documentElement.clientWidth;
@@ -309,29 +345,54 @@ function handleMouseMove({ clientX, clientY, target }) {
 
   if (state.hide || rootElement === target) return;
 
-  state.hoveredElements = [getElementRect(target)];
+  state.hoveredElements = state.selectList
+    ? elementList(target, path.slice(0, -4)).map((el) =>
+        getElementRect(el, true)
+      )
+    : [getElementRect(target)];
 }
 function handleClick(event) {
   const { target, path, ctrlKey } = event;
 
   if (target === rootElement || state.hide || state.isExecuting) return;
-
   event.stopPropagation();
   event.preventDefault();
 
-  const attributes = Array.from(target.attributes).map(({ name, value }) => ({
-    name,
-    value,
-  }));
+  const getElementDetail = (element) => {
+    const attributes = Array.from(element.attributes).map(
+      ({ name, value }) => ({
+        name,
+        value,
+      })
+    );
 
-  let targetElement = target;
-  const targetElementDetail = {
-    ...getElementRect(target),
-    attributes,
-    element: target,
-    highlight: false,
+    return {
+      ...getElementRect(element),
+      element,
+      attributes,
+      highlight: false,
+    };
   };
 
+  if (state.selectList) {
+    const firstElement = state.hoveredElements[0].element;
+
+    if (!firstElement) return;
+
+    const parentSelector = getCssSelector(firstElement.parentElement, {
+      includeTag: true,
+    });
+
+    updateSelectedElements(
+      `${parentSelector} > ${firstElement.tagName.toLowerCase()}`
+    );
+
+    return;
+  }
+
+  let targetElement = target;
+  const targetElementDetail = getElementDetail(target);
+
   if (state.selectorType === 'css' && ctrlKey) {
     let elementIndex = -1;
 

+ 32 - 9
src/content/element-selector/AppSelector.vue

@@ -1,13 +1,25 @@
 <template>
   <div class="mt-4">
-    <ui-select
-      :model-value="selectorType"
-      class="w-full"
-      @change="$emit('selector', $event)"
-    >
-      <option value="css">CSS Selector</option>
-      <option value="xpath">XPath</option>
-    </ui-select>
+    <div class="flex items-center">
+      <ui-select
+        :model-value="selectorType"
+        class="w-full"
+        @change="$emit('update:selectorType', $event)"
+      >
+        <option value="css">CSS Selector</option>
+        <option value="xpath">XPath</option>
+      </ui-select>
+      <ui-button
+        v-if="selectorType === 'css'"
+        :class="{ 'text-primary': selectList }"
+        icon
+        class="ml-2"
+        title="Select a list of elements"
+        @click="$emit('update:selectList', !selectList)"
+      >
+        <v-remixicon name="riListUnordered" />
+      </ui-button>
+    </div>
     <div class="mt-2 flex items-center">
       <ui-input
         :model-value="selector"
@@ -54,8 +66,19 @@ const props = defineProps({
     type: String,
     default: '',
   },
+  selectList: {
+    type: Boolean,
+    default: false,
+  },
 });
-const emit = defineEmits(['change', 'parent', 'child', 'selector']);
+const emit = defineEmits([
+  'change',
+  'list',
+  'parent',
+  'child',
+  'update:selectorType',
+  'update:selectList',
+]);
 
 const rootElement = inject('rootElement');
 

+ 2 - 0
src/content/element-selector/icons.js

@@ -5,6 +5,7 @@ import {
   riEyeOffLine,
   riFileCopyLine,
   riDragMoveLine,
+  riListUnordered,
   riArrowLeftLine,
   riArrowLeftSLine,
   riInformationLine,
@@ -18,6 +19,7 @@ export default {
   riEyeOffLine,
   riFileCopyLine,
   riDragMoveLine,
+  riListUnordered,
   riArrowLeftLine,
   riArrowLeftSLine,
   riInformationLine,

+ 53 - 0
src/content/element-selector/list-selector.js

@@ -0,0 +1,53 @@
+/* eslint-disable  no-cond-assign */
+import { getCssSelector } from 'css-selector-generator';
+
+export function getAllSiblings(el, selector) {
+  const siblings = [el];
+  const validateElement = (element) => {
+    const isValidSelector = selector ? element.querySelector(selector) : true;
+    const isSameTag = el.tagName === element.tagName;
+
+    return isValidSelector && isSameTag;
+  };
+
+  let nextSibling = el;
+  let prevSibling = el;
+  let elementIndex = 1;
+
+  while ((prevSibling = prevSibling?.previousElementSibling)) {
+    if (validateElement(prevSibling)) {
+      elementIndex += 1;
+
+      siblings.unshift(prevSibling);
+    }
+  }
+  while ((nextSibling = nextSibling?.nextElementSibling)) {
+    if (validateElement(nextSibling)) siblings.push(nextSibling);
+  }
+
+  return {
+    elements: siblings,
+    index: elementIndex,
+  };
+}
+
+export default function (el, maxDepth = 50, paths = []) {
+  if (maxDepth === 0) return null;
+
+  let selector = el.tagName.toLowerCase();
+  const { elements: siblings, index } = getAllSiblings(el, paths.join(' > '));
+
+  if (siblings.length > 1 && index > 1) selector += `:nth-of-type(${index})`;
+
+  paths.unshift(selector);
+
+  if (siblings.length === 1) {
+    el = el.parentElement;
+    getElementList(el, maxDepth - 1, paths);
+  }
+
+  const parentSelector = getCssSelector(el);
+  const listSelector = `${parentSelector} ${paths.join(' > ')}`;
+
+  return listSelector;
+}