Browse Source

feat: add wait for selector option

Ahmad Kholid 3 years ago
parent
commit
e7682d10c3

+ 42 - 15
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -25,26 +25,53 @@
         class="mb-1 w-full"
         @change="updateData({ selector: $event })"
       />
-      <template
+      <ui-expand
         v-if="!hideSelector && (data.findBy || 'cssSelector') === 'cssSelector'"
+        hide-header-icon
+        header-class="flex items-center w-full focus:ring-0"
       >
+        <template #header="{ show }">
+          <v-remixicon
+            name="riArrowLeftSLine"
+            :rotate="show ? 270 : 180"
+            class="mr-1 transition-transform -ml-1"
+          />
+          Selector options
+        </template>
+        <div class="mt-1">
+          <ui-checkbox
+            v-if="!data.disableMultiple && !hideMultiple"
+            :title="t('workflow.blocks.base.multiple.title')"
+            :model-value="data.multiple"
+            class="mr-6"
+            @change="updateData({ multiple: $event })"
+          >
+            {{ t('workflow.blocks.base.multiple.text') }}
+          </ui-checkbox>
+          <ui-checkbox
+            :model-value="data.markEl"
+            :title="t('workflow.blocks.base.markElement.title')"
+            @change="updateData({ markEl: $event })"
+          >
+            {{ t('workflow.blocks.base.markElement.text') }}
+          </ui-checkbox>
+        </div>
         <ui-checkbox
-          v-if="!data.disableMultiple && !hideMultiple"
-          :title="t('workflow.blocks.base.multiple.title')"
-          :model-value="data.multiple"
-          class="mr-6"
-          @change="updateData({ multiple: $event })"
+          :model-value="data.waitForSelector"
+          block
+          class="mt-1"
+          @change="updateData({ waitForSelector: $event })"
         >
-          {{ t('workflow.blocks.base.multiple.text') }}
+          {{ t('workflow.blocks.base.waitSelector.title') }}
         </ui-checkbox>
-        <ui-checkbox
-          :model-value="data.markEl"
-          :title="t('workflow.blocks.base.markElement.title')"
-          @change="updateData({ markEl: $event })"
-        >
-          {{ t('workflow.blocks.base.markElement.text') }}
-        </ui-checkbox>
-      </template>
+        <ui-input
+          v-if="data.waitForSelector"
+          :model-value="data.waitSelectorTimeout"
+          :label="t('workflow.blocks.base.waitSelector.timeout')"
+          class="mt-1 w-full"
+          @change="updateData({ waitSelectorTimeout: +$event })"
+        />
+      </ui-expand>
     </template>
     <slot></slot>
   </div>

+ 6 - 1
src/components/ui/UiExpand.vue

@@ -2,11 +2,12 @@
   <div :aria-expanded="show" class="ui-expand">
     <button :class="headerClass" @click="toggleExpand">
       <v-remixicon
+        v-if="!hideHeaderIcon"
         :rotate="show ? 90 : -90"
         name="riArrowLeftSLine"
         class="mr-2 transition-transform -ml-1"
       />
-      <slot name="header" />
+      <slot v-bind="{ show }" name="header" />
     </button>
     <transition-expand>
       <div v-if="show" :class="panelClass" class="ui-expand__panel">
@@ -31,6 +32,10 @@ const props = defineProps({
     type: String,
     default: 'px-4 py-2 w-full flex items-center h-full',
   },
+  hideHeaderIcon: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:modelValue']);
 

+ 2 - 2
src/content/blocks-handler/handler-attribute-value.js

@@ -1,4 +1,4 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function attributeValue(block) {
   return new Promise((resolve, reject) => {
@@ -10,7 +10,7 @@ function attributeValue(block) {
       return ['checkbox', 'radio'].includes(element.getAttribute('type'));
     };
 
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         let value = element.getAttribute(attributeName);
 

+ 10 - 7
src/content/blocks-handler/handler-element-exists.js

@@ -1,27 +1,30 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function elementExists(block) {
   return new Promise((resolve) => {
     let trying = 0;
 
-    const isExists = () => {
+    const isExists = async () => {
       try {
-        const element = handleElement(block, { returnElement: true });
+        const element = await handleSelector(block, { returnElement: true });
 
-        return !!element;
+        if (!element) throw new Error('element-not-found');
+
+        return true;
       } catch (error) {
-        console.error(error);
         return false;
       }
     };
 
-    function checkElement() {
+    async function checkElement() {
       if (trying > (block.data.tryCount || 1)) {
         resolve(false);
         return;
       }
 
-      if (isExists()) {
+      const isElementExist = await isExists();
+
+      if (isElementExist) {
         resolve(true);
       } else {
         trying += 1;

+ 2 - 2
src/content/blocks-handler/handler-element-scroll.js

@@ -1,4 +1,4 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function elementScroll(block) {
   function incScrollPos(element, data, vertical = true) {
@@ -17,7 +17,7 @@ function elementScroll(block) {
     const { data } = block;
     const behavior = data.smooth ? 'smooth' : 'auto';
 
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         if (data.scrollIntoView) {
           element.scrollIntoView({ behavior, block: 'center' });

+ 2 - 2
src/content/blocks-handler/handler-event-click.js

@@ -1,8 +1,8 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function eventClick(block) {
   return new Promise((resolve, reject) => {
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         if (element.click) {
           element.click();

+ 27 - 36
src/content/blocks-handler/handler-forms.js

@@ -1,48 +1,39 @@
-import { handleElement, markElement } from '../helper';
+import handleSelector, { markElement } from '../handle-selector';
 import handleFormElement from '@/utils/handle-form-element';
 
-function forms(block) {
-  return new Promise((resolve, reject) => {
-    const { data } = block;
-    const elements = handleElement(block, { returnElement: true });
+async function forms(block) {
+  const { data } = block;
+  const elements = await handleSelector(block, { returnElement: true });
 
-    if (!elements) {
-      reject(new Error('element-not-found'));
+  if (!elements) {
+    throw new Error('element-not-found');
+  }
 
-      return;
+  if (data.getValue) {
+    let result = '';
+
+    if (data.multiple) {
+      result = elements.map((element) => element.value || '');
+    } else {
+      result = elements.value || '';
     }
 
-    if (data.getValue) {
-      let result = '';
+    return result;
+  }
 
-      if (data.multiple) {
-        result = elements.map((element) => element.value || '');
-      } else {
-        result = elements.value || '';
-      }
+  if (data.multiple) {
+    const promises = Array.from(elements).map(async (element) => {
+      markElement(element, block);
+      await handleFormElement(element, data, eventResolve);
+    });
 
-      resolve(result);
-      return;
-    }
+    await Promise.allSettled(promises);
+  } else {
+    markElement(elements, block);
+    await handleFormElement(elements, data);
+  }
 
-    if (data.multiple) {
-      const promises = Array.from(elements).map((element) => {
-        return new Promise((eventResolve) => {
-          markElement(element, block);
-          handleFormElement(element, data, eventResolve);
-        });
-      });
-
-      Promise.allSettled(promises).then(() => {
-        resolve('');
-      });
-    } else if (elements) {
-      markElement(elements, block);
-      handleFormElement(elements, data, resolve);
-    } else {
-      resolve('');
-    }
-  });
+  return null;
 }
 
 export default forms;

+ 2 - 2
src/content/blocks-handler/handler-get-text.js

@@ -1,4 +1,4 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function getText(block) {
   return new Promise((resolve, reject) => {
@@ -17,7 +17,7 @@ function getText(block) {
       regex = new RegExp(regexData, regexExp.join(''));
     }
 
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         let text = includeTags ? element.outerHTML : element.innerText;
 

+ 10 - 13
src/content/blocks-handler/handler-link.js

@@ -1,22 +1,19 @@
-import { handleElement, markElement } from '../helper';
+import handleSelector, { markElement } from '../handle-selector';
 
-function link(block) {
-  return new Promise((resolve, reject) => {
-    const element = handleElement(block, { returnElement: true });
+async function link(block) {
+  const element = await handleSelector(block, { returnElement: true });
 
-    if (!element) {
-      reject(new Error('element-not-found'));
-      return;
-    }
+  if (!element) {
+    throw new Error('element-not-found');
+  }
 
-    markElement(element, block);
+  markElement(element, block);
 
-    const url = element.href;
+  const url = element.href;
 
-    if (url) window.location.href = url;
+  if (url) window.location.href = url;
 
-    resolve(url);
-  });
+  return url;
 }
 
 export default link;

+ 2 - 2
src/content/blocks-handler/handler-switch-to.js

@@ -1,8 +1,8 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function switchTo(block) {
   return new Promise((resolve, reject) => {
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         if (element.tagName !== 'IFRAME') {
           reject(new Error('not-iframe'));

+ 2 - 2
src/content/blocks-handler/handler-trigger-event.js

@@ -1,11 +1,11 @@
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 import simulateEvent from '@/utils/simulate-event';
 
 function triggerEvent(block) {
   return new Promise((resolve, reject) => {
     const { data } = block;
 
-    handleElement(block, {
+    handleSelector(block, {
       onSelected(element) {
         simulateEvent(element, data.eventName, data.eventParams);
       },

+ 2 - 2
src/content/blocks-handler/handler-upload-file.js

@@ -1,5 +1,5 @@
 import { sendMessage } from '@/utils/message';
-import { handleElement } from '../helper';
+import handleSelector from '../handle-selector';
 
 function injectFiles(element, files) {
   const notFileTypeAttr = element.getAttribute('type') !== 'file';
@@ -12,7 +12,7 @@ function injectFiles(element, files) {
 }
 
 export default async function (block) {
-  const elements = handleElement(block, { returnElement: true });
+  const elements = await handleSelector(block, { returnElement: true });
 
   if (!elements) throw new Error('element-not-found');
 

+ 46 - 1
src/content/helper.js → src/content/handle-selector.js

@@ -8,7 +8,35 @@ export function markElement(el, { id, data }) {
   }
 }
 
-export function handleElement(
+export function waitForSelector({
+  timeout,
+  selector,
+  documentCtx = document,
+} = {}) {
+  return new Promise((resolve) => {
+    let isTimeout = false;
+    const findSelector = () => {
+      if (isTimeout) return;
+
+      const element = documentCtx.querySelector(selector);
+
+      if (!element) {
+        setTimeout(findSelector, 200);
+      } else {
+        resolve(element);
+      }
+    };
+
+    findSelector();
+
+    setTimeout(() => {
+      isTimeout = true;
+      resolve(null);
+    }, timeout);
+  });
+}
+
+export default async function (
   { data, id, frameSelector },
   { onSelected, onError, onSuccess, returnElement }
 ) {
@@ -31,6 +59,23 @@ export function handleElement(
     documentCtx = iframeCtx;
   }
 
+  if (data.waitForSelector && data.findBy === 'cssSelector') {
+    const element = await waitForSelector({
+      documentCtx,
+      selector: data.selector,
+      timeout: data.waitSelectorTimeout,
+    });
+
+    if (!element) {
+      if (returnElement) return element;
+
+      if (onError) {
+        onError(new Error('element-not-found'));
+        return;
+      }
+    }
+  }
+
   try {
     data.blockIdAttr = `block--${id}`;
 

+ 0 - 1
src/content/index.js

@@ -59,7 +59,6 @@ import blocksHandler from './blocks-handler';
 
             selectors.push(finder(el));
           });
-          console.log(data, selectors);
 
           resolve(selectors);
           break;

+ 4 - 0
src/locales/en/blocks.json

@@ -26,6 +26,10 @@
         "multiple": {
           "title": "Select multiple element",
           "text": "Multiple"
+        },
+        "waitSelector": {
+          "title": "Wait for selector",
+          "timeout": "Selector timeout (ms)"
         }
       },
       "clipboard": {

+ 22 - 16
src/newtab/pages/workflows/[id].vue

@@ -256,7 +256,13 @@ import { sendMessage } from '@/utils/message';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflow-data';
 import { tasks } from '@/utils/shared';
 import { fetchApi } from '@/utils/api';
-import { debounce, isObject, objectHasKey, parseJSON } from '@/utils/helper';
+import {
+  debounce,
+  isObject,
+  objectHasKey,
+  parseJSON,
+  throttle,
+} from '@/utils/helper';
 import Log from '@/models/log';
 import decryptFlow, { getWorkflowPass } from '@/utils/decrypt-flow';
 import Workflow from '@/models/workflow';
@@ -408,6 +414,21 @@ const updateBlockData = debounce((data) => {
       new CustomEvent('change', { detail: toRaw(payload) })
     );
 }, 250);
+const executeWorkflow = throttle(() => {
+  if (editor.value.getNodesFromName('trigger').length === 0) {
+    /* eslint-disable-next-line */
+    toast.error(t('message.noTriggerBlock'));
+    return;
+  }
+
+  const payload = {
+    ...workflow.value,
+    isTesting: state.isDataChanged,
+    drawflow: JSON.stringify(editor.value.export()),
+  };
+
+  sendMessage('workflow:execute', payload, 'background');
+}, 300);
 
 async function updateHostedWorkflow() {
   if (!workflowData.isHost || Object.keys(hostWorkflowPayload).length === 0)
@@ -772,21 +793,6 @@ function editBlock(data) {
   state.isEditBlock = true;
   state.blockData = defu(data, tasks[data.id] || {});
 }
-function executeWorkflow() {
-  if (editor.value.getNodesFromName('trigger').length === 0) {
-    /* eslint-disable-next-line */
-    toast.error(t('message.noTriggerBlock'));
-    return;
-  }
-
-  const payload = {
-    ...workflow.value,
-    isTesting: state.isDataChanged,
-    drawflow: JSON.stringify(editor.value.export()),
-  };
-
-  sendMessage('workflow:execute', payload, 'background');
-}
 function handleEditorDataChanged() {
   state.isDataChanged = true;
 }

+ 29 - 28
src/utils/handle-form-element.js

@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
 import simulateEvent from './simulate-event';
 
 function formEvent(element, data) {
@@ -49,38 +48,40 @@ function inputText({ data, element, isEditable, index = 0, callback }) {
   }
 }
 
-export default function (element, data, callback) {
-  const textFields = ['INPUT', 'TEXTAREA'];
-  const isEditable =
-    element.hasAttribute('contenteditable') && element.isContentEditable;
+export default function (element, data) {
+  return new Promise((callback) => {
+    const textFields = ['INPUT', 'TEXTAREA'];
+    const isEditable =
+      element.hasAttribute('contenteditable') && element.isContentEditable;
 
-  if (isEditable) {
-    if (data.clearValue) element.innerText = '';
+    if (isEditable) {
+      if (data.clearValue) element.innerText = '';
 
-    inputText({ data, element, callback, isEditable });
-    return;
-  }
+      inputText({ data, element, callback, isEditable });
+      return;
+    }
 
-  if (data.type === 'text-field' && textFields.includes(element.tagName)) {
-    if (data.clearValue) element.value = '';
+    if (data.type === 'text-field' && textFields.includes(element.tagName)) {
+      if (data.clearValue) element.value = '';
 
-    inputText({ data, element, callback });
-    return;
-  }
+      inputText({ data, element, callback });
+      return;
+    }
 
-  if (data.type === 'checkbox' || data.type === 'radio') {
-    element.checked = data.selected;
-    formEvent(element, { type: data.type, value: data.selected });
-    callback(element.checked);
-    return;
-  }
+    if (data.type === 'checkbox' || data.type === 'radio') {
+      element.checked = data.selected;
+      formEvent(element, { type: data.type, value: data.selected });
+      callback(element.checked);
+      return;
+    }
 
-  if (data.type === 'select') {
-    element.value = data.value;
-    formEvent(element, data);
-    callback(element.value);
-    return;
-  }
+    if (data.type === 'select') {
+      element.value = data.value;
+      formEvent(element, data);
+      callback(element.value);
+      return;
+    }
 
-  callback('');
+    callback('');
+  });
 }

+ 16 - 0
src/utils/shared.js

@@ -245,6 +245,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       multiple: false,
@@ -280,6 +282,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       multiple: false,
@@ -330,6 +334,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: 'html',
       markEl: false,
       multiple: false,
@@ -356,6 +362,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       disableMultiple: true,
@@ -376,6 +384,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       multiple: false,
@@ -404,6 +414,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       multiple: false,
@@ -479,6 +491,8 @@ export const tasks = {
     data: {
       description: '',
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       markEl: false,
       multiple: false,
@@ -683,6 +697,8 @@ export const tasks = {
     refDataKeys: ['selector', 'filePaths'],
     data: {
       findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
       selector: '',
       filePaths: [],
     },