Browse Source

feat: directly select element inside `iframe`

Ahmad Kholid 3 years ago
parent
commit
7bc60c008a

+ 9 - 6
src/background/workflowEngine/blocksHandler/handlerLoopData.js

@@ -35,13 +35,16 @@ async function loopData({ data, id, outputs }, { refData }) {
           return parseJSON(variableVal, variableVal);
         },
         elements: async () => {
+          const isXPath = data.elementSelector.startsWith('/');
           const elements = await this._sendMessageToTab({
-            blockId: id,
-            isBlock: false,
-            max: data.maxLoop,
-            type: 'loop-elements',
-            selector: data.elementSelector,
-            frameSelector: this.frameSelector,
+            id,
+            name: 'loop-data',
+            data: {
+              multiple: true,
+              max: data.maxLoop,
+              selector: data.elementSelector,
+              findBy: isXPath ? 'xpath' : 'cssSelector',
+            },
           });
 
           return elements;

+ 1 - 1
src/background/workflowEngine/executeContentScript.js

@@ -39,8 +39,8 @@ export default async function (tabId, frameId = 0) {
 
     if (!isScriptExists) {
       await browser.tabs.executeScript(tabId, {
+        allFrames: true,
         runAt: 'document_end',
-        frameId: currentFrameId,
         file: './contentScript.bundle.js',
       });
     }

+ 26 - 0
src/content/blocksHandler/handlerLoopData.js

@@ -0,0 +1,26 @@
+import { nanoid } from 'nanoid';
+import handleSelector from '../handleSelector';
+
+export default async function loopElements(block) {
+  const elements = await handleSelector(block);
+  const selectors = [];
+  const attrId = nanoid(5);
+
+  let frameSelector = '';
+
+  if (block.data.$frameSelector) {
+    frameSelector = `${block.data.$frameSelector} |> `;
+  }
+
+  elements.forEach((el, index) => {
+    if (block.data.max > 0 && selectors.length - 1 > block.data.max) return;
+
+    const attrName = 'automa-loop';
+    const attrValue = `${attrId}--${index}`;
+
+    el.setAttribute(attrName, attrValue);
+    selectors.push(`${frameSelector}[${attrName}="${attrValue}"]`);
+  });
+
+  return selectors;
+}

+ 1 - 1
src/content/handleSelector.js

@@ -52,7 +52,7 @@ export function queryElements(data, documentCtx = document) {
 
 export default async function (
   { data, id, frameSelector, debugMode },
-  { onSelected, onError, onSuccess }
+  { onSelected, onError, onSuccess } = {}
 ) {
   if (!data || !data.selector) {
     if (onError) onError(new Error('selector-empty'));

+ 118 - 73
src/content/index.js

@@ -1,63 +1,140 @@
 import browser from 'webextension-polyfill';
-import { nanoid } from 'nanoid';
 import { toCamelCase } from '@/utils/helper';
-import FindElement from '@/utils/FindElement';
-import { getDocumentCtx } from './handleSelector';
-import executedBlock from './executedBlock';
 import blocksHandler from './blocksHandler';
+import showExecutedBlock from './showExecutedBlock';
 import handleTestCondition from './handleTestCondition';
 
-function messageListener({ data, source }) {
-  if (data !== 'automa:get-frame') return;
+const isMainFrame = window.self === window.top;
+
+function messageToFrame(frameElement, blockData) {
+  return new Promise((resolve, reject) => {
+    function onMessage({ data }) {
+      if (data.type !== 'automa:block-execute-result') return;
 
-  let frameRect = { x: 0, y: 0 };
+      if (data.result?.$isError) {
+        const error = new Error(data.result.message);
+        error.data = data.result.data;
 
-  document.querySelectorAll('iframe').forEach((iframe) => {
-    if (iframe.contentWindow !== source) return;
+        reject(error);
+      } else {
+        resolve(data.result);
+      }
 
-    frameRect = iframe.getBoundingClientRect();
+      window.removeEventListener('message', onMessage);
+    }
+    window.addEventListener('message', onMessage);
+
+    frameElement.contentWindow.postMessage(
+      {
+        type: 'automa:execute-block',
+        blockData: { ...blockData, frameSelector: '' },
+      },
+      '*'
+    );
   });
+}
+async function executeBlock(data) {
+  const removeExecutedBlock = showExecutedBlock(data, data.executedBlockOnWeb);
+
+  if (data.data.selector?.includes('|>') && isMainFrame) {
+    const [frameSelector, selector] = data.data.selector.split(/\|>(.+)/);
+    const frameElement = document.querySelector(frameSelector);
+    const frameError = (message) => {
+      const error = new Error(message);
+      error.data = { selector: frameSelector };
+
+      return error;
+    };
+
+    if (!frameElement) throw frameError('iframe-not-found');
+
+    const isFrameEelement = ['IFRAME', 'FRAME'].includes(frameElement.tagName);
+    if (!isFrameEelement) throw frameError('not-iframe');
+
+    data.data.selector = selector;
+    data.data.$frameSelector = frameSelector;
+
+    if (frameElement.contentDocument) {
+      data.frameSelector = frameSelector;
+    } else {
+      const result = await messageToFrame(frameElement, data);
+      return result;
+    }
+  }
 
-  source.postMessage(
-    {
-      frameRect,
-      type: 'automa:the-frame-rect',
-    },
-    '*'
-  );
+  const handler = blocksHandler[toCamelCase(data.name)];
+
+  if (handler) {
+    const result = await handler(data);
+    removeExecutedBlock();
+
+    return result;
+  }
+
+  const error = new Error(`"${data.name}" doesn't have a handler`);
+  console.error(error);
+
+  throw error;
 }
+function messageListener({ data, source }) {
+  if (data.type === 'automa:get-frame' && isMainFrame) {
+    let frameRect = { x: 0, y: 0 };
 
-(() => {
-  if (window.isAutomaInjected) return;
-  window.isAutomaInjected = true;
+    document.querySelectorAll('iframe').forEach((iframe) => {
+      if (iframe.contentWindow !== source) return;
+
+      frameRect = iframe.getBoundingClientRect();
+    });
 
-  if (window.self === window.top) {
-    window.addEventListener('message', messageListener);
+    source.postMessage(
+      {
+        frameRect,
+        type: 'automa:the-frame-rect',
+      },
+      '*'
+    );
+
+    return;
   }
 
-  browser.runtime.onMessage.addListener((data) => {
-    return new Promise((resolve, reject) => {
-      if (data.isBlock) {
-        const removeExecutedBlock = executedBlock(
-          data,
-          data.executedBlockOnWeb
+  if (data.type === 'automa:execute-block') {
+    executeBlock(data.blockData)
+      .then((result) => {
+        window.top.postMessage(
+          {
+            result,
+            type: 'automa:block-execute-result',
+          },
+          '*'
         );
+      })
+      .catch((error) => {
+        console.error(error);
+        window.top.postMessage(
+          {
+            result: {
+              $isError: true,
+              message: error.message,
+              data: error.data || {},
+            },
+            type: 'automa:block-execute-result',
+          },
+          '*'
+        );
+      });
+  }
+}
 
-        const handler = blocksHandler[toCamelCase(data.name)];
-
-        if (handler) {
-          handler(data)
-            .then((result) => {
-              removeExecutedBlock();
-              resolve(result);
-            })
-            .catch(reject);
+(() => {
+  if (window.isAutomaInjected) return;
 
-          return;
-        }
-        console.error(`"${data.name}" doesn't have a handler`);
+  window.isAutomaInjected = true;
+  window.addEventListener('message', messageListener);
 
-        resolve('');
+  browser.runtime.onMessage.addListener((data) => {
+    return new Promise((resolve, reject) => {
+      if (data.isBlock) {
+        executeBlock(data).then(resolve).catch(reject);
         return;
       }
 
@@ -70,38 +147,6 @@ function messageListener({ data, source }) {
         case 'content-script-exists':
           resolve(true);
           break;
-        case 'loop-elements': {
-          const selectors = [];
-          const attrId = nanoid(5);
-
-          const documentCtx = getDocumentCtx(data.frameSelector);
-          const selectorType = data.selector.startsWith('/')
-            ? 'xpath'
-            : 'cssSelector';
-          const elements = FindElement[selectorType](
-            { selector: data.selector, multiple: true },
-            documentCtx
-          );
-
-          if (!elements || elements?.length === 0) {
-            reject(new Error('element-not-found'));
-
-            return;
-          }
-
-          elements.forEach((el, index) => {
-            if (data.max > 0 && selectors.length - 1 > data.max) return;
-
-            const attrName = 'automa-loop';
-            const attrValue = `${attrId}--${index}`;
-
-            el.setAttribute(attrName, attrValue);
-            selectors.push(`[${attrName}="${attrValue}"]`);
-          });
-
-          resolve(selectors);
-          break;
-        }
         default:
       }
     });

+ 0 - 0
src/content/executedBlock.js → src/content/showExecutedBlock.js


+ 1 - 1
src/content/utils.js

@@ -119,7 +119,7 @@ function messageTopFrame(windowCtx) {
     }, 5000);
 
     windowCtx.addEventListener('message', messageListener);
-    windowCtx.top.postMessage('automa:get-frame', '*');
+    windowCtx.top.postMessage({ type: 'automa:get-frame' }, '*');
   });
 }
 export async function getElementPosition(element) {