소스 검색

feat: add autocomplete

Ahmad Kholid 3 년 전
부모
커밋
31e8f1ebab
32개의 변경된 파일851개의 추가작업 그리고 249개의 파일을 삭제
  1. 2 0
      package.json
  2. 19 7
      src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue
  3. 5 0
      src/components/newtab/shared/SharedConditionBuilder/index.vue
  4. 10 2
      src/components/newtab/workflow/WorkflowDataTable.vue
  5. 111 2
      src/components/newtab/workflow/WorkflowEditBlock.vue
  6. 15 8
      src/components/newtab/workflow/edit/EditAttributeValue.vue
  7. 31 21
      src/components/newtab/workflow/edit/EditCloseTab.vue
  8. 5 0
      src/components/newtab/workflow/edit/EditConditions.vue
  9. 19 7
      src/components/newtab/workflow/edit/EditElementExists.vue
  10. 19 7
      src/components/newtab/workflow/edit/EditExportData.vue
  11. 20 6
      src/components/newtab/workflow/edit/EditForms.vue
  12. 35 15
      src/components/newtab/workflow/edit/EditGetText.vue
  13. 44 36
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  14. 20 8
      src/components/newtab/workflow/edit/EditHandleDialog.vue
  15. 19 6
      src/components/newtab/workflow/edit/EditInteractionBase.vue
  16. 19 7
      src/components/newtab/workflow/edit/EditLoopData.vue
  17. 19 7
      src/components/newtab/workflow/edit/EditNewTab.vue
  18. 33 13
      src/components/newtab/workflow/edit/EditSaveAssets.vue
  19. 8 1
      src/components/newtab/workflow/edit/EditScrollElement.vue
  20. 43 29
      src/components/newtab/workflow/edit/EditSwitchTab.vue
  21. 18 6
      src/components/newtab/workflow/edit/EditSwitchTo.vue
  22. 18 6
      src/components/newtab/workflow/edit/EditTakeScreenshot.vue
  23. 8 1
      src/components/newtab/workflow/edit/EditTriggerEvent.vue
  24. 20 5
      src/components/newtab/workflow/edit/EditUploadFile.vue
  25. 21 9
      src/components/newtab/workflow/edit/EditWebhook.vue
  26. 5 0
      src/components/newtab/workflow/edit/EditWhileLoop.vue
  27. 183 36
      src/components/ui/UiAutocomplete.vue
  28. 5 1
      src/components/ui/UiTextarea.vue
  29. 13 2
      src/content/element-selector/App.vue
  30. 17 0
      src/newtab/pages/workflows/[id].vue
  31. 10 1
      src/utils/shared.js
  32. 37 0
      yarn.lock

+ 2 - 0
package.json

@@ -41,6 +41,7 @@
     "dayjs": "^1.10.7",
     "defu": "^5.0.1",
     "drawflow": "^0.0.51",
+    "floating-vue": "^2.0.0-beta.13",
     "idb": "^7.0.0",
     "mitt": "^3.0.0",
     "mousetrap": "^1.6.5",
@@ -51,6 +52,7 @@
     "v-remixicon": "^0.1.1",
     "vue": "^3.2.31",
     "vue-i18n": "^9.2.0-beta.29",
+    "vue-mention": "^2.0.0-alpha.3",
     "vue-router": "^4.0.11",
     "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",

+ 19 - 7
src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue

@@ -22,14 +22,22 @@
           </option>
         </optgroup>
       </ui-select>
-      <ui-input
-        v-for="(_, name) in item.data"
-        :key="item.id + name + index"
-        v-model="inputsData[index].data[name]"
-        :title="conditionBuilder.inputTypes[name].label"
-        :placeholder="conditionBuilder.inputTypes[name].label"
+      <ui-autocomplete
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
         class="flex-1"
-      />
+      >
+        <ui-input
+          v-for="(_, name) in item.data"
+          :key="item.id + name + index"
+          v-model="inputsData[index].data[name]"
+          :title="conditionBuilder.inputTypes[name].label"
+          :placeholder="conditionBuilder.inputTypes[name].label"
+          autocomplete="off"
+          class="w-full"
+        />
+      </ui-autocomplete>
     </div>
     <ui-select
       v-else-if="item.category === 'compare'"
@@ -56,6 +64,10 @@ const props = defineProps({
     type: Array,
     default: () => [],
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update']);
 

+ 5 - 0
src/components/newtab/shared/SharedConditionBuilder/index.vue

@@ -45,6 +45,7 @@
             </template>
             <div class="space-y-2 px-4 py-2">
               <condition-builder-inputs
+                :autocomplete="autocomplete"
                 :data="inputs.items"
                 @update="
                   conditions[index].conditions[inputsIndex].items = $event
@@ -96,6 +97,10 @@ const props = defineProps({
     type: Array,
     default: () => [],
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:modelValue', 'change']);
 

+ 10 - 2
src/components/newtab/workflow/WorkflowDataTable.vue

@@ -105,7 +105,10 @@ function addColumn() {
 watch(
   () => state.columns,
   debounce((newValue) => {
-    emit('update', { table: newValue });
+    const data = { table: newValue };
+
+    emit('update', data);
+    emit('change', data);
   }, 250),
   { deep: true }
 );
@@ -122,6 +125,11 @@ onMounted(() => {
       return column;
     }) || props.workflow.table;
 
-  if (isChanged) emit('update', { table: state.columns });
+  if (isChanged) {
+    const data = { table: state.columns };
+
+    emit('change', data);
+    emit('update', data);
+  }
 });
 </script>

+ 111 - 2
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -24,12 +24,14 @@
       :key="data.blockId"
       v-model:data="blockData"
       :block-id="data.blockId"
+      :autocomplete="autocompleteList"
     />
   </div>
 </template>
 <script>
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { tasks } from '@/utils/shared';
 
 const editComponents = require.context(
   './edit',
@@ -54,10 +56,24 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    editor: {
+      type: Object,
+      default: () => ({}),
+    },
+    workflow: {
+      type: Object,
+      default: () => ({}),
+    },
+    autocomplete: {
+      type: Object,
+      default: () => ({}),
+    },
+    dataChanged: Boolean,
   },
-  emits: ['close', 'update'],
+  emits: ['close', 'update', 'update:autocomplete'],
   setup(props, { emit }) {
     const { t } = useI18n();
+    const autocompleteData = ref({});
 
     const blockData = computed({
       get() {
@@ -67,10 +83,103 @@ export default {
         emit('update', value);
       },
     });
+    const autocompleteList = computed(() => {
+      const blockId = props.data.itemId || props.data.blockId;
+      const arr = [
+        autocompleteData.value.table,
+        autocompleteData.value[blockId],
+      ];
+
+      return arr.flatMap((items) => [...(items || [])]);
+    });
+
+    const dataKeywords = {
+      loopId: 'loopData',
+      refKey: 'googleSheets',
+      variableName: 'variables',
+    };
+    function addAutocompleteData(id, name, data) {
+      if (!autocompleteData.value[id]) autocompleteData.value[id] = new Set();
+
+      if (!tasks[name].autocomplete) return;
+
+      tasks[name].autocomplete.forEach((key) => {
+        if (!data[key]) return;
+
+        autocompleteData.value[id].add(`${dataKeywords[key]}@${data[key]}`);
+      });
+    }
+    function getGroupBlockData(blocks, currentItemId) {
+      let itemFound = currentItemId || true;
+      const blockId = currentItemId || props.data.blockId;
+
+      for (let index = blocks.length - 1; index > 0; index -= 1) {
+        const { id, data, itemId } = blocks[index];
+
+        if (itemFound) {
+          addAutocompleteData(blockId, id, data);
+        } else {
+          itemFound = itemId === currentItemId;
+        }
+      }
+    }
+    function traceBlockData(
+      id,
+      { name, inputs, data },
+      blocks,
+      maxDepth = 100
+    ) {
+      if (maxDepth === 0) return;
+
+      if (maxDepth !== 100) {
+        if (name === 'blocks-group') getGroupBlockData(data.blocks);
+        else addAutocompleteData(props.data.blockId, name, data);
+      }
+
+      inputs?.input_1?.connections.forEach(({ node }) => {
+        traceBlockData(id, blocks[node], blocks, maxDepth - 1);
+      });
+    }
+
+    watch(
+      () => [props.data.blockId, props.data.itemId],
+      () => {
+        const id = props.data.blockId;
+
+        if (
+          !props.autocomplete ||
+          !props.autocomplete[id] ||
+          props.dataChanged
+        ) {
+          const blocks = props.editor.export().drawflow.Home.data;
+          const currentBlock = blocks[id];
+
+          if (props.data.isInGroup)
+            getGroupBlockData(currentBlock.data.blocks, props.data.itemId);
+
+          traceBlockData(props.data.blockId, currentBlock, blocks);
+        }
+
+        if (!autocompleteData.value.table)
+          autocompleteData.value.table = new Set();
+        props.workflow.table.forEach((column) => {
+          autocompleteData.value.table.add(`table@${column.name}`);
+        });
+      },
+      { immediate: true }
+    );
+    watch(
+      autocompleteData,
+      () => {
+        emit('update:autocomplete', autocompleteData.value);
+      },
+      { deep: true }
+    );
 
     return {
       t,
       blockData,
+      autocompleteList,
     };
   },
 };

+ 15 - 8
src/components/newtab/workflow/edit/EditAttributeValue.vue

@@ -1,13 +1,16 @@
 <template>
-  <edit-interaction-base v-bind="{ data }" @change="updateData">
+  <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
     <hr />
-    <ui-input
-      :model-value="data.attributeName"
-      :label="t('workflow.blocks.attribute-value.forms.name')"
-      placeholder="name"
-      class="w-full"
-      @change="updateData({ attributeName: $event })"
-    />
+    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+      <ui-input
+        :model-value="data.attributeName"
+        :label="t('workflow.blocks.attribute-value.forms.name')"
+        autocomplete="off"
+        placeholder="name"
+        class="w-full"
+        @change="updateData({ attributeName: $event })"
+      />
+    </ui-autocomplete>
     <insert-workflow-data
       :data="data"
       extra-row
@@ -26,6 +29,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 31 - 21
src/components/newtab/workflow/edit/EditCloseTab.vue

@@ -30,29 +30,35 @@
           {{ t('workflow.blocks.close-tab.activeTab') }}
         </ui-checkbox>
       </div>
-      <ui-input
+      <ui-autocomplete
         v-if="!data.activeTab"
-        :model-value="data.url"
-        class="w-full mt-1"
-        placeholder="http://example.com/*"
-        @change="updateData({ url: $event })"
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
       >
-        <template #label>
-          {{ t('workflow.blocks.close-tab.url') }}
-          <a
-            :title="t('common.example', 2)"
-            rel="noopener"
-            target="_blank"
-            href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples"
-          >
-            <v-remixicon
-              name="riInformationLine"
-              size="18"
-              class="inline-block"
-            />
-          </a>
-        </template>
-      </ui-input>
+        <ui-input
+          :model-value="data.url"
+          class="w-full mt-1"
+          placeholder="http://example.com/*"
+          @change="updateData({ url: $event })"
+        >
+          <template #label>
+            {{ t('workflow.blocks.close-tab.url') }}
+            <a
+              :title="t('common.example', 2)"
+              rel="noopener"
+              target="_blank"
+              href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples"
+            >
+              <v-remixicon
+                name="riInformationLine"
+                size="18"
+                class="inline-block"
+              />
+            </a>
+          </template>
+        </ui-input>
+      </ui-autocomplete>
     </template>
     <ui-checkbox
       v-else
@@ -72,6 +78,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 5 - 0
src/components/newtab/workflow/edit/EditConditions.vue

@@ -53,6 +53,7 @@
             class="text-xl font-semibold mb-4 bg-transparent focus:ring-0"
           />
           <shared-condition-builder
+            :autocomplete="autocomplete"
             :model-value="conditions[state.conditionsIndex].conditions"
             @change="conditions[state.conditionsIndex].conditions = $event"
           />
@@ -77,6 +78,10 @@ const props = defineProps({
     type: String,
     default: '',
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 19 - 7
src/components/newtab/workflow/edit/EditElementExists.vue

@@ -10,13 +10,21 @@
         {{ t(`workflow.blocks.base.findElement.options.${type}`) }}
       </option>
     </ui-select>
-    <ui-input
-      :model-value="data.selector"
-      :label="t('workflow.blocks.element-exists.selector')"
-      placeholder=".element"
-      class="mb-1 w-full"
-      @change="updateData({ selector: $event })"
-    />
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mb-1"
+    >
+      <ui-input
+        :model-value="data.selector"
+        :label="t('workflow.blocks.element-exists.selector')"
+        autocomplete="off"
+        placeholder=".element"
+        class="w-full"
+        @change="updateData({ selector: $event })"
+      />
+    </ui-autocomplete>
     <ui-input
       :model-value="data.tryCount"
       :title="t('workflow.blocks.element-exists.tryFor.title')"
@@ -54,6 +62,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 19 - 7
src/components/newtab/workflow/edit/EditExportData.vue

@@ -16,13 +16,21 @@
         {{ t(`workflow.blocks.export-data.dataToExport.options.${option}`) }}
       </option>
     </ui-select>
-    <ui-input
-      :model-value="data.name"
-      label="File name"
-      class="w-full mt-2"
-      placeholder="unnamed"
-      @change="updateData({ name: $event })"
-    />
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-2"
+    >
+      <ui-input
+        :model-value="data.name"
+        autocomplete="off"
+        label="File name"
+        class="w-full"
+        placeholder="unnamed"
+        @change="updateData({ name: $event })"
+      />
+    </ui-autocomplete>
     <ui-select
       v-if="permission.has.downloads"
       :model-value="data.onConflict"
@@ -72,6 +80,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 20 - 6
src/components/newtab/workflow/edit/EditForms.vue

@@ -1,5 +1,8 @@
 <template>
-  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
+  <edit-interaction-base
+    v-bind="{ data, hide: hideBase, autocomplete }"
+    @change="updateData"
+  >
     <hr />
     <ui-checkbox
       :model-value="data.getValue"
@@ -32,12 +35,19 @@
         {{ t('workflow.blocks.forms.selected') }}
       </ui-checkbox>
       <template v-if="data.type === 'text-field' || data.type === 'select'">
-        <ui-textarea
-          :model-value="data.value"
-          :placeholder="t('workflow.blocks.forms.text-field.value')"
+        <ui-autocomplete
+          :items="autocomplete"
+          :trigger-char="['{{', '}}']"
+          block
           class="w-full mb-1"
-          @change="updateData({ value: $event })"
-        />
+        >
+          <ui-textarea
+            :model-value="data.value"
+            :placeholder="t('workflow.blocks.forms.text-field.value')"
+            class="w-full"
+            @change="updateData({ value: $event })"
+          />
+        </ui-autocomplete>
         <ui-checkbox
           :model-value="data.clearValue"
           @change="updateData({ clearValue: $event })"
@@ -72,6 +82,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 35 - 15
src/components/newtab/workflow/edit/EditGetText.vue

@@ -1,5 +1,5 @@
 <template>
-  <edit-interaction-base v-bind="{ data }" @change="updateData">
+  <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
     <hr />
     <div class="flex rounded-lg bg-input px-4 items-center transition">
       <span>/</span>
@@ -27,22 +27,38 @@
       </ui-popover>
     </div>
     <div class="mt-2 flex space-x-2">
-      <ui-input
-        :model-value="data.prefixText"
-        :title="t('workflow.blocks.get-text.prefixText.title')"
-        :label="t('workflow.blocks.get-text.prefixText.placeholder')"
-        placeholder="Text"
+      <ui-autocomplete
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
         class="w-full"
-        @change="updateData({ prefixText: $event })"
-      />
-      <ui-input
-        :model-value="data.suffixText"
-        :title="t('workflow.blocks.get-text.suffixText.title')"
-        :label="t('workflow.blocks.get-text.suffixText.placeholder')"
-        placeholder="Text"
+      >
+        <ui-input
+          :model-value="data.prefixText"
+          :title="t('workflow.blocks.get-text.prefixText.title')"
+          :label="t('workflow.blocks.get-text.prefixText.placeholder')"
+          autocomplete="off"
+          placeholder="Text"
+          class="w-full"
+          @change="updateData({ prefixText: $event })"
+        />
+      </ui-autocomplete>
+      <ui-autocomplete
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
         class="w-full"
-        @change="updateData({ suffixText: $event })"
-      />
+      >
+        <ui-input
+          :model-value="data.suffixText"
+          :title="t('workflow.blocks.get-text.suffixText.title')"
+          :label="t('workflow.blocks.get-text.suffixText.placeholder')"
+          autocomplete="off"
+          placeholder="Text"
+          class="w-full"
+          @change="updateData({ suffixText: $event })"
+        />
+      </ui-autocomplete>
     </div>
     <ui-checkbox
       :model-value="data.includeTags"
@@ -71,6 +87,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 44 - 36
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -18,42 +18,46 @@
         {{ t('workflow.blocks.google-sheets.select.update') }}
       </option>
     </ui-select>
-    <ui-input
-      :model-value="data.spreadsheetId"
-      class="w-full"
-      placeholder="abcd123"
-      @change="updateData({ spreadsheetId: $event })"
-    >
-      <template #label>
-        {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}*
-        <a
-          href="https://docs.automa.site/blocks/google-sheets.html#spreadsheet-id"
-          target="_blank"
-          rel="noopener"
-          :title="t('workflow.blocks.google-sheets.spreadsheetId.link')"
-        >
-          <v-remixicon name="riInformationLine" size="18" class="inline" />
-        </a>
-      </template>
-    </ui-input>
-    <ui-input
-      :model-value="data.range"
-      class="w-full mt-1"
-      placeholder="Sheet1!A1:B2"
-      @change="updateData({ range: $event })"
-    >
-      <template #label>
-        {{ t('workflow.blocks.google-sheets.range.label') }}*
-        <a
-          href="https://docs.automa.site/blocks/google-sheets.html#range"
-          target="_blank"
-          rel="noopener"
-          :title="t('workflow.blocks.google-sheets.range.link')"
-        >
-          <v-remixicon name="riInformationLine" size="18" class="inline" />
-        </a>
-      </template>
-    </ui-input>
+    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+      <ui-input
+        :model-value="data.spreadsheetId"
+        class="w-full"
+        placeholder="abcd123"
+        @change="updateData({ spreadsheetId: $event })"
+      >
+        <template #label>
+          {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}*
+          <a
+            href="https://docs.automa.site/blocks/google-sheets.html#spreadsheet-id"
+            target="_blank"
+            rel="noopener"
+            :title="t('workflow.blocks.google-sheets.spreadsheetId.link')"
+          >
+            <v-remixicon name="riInformationLine" size="18" class="inline" />
+          </a>
+        </template>
+      </ui-input>
+    </ui-autocomplete>
+    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+      <ui-input
+        :model-value="data.range"
+        class="w-full mt-1"
+        placeholder="Sheet1!A1:B2"
+        @change="updateData({ range: $event })"
+      >
+        <template #label>
+          {{ t('workflow.blocks.google-sheets.range.label') }}*
+          <a
+            href="https://docs.automa.site/blocks/google-sheets.html#range"
+            target="_blank"
+            rel="noopener"
+            :title="t('workflow.blocks.google-sheets.range.link')"
+          >
+            <v-remixicon name="riInformationLine" size="18" class="inline" />
+          </a>
+        </template>
+      </ui-input>
+    </ui-autocomplete>
     <template v-if="data.type === 'get'">
       <ui-input
         :model-value="data.refKey"
@@ -167,6 +171,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 20 - 8
src/components/newtab/workflow/edit/EditHandleDialog.vue

@@ -14,15 +14,23 @@
     >
       {{ t('workflow.blocks.handle-dialog.accept') }}
     </ui-checkbox>
-    <ui-input
+    <ui-autocomplete
       v-if="data.accept"
-      :model-value="data.promptText"
-      :label="t('workflow.blocks.handle-dialog.promptText.label')"
-      :title="t('workflow.blocks.handle-dialog.promptText.description')"
-      placeholder="Text"
-      class="w-full mt-1"
-      @change="updateData({ promptText: $event })"
-    />
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-1"
+    >
+      <ui-input
+        :model-value="data.promptText"
+        :label="t('workflow.blocks.handle-dialog.promptText.label')"
+        :title="t('workflow.blocks.handle-dialog.promptText.description')"
+        autocomplete="off"
+        placeholder="Text"
+        class="w-full"
+        @change="updateData({ promptText: $event })"
+      />
+    </ui-autocomplete>
   </div>
 </template>
 <script setup>
@@ -33,6 +41,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 19 - 6
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -20,13 +20,22 @@
           {{ t(`workflow.blocks.base.findElement.options.${type}`) }}
         </option>
       </ui-select>
-      <ui-input
+      <ui-autocomplete
         v-if="!hideSelector"
-        :model-value="data.selector"
-        :placeholder="t('workflow.blocks.base.selector')"
-        class="mb-1 w-full"
-        @change="updateData({ selector: $event })"
-      />
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
+        class="mb-1"
+      >
+        <ui-input
+          v-if="!hideSelector"
+          :model-value="data.selector"
+          :placeholder="t('workflow.blocks.base.selector')"
+          autocomplete="off"
+          class="w-full"
+          @change="updateData({ selector: $event })"
+        />
+      </ui-autocomplete>
       <ui-expand
         v-if="!hideSelector"
         hide-header-icon
@@ -102,6 +111,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data', 'change']);
 

+ 19 - 7
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -44,14 +44,22 @@
       class="w-full mt-2"
       @change="updateData({ variableName: $event })"
     />
-    <ui-input
+    <ui-autocomplete
       v-else-if="data.loopThrough === 'elements'"
-      :model-value="data.elementSelector"
-      :label="t('workflow.blocks.base.selector')"
-      placeholder=".selector"
-      class="mt-2 w-full"
-      @change="updateData({ elementSelector: $event })"
-    />
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-2"
+    >
+      <ui-input
+        :model-value="data.elementSelector"
+        :label="t('workflow.blocks.base.selector')"
+        autocomplete="off"
+        placeholder=".selector"
+        class="w-full"
+        @change="updateData({ elementSelector: $event })"
+      />
+    </ui-autocomplete>
     <ui-button
       v-else-if="data.loopThrough === 'custom-data'"
       class="w-full mt-4"
@@ -174,6 +182,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 19 - 7
src/components/newtab/workflow/edit/EditNewTab.vue

@@ -6,14 +6,22 @@
       class="w-full"
       @change="updateData({ description: $event })"
     />
-    <ui-input
+    <ui-autocomplete
       v-if="!data.activeTab"
-      :model-value="data.url"
-      :label="t('workflow.blocks.new-tab.url')"
-      class="w-full mt-2"
-      placeholder="http://example.com/"
-      @change="updateData({ url: $event })"
-    />
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-2"
+    >
+      <ui-input
+        :model-value="data.url"
+        :label="t('workflow.blocks.new-tab.url')"
+        class="w-full"
+        autocomplete="off"
+        placeholder="http://example.com/"
+        @change="updateData({ url: $event })"
+      />
+    </ui-autocomplete>
     <ui-checkbox
       :model-value="data.updatePrevTab"
       class="leading-tight mt-2"
@@ -60,6 +68,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 33 - 13
src/components/newtab/workflow/edit/EditSaveAssets.vue

@@ -2,6 +2,7 @@
   <edit-interaction-base
     :data="data"
     :hide="!permission.has.downloads"
+    :autocomplete="autocomplete"
     :hide-selector="data.type !== 'element'"
     @change="updateData"
   >
@@ -27,22 +28,37 @@
         </ui-button>
       </template>
     </template>
-    <ui-input
+    <ui-autocomplete
       v-if="data.type === 'url'"
-      :model-value="data.url"
-      label="URL"
-      class="w-full"
-      placeholder="https://example.com/picture.png"
-      @change="updateData({ url: $event })"
-    />
-    <template v-if="permission.has.downloads">
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+    >
       <ui-input
-        :model-value="data.filename"
-        :label="t('workflow.blocks.save-assets.filename')"
-        class="w-full mt-4"
-        placeholder="image.jpeg"
-        @change="updateData({ filename: $event })"
+        :model-value="data.url"
+        label="URL"
+        class="w-full"
+        autocomplete="off"
+        placeholder="https://example.com/picture.png"
+        @change="updateData({ url: $event })"
       />
+    </ui-autocomplete>
+    <template v-if="permission.has.downloads">
+      <ui-autocomplete
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
+        class="mt-4"
+      >
+        <ui-input
+          :model-value="data.filename"
+          :label="t('workflow.blocks.save-assets.filename')"
+          class="w-full"
+          autocomplete="off"
+          placeholder="image.jpeg"
+          @change="updateData({ filename: $event })"
+        />
+      </ui-autocomplete>
       <ui-select
         :model-value="data.onConflict"
         :label="t('workflow.blocks.handle-download.onConflict')"
@@ -66,6 +82,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 8 - 1
src/components/newtab/workflow/edit/EditScrollElement.vue

@@ -1,5 +1,8 @@
 <template>
-  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
+  <edit-interaction-base
+    v-bind="{ data, autocomplete, hide: hideBase }"
+    @change="updateData"
+  >
     <div v-if="!data.scrollIntoView" class="flex items-center mt-3 space-x-2">
       <ui-input
         :model-value="data.scrollX || 0"
@@ -58,6 +61,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 43 - 29
src/components/newtab/workflow/edit/EditSwitchTab.vue

@@ -1,28 +1,30 @@
 <template>
   <div>
-    <ui-input
-      :model-value="data.matchPattern"
-      placeholder="https://example.com/*"
-      class="w-full"
-      @change="updateData({ matchPattern: $event })"
-    >
-      <template #label>
-        {{ t('workflow.blocks.switch-tab.matchPattern') }}
-        <a
-          :title="t('common.example', 2)"
-          href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples"
-          target="_blank"
-          rel="noopener"
-          class="inline-block ml-1"
-        >
-          <v-remixicon
-            name="riInformationLine"
-            class="inline-block"
-            size="18"
-          />
-        </a>
-      </template>
-    </ui-input>
+    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+      <ui-input
+        :model-value="data.matchPattern"
+        placeholder="https://example.com/*"
+        class="w-full"
+        @change="updateData({ matchPattern: $event })"
+      >
+        <template #label>
+          {{ t('workflow.blocks.switch-tab.matchPattern') }}
+          <a
+            :title="t('common.example', 2)"
+            href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples"
+            target="_blank"
+            rel="noopener"
+            class="inline-block ml-1"
+          >
+            <v-remixicon
+              name="riInformationLine"
+              class="inline-block"
+              size="18"
+            />
+          </a>
+        </template>
+      </ui-input>
+    </ui-autocomplete>
     <ui-checkbox
       :model-value="data.createIfNoMatch"
       class="mt-1"
@@ -30,14 +32,22 @@
     >
       {{ t('workflow.blocks.switch-tab.createIfNoMatch') }}
     </ui-checkbox>
-    <ui-input
+    <ui-autocomplete
       v-if="data.createIfNoMatch"
-      :model-value="data.url"
-      :label="t('workflow.blocks.switch-tab.url')"
-      placeholder="https://example.com"
-      class="w-full mt-2"
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-2"
       @change="updateData({ url: $event })"
-    />
+    >
+      <ui-input
+        :model-value="data.url"
+        :label="t('workflow.blocks.switch-tab.url')"
+        placeholder="https://example.com"
+        class="w-full"
+        @change="updateData({ url: $event })"
+      />
+    </ui-autocomplete>
   </div>
 </template>
 <script setup>
@@ -48,6 +58,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 18 - 6
src/components/newtab/workflow/edit/EditSwitchTo.vue

@@ -19,13 +19,21 @@
         {{ t('workflow.blocks.switch-to.windowTypes.iframe') }}
       </option>
     </ui-select>
-    <ui-input
+    <ui-autocomplete
       v-if="data.windowType === 'iframe'"
-      :model-value="data.selector"
-      :placeholder="t('workflow.blocks.switch-to.iframeSelector')"
-      class="mb-1 w-full"
-      @change="updateData({ selector: $event })"
-    />
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mt-2"
+    >
+      <ui-input
+        :model-value="data.selector"
+        :placeholder="t('workflow.blocks.switch-to.iframeSelector')"
+        autocomplete="off"
+        class="mb-1 w-full"
+        @change="updateData({ selector: $event })"
+      />
+    </ui-autocomplete>
   </div>
 </template>
 <script setup>
@@ -36,6 +44,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 18 - 6
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -29,13 +29,21 @@
       {{ t('workflow.blocks.take-screenshot.saveToComputer') }}
     </ui-checkbox>
     <div v-if="data.saveToComputer" class="flex items-center mt-1">
-      <ui-input
-        :model-value="data.fileName"
-        :placeholder="t('common.fileName')"
+      <ui-autocomplete
+        :items="autocomplete"
+        :trigger-char="['{{', '}}']"
+        block
         class="flex-1 mr-2"
-        title="File name"
-        @change="updateData({ fileName: $event })"
-      />
+      >
+        <ui-input
+          :model-value="data.fileName"
+          :placeholder="t('common.fileName')"
+          autocomplete="off"
+          class="flex-1 mr-2"
+          title="File name"
+          @change="updateData({ fileName: $event })"
+        />
+      </ui-autocomplete>
       <ui-select
         :model-value="data.ext || 'png'"
         placeholder="Type"
@@ -95,6 +103,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 8 - 1
src/components/newtab/workflow/edit/EditTriggerEvent.vue

@@ -1,5 +1,8 @@
 <template>
-  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
+  <edit-interaction-base
+    v-bind="{ data, autocomplete, hide: hideBase }"
+    @change="updateData"
+  >
     <ui-select
       :model-value="data.eventName"
       :placeholder="t('workflow.blocks.trigger-event.selectEvent')"
@@ -78,6 +81,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 20 - 5
src/components/newtab/workflow/edit/EditUploadFile.vue

@@ -1,5 +1,8 @@
 <template>
-  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
+  <edit-interaction-base
+    v-bind="{ data, autocomplete, hide: hideBase }"
+    @change="updateData"
+  >
     <template v-if="hasFileAccess">
       <div class="mt-4 space-y-2">
         <div
@@ -7,11 +10,19 @@
           :key="index"
           class="flex items-center group"
         >
-          <ui-input
-            v-model="filePaths[index]"
-            :placeholder="t('workflow.blocks.upload-file.filePath')"
+          <ui-autocomplete
+            :items="autocomplete"
+            :trigger-char="['{{', '}}']"
+            block
             class="mr-2"
-          />
+          >
+            <ui-input
+              v-model="filePaths[index]"
+              :placeholder="t('workflow.blocks.upload-file.filePath')"
+              autocomplete="off"
+              class="w-full"
+            />
+          </ui-autocomplete>
           <v-remixicon
             name="riDeleteBin7Line"
             class="invisible cursor-pointer group-hover:visible"
@@ -55,6 +66,10 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 21 - 9
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -16,15 +16,23 @@
         {{ method }}
       </option>
     </ui-select>
-    <ui-input
-      :model-value="data.url"
-      :label="`${t('workflow.blocks.webhook.url')}*`"
-      placeholder="http://api.example.com"
-      class="mb-2 w-full"
-      required
-      type="url"
-      @change="updateData({ url: $event })"
-    />
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      class="mb-2"
+    >
+      <ui-input
+        :model-value="data.url"
+        :label="`${t('workflow.blocks.webhook.url')}*`"
+        placeholder="http://api.example.com"
+        class="w-full"
+        autocomplete="off"
+        required
+        type="url"
+        @change="updateData({ url: $event })"
+      />
+    </ui-autocomplete>
     <ui-select
       :model-value="data.contentType"
       :label="t('workflow.blocks.webhook.contentType')"
@@ -158,6 +166,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 5 - 0
src/components/newtab/workflow/edit/EditWhileLoop.vue

@@ -27,6 +27,7 @@
         </div>
         <shared-condition-builder
           :model-value="data.conditions"
+          :autocomplete="autocomplete"
           class="overflow-auto p-4 mt-4 scroll"
           style="height: calc(100vh - 8rem)"
           @change="updateData({ conditions: $event })"
@@ -46,6 +47,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  autocomplete: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 183 - 36
src/components/ui/UiAutocomplete.vue

@@ -1,21 +1,15 @@
 <template>
   <ui-popover
+    :id="componentId"
     v-model="state.showPopover"
+    :class="{ block }"
+    :padding="`p-2 max-h-56 overflow-auto scroll ${componentId}`"
     trigger-width
     trigger="manual"
-    :padding="`p-2 max-h-56 overflow-auto scroll ${componentId}`"
+    class="ui-autocomplete"
   >
     <template #trigger>
-      <ui-input
-        v-bind="{ modelValue, placeholder, label, prependIcon }"
-        autocomplete="off"
-        @focus="state.showPopover = true"
-        @blur="state.showPopover = false"
-        @keydown="handleKeydown"
-        @change="updateValue"
-        @keyup.enter="selectItem(state.activeIndex)"
-        @keyup.esc="state.showPopover = false"
-      />
+      <slot />
     </template>
     <p v-if="filteredItems.length === 0" class="text-center">No data to show</p>
     <ui-list v-else class="space-y-1">
@@ -36,7 +30,13 @@
   </ui-popover>
 </template>
 <script setup>
-import { computed, onMounted, shallowReactive, watch } from 'vue';
+import {
+  computed,
+  onMounted,
+  onBeforeUnmount,
+  shallowReactive,
+  watch,
+} from 'vue';
 import { useComponentId } from '@/composable/componentId';
 import { debounce } from '@/utils/helper';
 
@@ -62,15 +62,34 @@ const props = defineProps({
     default: '',
   },
   prependIcon: {
+    type: String,
+    default: null,
+  },
+  triggerChar: {
+    type: Array,
+    default: () => [],
+  },
+  inputClass: {
     type: String,
     default: '',
   },
+  block: {
+    type: Boolean,
+    default: false,
+  },
+  component: {
+    type: String,
+    default: 'UiInput',
+  },
 });
 const emit = defineEmits(['update:modelValue', 'change']);
 
+let input = null;
 const componentId = useComponentId('autocomplete');
 
 const state = shallowReactive({
+  charIndex: -1,
+  searchText: '',
   activeIndex: -1,
   showPopover: false,
   inputChanged: false,
@@ -78,32 +97,71 @@ const state = shallowReactive({
 
 const getItem = (item) => item[props.itemLabel] || item;
 
-const filteredItems = computed(() =>
-  props.items.filter(
+const filteredItems = computed(() => {
+  if (!state.showPopover) return [];
+
+  const triggerChar = props.triggerChar.length > 0;
+  const searchText = (
+    triggerChar ? state.searchText : props.modelValue
+  ).toLocaleLowerCase();
+
+  return props.items.filter(
     (item) =>
       !state.inputChanged ||
-      getItem(item)
-        ?.toLocaleLowerCase()
-        .includes(props.modelValue.toLocaleLowerCase())
-  )
-);
+      getItem(item)?.toLocaleLowerCase().includes(searchText)
+  );
+});
 
-function handleKeydown(event) {
-  if (!state.showPopover) state.showPopover = true;
+function getLastKeyBeforeCaret(caretIndex) {
+  const getPosition = (val, index) => ({
+    index,
+    charIndex: input.value.lastIndexOf(val, caretIndex - 1),
+  });
+  const [charData] = props.triggerChar
+    .map(getPosition)
+    .sort((a, b) => b.charIndex - a.charIndex);
 
-  const itemsLength = filteredItems.value.length - 1;
+  if (charData.index > 0) return -1;
 
-  if (event.key === 'ArrowUp') {
-    if (state.activeIndex <= 0) state.activeIndex = itemsLength;
-    else state.activeIndex -= 1;
+  return charData.charIndex;
+}
+function getSearchText(caretIndex, charIndex) {
+  if (charIndex !== -1) {
+    const charsLength = props.triggerChar.length;
+    const text = input.value.substring(charIndex + charsLength, caretIndex);
 
-    event.preventDefault();
-  } else if (event.key === 'ArrowDown') {
-    if (state.activeIndex >= itemsLength) state.activeIndex = 0;
-    else state.activeIndex += 1;
+    if (!/\s/.test(text)) {
+      return text;
+    }
+  }
 
-    event.preventDefault();
+  return null;
+}
+function showPopover() {
+  if (props.triggerChar.length < 1) {
+    state.showPopover = true;
+    return;
   }
+
+  const { selectionStart } = input;
+
+  if (selectionStart >= 0) {
+    const charIndex = getLastKeyBeforeCaret(selectionStart);
+    const text = getSearchText(selectionStart, charIndex);
+
+    if (charIndex >= 0 && text) {
+      state.inputChanged = true;
+      state.showPopover = true;
+      state.searchText = text;
+      state.charIndex = charIndex;
+
+      return;
+    }
+  }
+
+  state.charIndex = -1;
+  state.searchText = '';
+  state.showPopover = false;
 }
 function checkInView(container, element, partial = false) {
   const cTop = container.scrollTop;
@@ -120,21 +178,94 @@ function checkInView(container, element, partial = false) {
   return isTotal || isPartial;
 }
 function updateValue(value) {
-  if (!state.showPopover) state.showPopover = true;
-
   state.inputChanged = true;
 
   emit('change', value);
   emit('update:modelValue', value);
+
+  input.value = value;
+  input.dispatchEvent(new Event('input'));
 }
-function selectItem(index) {
-  const selectedItem = filteredItems.value[index];
+function selectItem(itemIndex) {
+  let selectedItem = filteredItems.value[itemIndex];
 
   if (!selectedItem) return;
 
-  updateValue(getItem(selectedItem));
+  selectedItem = getItem(selectedItem);
+
+  let caretPosition;
+  const isTriggerChar = state.charIndex >= 0 && state.searchText;
+
+  if (isTriggerChar) {
+    const val = input.value;
+    const index = state.charIndex;
+    const charLength = props.triggerChar[0].length;
+
+    caretPosition = index + charLength + selectedItem.length;
+    selectedItem =
+      val.slice(0, index + charLength) +
+      selectedItem +
+      val.slice(state.searchText.length + index + charLength, val.length);
+  }
+
+  updateValue(selectedItem);
+
+  if (isTriggerChar) {
+    setTimeout(() => {
+      input.selectionEnd = caretPosition;
+
+      if (!/textarea/i.test(props.component)) {
+        input.blur();
+        input.focus();
+      }
+    }, 300);
+  }
+}
+function handleKeydown(event) {
+  const itemsLength = filteredItems.value.length - 1;
+
+  if (event.key === 'ArrowUp') {
+    if (state.activeIndex <= 0) state.activeIndex = itemsLength;
+    else state.activeIndex -= 1;
+
+    event.preventDefault();
+  } else if (event.key === 'ArrowDown') {
+    if (state.activeIndex >= itemsLength) state.activeIndex = 0;
+    else state.activeIndex += 1;
+
+    event.preventDefault();
+  } else if (event.key === 'Enter' && state.showPopover) {
+    selectItem(state.activeIndex);
+
+    event.preventDefault();
+  } else if (event.key === 'Escape') {
+    state.showPopover = false;
+  }
+}
+function handleBlur() {
   state.showPopover = false;
 }
+function handleFocus() {
+  if (props.triggerChar.length < 1) return;
+
+  showPopover();
+}
+function attachEvents() {
+  if (!input) return;
+
+  input.addEventListener('blur', handleBlur);
+  input.addEventListener('focus', handleFocus);
+  input.addEventListener('input', showPopover);
+  input.addEventListener('keydown', handleKeydown);
+}
+function detachEvents() {
+  if (!input) return;
+
+  input.removeEventListener('blur', handleBlur);
+  input.removeEventListener('focus', handleFocus);
+  input.removeEventListener('input', showPopover);
+  input.removeEventListener('keydown', handleKeydown);
+}
 
 watch(
   () => state.activeIndex,
@@ -159,11 +290,27 @@ watch(
 
 onMounted(() => {
   if (props.modelValue) {
-    const activeIndex = props.items(
+    const activeIndex = props.items.findIndex(
       (item) => getItem(item) === props.modelValue
     );
 
     if (activeIndex !== -1) state.activeIndex = activeIndex;
   }
+
+  input = document.querySelector(
+    `#${componentId} input, #${componentId} textarea`
+  );
+
+  attachEvents();
+});
+onBeforeUnmount(() => {
+  detachEvents();
 });
 </script>
+<style>
+.ui-autocomplete.block,
+.ui-autocomplete.block .ui-popover__trigger {
+  width: 100%;
+  display: block;
+}
+</style>

+ 5 - 1
src/components/ui/UiTextarea.vue

@@ -7,6 +7,10 @@
     :class="{ 'overflow-hidden resize-none': autoresize }"
     :style="{ height }"
     @input="emitValue"
+    @keyup="$emit('keyup', $event)"
+    @keydown="$emit('keydown', $event)"
+    @focus="$emit('focus', $event)"
+    @blur="$emit('blur', $event)"
   ></textarea>
 </template>
 <script>
@@ -41,7 +45,7 @@ export default {
     },
     block: Boolean,
   },
-  emits: ['update:modelValue', 'change'],
+  emits: ['update:modelValue', 'change', 'focus', 'blur', 'keyup', 'keydown'],
   setup(props, { emit }) {
     const textareaId = useComponentId('textarea');
     const textarea = ref(null);

+ 13 - 2
src/content/element-selector/App.vue

@@ -307,7 +307,10 @@ function getElementList(target) {
   if (automaListEl) {
     if (target.hasAttribute('automa-el-list')) return [];
 
-    const childSelector = finder(target, { root: automaListEl });
+    const childSelector = finder(target, {
+      root: automaListEl,
+      idName: () => false,
+    });
     const elements = document.querySelectorAll(
       `${state.listSelector} ${childSelector}`
     );
@@ -368,7 +371,10 @@ function handleClick(event) {
 
     const isInList = target.closest('[automa-el-list]');
     if (isInList) {
-      const childSelector = finder(target, { root: isInList });
+      const childSelector = finder(target, {
+        root: isInList,
+        idName: () => false,
+      });
       updateSelectedElements(`${state.listSelector} ${childSelector}`, true);
 
       return;
@@ -514,6 +520,11 @@ function destroy() {
     selectedElements: [],
   });
 
+  const prevSelectedList = document.querySelectorAll('[automa-el-list]');
+  prevSelectedList.forEach((element) => {
+    element.removeAttribute('automa-el-list');
+  });
+
   document.documentElement.style.fontSize = originalFontSize;
 }
 

+ 17 - 0
src/newtab/pages/workflows/[id].vue

@@ -6,7 +6,11 @@
     >
       <workflow-edit-block
         v-if="state.isEditBlock && workflowData.active !== 'shared'"
+        v-model:autocomplete="autocomplete.cache"
         :data="state.blockData"
+        :data-changed="autocomplete.dataChanged"
+        :editor="editor"
+        :workflow="workflow"
         @update="updateBlockData"
         @close="(state.isEditBlock = false), (state.blockData = {})"
       />
@@ -260,6 +264,11 @@ const shortcut = useShortcut('editor:toggle-sidebar', toggleSidebar);
 
 const editor = shallowRef(null);
 const activeTab = shallowRef('editor');
+
+const autocomplete = reactive({
+  cache: null,
+  dataChanged: false,
+});
 const workflowPayload = reactive({
   data: {},
   isUpdating: false,
@@ -304,6 +313,11 @@ const workflowModals = {
     component: WorkflowDataTable,
     title: t('workflow.table.title'),
     docs: 'https://docs.automa.site/api-reference/table.html',
+    events: {
+      change() {
+        autocomplete.dataChanged = true;
+      },
+    },
   },
   'workflow-share': {
     icon: 'riShareLine',
@@ -368,6 +382,7 @@ const updateBlockData = debounce((data) => {
 
   state.blockData.data = data;
   state.isDataChanged = true;
+  autocomplete.dataChanged = true;
 
   if (state.blockData.isInGroup) {
     payload = { itemId: state.blockData.itemId, data };
@@ -699,6 +714,7 @@ function deleteBlock(id) {
   }
 
   state.isDataChanged = true;
+  autocomplete.dataChanged = true;
 }
 function updateWorkflow(data) {
   if (workflowData.active === 'shared') return;
@@ -764,6 +780,7 @@ function editBlock(data) {
 }
 function handleEditorDataChanged() {
   state.isDataChanged = true;
+  autocomplete.dataChanged = true;
 }
 function deleteWorkflow() {
   dialog.confirm({

+ 10 - 1
src/utils/shared.js

@@ -197,6 +197,7 @@ export const tasks = {
     maxConnection: 1,
     allowedInputs: true,
     refDataKeys: ['fileName'],
+    autocomplete: ['variableName'],
     data: {
       fileName: '',
       ext: 'png',
@@ -281,6 +282,7 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     refDataKeys: ['selector', 'prefixText', 'suffixText', 'extraRowValue'],
+    autocomplete: ['variableName'],
     data: {
       description: '',
       findBy: 'cssSelector',
@@ -385,6 +387,7 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     refDataKeys: ['selector', 'attributeName', 'extraRowValue'],
+    autocomplete: ['variableName'],
     data: {
       description: '',
       findBy: 'cssSelector',
@@ -415,6 +418,7 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     refDataKeys: ['selector', 'value'],
+    autocomplete: ['variableName'],
     data: {
       description: '',
       findBy: 'cssSelector',
@@ -518,6 +522,7 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     refDataKeys: ['customData', 'range', 'spreadsheetId'],
+    autocomplete: ['refKey'],
     data: {
       range: '',
       refKey: '',
@@ -579,6 +584,7 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     refDataKeys: ['body', 'url'],
+    autocomplete: ['variableName'],
     data: {
       description: '',
       url: '',
@@ -627,6 +633,7 @@ export const tasks = {
       'referenceKey',
       'elementSelector',
     ],
+    autocomplete: ['variableName', 'loopId'],
     data: {
       loopId: '',
       maxLoop: 0,
@@ -684,6 +691,7 @@ export const tasks = {
     outputs: 1,
     allowedInputs: true,
     maxConnection: 1,
+    autocomplete: ['variableName'],
     data: {
       description: '',
       assignVariable: false,
@@ -825,7 +833,8 @@ export const tasks = {
     outputs: 1,
     allowedInputs: true,
     maxConnection: 1,
-    refDataKeys: ['promptText'],
+    refDataKeys: ['filename'],
+    autocomplete: ['variableName'],
     data: {
       description: '',
       filename: '',

+ 37 - 0
yarn.lock

@@ -1153,6 +1153,18 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
+"@floating-ui/core@^0.3.0":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.3.1.tgz#3dde0ad0724d4b730567c92f49f0950910e18871"
+  integrity sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==
+
+"@floating-ui/dom@^0.1.10":
+  version "0.1.10"
+  resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.1.10.tgz#ce304136a52c71ef157826d2ebf52d68fa2deed5"
+  integrity sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==
+  dependencies:
+    "@floating-ui/core" "^0.3.0"
+
 "@humanwhocodes/config-array@^0.5.0":
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -3874,6 +3886,14 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
   integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
 
+floating-vue@^2.0.0-beta.13:
+  version "2.0.0-beta.13"
+  resolved "https://registry.yarnpkg.com/floating-vue/-/floating-vue-2.0.0-beta.13.tgz#471c8dbee8c5eb19b6410b14a2865b199b099bb1"
+  integrity sha512-C2bGEtdbOXm+2rmkn8W6dTQeh3xJT7YbdHnrbanYDS3vK/1lumdXhYA6j2Qs+9shViNjoVUUND1EhLxLDP2OZA==
+  dependencies:
+    "@floating-ui/dom" "^0.1.10"
+    vue-resize "^2.0.0-alpha.1"
+
 follow-redirects@^1.0.0:
   version "1.14.9"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
@@ -6943,6 +6963,11 @@ text-table@^0.2.0:
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
+textarea-caret@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
+  integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
+
 through@^2.3.8:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -7213,6 +7238,18 @@ vue-loader@16.8.1:
     hash-sum "^2.0.0"
     loader-utils "^2.0.0"
 
+vue-mention@^2.0.0-alpha.3:
+  version "2.0.0-alpha.3"
+  resolved "https://registry.yarnpkg.com/vue-mention/-/vue-mention-2.0.0-alpha.3.tgz#8b82df71ec8daf47d5d3dc31be3b377275f5bb73"
+  integrity sha512-NtM6Z6UpqHByKJPyiy2SrBy3K7wyi/6bvXltaRfWcSQdNwW3YrWzrr1M7lYB4NoWRhDFuk+4X1GpY8HH06g+XQ==
+  dependencies:
+    textarea-caret "^3.1.0"
+
+vue-resize@^2.0.0-alpha.1:
+  version "2.0.0-alpha.1"
+  resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
+  integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
+
 vue-router@^4.0.11:
   version "4.0.14"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.14.tgz#ce2028c1c5c33e30c7287950c973f397fce1bd65"