Browse Source

refactor: "record workflow" element selector

Ahmad Kholid 3 years ago
parent
commit
6fceac79fc

+ 1 - 0
src/background/index.js

@@ -172,6 +172,7 @@ async function checkRecordingWorkflow(tabId, tabUrl) {
   if (!isRecording) return;
 
   await browser.tabs.executeScript(tabId, {
+    allFrames: true,
     file: 'recordWorkflow.bundle.js',
   });
 }

+ 0 - 1
src/components/content/shared/SharedElementHighlighter.vue

@@ -1,5 +1,4 @@
 <template>
-  {{ items }}
   <rect
     v-for="(item, index) in items"
     v-bind="{

+ 13 - 2
src/components/content/shared/SharedElementSelector.vue

@@ -9,7 +9,7 @@
       left: 0;
       pointer-events: none;
       position: fixed;
-      z-index: 10;
+      z-index: 999999;
     "
   >
     <shared-element-highlighter
@@ -58,7 +58,9 @@ const props = defineProps({
     default: () => [],
   },
   list: Boolean,
+  pause: Boolean,
   disabled: Boolean,
+  onlyInList: Boolean,
   withAttributes: Boolean,
 });
 const emit = defineEmits(['selected']);
@@ -132,6 +134,8 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
   let { 1: target } = document.elementsFromPoint(clientX, clientY);
   if (!target) return;
 
+  const onlyInList = props.onlyInList && elementsState.selected.length > 0;
+
   if (target.tagName === 'IFRAME' || target.tagName === 'FRAME') {
     if (type === 'selected') removeElementsList();
 
@@ -150,6 +154,7 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
         left,
         clientX,
         clientY,
+        onlyInList,
         list: isSelectList,
         type: 'automa:get-element-rect',
         withAttributes: props.withAttributes,
@@ -175,7 +180,11 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
   const withAttribute = props.withAttributes && type === 'selected';
 
   if (isSelectList) {
-    const elements = findElementList(target, frameElement) || [];
+    const elements =
+      findElementList(target, {
+        onlyInList,
+        frameElement,
+      }) || [];
 
     if (type === 'hovered') hoveredElements = elements;
 
@@ -214,6 +223,8 @@ function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
   }
 }
 function onMousemove(event) {
+  if (props.pause) return;
+
   retrieveElementsRect(event, 'hovered');
 }
 function onClick(event) {

+ 2 - 0
src/content/elementSelector/App.vue

@@ -68,6 +68,7 @@
     </div>
   </div>
   <shared-element-selector
+    :hide="state.hide"
     :disabled="state.hide"
     :list="state.selectList"
     :selector-type="state.selectorType"
@@ -125,6 +126,7 @@ function toggleHighlightElement({ index, highlight }) {
 function onElementsSelected({ selector, elements, path }) {
   if (path) {
     selectedElement.path = path;
+    selectedElement.pathIndex = 0;
   }
 
   state.elSelector = selector;

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

@@ -83,7 +83,7 @@ export function getElementList(el, maxDepth = 50, paths = []) {
   return siblings;
 }
 
-export default function (target, frameElement) {
+export default function (target, { frameElement, onlyInList } = {}) {
   if (!target) return [];
 
   const automaListEl = target.closest('[automa-el-list]');
@@ -107,5 +107,7 @@ export default function (target, frameElement) {
     return Array.from(elements);
   }
 
+  if (onlyInList) return [];
+
   return getElementList(target) || [target];
 }

+ 4 - 1
src/content/elementSelector/selectorFrameContext.js

@@ -48,7 +48,10 @@ function getElementsRect(data) {
     let elementsRect = [];
 
     if (data.list) {
-      const elements = findElementList(element) || [];
+      const elements =
+        findElementList(element, {
+          onlyInList: data.onlyInList,
+        }) || [];
 
       hoveredElements = elements;
       elementsRect = elements.map((el) => getElementRectWithOffset(el, data));

+ 84 - 204
src/content/services/recordWorkflow/App.vue

@@ -74,11 +74,15 @@
           <template v-else>
             <div class="flex items-center space-x-2 w-full">
               <input
-                :value="selectState.childSelector"
+                :value="selectState.childSelector || selectState.parentSelector"
                 class="px-4 py-2 rounded-lg bg-input w-full"
                 readonly
               />
-              <template v-if="!selectState.list">
+              <template
+                v-if="
+                  !selectState.list && !selectState.childSelector.includes('|>')
+                "
+              >
                 <button @click="selectElementPath('up')">
                   <v-remixicon name="riArrowLeftLine" rotate="90" />
                 </button>
@@ -114,11 +118,11 @@
               >
                 <option value="" selected disabled>Select attribute</option>
                 <option
-                  v-for="attr in addBlockState.attributes"
-                  :key="attr.id"
-                  :value="attr.id"
+                  v-for="(value, name) in addBlockState.attributes"
+                  :key="name"
+                  :value="name"
                 >
-                  {{ attr.name }}
+                  {{ name }}({{ value.slice(0, 64) }})
                 </option>
               </select>
               <label
@@ -175,43 +179,30 @@
       </div>
     </div>
   </div>
-  <shared-element-highlighter
-    v-if="selectState.status === 'selecting'"
-    :data="elementsHighlightData"
-    :items="{
-      hoveredElements: selectState.hoveredElements,
-      selectedElements: selectState.selectedElements,
-    }"
-    style="z-index: 9999999"
-    @update="selectState[$event.key] = $event.items"
+  <shared-element-selector
+    v-if="selectState.isSelecting"
+    :selected-els="selectState.selectedElements"
+    with-attributes
+    only-in-list
+    :list="selectState.list"
+    :pause="
+      selectState.selectedElements.length > 0 &&
+      selectState.list &&
+      !selectState.listId
+    "
+    @selected="onElementsSelected"
   />
-  <teleport to="body">
-    <div
-      v-if="selectState.status === 'selecting'"
-      style="
-        z-index: 999999;
-        position: fixed;
-        left: 0;
-        top: 0;
-        width: 100%;
-        height: 100%;
-        background-color: rgba(0, 0, 0, 0.3);
-      "
-    ></div>
-  </teleport>
 </template>
 <script setup>
 import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue';
 import { finder } from '@medv/finder';
 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 { tasks } from '@/utils/shared';
+import SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';
+import { getElementRect } from '../../utils';
 import addBlock from './addBlock';
 
-let prevHoverElement = null;
 const mouseRelativePos = { x: 0, y: 0 };
 const elementsPath = {
   path: [],
@@ -229,7 +220,7 @@ const selectState = reactive({
   isInList: false,
   listSelector: '',
   childSelector: '',
-  hoveredElements: [],
+  isSelecting: false,
   selectedElements: [],
 });
 const draggingState = reactive({
@@ -254,20 +245,41 @@ const blocksList = {
   default: ['get-text', 'attribute-value'],
 };
 
-function getElementRect(target, withElement = false) {
-  if (!target) return {};
+function getElementBlocks(element) {
+  if (!element) return;
 
-  const { x, y, height, width } = target.getBoundingClientRect();
-  const result = {
-    width: width + 4,
-    height: height + 4,
-    x: x - 2,
-    y: y - 2,
-  };
+  const elTag = element.tagName;
+  const blocks = [...(blocksList[elTag] || blocksList.default)];
+  const attrBlockIndex = blocks.indexOf('attribute-value');
 
-  if (withElement) result.element = target;
+  if (attrBlockIndex !== -1) {
+    addBlockState.attributes = element.attributes;
+  }
 
-  return result;
+  addBlockState.blocks = blocks;
+}
+function onElementsSelected({ selector, elements, path }) {
+  if (path) {
+    elementsPath.path = path;
+    selectState.pathIndex = 0;
+  }
+
+  getElementBlocks(elements[0]);
+  selectState.selectedElements = elements;
+
+  if (selectState.list) {
+    if (!selectState.listSelector) {
+      selectState.isInList = false;
+      selectState.listSelector = selector;
+      selectState.childSelector = selector;
+      return;
+    }
+
+    selectState.isInList = true;
+    selector = selector.replace(selectState.listSelector, '');
+  }
+
+  selectState.childSelector = selector;
 }
 function addFlowItem() {
   const saveData = Boolean(addBlockState.column);
@@ -286,12 +298,15 @@ function addFlowItem() {
     },
   };
 
-  if (selectState.isInList || selectState.listId) {
-    const childSelector = selectState.isInList ? selectState.childSelector : '';
-
-    block.data.selector = `{{loopData@${selectState.listId}}} ${childSelector}`;
-  } else if (selectState.list) {
-    block.data.multiple = true;
+  if (selectState.list) {
+    if (selectState.isInList || selectState.listId) {
+      const childSelector = selectState.isInList
+        ? selectState.childSelector
+        : '';
+      block.data.selector = `{{loopData@${selectState.listId}}} ${childSelector}`;
+    } else {
+      block.data.multiple = true;
+    }
   }
 
   if (addBlockState.activeBlock === 'attribute-value') {
@@ -344,8 +359,10 @@ function clearSelectState() {
   selectState.listId = '';
   selectState.list = false;
   selectState.status = 'idle';
+  selectState.listSelector = '';
+  selectState.childSelector = '';
+  selectState.parentSelector = '';
   selectState.isSelecting = false;
-  selectState.hoveredElements = [];
   selectState.selectedElements = [];
 
   const selectedList = document.querySelectorAll('[automa-el-list]');
@@ -353,6 +370,16 @@ function clearSelectState() {
     element.removeAttribute('automa-el-list');
   });
 
+  const frameElements = document.querySelectorAll('iframe, frame');
+  frameElements.forEach((element) => {
+    element.contentWindow.postMessage(
+      {
+        type: 'automa:reset-element-selector',
+      },
+      '*'
+    );
+  });
+
   document.body.removeAttribute('automa-selecting');
 }
 function saveElementListId() {
@@ -371,37 +398,6 @@ function saveElementListId() {
     },
   });
 }
-function getElementListChild(target, root) {
-  const result = {
-    elements: [],
-    childSelector: null,
-  };
-
-  if (!target.hasAttribute('automa-el-list')) {
-    result.childSelector = finder(target, {
-      root,
-      idName: () => false,
-    });
-
-    const selector = `${selectState.listSelector} ${result.childSelector}`;
-
-    result.elements = Array.from(document.querySelectorAll(selector));
-  }
-
-  return result;
-}
-function getElementList(target, forceList = false) {
-  const automaListEl = target.closest('[automa-el-list]');
-
-  if (automaListEl) {
-    return getElementListChild(target, automaListEl).elements;
-  }
-  if (forceList) {
-    return [];
-  }
-
-  return findElementList(target) || [target];
-}
 function toggleDragging(value, event) {
   if (value) {
     const bounds = rootEl.value.getBoundingClientRect();
@@ -433,133 +429,17 @@ function startSelecting(list = false) {
 
   window.addEventListener('keyup', onKeyup);
 }
-function onMousemove({ clientX, clientY, target: eventTarget }) {
-  if (draggingState.dragging) {
-    draggingState.xPos = clientX - mouseRelativePos.x;
-    draggingState.yPos = clientY - mouseRelativePos.y;
-
-    return;
-  }
-
-  if (!selectState.isSelecting) return;
-
-  const elementSelected = selectState.selectedElements.length > 0;
-  const disable = selectState.list && !selectState.listId && elementSelected;
-  if (disable) return;
-
-  if (
-    selectState.status === 'selecting' &&
-    eventTarget.id !== 'automa-recording'
-  ) {
-    const { 1: target } = document.elementsFromPoint(clientX, clientY);
-
-    if (prevHoverElement === target) return;
-
-    prevHoverElement = target;
-    let elementsRect = [];
-
-    if (selectState.list) {
-      const elements = getElementList(target, elementSelected) || [];
-      elementsRect = elements.map((el) => getElementRect(el, true));
-    } else {
-      elementsRect = [getElementRect(target, true)];
-    }
-
-    selectState.hoveredElements = elementsRect;
-  }
-}
-function onClick(event) {
-  if (!selectState.isSelecting) return;
-
-  const { target: eventTarget, clientY, clientX } = event;
-
-  if (eventTarget.id === 'automa-recording') return;
-
-  const disable =
-    selectState.list &&
-    !selectState.listId &&
-    selectState.selectedElements.length > 0;
-  if (disable) return;
-
-  const { 1: target } = document.elementsFromPoint(clientX, clientY);
-  const isInList = target.closest('[automa-el-list]');
-  const getElementBlocks = (element) => {
-    const elTag = element.tagName;
-    const blocks = [...(blocksList[elTag] || blocksList.default)];
-    const attrBlockIndex = blocks.indexOf('attribute-value');
-
-    if (attrBlockIndex !== -1) {
-      const attributes = Array.from(element.attributes).reduce(
-        (acc, { name, value }) => {
-          if (name === 'automa-el-list') return acc;
-
-          acc.push({ id: name, name: `${name} (${value})`, value });
-
-          return acc;
-        },
-        []
-      );
-
-      if (attributes.length === 0) blocks.splice(attrBlockIndex, 1);
-
-      addBlockState.attributes = attributes;
-    }
-
-    addBlockState.blocks = blocks;
-  };
-
-  if (isInList) {
-    const { elements, childSelector } = getElementListChild(target, isInList);
-
-    getElementBlocks(elements[0]);
-
-    selectState.isInList = true;
-    selectState.childSelector = childSelector;
-    selectState.selectedElements = elements.map((element) =>
-      getElementRect(element)
-    );
-
-    return;
-  }
-
-  const prevSelectedList = document.querySelectorAll('[automa-el-list]');
-  prevSelectedList.forEach((element) => {
-    element.removeAttribute('automa-el-list');
-  });
-
-  const firstElement = selectState.hoveredElements[0].element;
-  if (!firstElement) return;
-
-  elementsPath.path = [];
-
-  if (selectState.list) {
-    selectState.hoveredElements.forEach(({ element }) => {
-      element.setAttribute('automa-el-list', '');
-    });
-
-    const parentSelector = finder(firstElement.parentElement);
-    const childSelector = firstElement.tagName.toLowerCase();
-    const elementSelector = `${parentSelector} > ${childSelector}`;
-
-    selectState.listSelector = elementSelector;
-    selectState.childSelector = childSelector;
-  } else {
-    selectState.childSelector = finder(firstElement);
-    elementsPath.path = getElementPath(firstElement);
-  }
-
-  selectState.isInList = false;
-  selectState.selectedElements = selectState.hoveredElements;
+function onMousemove({ clientX, clientY }) {
+  if (!draggingState.dragging) return;
 
-  getElementBlocks(firstElement);
+  draggingState.xPos = clientX - mouseRelativePos.x;
+  draggingState.yPos = clientY - mouseRelativePos.y;
 }
 function attachListeners() {
-  window.addEventListener('click', onClick);
   window.addEventListener('mousemove', onMousemove);
 }
 function detachListeners() {
   window.removeEventListener('keyup', onKeyup);
-  window.removeEventListener('click', onClick);
   window.removeEventListener('mousemove', onMousemove);
 }
 

+ 23 - 11
src/content/services/recordWorkflow/index.js

@@ -1,22 +1,34 @@
 import browser from 'webextension-polyfill';
 import initElementSelector from './main';
 import initRecordEvents from './recordEvents';
+import selectorFrameContext from '../../elementSelector/selectorFrameContext';
 
 (async () => {
   try {
-    const element = document.querySelector('#automa-recording');
-    if (element) return;
+    const isMainFrame = window.self === window.top;
 
-    const destroyRecordEvents = await initRecordEvents();
-    const elementSelectorInstance = await initElementSelector();
+    if (isMainFrame) {
+      const element = document.querySelector('#automa-recording');
+      if (element) return;
 
-    browser.runtime.onMessage.addListener(function messageListener({ type }) {
-      if (type === 'recording:stop') {
-        destroyRecordEvents();
-        elementSelectorInstance.unmount();
-        browser.runtime.onMessage.removeListener(messageListener);
-      }
-    });
+      const destroyRecordEvents = await initRecordEvents();
+      const elementSelectorInstance = await initElementSelector();
+
+      browser.runtime.onMessage.addListener(function messageListener({ type }) {
+        if (type === 'recording:stop') {
+          destroyRecordEvents();
+          elementSelectorInstance.unmount();
+          browser.runtime.onMessage.removeListener(messageListener);
+        }
+      });
+    } 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);
   }

+ 1 - 0
src/content/utils.js

@@ -19,6 +19,7 @@ export function getElementRect(target, withAttributes) {
     });
 
     result.attributes = attributes;
+    result.tagName = target.tagName;
   }
 
   return result;

+ 1 - 0
src/popup/pages/Home.vue

@@ -264,6 +264,7 @@ async function recordWorkflow(options = {}) {
     for (const tab of tabs) {
       if (tab.url.startsWith('http')) {
         await browser.tabs.executeScript(tab.id, {
+          allFrames: true,
           file: 'recordWorkflow.bundle.js',
         });
       }