Ahmad Kholid 3 years ago
parent
commit
79cae69343
41 changed files with 1059 additions and 153 deletions
  1. 1 1
      package.json
  2. 9 7
      src/background/collection-engine/flow-handler.js
  3. 12 7
      src/background/index.js
  4. 8 6
      src/background/workflow-engine/blocks-handler/handler-execute-workflow.js
  5. 38 0
      src/background/workflow-engine/blocks-handler/handler-hover-element.js
  6. 6 8
      src/background/workflow-engine/blocks-handler/handler-interaction-block.js
  7. 26 2
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  8. 5 1
      src/background/workflow-engine/blocks-handler/handler-switch-tab.js
  9. 79 12
      src/background/workflow-engine/engine.js
  10. 15 0
      src/background/workflow-engine/helper.js
  11. 8 1
      src/components/newtab/logs/LogsFilters.vue
  12. 20 5
      src/components/newtab/workflow/WorkflowDetailsCard.vue
  13. 35 24
      src/components/newtab/workflow/WorkflowSettings.vue
  14. 2 2
      src/components/newtab/workflow/edit/EditInsertData.vue
  15. 16 0
      src/components/newtab/workflow/edit/EditNewTab.vue
  16. 2 1
      src/components/newtab/workflow/edit/EditTriggerEvent.vue
  17. 23 2
      src/components/newtab/workflow/edit/TriggerEventKeyboard.vue
  18. 29 3
      src/content/blocks-handler/handler-event-click.js
  19. 33 0
      src/content/blocks-handler/handler-hover-element.js
  20. 105 14
      src/content/blocks-handler/handler-trigger-event.js
  21. 0 0
      src/content/element-selector/AppHeader.vue
  22. 2 0
      src/content/element-selector/comps-ui.js
  23. 32 17
      src/content/element-selector/index.js
  24. 11 10
      src/content/element-selector/main.js
  25. 7 5
      src/content/handle-selector.js
  26. 0 5
      src/content/index.js
  27. 0 0
      src/lib/v-remixicon.js
  28. 5 0
      src/locales/en/blocks.json
  29. 11 1
      src/locales/en/newtab.json
  30. 3 1
      src/manifest.json
  31. 1 0
      src/models/workflow.js
  32. 14 0
      src/newtab/pages/Logs.vue
  33. 5 9
      src/popup/pages/Home.vue
  34. 1 1
      src/utils/data-exporter.js
  35. 11 5
      src/utils/handle-form-element.js
  36. 4 0
      src/utils/helper.js
  37. 25 1
      src/utils/shared.js
  38. 1 1
      src/utils/simulate-event/index.js
  39. 44 0
      src/utils/simulate-event/mouse-event.js
  40. 409 0
      src/utils/us-keyboard-layout.js
  41. 1 1
      webpack.config.js

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "1.2.1",
+  "version": "1.3.0",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {

+ 9 - 7
src/background/collection-engine/flow-handler.js

@@ -30,13 +30,15 @@ export function workflow(flow) {
       blocksHandler,
       states: this.states,
       logger: this.logger,
-      parentWorkflow: {
-        id: this.id,
-        isCollection: true,
-        name: this.collection.name,
-      },
-      data: {
-        globalData: globalData.trim() === '' ? null : globalData,
+      options: {
+        parentWorkflow: {
+          id: this.id,
+          isCollection: true,
+          name: this.collection.name,
+        },
+        data: {
+          globalData: globalData.trim() === '' ? null : globalData,
+        },
       },
     });
 

+ 12 - 7
src/background/index.js

@@ -64,7 +64,7 @@ const workflow = {
     }
 
     const engine = new WorkflowEngine(workflowData, {
-      ...options,
+      options,
       blocksHandler,
       logger: this.logger,
       states: this.states,
@@ -155,7 +155,7 @@ async function checkVisitWebTriggers(changeInfo, tab) {
   if (triggeredWorkflow) {
     const workflowData = await workflow.get(triggeredWorkflow.id);
 
-    if (workflowData) workflow.execute(workflowData);
+    if (workflowData) workflow.execute(workflowData, { tabId: tab.id });
   }
 }
 async function checkRecordingWorkflow({ status }, { url, id }) {
@@ -353,13 +353,18 @@ message.on('set:active-tab', (tabId) => {
   return browser.tabs.update(tabId, { active: true });
 });
 
-message.on('get:sender', (_, sender) => {
-  return sender;
-});
-message.on('get:tab-screenshot', (options) => {
-  return browser.tabs.captureVisibleTab(options);
+message.on('debugger:send-command', ({ tabId, method, params }) => {
+  return new Promise((resolve) => {
+    console.log(tabId, method, params);
+    chrome.debugger.sendCommand({ tabId }, method, params, resolve);
+  });
 });
+
+message.on('get:sender', (_, sender) => sender);
 message.on('get:file', (path) => getFile(path));
+message.on('get:tab-screenshot', (options) =>
+  browser.tabs.captureVisibleTab(options)
+);
 
 message.on('collection:execute', (collection) => {
   const engine = new CollectionEngine(collection, {

+ 8 - 6
src/background/workflow-engine/blocks-handler/handler-execute-workflow.js

@@ -52,9 +52,14 @@ async function executeWorkflow({ outputs, data }) {
       throw errorInstance;
     }
     const options = {
-      parentWorkflow: {
-        id: this.id,
-        name: this.workflow.name,
+      options: {
+        data: {
+          globalData: isWhitespace(data.globalData) ? null : data.globalData,
+        },
+        parentWorkflow: {
+          id: this.id,
+          name: this.workflow.name,
+        },
       },
       events: {
         onInit: (engine) => {
@@ -77,9 +82,6 @@ async function executeWorkflow({ outputs, data }) {
       states: this.states,
       logger: this.logger,
       blocksHandler: this.blocksHandler,
-      data: {
-        globalData: isWhitespace(data.globalData) ? null : data.globalData,
-      },
     };
 
     if (workflow.drawflow.includes(this.workflow.id)) {

+ 38 - 0
src/background/workflow-engine/blocks-handler/handler-hover-element.js

@@ -0,0 +1,38 @@
+import { getBlockConnection, attachDebugger } from '../helper';
+
+export async function hoverElement(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    if (!this.activeTab.id) throw new Error('no-tab');
+
+    const { debugMode, executedBlockOnWeb } = this.workflow.settings;
+
+    if (!debugMode) {
+      await attachDebugger(this.activeTab.id);
+    }
+
+    await this._sendMessageToTab({
+      ...block,
+      debugMode,
+      executedBlockOnWeb,
+      activeTabId: this.activeTab.id,
+      frameSelector: this.frameSelector,
+    });
+
+    if (!debugMode) {
+      chrome.debugger.detach({ tabId: this.activeTab.id });
+    }
+
+    return {
+      data: '',
+      nextBlockId,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default hoverElement;

+ 6 - 8
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -32,12 +32,14 @@ async function interactionHandler(block, { refData }) {
   const nextBlockId = getBlockConnection(block);
   const messagePayload = {
     ...block,
-    refData,
     debugMode,
     executedBlockOnWeb,
+    activeTabId: this.activeTab.id,
     frameSelector: this.frameSelector,
   };
 
+  if (block.name === 'javascript-code') messagePayload.refData = refData;
+
   try {
     const data = await this._sendMessageToTab(messagePayload, {
       frameId: this.activeTab.frameId || 0,
@@ -80,13 +82,7 @@ async function interactionHandler(block, { refData }) {
       }
     }
 
-    const isJavascriptBlock = block.name === 'javascript-code';
-
-    if (block.data.assignVariable && !isJavascriptBlock) {
-      this.referenceData.variables[block.data.variableName] = data;
-    }
-
-    if (isJavascriptBlock) {
+    if (block.name === 'javascript-code') {
       if (data?.variables) {
         Object.keys(data.variables).forEach((varName) => {
           this.referenceData.variables[varName] = data.variables[varName];
@@ -99,6 +95,8 @@ async function interactionHandler(block, { refData }) {
           : [data.columns.data];
         this.addDataToColumn(arrData);
       }
+    } else if (block.data.assignVariable) {
+      this.referenceData.variables[block.data.variableName] = data;
     }
 
     return {

+ 26 - 2
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -1,5 +1,9 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
+import {
+  getBlockConnection,
+  attachDebugger,
+  sendDebugCommand,
+} from '../helper';
 import { isWhitespace } from '@/utils/helper';
 
 async function newTab(block) {
@@ -14,7 +18,8 @@ async function newTab(block) {
   const nextBlockId = getBlockConnection(block);
 
   try {
-    const { updatePrevTab, url, active, inGroup } = block.data;
+    const { updatePrevTab, url, active, inGroup, customUserAgent, userAgent } =
+      block.data;
     const isInvalidUrl = !/^https?/.test(url);
 
     if (isInvalidUrl) {
@@ -40,6 +45,21 @@ async function newTab(block) {
 
     this.activeTab.url = url;
     if (tab) {
+      if (this.workflow.settings.debugMode || customUserAgent) {
+        await attachDebugger(tab.id, this.activeTab.id);
+
+        if (customUserAgent) {
+          const res = await sendDebugCommand(
+            tab.id,
+            'Network.setUserAgentOverride',
+            {
+              userAgent,
+            }
+          );
+          console.log('agent:', res);
+        }
+      }
+
       this.activeTab.id = tab.id;
       this.windowId = tab.windowId;
     }
@@ -63,6 +83,10 @@ async function newTab(block) {
 
     this.activeTab.frameId = 0;
 
+    if (!this.workflow.settings.debugMode && customUserAgent) {
+      chrome.debugger.detach({ tabId: tab.id });
+    }
+
     return {
       data: url,
       nextBlockId,

+ 5 - 1
src/background/workflow-engine/blocks-handler/handler-switch-tab.js

@@ -1,5 +1,5 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
+import { getBlockConnection, attachDebugger } from '../helper';
 
 export default async function ({ data, outputs }) {
   const nextBlockId = getBlockConnection({ outputs });
@@ -32,6 +32,10 @@ export default async function ({ data, outputs }) {
     await browser.tabs.update(tab.id, { active: true });
   }
 
+  if (this.workflow.settings.debugMode) {
+    await attachDebugger(tab.id, this.activeTab.id);
+  }
+
   this.activeTab.id = tab.id;
   this.activeTab.frameId = 0;
   this.activeTab.url = tab.url;

+ 79 - 12
src/background/workflow-engine/engine.js

@@ -2,14 +2,20 @@ import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
 import { convertData, waitTabLoaded } from './helper';
-import { toCamelCase, parseJSON, isObject, objectHasKey } from '@/utils/helper';
+import {
+  toCamelCase,
+  sleep,
+  parseJSON,
+  isObject,
+  objectHasKey,
+} from '@/utils/helper';
 import referenceData from '@/utils/reference-data';
 import executeContentScript from './execute-content-script';
 
 class WorkflowEngine {
   constructor(
     workflow,
-    { states, logger, blocksHandler, tabId, parentWorkflow, data }
+    { states, logger, blocksHandler, parentWorkflow, options }
   ) {
     this.id = nanoid();
     this.states = states;
@@ -23,6 +29,7 @@ class WorkflowEngine {
     this.repeatedTasks = {};
 
     this.windowId = null;
+    this.triggerBlock = null;
     this.currentBlock = null;
     this.childWorkflowId = null;
 
@@ -34,15 +41,20 @@ class WorkflowEngine {
     this.eventListeners = {};
     this.columns = { column: { index: 0, type: 'any' } };
 
-    const globalData = data?.globalData || workflow.globalData;
-    const variables = isObject(data?.variables) ? data.variables : {};
+    const globalData = options?.data?.globalData || workflow.globalData;
+    const variables = isObject(options?.data?.variables)
+      ? options?.data.variables
+      : {};
+
+    options.data = { globalData, variables };
+    this.options = options;
 
     this.activeTab = {
       url: '',
-      id: tabId,
       frameId: 0,
       frames: {},
       groupId: null,
+      id: options?.tabId,
     };
     this.referenceData = {
       variables,
@@ -59,7 +71,38 @@ class WorkflowEngine {
     };
   }
 
-  init(currentBlock) {
+  reset() {
+    this.loopList = {};
+    this.repeatedTasks = {};
+
+    this.windowId = null;
+    this.currentBlock = null;
+    this.childWorkflowId = null;
+
+    this.isDestroyed = false;
+    this.isUsingProxy = false;
+
+    this.history = [];
+    this.columns = { column: { index: 0, type: 'any' } };
+
+    this.activeTab = {
+      url: '',
+      frameId: 0,
+      frames: {},
+      groupId: null,
+      id: this.options?.tabId,
+    };
+    this.referenceData = {
+      table: [],
+      loopData: {},
+      workflow: {},
+      googleSheets: {},
+      variables: this.options.variables,
+      globalData: this.referenceData.globalData,
+    };
+  }
+
+  init() {
     if (this.workflow.isDisabled) return;
 
     if (!this.states) {
@@ -98,7 +141,7 @@ class WorkflowEngine {
     this.blocks = blocks;
     this.startedTimestamp = Date.now();
     this.workflow.table = columns;
-    this.currentBlock = currentBlock || triggerBlock;
+    this.currentBlock = triggerBlock;
 
     this.states.on('stop', this.onWorkflowStopped);
 
@@ -196,6 +239,11 @@ class WorkflowEngine {
     try {
       if (this.isDestroyed) return;
       if (this.isUsingProxy) chrome.proxy.settings.clear({});
+      if (this.workflow.settings.debugMode && this.activeTab.id) {
+        await sleep(1000);
+
+        chrome.debugger.detach({ tabId: this.activeTab.id });
+      }
 
       const endedTimestamp = Date.now();
       this.executeQueue();
@@ -255,7 +303,8 @@ class WorkflowEngine {
     this.dispatchEvent('update', { state: this.state });
 
     const startExecutedTime = Date.now();
-    const blockHandler = this.blocksHandler[toCamelCase(block?.name)];
+
+    const blockHandler = this.blocksHandler[toCamelCase(block.name)];
     const handler =
       !blockHandler && tasks[block.name].category === 'interaction'
         ? this.blocksHandler.interactionBlock
@@ -307,13 +356,31 @@ class WorkflowEngine {
         ...(error.data || {}),
       });
 
-      if (
-        this.workflow.settings.onError === 'keep-running' &&
-        error.nextBlockId
-      ) {
+      const { onError } = this.workflow.settings;
+
+      if (onError === 'keep-running' && error.nextBlockId) {
         setTimeout(() => {
           this.executeBlock(this.blocks[error.nextBlockId], error.data || '');
         }, blockDelay);
+      } else if (onError === 'restart-workflow' && !this.parentWorkflow) {
+        const restartKey = `restart-count:${this.id}`;
+        const restartCount = +localStorage.getItem(restartKey) || 0;
+        const maxRestart = this.workflow.settings.restartTimes ?? 3;
+
+        if (restartCount >= maxRestart) {
+          localStorage.removeItem(restartKey);
+          this.destroy();
+          return;
+        }
+
+        this.reset();
+
+        const triggerBlock = Object.values(this.blocks).find(
+          ({ name }) => name === 'trigger'
+        );
+        this.executeBlock(triggerBlock);
+
+        localStorage.setItem(restartKey, restartCount + 1);
       } else {
         this.destroy('error', error.message);
       }

+ 15 - 0
src/background/workflow-engine/helper.js

@@ -1,3 +1,18 @@
+export function sendDebugCommand(tabId, method, params = {}) {
+  return new Promise((resolve) => {
+    chrome.debugger.sendCommand({ tabId }, method, params, resolve);
+  });
+}
+
+export function attachDebugger(tabId, prevTab) {
+  return new Promise((resolve) => {
+    if (prevTab && tabId !== prevTab)
+      chrome.debugger.detach({ tabId: prevTab });
+
+    chrome.debugger.attach({ tabId }, '1.3', resolve);
+  });
+}
+
 export function waitTabLoaded(tabId) {
   return new Promise((resolve, reject) => {
     const activeTabStatus = () => {

+ 8 - 1
src/components/newtab/logs/LogsFilters.vue

@@ -68,6 +68,13 @@
         </ui-select>
       </div>
     </ui-popover>
+    <ui-button
+      v-tooltip:bottom="t('log.clearLogs.title')"
+      icon
+      @click="$emit('clear')"
+    >
+      <v-remixicon name="riDeleteBin7Line" />
+    </ui-button>
   </div>
 </template>
 <script setup>
@@ -84,7 +91,7 @@ defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['updateSorts', 'updateFilters']);
+const emit = defineEmits(['updateSorts', 'updateFilters', 'clear']);
 
 const { t } = useI18n();
 const shortcut = useShortcut('action:search', () => {

+ 20 - 5
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -65,16 +65,23 @@
     >
       <p>{{ t('workflow.cantEdit') }}</p>
     </div>
-    <template v-for="(items, catId) in blocks" :key="catId">
-      <div class="flex items-center top-0 space-x-2 mb-2">
+    <ui-expand
+      v-for="(items, catId) in blocks"
+      :key="catId"
+      v-model="expandList[catId]"
+      hide-header-icon
+      header-class="flex items-center py-2 focus:ring-0 w-full text-left text-gray-600 dark:text-gray-200"
+    >
+      <template #header="{ show }">
         <span
           :class="categories[catId].color"
           class="h-3 w-3 rounded-full"
         ></span>
-        <p class="capitalize text-gray-600 dark:text-gray-200">
+        <p class="capitalize flex-1 ml-2">
           {{ categories[catId].name }}
         </p>
-      </div>
+        <v-remixicon :name="show ? 'riSubtractLine' : 'riAddLine'" size="20" />
+      </template>
       <div class="grid grid-cols-2 gap-2 mb-4">
         <div
           v-for="block in items"
@@ -107,7 +114,7 @@
           </p>
         </div>
       </div>
-    </template>
+    </ui-expand>
   </div>
 </template>
 <script setup>
@@ -159,13 +166,21 @@ const blocksArr = Object.entries(tasks).map(([key, block]) => ({
   id: key,
   name: t(`workflow.blocks.${key}.name`),
 }));
+const categoriesExpand = Object.keys(categories).reduce((acc, key) => {
+  acc[key] = true;
+
+  return acc;
+}, {});
 
 const query = ref('');
+const expandList = ref(categoriesExpand);
+
 const blocks = computed(() =>
   blocksArr.reduce((arr, block) => {
     if (
       block.name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
     ) {
+      expandList.value[block.category] = true;
       (arr[block.category] = arr[block.category] || []).push(block);
     }
 

+ 35 - 24
src/components/newtab/workflow/WorkflowSettings.vue

@@ -1,26 +1,31 @@
 <template>
   <div class="workflow-settings">
-    <div class="mb-4">
-      <p class="mb-1 capitalize">
-        {{ t('workflow.settings.onError.title') }}
-      </p>
-      <div class="space-x-4 flex w-full max-w-sm items-center">
-        <div
-          v-for="item in onError"
-          :key="item.id"
-          class="p-3 rounded-lg border transition-colors w-full hoverable"
-          @click="settings.onError = item.id"
-        >
-          <ui-radio
-            :model-value="settings.onError"
-            :value="item.id"
-            class="capitalize"
-            @change="settings.onError = $event"
-          >
-            {{ item.name }}
-          </ui-radio>
-        </div>
+    <div class="mb-4 flex">
+      <div class="flex-1">
+        <p class="mb-1 capitalize">
+          {{ t('workflow.settings.onError.title') }}
+        </p>
+        <ui-select v-model="settings.onError" class="w-full max-w-sm">
+          <option v-for="item in onError" :key="item.id" :value="item.id">
+            {{ t(`workflow.settings.onError.items.${item.name}`) }}
+          </option>
+        </ui-select>
       </div>
+      <label v-if="settings.onError === 'restart-workflow'" class="ml-2">
+        <p class="mb-1 capitalize">
+          {{ t('workflow.settings.restartWorkflow.for') }}
+        </p>
+        <div class="flex items-center bg-input transition-colors rounded-lg">
+          <input
+            v-model.number="settings.restartTimes"
+            type="number"
+            class="py-2 px-4 w-32 rounded-lg bg-transparent"
+          />
+          <span class="px-2">
+            {{ t('workflow.settings.restartWorkflow.times') }}
+          </span>
+        </div>
+      </label>
     </div>
     <div>
       <p class="mb-1 capitalize">
@@ -35,7 +40,7 @@
         class="w-full max-w-sm"
       />
     </div>
-    <div v-if="false" class="flex mt-6">
+    <div class="flex mt-6">
       <ui-switch v-model="settings.debugMode" class="mr-4" />
       <p class="capitalize">{{ t('workflow.settings.debugMode') }}</p>
     </div>
@@ -67,15 +72,21 @@ const { t } = useI18n();
 const onError = [
   {
     id: 'keep-running',
-    name: t('workflow.settings.onError.items.keepRunning'),
+    name: 'keepRunning',
   },
   {
     id: 'stop-workflow',
-    name: t('workflow.settings.onError.items.stopWorkflow'),
+    name: 'stopWorkflow',
+  },
+  {
+    id: 'restart-workflow',
+    name: 'restartWorkflow',
   },
 ];
 
-const settings = reactive({});
+const settings = reactive({
+  restartTimes: 3,
+});
 
 watch(
   settings,

+ 2 - 2
src/components/newtab/workflow/edit/EditInsertData.vue

@@ -39,8 +39,8 @@
           >
             <option
               v-for="column in workflow.data.value.table"
-              :key="column.name"
-              :value="column.name"
+              :key="column.id"
+              :value="column.id"
             >
               {{ column.name }}
             </option>

+ 16 - 0
src/components/newtab/workflow/edit/EditNewTab.vue

@@ -35,6 +35,22 @@
     >
       {{ t('workflow.blocks.new-tab.tabToGroup') }}
     </ui-checkbox>
+    <ui-checkbox
+      v-if="false"
+      :model-value="data.customUserAgent"
+      block
+      class="mt-2"
+      @change="updateData({ customUserAgent: $event })"
+    >
+      {{ t('workflow.blocks.new-tab.customUserAgent') }}
+    </ui-checkbox>
+    <ui-input
+      v-if="data.customUserAgent"
+      :model-value="data.userAgent"
+      placeholder="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+      class="mt-1 w-full"
+      @change="updateData({ userAgent: $event })"
+    />
   </div>
 </template>
 <script setup>

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

@@ -11,7 +11,7 @@
       </option>
     </ui-select>
     <button
-      class="mb-2 block flex items-center w-full text-left mt-2 focus:ring-0"
+      class="mb-2 block flex items-center w-full text-left mt-1 focus:ring-0"
       @click="showOptions = !showOptions"
     >
       <v-remixicon
@@ -49,6 +49,7 @@
         <component
           :is="eventComponents[data.eventType]"
           v-if="eventComponents[data.eventType]"
+          :key="data.eventName"
           :params="params"
           @update="updateParams({ ...params, ...$event })"
         />

+ 23 - 2
src/components/newtab/workflow/edit/TriggerEventKeyboard.vue

@@ -8,9 +8,20 @@
       {{ item }}
     </ui-checkbox>
   </div>
-  <ui-input v-model="defaultParams.code" class="w-full mt-2" label="code" />
+  <ui-input
+    v-model="defaultParams.key"
+    class="w-full mt-2"
+    label="key"
+    placeholder="a"
+    @change="findKeyDefintion"
+  />
   <div class="flex items-center mt-1 space-x-2">
-    <ui-input v-model="defaultParams.key" class="flex-1" label="key" />
+    <ui-input
+      v-model="defaultParams.code"
+      class="flex-1"
+      label="code"
+      placeholder="KeyA"
+    />
     <ui-input
       v-model.number="defaultParams.keyCode"
       type="number"
@@ -25,6 +36,7 @@
 <script setup>
 import { shallowReactive, watch, onMounted } from 'vue';
 import { objectHasKey } from '@/utils/helper';
+import { keyDefinitions } from '@/utils/us-keyboard-layout';
 
 const props = defineProps({
   params: {
@@ -45,6 +57,15 @@ const defaultParams = shallowReactive({
   repeat: false,
 });
 
+function findKeyDefintion(value) {
+  const keyDefinition = keyDefinitions[value];
+
+  if (!keyDefinition) return;
+
+  defaultParams.code = keyDefinitions[value].code;
+  defaultParams.keyCode = keyDefinitions[value].keyCode;
+}
+
 watch(
   defaultParams,
   (value) => {

+ 29 - 3
src/content/blocks-handler/handler-event-click.js

@@ -1,9 +1,37 @@
 import handleSelector from '../handle-selector';
+import { sendMessage } from '@/utils/message';
 
 function eventClick(block) {
   return new Promise((resolve, reject) => {
     handleSelector(block, {
-      onSelected(element) {
+      async onSelected(element) {
+        if (block.debugMode) {
+          const { width, height, x, y } = element.getBoundingClientRect();
+          const payload = {
+            tabId: block.activeTabId,
+            method: 'Input.dispatchMouseEvent',
+            params: {
+              x: x + width / 2,
+              y: y + height / 2,
+              button: 'left',
+            },
+          };
+          const executeCommand = (type) => {
+            payload.params.type = type;
+
+            if (type === 'mousePressed') {
+              payload.params.clickCount = 1;
+            }
+
+            return sendMessage('debugger:send-command', payload, 'background');
+          };
+
+          await executeCommand('mousePressed');
+          await executeCommand('mouseReleased');
+
+          return;
+        }
+
         if (element.click) {
           element.click();
         } else {
@@ -17,8 +45,6 @@ function eventClick(block) {
         resolve('');
       },
     });
-
-    resolve('');
   });
 }
 

+ 33 - 0
src/content/blocks-handler/handler-hover-element.js

@@ -0,0 +1,33 @@
+import handleSelector from '../handle-selector';
+import { sendMessage } from '@/utils/message';
+
+function eventClick(block) {
+  return new Promise((resolve, reject) => {
+    handleSelector(block, {
+      async onSelected(element) {
+        const { width, height, x, y } = element.getBoundingClientRect();
+        const payload = {
+          tabId: block.activeTabId,
+          method: 'Input.dispatchMouseEvent',
+          params: {
+            type: 'mousePressed',
+            x: x + width / 2,
+            y: y + height / 2,
+            button: 'left',
+            clickCount: 1,
+          },
+        };
+
+        await sendMessage('debugger:send-command', payload, 'background');
+      },
+      onError(error) {
+        reject(error);
+      },
+      onSuccess() {
+        resolve('');
+      },
+    });
+  });
+}
+
+export default eventClick;

+ 105 - 14
src/content/blocks-handler/handler-trigger-event.js

@@ -1,22 +1,113 @@
 import handleSelector from '../handle-selector';
+import { sendMessage } from '@/utils/message';
 import simulateEvent from '@/utils/simulate-event';
+import simulateMouseEvent from '@/utils/simulate-event/mouse-event';
 
-function triggerEvent(block) {
-  return new Promise((resolve, reject) => {
-    const { data } = block;
-
-    handleSelector(block, {
-      onSelected(element) {
-        simulateEvent(element, data.eventName, data.eventParams);
-      },
-      onSuccess() {
-        resolve(data.eventName);
-      },
-      onError(error) {
-        reject(error);
-      },
+const modifiers = {
+  altKey: 1,
+  ctrlKey: 2,
+  metKey: 3,
+  shiftKey: 4,
+};
+const eventHandlers = {
+  'mouse-event': async ({ params, sendCommand, name }) => {
+    const mouseButtons = {
+      0: { id: 1, name: 'left' },
+      1: { id: 4, name: 'middle' },
+      2: { id: 2, name: 'right' },
+    };
+    const commandParams = {
+      button: mouseButtons[params.button]?.name || 'left',
+    };
+
+    if (params.clientX) commandParams.x = params.clientX;
+    if (params.clientY) commandParams.y = params.clientY;
+
+    Object.keys(modifiers).forEach((key) => {
+      if (commandParams.modifiers) return;
+      if (params[key]) commandParams.modifiers = modifiers[key];
+    });
+
+    const mouseEvents = simulateMouseEvent({ sendCommand, commandParams });
+    const eventHandler = {
+      mouseover: 'mouseenter',
+      mouseout: 'mouseleave',
+    };
+    const eventName = eventHandler[name] || name;
+
+    await mouseEvents[eventName]();
+  },
+  'keyboard-event': async ({ name, params, sendCommand }) => {
+    const commandParams = {
+      key: params.key ?? '',
+      code: params.code ?? '',
+      autoRepeat: params.repeat,
+      windowsVirtualKeyCode: params.keyCode ?? 0,
+      type: name === 'keyup' ? 'keyUp' : 'keyDown',
+    };
+
+    Object.keys(modifiers).forEach((key) => {
+      if (commandParams.modifiers) return;
+      if (params[key]) commandParams.modifiers = modifiers[key];
     });
 
+    await sendCommand('Input.dispatchKeyEvent', commandParams);
+  },
+};
+
+function triggerEvent({ data, id, frameSelector, debugMode, activeTabId }) {
+  return new Promise((resolve, reject) => {
+    handleSelector(
+      { data, id, frameSelector },
+      {
+        async onSelected(element) {
+          const eventHandler = eventHandlers[data.eventType];
+
+          if (debugMode && eventHandler) {
+            const { x, y, width, height } = element.getBoundingClientRect();
+            const elCoordinate = {
+              x: x + width / 2,
+              y: y + height / 2,
+            };
+            const sendCommand = (method, params = {}) => {
+              const payload = {
+                method,
+                params: {
+                  x: elCoordinate.x,
+                  y: elCoordinate.y,
+                  ...params,
+                },
+                tabId: activeTabId,
+              };
+
+              return sendMessage(
+                'debugger:send-command',
+                payload,
+                'background'
+              );
+            };
+
+            await eventHandler({
+              element,
+              sendCommand,
+              name: data.eventName,
+              params: data.eventParams,
+            });
+
+            return;
+          }
+
+          simulateEvent(element, data.eventName, data.eventParams);
+        },
+        onSuccess() {
+          resolve(data.eventName);
+        },
+        onError(error) {
+          reject(error);
+        },
+      }
+    );
+
     resolve(data.eventName);
   });
 }

+ 0 - 0
src/content/element-selector/AppHeader.vue


+ 2 - 0
src/content/element-selector/comps-ui.js

@@ -4,6 +4,7 @@ import UiTabs from '@/components/ui/UiTabs.vue';
 import UiInput from '@/components/ui/UiInput.vue';
 import UiButton from '@/components/ui/UiButton.vue';
 import UiSelect from '@/components/ui/UiSelect.vue';
+import UiExpand from '@/components/ui/UiExpand.vue';
 import UiTextarea from '@/components/ui/UiTextarea.vue';
 import UiCheckbox from '@/components/ui/UiCheckbox.vue';
 import UiTabPanel from '@/components/ui/UiTabPanel.vue';
@@ -16,6 +17,7 @@ export default function (app) {
   app.component('UiInput', UiInput);
   app.component('UiButton', UiButton);
   app.component('UiSelect', UiSelect);
+  app.component('UiExpand', UiExpand);
   app.component('UiTextarea', UiTextarea);
   app.component('UiCheckbox', UiCheckbox);
   app.component('UiTabPanel', UiTabPanel);

+ 32 - 17
src/content/element-selector/index.js

@@ -1,3 +1,6 @@
+import browser from 'webextension-polyfill';
+import initElementSelector from './main';
+
 async function getStyles() {
   try {
     const response = await fetch(chrome.runtime.getURL('/elementSelector.css'));
@@ -24,17 +27,35 @@ async function getStyles() {
   }
 }
 
-export default async function () {
-  try {
-    const rootElementExist = document.querySelector(
-      '#app-container.automa-element-selector'
-    );
+function elementSelectorInstance() {
+  const rootElementExist = document.querySelector(
+    '#app-container.automa-element-selector'
+  );
+
+  if (rootElementExist) {
+    rootElementExist.style.display = 'block';
+
+    return true;
+  }
+
+  return false;
+}
+
+(async function () {
+  browser.runtime.onMessage.addListener((data) => {
+    return new Promise((resolve) => {
+      if (data.type === 'automa-element-selector') {
+        elementSelectorInstance();
+
+        resolve(true);
+      }
+    });
+  });
 
-    if (rootElementExist) {
-      rootElementExist.style.display = 'block';
+  try {
+    const isAppExists = elementSelectorInstance();
 
-      return;
-    }
+    if (isAppExists) return;
 
     const rootElement = document.createElement('div');
     rootElement.setAttribute('id', 'app-container');
@@ -45,22 +66,16 @@ export default async function () {
     automaStyle.classList.add('automa-element-selector');
     automaStyle.innerHTML = `.automa-element-selector { pointer-events: none } \n [automa-isDragging] { user-select: none }`;
 
-    const scriptEl = document.createElement('script');
-    scriptEl.setAttribute('type', 'module');
-    scriptEl.setAttribute(
-      'src',
-      chrome.runtime.getURL('/elementSelector.bundle.js')
-    );
+    initElementSelector(rootElement);
 
     const appStyle = document.createElement('style');
     appStyle.innerHTML = await getStyles();
 
     rootElement.shadowRoot.appendChild(appStyle);
-    rootElement.shadowRoot.appendChild(scriptEl);
 
     document.documentElement.appendChild(rootElement);
     document.documentElement.appendChild(automaStyle);
   } catch (error) {
     console.error(error);
   }
-}
+})();

+ 11 - 10
src/content/element-selector/main.js

@@ -6,15 +6,16 @@ import icons from './icons';
 import vueI18n from './vue-i18n';
 import '@/assets/css/tailwind.css';
 
-const rootElement = document.querySelector('div.automa-element-selector');
-const appRoot = document.createElement('div');
-appRoot.setAttribute('id', 'app');
+export default function (rootElement) {
+  const appRoot = document.createElement('div');
+  appRoot.setAttribute('id', 'app');
 
-rootElement.shadowRoot.appendChild(appRoot);
+  rootElement.shadowRoot.appendChild(appRoot);
 
-createApp(App)
-  .provide('rootElement', rootElement)
-  .use(vueI18n)
-  .use(vRemixicon, icons)
-  .use(compsUi)
-  .mount(appRoot);
+  createApp(App)
+    .provide('rootElement', rootElement)
+    .use(vueI18n)
+    .use(vRemixicon, icons)
+    .use(compsUi)
+    .mount(appRoot);
+}

+ 7 - 5
src/content/handle-selector.js

@@ -91,13 +91,15 @@ export default async function (
     }
 
     if (data.multiple && selectorType === 'cssSelector') {
-      element.forEach((el) => {
-        markElement(el, { id, data });
-        onSelected(el);
-      });
+      await Promise.allSettled(
+        element.map((el) => {
+          markElement(el, { id, data });
+          return onSelected(el);
+        })
+      );
     } else if (element) {
       markElement(element, { id, data });
-      onSelected(element);
+      await onSelected(element);
     }
 
     if (onSuccess) onSuccess();

+ 0 - 5
src/content/index.js

@@ -1,7 +1,6 @@
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { toCamelCase } from '@/utils/helper';
-import elementSelector from './element-selector';
 import executedBlock from './executed-block';
 import blocksHandler from './blocks-handler';
 
@@ -40,10 +39,6 @@ import blocksHandler from './blocks-handler';
         case 'content-script-exists':
           resolve(true);
           break;
-        case 'select-element':
-          elementSelector();
-          resolve(true);
-          break;
         case 'give-me-the-frame-id':
           browser.runtime.sendMessage({
             type: 'this-is-the-frame-id',

File diff suppressed because it is too large
+ 0 - 0
src/lib/v-remixicon.js


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

@@ -52,6 +52,10 @@
         "noPermission": "Don't have permission to access the clipboard",
         "grantPermission": "Grant permission"
       },
+      "hover-element": {
+        "name": "Hover element",
+        "description": "Hover over an element"
+      },
       "upload-file": {
         "name": "Upload file",
         "description": "Upload file into <input type=\"file\"> element",
@@ -270,6 +274,7 @@
         "name": "New tab",
         "description": "",
         "url": "New Tab URL",
+        "customUserAgent": "Use custom User-Agent",
         "activeTab": "Set as active tab",
         "tabToGroup": "Add tab to group",
         "updatePrevTab": {

+ 11 - 1
src/locales/en/newtab.json

@@ -158,11 +158,16 @@
       "saveLog": "Save workflow log",
       "executedBlockOnWeb": "Show executed block on web page",
       "debugMode": "Debug mode",
+      "restartWorkflow": {
+        "for": "Restart for",
+        "times": "Times"
+      },
       "onError": {
         "title": "On workflow error",
         "items": {
           "keepRunning": "Keep running",
-          "stopWorkflow": "Stop workflow"
+          "stopWorkflow": "Stop workflow",
+          "restartWorkflow": "Restart workflow"
         }
       },
       "timeout": {
@@ -199,6 +204,10 @@
     "selectAll": "Select all",
     "deselectAll": "Deselect all",
     "deleteSelected": "Delete selected logs",
+    "clearLogs": {
+      "title": "Clear logs",
+      "description": "Are you sure to clear all logs?"
+    },
     "types": {
       "stop": "Workflow is stopped",
       "finish": "Finish"
@@ -226,6 +235,7 @@
       "not-iframe": "Element with \"{selector}\" selector is not an Iframe element",
       "iframe-not-found": "Can't find an Iframe element with \"{selector}\" selector.",
       "workflow-infinite-loop": "Can't execute the workflow to prevent an infinite loop",
+      "not-debug-mode": "The workflow must run in debug mode for this block to work properly",
       "no-iframe-id": "Can't find Frame ID for the iframe element with \"{selector}\" selector",
       "no-tab": "Can't connect to a tab, use \"New tab\" or \"Active tab\" block before using the \"{name}\" block."
     },

+ 3 - 1
src/manifest.json

@@ -46,13 +46,15 @@
     }
   ],
   "optional_permissions": [
-    "clipboardRead"
+    "clipboardRead",
+    "debugger"
   ],
   "permissions": [
     "tabs",
     "proxy",
     "alarms",
     "storage",
+    "debugger",
     "webNavigation",
     "unlimitedStorage",
     "<all_urls>"

+ 1 - 0
src/models/workflow.js

@@ -34,6 +34,7 @@ class Workflow extends Model {
         blockDelay: 0,
         saveLog: true,
         debugMode: false,
+        restartTimes: 3,
         onError: 'stop-workflow',
         executedBlockOnWeb: false,
       }),

+ 14 - 0
src/newtab/pages/Logs.vue

@@ -4,6 +4,7 @@
     <logs-filters
       :sorts="sortsBuilder"
       :filters="filtersBuilder"
+      @clear="clearLogs"
       @updateSorts="sortsBuilder[$event.key] = $event.value"
       @updateFilters="filtersBuilder[$event.key] = $event.value"
     />
@@ -184,6 +185,19 @@ function deleteSelectedLogs() {
     },
   });
 }
+function clearLogs() {
+  dialog.confirm({
+    title: t('log.clearLogs.title'),
+    okVariant: 'danger',
+    body: t('log.clearLogs.description'),
+    onConfirm: () => {
+      Log.deleteAll().then(() => {
+        selectedLogs.value = [];
+        store.dispatch('saveToStorage', 'logs');
+      });
+    },
+  });
+}
 function selectAllLogs() {
   if (selectedLogs.value.length >= logs.value.length) {
     selectedLogs.value = [];

+ 5 - 9
src/popup/pages/Home.vue

@@ -18,7 +18,7 @@
         "
         icon
         class="mr-2"
-        @click="selectElement"
+        @click="initElementSelector"
       >
         <v-remixicon name="riFocus3Line" />
       </ui-button>
@@ -124,25 +124,21 @@ function deleteWorkflow({ id, name }) {
 function openDashboard(url) {
   sendMessage('open:dashboard', url, 'background');
 }
-async function selectElement() {
+async function initElementSelector() {
   const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
 
   try {
     await browser.tabs.sendMessage(tab.id, {
-      type: 'content-script-exists',
-    });
-
-    browser.tabs.sendMessage(tab.id, {
-      type: 'select-element',
+      type: 'automa-element-selector',
     });
   } catch (error) {
     if (error.message.includes('Could not establish connection.')) {
       await browser.tabs.executeScript(tab.id, {
         allFrames: true,
-        file: './contentScript.bundle.js',
+        file: './elementSelector.bundle.js',
       });
 
-      selectElement();
+      initElementSelector();
     }
 
     console.error(error);

+ 1 - 1
src/utils/data-exporter.js

@@ -56,7 +56,7 @@ export default function (data, { name, type, addBOMHeader }, converted) {
 
   const payload = [result];
 
-  if (addBOMHeader) {
+  if (type === 'csv' && addBOMHeader) {
     payload.unshift(new Uint8Array([0xef, 0xbb, 0xbf]));
   }
 

+ 11 - 5
src/utils/handle-form-element.js

@@ -1,22 +1,28 @@
+import { keyDefinitions } from '@/utils/us-keyboard-layout';
 import simulateEvent from './simulate-event';
 
 function formEvent(element, data) {
   if (data.type === 'text-field') {
-    const code = /\s/.test(data.value)
-      ? 'Space'
-      : `key${data.value.toUpperCase()}`;
+    const currentKey = /\s/.test(data.value) ? 'Space' : data.value;
+    const { key, keyCode, code } = keyDefinitions[currentKey] || {
+      key: currentKey,
+      keyCode: 0,
+      code: `Key${currentKey}`,
+    };
 
     simulateEvent(element, 'keydown', {
+      key,
       code,
+      keyCode,
       bubbles: true,
       cancelable: true,
-      key: data.value,
     });
     simulateEvent(element, 'keyup', {
+      key,
       code,
+      keyCode,
       bubbles: true,
       cancelable: true,
-      key: data.value,
     });
   }
 

+ 4 - 0
src/utils/helper.js

@@ -1,3 +1,7 @@
+export function sleep(timeout = 500) {
+  return new Promise((resolve) => setTimeout(resolve, timeout));
+}
+
 export function findTriggerBlock(drawflow = {}) {
   if (!drawflow) return null;
 

+ 25 - 1
src/utils/shared.js

@@ -75,9 +75,11 @@ export const tasks = {
     data: {
       description: '',
       url: '',
+      userAgent: '',
       active: true,
       inGroup: false,
       updatePrevTab: false,
+      customUserAgent: false,
     },
   },
   'switch-tab': {
@@ -495,7 +497,7 @@ export const tasks = {
       findBy: 'cssSelector',
       waitForSelector: false,
       waitSelectorTimeout: 5000,
-      selector: '',
+      selector: 'html',
       markEl: false,
       multiple: false,
       eventName: '',
@@ -721,6 +723,28 @@ export const tasks = {
       filePaths: [],
     },
   },
+  'hover-element': {
+    name: 'Hover element',
+    description: 'Hover over an element',
+    icon: 'mdiCursorDefaultClickOutline',
+    component: 'BlockBasic',
+    editComponent: 'EditInteractionBase',
+    category: 'interaction',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    refDataKeys: ['selector'],
+    data: {
+      description: '',
+      findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
+      selector: '',
+      markEl: false,
+      multiple: false,
+    },
+  },
 };
 
 export const categories = {

+ 1 - 1
src/utils/simulate-event.js → src/utils/simulate-event/index.js

@@ -1,4 +1,4 @@
-import { eventList } from './shared';
+import { eventList } from '../shared';
 
 export function getEventObj(name, params) {
   const eventType = eventList.find(({ id }) => id === name)?.type ?? '';

+ 44 - 0
src/utils/simulate-event/mouse-event.js

@@ -0,0 +1,44 @@
+export default function ({ sendCommand, commandParams }) {
+  async function mousedown() {
+    commandParams.type = 'mousePressed';
+    await sendCommand('Input.dispatchMouseEvent', commandParams);
+  }
+  async function mouseup() {
+    commandParams.type = 'mouseReleased';
+    await sendCommand('Input.dispatchMouseEvent', commandParams);
+  }
+  async function click() {
+    if (!commandParams.clickCount) commandParams.clickCount = 1;
+
+    await mousedown();
+    await mouseup();
+  }
+  async function dblclick() {
+    commandParams.clickCount = 2;
+    await click();
+  }
+  async function mousemove() {
+    commandParams.type = 'mouseMoved';
+    await sendCommand('Input.dispatchMouseEvent', commandParams);
+  }
+  async function mouseenter() {
+    await mousemove();
+  }
+  async function mouseleave() {
+    await mousemove();
+
+    commandParams.x = -100;
+    commandParams.y = -100;
+    await mousemove();
+  }
+
+  return {
+    mousedown,
+    mouseup,
+    click,
+    dblclick,
+    mousemove,
+    mouseenter,
+    mouseleave,
+  };
+}

+ 409 - 0
src/utils/us-keyboard-layout.js

@@ -0,0 +1,409 @@
+/*
+  Forked from:
+  https://github.com/puppeteer/puppeteer/blob/e11fe713407c2430526ae616d0adfb5dd278b5de/src/common/USKeyboardLayout.ts
+*/
+
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @internal
+ */
+export const keyDefinitions = {
+  0: { keyCode: 48, key: '0', code: 'Digit0' },
+  1: { keyCode: 49, key: '1', code: 'Digit1' },
+  2: { keyCode: 50, key: '2', code: 'Digit2' },
+  3: { keyCode: 51, key: '3', code: 'Digit3' },
+  4: { keyCode: 52, key: '4', code: 'Digit4' },
+  5: { keyCode: 53, key: '5', code: 'Digit5' },
+  6: { keyCode: 54, key: '6', code: 'Digit6' },
+  7: { keyCode: 55, key: '7', code: 'Digit7' },
+  8: { keyCode: 56, key: '8', code: 'Digit8' },
+  9: { keyCode: 57, key: '9', code: 'Digit9' },
+  Power: { key: 'Power', code: 'Power' },
+  Eject: { key: 'Eject', code: 'Eject' },
+  Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' },
+  Help: { keyCode: 6, code: 'Help', key: 'Help' },
+  Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },
+  Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },
+  Numpad5: {
+    keyCode: 12,
+    shiftKeyCode: 101,
+    key: 'Clear',
+    code: 'Numpad5',
+    shiftKey: '5',
+    location: 3,
+  },
+  NumpadEnter: {
+    keyCode: 13,
+    code: 'NumpadEnter',
+    key: 'Enter',
+    text: '\r',
+    location: 3,
+  },
+  Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
+  '\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
+  '\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
+  ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 },
+  ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 },
+  ControlLeft: {
+    keyCode: 17,
+    code: 'ControlLeft',
+    key: 'Control',
+    location: 1,
+  },
+  ControlRight: {
+    keyCode: 17,
+    code: 'ControlRight',
+    key: 'Control',
+    location: 2,
+  },
+  AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 },
+  AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 },
+  Pause: { keyCode: 19, code: 'Pause', key: 'Pause' },
+  CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
+  Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },
+  Convert: { keyCode: 28, code: 'Convert', key: 'Convert' },
+  NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' },
+  Space: { keyCode: 32, code: 'Space', key: ' ' },
+  Numpad9: {
+    keyCode: 33,
+    shiftKeyCode: 105,
+    key: 'PageUp',
+    code: 'Numpad9',
+    shiftKey: '9',
+    location: 3,
+  },
+  PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' },
+  Numpad3: {
+    keyCode: 34,
+    shiftKeyCode: 99,
+    key: 'PageDown',
+    code: 'Numpad3',
+    shiftKey: '3',
+    location: 3,
+  },
+  PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' },
+  End: { keyCode: 35, code: 'End', key: 'End' },
+  Numpad1: {
+    keyCode: 35,
+    shiftKeyCode: 97,
+    key: 'End',
+    code: 'Numpad1',
+    shiftKey: '1',
+    location: 3,
+  },
+  Home: { keyCode: 36, code: 'Home', key: 'Home' },
+  Numpad7: {
+    keyCode: 36,
+    shiftKeyCode: 103,
+    key: 'Home',
+    code: 'Numpad7',
+    shiftKey: '7',
+    location: 3,
+  },
+  ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
+  Numpad4: {
+    keyCode: 37,
+    shiftKeyCode: 100,
+    key: 'ArrowLeft',
+    code: 'Numpad4',
+    shiftKey: '4',
+    location: 3,
+  },
+  Numpad8: {
+    keyCode: 38,
+    shiftKeyCode: 104,
+    key: 'ArrowUp',
+    code: 'Numpad8',
+    shiftKey: '8',
+    location: 3,
+  },
+  ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
+  ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
+  Numpad6: {
+    keyCode: 39,
+    shiftKeyCode: 102,
+    key: 'ArrowRight',
+    code: 'Numpad6',
+    shiftKey: '6',
+    location: 3,
+  },
+  Numpad2: {
+    keyCode: 40,
+    shiftKeyCode: 98,
+    key: 'ArrowDown',
+    code: 'Numpad2',
+    shiftKey: '2',
+    location: 3,
+  },
+  ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
+  Select: { keyCode: 41, code: 'Select', key: 'Select' },
+  Open: { keyCode: 43, code: 'Open', key: 'Execute' },
+  PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
+  Insert: { keyCode: 45, code: 'Insert', key: 'Insert' },
+  Numpad0: {
+    keyCode: 45,
+    shiftKeyCode: 96,
+    key: 'Insert',
+    code: 'Numpad0',
+    shiftKey: '0',
+    location: 3,
+  },
+  Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },
+  NumpadDecimal: {
+    keyCode: 46,
+    shiftKeyCode: 110,
+    code: 'NumpadDecimal',
+    key: '\u0000',
+    shiftKey: '.',
+    location: 3,
+  },
+  Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' },
+  Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' },
+  Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' },
+  Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' },
+  Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' },
+  Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' },
+  Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' },
+  Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' },
+  Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' },
+  Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' },
+  KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' },
+  KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' },
+  KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' },
+  KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' },
+  KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' },
+  KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' },
+  KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' },
+  KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' },
+  KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' },
+  KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' },
+  KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' },
+  KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' },
+  KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' },
+  KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' },
+  KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' },
+  KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' },
+  KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' },
+  KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' },
+  KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' },
+  KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' },
+  KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' },
+  KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' },
+  KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' },
+  KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' },
+  KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' },
+  KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' },
+  MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 },
+  MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 },
+  ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
+  NumpadMultiply: {
+    keyCode: 106,
+    code: 'NumpadMultiply',
+    key: '*',
+    location: 3,
+  },
+  NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 },
+  NumpadSubtract: {
+    keyCode: 109,
+    code: 'NumpadSubtract',
+    key: '-',
+    location: 3,
+  },
+  NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 },
+  F1: { keyCode: 112, code: 'F1', key: 'F1' },
+  F2: { keyCode: 113, code: 'F2', key: 'F2' },
+  F3: { keyCode: 114, code: 'F3', key: 'F3' },
+  F4: { keyCode: 115, code: 'F4', key: 'F4' },
+  F5: { keyCode: 116, code: 'F5', key: 'F5' },
+  F6: { keyCode: 117, code: 'F6', key: 'F6' },
+  F7: { keyCode: 118, code: 'F7', key: 'F7' },
+  F8: { keyCode: 119, code: 'F8', key: 'F8' },
+  F9: { keyCode: 120, code: 'F9', key: 'F9' },
+  F10: { keyCode: 121, code: 'F10', key: 'F10' },
+  F11: { keyCode: 122, code: 'F11', key: 'F11' },
+  F12: { keyCode: 123, code: 'F12', key: 'F12' },
+  F13: { keyCode: 124, code: 'F13', key: 'F13' },
+  F14: { keyCode: 125, code: 'F14', key: 'F14' },
+  F15: { keyCode: 126, code: 'F15', key: 'F15' },
+  F16: { keyCode: 127, code: 'F16', key: 'F16' },
+  F17: { keyCode: 128, code: 'F17', key: 'F17' },
+  F18: { keyCode: 129, code: 'F18', key: 'F18' },
+  F19: { keyCode: 130, code: 'F19', key: 'F19' },
+  F20: { keyCode: 131, code: 'F20', key: 'F20' },
+  F21: { keyCode: 132, code: 'F21', key: 'F21' },
+  F22: { keyCode: 133, code: 'F22', key: 'F22' },
+  F23: { keyCode: 134, code: 'F23', key: 'F23' },
+  F24: { keyCode: 135, code: 'F24', key: 'F24' },
+  NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' },
+  ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
+  AudioVolumeMute: {
+    keyCode: 173,
+    code: 'AudioVolumeMute',
+    key: 'AudioVolumeMute',
+  },
+  AudioVolumeDown: {
+    keyCode: 174,
+    code: 'AudioVolumeDown',
+    key: 'AudioVolumeDown',
+  },
+  AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' },
+  MediaTrackNext: {
+    keyCode: 176,
+    code: 'MediaTrackNext',
+    key: 'MediaTrackNext',
+  },
+  MediaTrackPrevious: {
+    keyCode: 177,
+    code: 'MediaTrackPrevious',
+    key: 'MediaTrackPrevious',
+  },
+  MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' },
+  MediaPlayPause: {
+    keyCode: 179,
+    code: 'MediaPlayPause',
+    key: 'MediaPlayPause',
+  },
+  Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' },
+  Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' },
+  NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 },
+  Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' },
+  Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' },
+  Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' },
+  Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' },
+  Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' },
+  BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' },
+  Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' },
+  BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' },
+  Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: "'" },
+  AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' },
+  Props: { keyCode: 247, code: 'Props', key: 'CrSel' },
+  Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' },
+  Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 },
+  Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 },
+  Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 },
+  Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 },
+  Accept: { keyCode: 30, key: 'Accept' },
+  ModeChange: { keyCode: 31, key: 'ModeChange' },
+  ' ': { keyCode: 32, key: ' ', code: 'Space' },
+  Print: { keyCode: 42, key: 'Print' },
+  Execute: { keyCode: 43, key: 'Execute', code: 'Open' },
+  '\u0000': { keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3 },
+  a: { keyCode: 65, key: 'a', code: 'KeyA' },
+  b: { keyCode: 66, key: 'b', code: 'KeyB' },
+  c: { keyCode: 67, key: 'c', code: 'KeyC' },
+  d: { keyCode: 68, key: 'd', code: 'KeyD' },
+  e: { keyCode: 69, key: 'e', code: 'KeyE' },
+  f: { keyCode: 70, key: 'f', code: 'KeyF' },
+  g: { keyCode: 71, key: 'g', code: 'KeyG' },
+  h: { keyCode: 72, key: 'h', code: 'KeyH' },
+  i: { keyCode: 73, key: 'i', code: 'KeyI' },
+  j: { keyCode: 74, key: 'j', code: 'KeyJ' },
+  k: { keyCode: 75, key: 'k', code: 'KeyK' },
+  l: { keyCode: 76, key: 'l', code: 'KeyL' },
+  m: { keyCode: 77, key: 'm', code: 'KeyM' },
+  n: { keyCode: 78, key: 'n', code: 'KeyN' },
+  o: { keyCode: 79, key: 'o', code: 'KeyO' },
+  p: { keyCode: 80, key: 'p', code: 'KeyP' },
+  q: { keyCode: 81, key: 'q', code: 'KeyQ' },
+  r: { keyCode: 82, key: 'r', code: 'KeyR' },
+  s: { keyCode: 83, key: 's', code: 'KeyS' },
+  t: { keyCode: 84, key: 't', code: 'KeyT' },
+  u: { keyCode: 85, key: 'u', code: 'KeyU' },
+  v: { keyCode: 86, key: 'v', code: 'KeyV' },
+  w: { keyCode: 87, key: 'w', code: 'KeyW' },
+  x: { keyCode: 88, key: 'x', code: 'KeyX' },
+  y: { keyCode: 89, key: 'y', code: 'KeyY' },
+  z: { keyCode: 90, key: 'z', code: 'KeyZ' },
+  Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 },
+  '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 },
+  '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 },
+  '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 },
+  '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 },
+  ';': { keyCode: 186, key: ';', code: 'Semicolon' },
+  '=': { keyCode: 187, key: '=', code: 'Equal' },
+  ',': { keyCode: 188, key: ',', code: 'Comma' },
+  '.': { keyCode: 190, key: '.', code: 'Period' },
+  '`': { keyCode: 192, key: '`', code: 'Backquote' },
+  '[': { keyCode: 219, key: '[', code: 'BracketLeft' },
+  '\\': { keyCode: 220, key: '\\', code: 'Backslash' },
+  ']': { keyCode: 221, key: ']', code: 'BracketRight' },
+  "'": { keyCode: 222, key: "'", code: 'Quote' },
+  Attn: { keyCode: 246, key: 'Attn' },
+  CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' },
+  ExSel: { keyCode: 248, key: 'ExSel' },
+  EraseEof: { keyCode: 249, key: 'EraseEof' },
+  Play: { keyCode: 250, key: 'Play' },
+  ZoomOut: { keyCode: 251, key: 'ZoomOut' },
+  ')': { keyCode: 48, key: ')', code: 'Digit0' },
+  '!': { keyCode: 49, key: '!', code: 'Digit1' },
+  '@': { keyCode: 50, key: '@', code: 'Digit2' },
+  '#': { keyCode: 51, key: '#', code: 'Digit3' },
+  $: { keyCode: 52, key: '$', code: 'Digit4' },
+  '%': { keyCode: 53, key: '%', code: 'Digit5' },
+  '^': { keyCode: 54, key: '^', code: 'Digit6' },
+  '&': { keyCode: 55, key: '&', code: 'Digit7' },
+  '(': { keyCode: 57, key: '(', code: 'Digit9' },
+  A: { keyCode: 65, key: 'A', code: 'KeyA' },
+  B: { keyCode: 66, key: 'B', code: 'KeyB' },
+  C: { keyCode: 67, key: 'C', code: 'KeyC' },
+  D: { keyCode: 68, key: 'D', code: 'KeyD' },
+  E: { keyCode: 69, key: 'E', code: 'KeyE' },
+  F: { keyCode: 70, key: 'F', code: 'KeyF' },
+  G: { keyCode: 71, key: 'G', code: 'KeyG' },
+  H: { keyCode: 72, key: 'H', code: 'KeyH' },
+  I: { keyCode: 73, key: 'I', code: 'KeyI' },
+  J: { keyCode: 74, key: 'J', code: 'KeyJ' },
+  K: { keyCode: 75, key: 'K', code: 'KeyK' },
+  L: { keyCode: 76, key: 'L', code: 'KeyL' },
+  M: { keyCode: 77, key: 'M', code: 'KeyM' },
+  N: { keyCode: 78, key: 'N', code: 'KeyN' },
+  O: { keyCode: 79, key: 'O', code: 'KeyO' },
+  P: { keyCode: 80, key: 'P', code: 'KeyP' },
+  Q: { keyCode: 81, key: 'Q', code: 'KeyQ' },
+  R: { keyCode: 82, key: 'R', code: 'KeyR' },
+  S: { keyCode: 83, key: 'S', code: 'KeyS' },
+  T: { keyCode: 84, key: 'T', code: 'KeyT' },
+  U: { keyCode: 85, key: 'U', code: 'KeyU' },
+  V: { keyCode: 86, key: 'V', code: 'KeyV' },
+  W: { keyCode: 87, key: 'W', code: 'KeyW' },
+  X: { keyCode: 88, key: 'X', code: 'KeyX' },
+  Y: { keyCode: 89, key: 'Y', code: 'KeyY' },
+  Z: { keyCode: 90, key: 'Z', code: 'KeyZ' },
+  ':': { keyCode: 186, key: ':', code: 'Semicolon' },
+  '<': { keyCode: 188, key: '<', code: 'Comma' },
+  _: { keyCode: 189, key: '_', code: 'Minus' },
+  '>': { keyCode: 190, key: '>', code: 'Period' },
+  '?': { keyCode: 191, key: '?', code: 'Slash' },
+  '~': { keyCode: 192, key: '~', code: 'Backquote' },
+  '{': { keyCode: 219, key: '{', code: 'BracketLeft' },
+  '|': { keyCode: 220, key: '|', code: 'Backslash' },
+  '}': { keyCode: 221, key: '}', code: 'BracketRight' },
+  '"': { keyCode: 222, key: '"', code: 'Quote' },
+  SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 },
+  SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 },
+  Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 },
+  Call: { key: 'Call', code: 'Call', location: 4 },
+  EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 },
+  VolumeDown: {
+    keyCode: 182,
+    key: 'VolumeDown',
+    code: 'VolumeDown',
+    location: 4,
+  },
+  VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 },
+};

+ 1 - 1
webpack.config.js

@@ -69,7 +69,7 @@ const options = {
       'src',
       'content',
       'element-selector',
-      'main.js'
+      'index.js'
     ),
   },
   chromeExtensionBoilerplate: {

Some files were not shown because too many files changed in this diff