Browse Source

feat: improve the element selector

Ahmad Kholid 3 years ago
parent
commit
a62c5c7ff3

+ 2 - 2
src/assets/css/tailwind.css

@@ -2,11 +2,11 @@
 @tailwind components;
 @tailwind utilities;
 
-body {
+:host, body {
   font-family: 'Inter var';
   font-size: 16px;
   font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
-  @apply bg-gray-50 dark:bg-gray-900;
+  @apply bg-gray-50 dark:bg-gray-900 leading-normal;
 }
 table th,
 table td {

+ 6 - 0
src/background/index.js

@@ -50,6 +50,12 @@ function executeCollection(collection) {
 
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
   if (changeInfo.status === 'complete') {
+    if (tab.url.includes('http://localhost')) {
+      browser.tabs.executeScript(tabId, {
+        file: 'contentScript.bundle.js',
+      });
+    }
+
     const { visitWebTriggers } = await browser.storage.local.get(
       'visitWebTriggers'
     );

+ 5 - 1
src/components/newtab/workflow/edit/EditForms.vue

@@ -1,5 +1,5 @@
 <template>
-  <edit-interaction-base v-bind="{ data }" @change="updateData">
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <ui-select
       :model-value="data.type"
       class="block w-full mt-4 mb-3"
@@ -53,6 +53,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  hideBase: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 37 - 31
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -1,37 +1,39 @@
 <template>
   <div>
     <slot name="prepend" />
-    <ui-textarea
-      :model-value="data.description"
-      :placeholder="t('common.description')"
-      autoresize
-      class="w-full mb-2"
-      @change="updateData({ description: $event })"
-    />
-    <ui-input
-      v-if="!hideSelector"
-      :model-value="data.selector"
-      :placeholder="t('workflow.blocks.base.selector')"
-      class="mb-1 w-full"
-      @change="updateData({ selector: $event })"
-    />
-    <template v-if="!hideSelector">
-      <ui-checkbox
-        v-if="!data.disableMultiple && !hideMultiple"
-        :title="t('workflow.blocks.base.multiple.title')"
-        :model-value="data.multiple"
-        class="mr-6"
-        @change="updateData({ multiple: $event })"
-      >
-        {{ t('workflow.blocks.base.multiple.text') }}
-      </ui-checkbox>
-      <ui-checkbox
-        :model-value="data.markEl"
-        :title="t('workflow.blocks.base.markElement.title')"
-        @change="updateData({ markEl: $event })"
-      >
-        {{ t('workflow.blocks.base.markElement.text') }}
-      </ui-checkbox>
+    <template v-if="!hide">
+      <ui-textarea
+        :model-value="data.description"
+        :placeholder="t('common.description')"
+        autoresize
+        class="w-full mb-2"
+        @change="updateData({ description: $event })"
+      />
+      <ui-input
+        v-if="!hideSelector"
+        :model-value="data.selector"
+        :placeholder="t('workflow.blocks.base.selector')"
+        class="mb-1 w-full"
+        @change="updateData({ selector: $event })"
+      />
+      <template v-if="!hideSelector">
+        <ui-checkbox
+          v-if="!data.disableMultiple && !hideMultiple"
+          :title="t('workflow.blocks.base.multiple.title')"
+          :model-value="data.multiple"
+          class="mr-6"
+          @change="updateData({ multiple: $event })"
+        >
+          {{ t('workflow.blocks.base.multiple.text') }}
+        </ui-checkbox>
+        <ui-checkbox
+          :model-value="data.markEl"
+          :title="t('workflow.blocks.base.markElement.title')"
+          @change="updateData({ markEl: $event })"
+        >
+          {{ t('workflow.blocks.base.markElement.text') }}
+        </ui-checkbox>
+      </template>
     </template>
     <slot></slot>
   </div>
@@ -44,6 +46,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  hide: {
+    type: Boolean,
+    default: false,
+  },
   hideSelector: {
     type: Boolean,
     default: false,

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

@@ -1,5 +1,5 @@
 <template>
-  <edit-interaction-base v-bind="{ data }" @change="updateData">
+  <edit-interaction-base v-bind="{ data, 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"
@@ -54,6 +54,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  hideBase: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:data']);
 

+ 18 - 30
src/components/newtab/workflow/edit/EditTriggerEvent.vue

@@ -1,5 +1,5 @@
 <template>
-  <edit-interaction-base v-bind="{ data }" @change="updateData">
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <ui-select
       :model-value="data.eventName"
       :placeholder="t('workflow.blocks.trigger-event.selectEvent')"
@@ -46,8 +46,8 @@
           </ui-checkbox>
         </div>
         <component
-          :is="componentName"
-          v-if="componentName"
+          :is="eventComponents[data.eventType]"
+          v-if="eventComponents[data.eventType]"
           :params="params"
           @update="updateParams({ ...params, ...$event })"
         />
@@ -55,52 +55,42 @@
     </transition-expand>
   </edit-interaction-base>
 </template>
-<script>
-import TriggerEventMouse from './TriggerEventMouse.vue';
-import TriggerEventTouch from './TriggerEventTouch.vue';
-import TriggerEventWheel from './TriggerEventWheel.vue';
-import TriggerEventInput from './TriggerEventInput.vue';
-import TriggerEventKeyboard from './TriggerEventKeyboard.vue';
-
-export default {
-  components: {
-    TriggerEventMouse,
-    TriggerEventWheel,
-    TriggerEventTouch,
-    TriggerEventInput,
-    TriggerEventKeyboard,
-  },
-};
-</script>
 <script setup>
-/* eslint-disable */
 import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { eventList } from '@/utils/shared';
 import { toCamelCase } from '@/utils/helper';
 import EditInteractionBase from './EditInteractionBase.vue';
+import TriggerEventMouse from './TriggerEventMouse.vue';
+import TriggerEventTouch from './TriggerEventTouch.vue';
+import TriggerEventWheel from './TriggerEventWheel.vue';
+import TriggerEventInput from './TriggerEventInput.vue';
+import TriggerEventKeyboard from './TriggerEventKeyboard.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
+  hideBase: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
 
 const eventComponents = {
-  'mouse-event': 'TriggerEventMouse',
+  'mouse-event': TriggerEventMouse,
   'focus-event': '',
-  'event': '',
-  'touch-event': 'TriggerEventTouch',
-  'keyboard-event': 'TriggerEventKeyboard',
-  'wheel-event': 'TriggerEventWheel',
-  'input-event': 'TriggerEventInput',
+  event: '',
+  'touch-event': TriggerEventTouch,
+  'keyboard-event': TriggerEventKeyboard,
+  'wheel-event': TriggerEventWheel,
+  'input-event': TriggerEventInput,
 };
 
-const componentName = ref(eventComponents[props.data.eventType]);
 const params = ref(props.data.eventParams);
 const showOptions = ref(false);
 
@@ -120,8 +110,6 @@ function handleSelectChange(value) {
   const eventType = eventList.find(({ id }) => id === value).type;
   const payload = { eventName: value, eventType };
 
-  componentName.value = eventComponents[eventType];
-
   if (eventType !== props.eventType) {
     const defaultParams = { bubbles: true, cancelable: true };
 

+ 331 - 0
src/content/element-selector/App.vue

@@ -0,0 +1,331 @@
+<template>
+  <div
+    :class="{
+      'select-none': state.isDragging,
+      'bg-black bg-opacity-30': !state.hide,
+    }"
+    class="root fixed h-full w-full pointer-events-none top-0 left-0"
+    style="z-index: 9999999999; font-family: Inter, sans-serif"
+  >
+    <div
+      ref="cardEl"
+      :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
+      class="
+        absolute
+        root-card
+        bg-white
+        shadow-xl
+        z-50
+        p-4
+        pointer-events-auto
+        rounded-lg
+        w-80
+      "
+    >
+      <div
+        class="
+          absolute
+          p-2
+          drag-button
+          shadow-xl
+          bg-white
+          p-1
+          cursor-move
+          rounded-lg
+        "
+        style="top: -15px; left: -15px"
+      >
+        <v-remixicon
+          name="riDragMoveLine"
+          @mousedown="state.isDragging = true"
+        />
+      </div>
+      <div class="flex items-center">
+        <p class="ml-1 text-lg font-semibold">Automa</p>
+        <div class="flex-grow"></div>
+        <ui-button icon class="mr-2" @click="state.hide = !state.hide">
+          <v-remixicon :name="state.hide ? 'riEyeOffLine' : 'riEyeLine'" />
+        </ui-button>
+        <ui-button icon @click="destroy">
+          <v-remixicon name="riCloseLine" />
+        </ui-button>
+      </div>
+      <app-selector
+        :selector="state.elSelector"
+        :selected-count="state.selectedElements.length"
+        @child="selectChildElement"
+        @parent="selectParentElement"
+        @change="updateSelectedElements"
+      />
+      <template v-if="!state.hide && state.selectedElements.length > 0">
+        <ui-tabs v-model="state.activeTab" class="mt-2" fill>
+          <ui-tab value="attributes"> Attributes </ui-tab>
+          <ui-tab value="blocks"> Blocks </ui-tab>
+        </ui-tabs>
+        <ui-tab-panels
+          v-model="state.activeTab"
+          class="overflow-y-auto scroll"
+          style="max-height: calc(100vh - 15rem)"
+        >
+          <ui-tab-panel value="attributes">
+            <app-element-attributes
+              :elements="state.selectedElements"
+              @highlight="
+                state.selectedElements[$event.index].highlight =
+                  $event.highlight
+              "
+            />
+          </ui-tab-panel>
+          <ui-tab-panel value="blocks">
+            <app-blocks
+              :elements="state.selectedElements"
+              @update="updateCardSize"
+            />
+          </ui-tab-panel>
+        </ui-tab-panels>
+      </template>
+    </div>
+    <svg
+      v-if="!state.hide"
+      class="h-full w-full absolute top-0 pointer-events-none left-0 z-10"
+    >
+      <rect
+        v-bind="hoverElementRect"
+        stroke-width="2"
+        stroke="#fbbf24"
+        fill="rgba(251, 191, 36, 0.2)"
+      ></rect>
+      <rect
+        v-for="(item, index) in state.selectedElements"
+        v-bind="item"
+        :key="index"
+        :stroke="item.highlight ? '#2563EB' : '#f87171'"
+        :fill="
+          item.highlight ? 'rgb(37, 99, 235, 0.2)' : 'rgba(248, 113, 113, 0.2)'
+        "
+        stroke-width="2"
+      ></rect>
+    </svg>
+  </div>
+</template>
+<script setup>
+import { reactive, ref, watch, inject, nextTick } from 'vue';
+import { finder } from '@medv/finder';
+import { debounce } from '@/utils/helper';
+import AppBlocks from './AppBlocks.vue';
+import AppSelector from './AppSelector.vue';
+import AppElementAttributes from './AppElementAttributes.vue';
+
+const selectedElement = {
+  path: [],
+  pathIndex: 0,
+};
+let lastScrollPosY = window.scrollY;
+let lastScrollPosX = window.scrollX;
+
+const rootElement = inject('rootElement');
+
+const cardEl = ref('cardEl');
+const state = reactive({
+  hide: false,
+  elSelector: '',
+  isDragging: false,
+  activeTab: 'blocks',
+  selectedElements: [],
+});
+const hoverElementRect = reactive({
+  x: 0,
+  y: 0,
+  height: 0,
+  width: 0,
+});
+const cardRect = reactive({
+  x: 0,
+  y: 0,
+  height: 0,
+  width: 0,
+});
+
+function getElementRect(target) {
+  if (!target) return {};
+
+  const { x, y, height, width } = target.getBoundingClientRect();
+
+  return {
+    width: width + 4,
+    height: height + 4,
+    x: x - 2,
+    y: y - 2,
+  };
+}
+function updateSelectedElements(selector) {
+  state.elSelector = selector;
+
+  try {
+    const elements = document.querySelectorAll(selector);
+
+    state.selectedElements = Array.from(elements).map((element) => {
+      const attributes = Array.from(element.attributes).map(
+        ({ name, value }) => ({ name, value })
+      );
+
+      return {
+        element,
+        attributes,
+        highlight: false,
+        ...getElementRect(element),
+      };
+    });
+  } catch (error) {
+    state.selectedElements = [];
+  }
+}
+function handleMouseMove({ clientX, clientY, target }) {
+  if (state.isDragging) {
+    const height = window.innerHeight;
+    const width = document.documentElement.clientWidth;
+
+    if (clientY < 10) clientY = 0;
+    else if (cardRect.height + clientY > height)
+      clientY = height - cardRect.height;
+
+    if (clientX < 10) clientX = 0;
+    else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
+
+    cardRect.x = clientX;
+    cardRect.y = clientY;
+
+    return;
+  }
+
+  if (state.hide || rootElement === target) return;
+
+  Object.assign(hoverElementRect, getElementRect(target));
+}
+function handleClick(event) {
+  if (event.target === rootElement || state.hide) return;
+
+  event.preventDefault();
+  event.stopPropagation();
+
+  const attributes = Array.from(event.target.attributes).map(
+    ({ name, value }) => ({ name, value })
+  );
+  state.selectedElements = [
+    {
+      ...getElementRect(event.target),
+      attributes,
+      element: event.target,
+      highlight: false,
+    },
+  ];
+  state.elSelector = finder(event.target);
+
+  selectedElement.index = 0;
+  selectedElement.path = event.path;
+}
+function selectChildElement() {
+  if (selectedElement.path.length === 0 || state.hide) return;
+
+  const currentEl = selectedElement.path[selectedElement.pathIndex];
+  let childElement = currentEl;
+
+  if (selectedElement.pathIndex <= 0) {
+    const childEl = Array.from(currentEl.children).find(
+      (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
+    );
+
+    if (currentEl.childElementCount === 0 || currentEl === childEl) return;
+
+    childElement = childEl;
+    selectedElement.path.unshift(childEl);
+  } else {
+    selectedElement.pathIndex -= 1;
+    childElement = selectedElement.path[selectedElement.pathIndex];
+  }
+
+  updateSelectedElements(finder(childElement));
+}
+function selectParentElement() {
+  if (selectedElement.path.length === 0 || state.hide) return;
+
+  const parentElement = selectedElement.path[selectedElement.pathIndex];
+
+  if (parentElement.tagName === 'HTML') return;
+
+  selectedElement.pathIndex += 1;
+
+  updateSelectedElements(finder(parentElement));
+}
+function handleMouseUp() {
+  if (state.isDragging) state.isDragging = false;
+}
+function updateCardSize() {
+  setTimeout(() => {
+    cardRect.height = cardEl.value.getBoundingClientRect().height;
+  }, 250);
+}
+const handleScroll = debounce(() => {
+  if (state.hide) return;
+
+  const yPos = window.scrollY - lastScrollPosY;
+  const xPos = window.scrollX - lastScrollPosX;
+
+  state.selectedElements.forEach((_, index) => {
+    state.selectedElements[index].x -= xPos;
+    state.selectedElements[index].y -= yPos;
+  });
+
+  hoverElementRect.x -= xPos;
+  hoverElementRect.y -= yPos;
+
+  lastScrollPosX = window.scrollX;
+  lastScrollPosY = window.scrollY;
+}, 100);
+function destroy() {
+  window.removeEventListener('scroll', handleScroll);
+  window.removeEventListener('mouseup', handleMouseUp);
+  window.removeEventListener('mousemove', handleMouseMove);
+  document.removeEventListener('click', handleClick, true);
+
+  const automaElements = document.querySelectorAll('automa-element-selector');
+  automaElements.forEach((element) => {
+    element.remove();
+  });
+
+  rootElement.remove();
+}
+
+window.addEventListener('scroll', handleScroll);
+window.addEventListener('mouseup', handleMouseUp);
+window.addEventListener('mousemove', handleMouseMove);
+document.addEventListener('click', handleClick, true);
+
+watch(
+  () => state.isDragging,
+  (value) => {
+    document.body.toggleAttribute('automa-isDragging', value);
+  }
+);
+watch(() => [state.elSelector, state.activeTab, state.hide], updateCardSize);
+
+nextTick(() => {
+  setTimeout(() => {
+    const { height, width } = cardEl.value.getBoundingClientRect();
+
+    cardRect.x = window.innerWidth - (width + 35);
+    cardRect.y = 20;
+    cardRect.width = width;
+    cardRect.height = height;
+  }, 250);
+});
+</script>
+<style>
+.drag-button {
+  transform: scale(0);
+  transition: transform 200ms ease-in-out;
+}
+.root-card:hover .drag-button {
+  transform: scale(1);
+}
+</style>

+ 97 - 0
src/content/element-selector/AppBlocks.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="events mt-4">
+    <div class="flex items-center">
+      <ui-select
+        v-model="state.selectedBlock"
+        class="flex-1 mr-4"
+        placeholder="Select block"
+        @change="onSelectChanged"
+      >
+        <option v-for="(block, id) in blocks" :key="id" :value="id">
+          {{ block.name }}
+        </option>
+      </ui-select>
+      <ui-button
+        :disabled="!state.selectedBlock"
+        variant="accent"
+        @click="executeBlock"
+      >
+        Execute
+      </ui-button>
+    </div>
+    <component
+      :is="blocks[state.selectedBlock].component"
+      v-if="state.selectedBlock && blocks[state.selectedBlock].component"
+      :data="state.params"
+      :hide-base="true"
+      @update:data="updateParams"
+    />
+  </div>
+</template>
+<script setup>
+import { shallowReactive } from 'vue';
+import { tasks } from '@/utils/shared';
+import {
+  forms,
+  getText,
+  eventClick,
+  triggerEvent,
+  elementScroll,
+} from '../blocks-handler';
+import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
+import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
+import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
+
+const props = defineProps({
+  elements: {
+    type: Array,
+    default: () => [],
+  },
+});
+const emit = defineEmits(['update']);
+
+const blocks = {
+  forms: {
+    ...tasks.forms,
+    component: EditForms,
+    handler: forms,
+  },
+  'get-text': {
+    ...tasks['get-text'],
+    component: '',
+    handler: getText,
+  },
+  'event-click': {
+    ...tasks['event-click'],
+    component: '',
+    handler: eventClick,
+  },
+  'trigger-event': {
+    ...tasks['trigger-event'],
+    component: EditTriggerEvent,
+    handler: triggerEvent,
+  },
+  'element-scroll': {
+    ...tasks['element-scroll'],
+    component: EditScrollElement,
+    handler: elementScroll,
+  },
+};
+
+const state = shallowReactive({
+  params: {},
+  selectedBlock: '',
+});
+
+function updateParams(data = {}) {
+  state.params = data;
+  emit('update');
+}
+function onSelectChanged(value) {
+  state.params = tasks[value].data;
+  emit('update');
+}
+function executeBlock() {
+  console.log(blocks[state.selectedBlock], props.elements);
+}
+</script>

+ 39 - 0
src/content/element-selector/AppElementAttributes.vue

@@ -0,0 +1,39 @@
+<template>
+  <ul class="space-y-4 mt-2">
+    <li
+      v-for="(element, index) in elements"
+      :key="index"
+      @mouseenter="$emit('highlight', { highlight: true, index })"
+      @mouseleave="$emit('highlight', { highlight: false, index })"
+    >
+      <p
+        class="mb-1 cursor-pointer"
+        title="Scroll into view"
+        @click="
+          element.element.scrollIntoView({ block: 'center', inline: 'center' })
+        "
+      >
+        #{{ index + 1 }} Element
+      </p>
+      <div
+        v-for="attribute in element.attributes"
+        :key="attribute.name"
+        class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
+      >
+        <p class="text-sm text-overflow leading-tight text-gray-600">
+          {{ attribute.name }}
+        </p>
+        <p class="text-overflow">{{ attribute.value }}</p>
+      </div>
+    </li>
+  </ul>
+</template>
+<script setup>
+defineProps({
+  elements: {
+    type: Array,
+    default: () => [],
+  },
+});
+defineEmits(['highlight']);
+</script>

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


+ 57 - 0
src/content/element-selector/AppSelector.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="mt-4 flex items-center">
+    <ui-input
+      :model-value="selector"
+      placeholder="Element selector"
+      class="leading-normal flex-1 h-full element-selector"
+      @change="updateSelector"
+    >
+      <template #prepend>
+        <button class="absolute ml-2 left-0" @click="copySelector">
+          <v-remixicon name="riFileCopyLine" />
+        </button>
+      </template>
+    </ui-input>
+    <template v-if="selectedCount === 1">
+      <button class="mr-2 ml-4" @click="$emit('parent')">
+        <v-remixicon rotate="90" name="riArrowLeftLine" />
+      </button>
+      <button @click="$emit('child')">
+        <v-remixicon rotate="-90" name="riArrowLeftLine" />
+      </button>
+    </template>
+  </div>
+</template>
+<script setup>
+import { inject } from 'vue';
+import { debounce } from '@/utils/helper';
+import UiInput from '@/components/ui/UiInput.vue';
+
+const props = defineProps({
+  selector: {
+    type: String,
+    default: '',
+  },
+  selectedCount: {
+    type: Number,
+    default: 0,
+  },
+});
+const emit = defineEmits(['change', 'parent', 'child']);
+
+const rootElement = inject('rootElement');
+
+const updateSelector = debounce((value) => {
+  if (value === props.selector) return;
+
+  emit('change', value);
+}, 250);
+function copySelector() {
+  rootElement.shadowRoot.querySelector('input')?.select();
+
+  navigator.clipboard.writeText(props.selector).catch((error) => {
+    document.execCommand('copy');
+    console.error(error);
+  });
+}
+</script>

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

@@ -0,0 +1,26 @@
+import VAutofocus from '@/directives/VAutofocus';
+import UiTab from '@/components/ui/UiTab.vue';
+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 UiTextarea from '@/components/ui/UiTextarea.vue';
+import UiCheckbox from '@/components/ui/UiCheckbox.vue';
+import UiTabPanel from '@/components/ui/UiTabPanel.vue';
+import UiTabPanels from '@/components/ui/UiTabPanels.vue';
+import TransitionExpand from '@/components/transitions/TransitionExpand.vue';
+
+export default function (app) {
+  app.component('UiTab', UiTab);
+  app.component('UiTabs', UiTabs);
+  app.component('UiInput', UiInput);
+  app.component('UiButton', UiButton);
+  app.component('UiSelect', UiSelect);
+  app.component('UiTextarea', UiTextarea);
+  app.component('UiCheckbox', UiCheckbox);
+  app.component('UiTabPanel', UiTabPanel);
+  app.component('UiTabPanels', UiTabPanels);
+  app.component('TransitionExpand', TransitionExpand);
+
+  app.directive('autofocus', VAutofocus);
+}

+ 25 - 0
src/content/element-selector/icons.js

@@ -0,0 +1,25 @@
+import {
+  riEyeLine,
+  riCheckLine,
+  riCloseLine,
+  riEyeOffLine,
+  riFileCopyLine,
+  riDragMoveLine,
+  riArrowLeftLine,
+  riArrowLeftSLine,
+  riInformationLine,
+  riArrowDropDownLine,
+} from 'v-remixicon/icons';
+
+export default {
+  riEyeLine,
+  riCheckLine,
+  riCloseLine,
+  riEyeOffLine,
+  riFileCopyLine,
+  riDragMoveLine,
+  riArrowLeftLine,
+  riArrowLeftSLine,
+  riInformationLine,
+  riArrowDropDownLine,
+};

+ 61 - 15
src/content/element-selector/index.js

@@ -1,21 +1,67 @@
-import '@webcomponents/custom-elements';
-import { defineCustomElement } from 'vue';
-import ElementSelector from './ElementSelector.ce.vue';
+async function getStyles() {
+  try {
+    const response = await fetch(chrome.runtime.getURL('/elementSelector.css'));
+    const mainCSS = await response.text();
 
-/* to-do attribute list */
+    const fontCSS = `
+      @font-face {
+        font-family: Inter var;
+        font-weight: 100 900;
+        font-display: swap;
+        font-style: normal;
+        font-named-instance: "Regular";
+        src: url('${chrome.runtime.getURL(
+          '/Inter-roman-latin.var.woff2'
+        )}') format("woff2");
+      }
+    `;
 
-export default function () {
-  const isElementExists = document.querySelector('element-selector');
+    return `${mainCSS}\n${fontCSS}`;
+  } catch (error) {
+    console.error(error);
+    return '';
+  }
+}
+function getLocale() {
+  return new Promise((resolve) => {
+    chrome.storage.local.get('settings', ({ settings }) => {
+      resolve(settings?.locale || 'en');
+    });
+  });
+}
+
+export default async function () {
+  try {
+    const rootElement = document.createElement('div');
+    rootElement.classList.add('automa-element-selector');
+    rootElement.attachShadow({ mode: 'open' });
+
+    const automaStyle = document.createElement('style');
+    automaStyle.classList.add('automa-element-selector');
+    automaStyle.innerHTML = `.automa-element-selector { pointer-events: none } \n [automa-isDragging] { user-select: none }`;
 
-  if (isElementExists) return;
-  if (!customElements.get('element-selector')) {
-    window.customElements.define(
-      'element-selector',
-      defineCustomElement(ElementSelector)
+    const scriptEl = document.createElement('script');
+    scriptEl.setAttribute('type', 'module');
+    scriptEl.setAttribute(
+      'src',
+      chrome.runtime.getURL('/elementSelector.bundle.js')
     );
-  }
 
-  document.documentElement.appendChild(
-    document.createElement('element-selector')
-  );
+    const appContainer = document.createElement('div');
+    appContainer.setAttribute('data-id', chrome.runtime.id);
+    appContainer.setAttribute('data-locale', await getLocale());
+    appContainer.setAttribute('id', 'app');
+
+    const appStyle = document.createElement('style');
+    appStyle.innerHTML = await getStyles();
+
+    rootElement.shadowRoot.appendChild(appContainer);
+    rootElement.shadowRoot.appendChild(appStyle);
+    rootElement.shadowRoot.appendChild(scriptEl);
+
+    document.documentElement.appendChild(rootElement);
+    document.documentElement.appendChild(automaStyle);
+  } catch (error) {
+    console.error(error);
+  }
 }

+ 17 - 0
src/content/element-selector/main.js

@@ -0,0 +1,17 @@
+import { createApp } from 'vue';
+import vRemixicon from 'v-remixicon';
+import App from './App.vue';
+import compsUi from './comps-ui';
+import icons from './icons';
+import vueI18n from './vue-i18n';
+import '@/assets/css/tailwind.css';
+
+const rootElement = document.querySelector('div.automa-element-selector');
+const appRoot = rootElement.shadowRoot.querySelector('#app');
+
+createApp(App)
+  .provide('rootElement', rootElement)
+  .use(vueI18n)
+  .use(vRemixicon, icons)
+  .use(compsUi)
+  .mount(appRoot);

+ 13 - 0
src/content/element-selector/vue-i18n.js

@@ -0,0 +1,13 @@
+import { createI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler';
+import enCommon from '@/locales/en/common.json';
+import enBlocks from '@/locales/en/blocks.json';
+
+const i18n = createI18n({
+  locale: 'en',
+  legacy: false,
+});
+
+i18n.global.mergeLocaleMessage('en', enCommon);
+i18n.global.mergeLocaleMessage('en', enBlocks);
+
+export default i18n;

+ 1 - 0
src/content/index.js

@@ -29,4 +29,5 @@ import * as blocksHandler from './blocks-handler';
       }
     });
   });
+  elementSelector();
 })();

+ 1 - 0
src/lib/dayjs.js

@@ -1,6 +1,7 @@
 import dayjs from 'dayjs';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import 'dayjs/locale/zh';
+import 'dayjs/locale/zh-tw';
 
 dayjs.extend(relativeTime);
 

+ 2 - 0
src/lib/v-remixicon.js

@@ -13,6 +13,7 @@ import {
   riCodeSSlashLine,
   riRecordCircleLine,
   riErrorWarningLine,
+  riEyeLine,
   riCalendarLine,
   riFileTextLine,
   riFilter2Line,
@@ -86,6 +87,7 @@ export const icons = {
   riCodeSSlashLine,
   riRecordCircleLine,
   riErrorWarningLine,
+  riEyeLine,
   riCalendarLine,
   riFileTextLine,
   riFilter2Line,

+ 4 - 4
src/lib/vue-i18n.js

@@ -10,7 +10,7 @@ const i18n = createI18n({
 
 export function setI18nLanguage(locale) {
   i18n.global.locale.value = locale;
-
+  console.log(i18n.global);
   document.querySelector('html').setAttribute('lang', locale);
 }
 
@@ -26,7 +26,7 @@ export async function loadLocaleMessages(locale, location) {
   const importLocale = async (path, merge = false) => {
     try {
       const messages = await import(
-        /* webpackChunkName: "locale-[request]" */ `../locales/${locale}/${path}`
+        /* webpackChunkName: "locales/locale-[request]" */ `../locales/${locale}/${path}`
       );
 
       if (merge) {
@@ -39,12 +39,12 @@ export async function loadLocaleMessages(locale, location) {
     }
   };
 
-  dayjs.locale(locale);
-
   if (locale !== 'en' && !i18n.global.availableLocales.includes('en')) {
     await loadLocaleMessages('en', location);
   }
 
+  dayjs.locale(locale);
+
   await importLocale('common.json');
   await importLocale(`${location}.json`, true);
   await importLocale('blocks.json', true);

+ 68 - 68
src/locales/en/newtab.json

@@ -1,128 +1,128 @@
 {
   "home": {
-    "viewAll": "檢視全部"
+    "viewAll": "View all"
   },
   "settings": {
     "language": {
-      "label": "語言",
-      "helpTranslate": "找不到你的語言嗎? 請協助我們翻譯。",
-      "reloadPage": "重整頁面使其生效"
-    }
+      "label": "Language",
+      "helpTranslate": "Can't find your language? Help translate.",
+      "reloadPage": "Reload the page to take effect"
+    },
   },
   "workflow": {
-    "import": "匯入工作流",
-    "new": "新增工作流",
-    "delete": "刪除工作流",
-    "name": "工作流名稱",
-    "rename": "重新命名工作流",
-    "add": "建立工作流",
-    "clickToEnable": "點擊啟用",
+    "import": "Import workflow",
+    "new": "New workflow",
+    "delete": "Delete workflow",
+    "name": "Workflow name",
+    "rename": "Rename workflow",
+    "add": "Add workflow",
+    "clickToEnable": "Click to enable",
     "dataColumns": {
-      "title": "資料欄位",
-      "placeholder": "搜尋或建立欄位",
+      "title": "Data columns",
+      "placeholder": "Search or add column",
       "column": {
-        "name": "欄位名稱",
-        "type": "資料型別"
+        "name": "Column name",
+        "type": "Data type"
       }
     },
     "sidebar": {
-      "workflowIcon": "工作流圖示"
+      "workflowIcon": "Workflow icon"
     },
     "editor": {
-      "zoomIn": "放大",
-      "zoomOut": "縮小",
-      "resetZoom": "重設縮放",
-      "duplicate": "複製"
+      "zoomIn": "Zoom in",
+      "zoomOut": "Zoom out",
+      "resetZoom": "Reset zoom",
+      "duplicate": "Duplicate"
     },
     "settings": {
       "onError": {
-        "title": "工作流發生錯誤",
+        "title": "On workflow error",
         "items": {
-          "keepRunning": "繼續執行",
-          "stopWorkflow": "停止工作流"
+          "keepRunning": "Keep running",
+          "stopWorkflow": "Stop workflow"
         }
       },
       "timeout": {
-        "title": "工作流超時 (毫秒)"
+        "title": "Workflow timeout (milliseconds)"
       }
     }
   },
   "collection": {
-    "description": "依序執行工作流",
-    "new": "新增集合",
-    "delete": "刪除集合",
-    "add": "建立集合",
-    "rename": "重新命名集合",
-    "flow": "工作流",
-    "dragDropText": "拖曳工作流或區塊至此",
+    "description": "Execute your workflows in sequence",
+    "new": "New collection",
+    "delete": "Delete collection",
+    "add": "Add collection",
+    "rename": "Rename collection",
+    "flow": "Flow",
+    "dragDropText": "Drop a workflow or block in here",
     "options": {
       "atOnce": {
-        "title": "立即執行集合中的所有工作流",
-        "description": "使用此選項不會執行區塊"
+        "title": "Execute all workflows in the collection at once",
+        "description": "Block not gonna executed when using this option"
       }
     },
     "globalData": {
-      "note": "這將覆蓋工作流的全域資料"
+      "note": "This will overwrite the global data of the workflow"
     }
   },
   "log": {
-    "goBack": "返回 \"{name}\" 紀錄",
-    "startedDate": "開始日期",
-    "duration": "期間",
-    "selectAll": "全選",
-    "deselectAll": "取消全選",
-    "deleteSelected": "刪除選取的紀錄",
+    "goBack": "Go back to \"{name}\" log",
+    "startedDate": "Started date",
+    "duration": "Duration",
+    "selectAll": "Select all",
+    "deselectAll": "Deselect all",
+    "deleteSelected": "Delete selected logs",
     "types": {
-      "stop": "工作流停止",
-      "finish": "工作流完成"
+      "stop": "Workflow is stopped",
+      "finish": "Finish"
     },
     "messages": {
-      "workflow-disabled": "工作流已禁用",
-      "stop-timeout": "工作流因逾時而停止",
-      "no-iframe-id": "Can't find Frame ID for the frame element with \"{selector}\" selector",
+      "workflow-disabled": "Workflow is disabled",
+      "stop-timeout": "Workflow is stopped because of timeout",
+      "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."
     },
     "description": {
       "text": "{status} on {date} in {duration}",
       "status": {
-        "success": "成功",
-        "error": "失敗",
-        "stopped": "停止"
+        "success": "Succeeded",
+        "error": "Failed",
+        "stopped": "Stopped"
       }
     },
     "delete": {
-      "title": "刪除紀錄",
-      "description": "確定要刪除所有選取的紀錄嗎?"
+      "title": "Delete log",
+      "description": "Are you sure want to delete all the selected logs?"
     },
     "exportData": {
-      "title": "匯出資料",
+      "title": "Export data",
       "types": {
         "json": "JSON",
         "csv": "CSV",
-        "plain-text": "純文字"
-      }
+        "plain-text": "Plain text"
+      },
     },
     "filter": {
-      "title": "篩選",
-      "byStatus": " status",
+      "title": "Filter",
+      "byStatus": "By status",
       "byDate": {
-        "title": " date",
+        "title": "By date",
         "items": {
-          "lastDay": "最後1天",
-          "last7Days": "最後7天",
-          "last30Days": "最後30天"
+          "lastDay": "Last day",
+          "last7Days": "Last seven days",
+          "last30Days": "Last thirty days",
         }
-      }
-    }
+      },
+    },
   },
   "components": {
     "pagination": {
-      "text1": "顯示",
-      "text2": "項, 總共 {count} 項",
-      "nextPage": "下一頁",
-      "currentPage": "目前頁數",
-      "prevPage": "上一頁",
+      "text1": "Showing",
+      "text2": "items out of {count}",
+      "nextPage": "Next page",
+      "currentPage": "Current page",
+      "prevPage": "Previous page",
       "of": "of {page}"
     }
-  }
+  },
 }

+ 6 - 6
src/locales/en/popup.json

@@ -1,13 +1,13 @@
 {
   "home": {
     "elementSelector": {
-      "name": "元素選擇器",
-      "noAccess": "無權訪問這個網站"
+      "name": "Element selector",
+      "noAccess": "Don't have access to this site"
     },
     "workflow": {
-      "new": "新增工作流",
-      "rename": "重新命名工作流",
-      "delete": "刪除工作流"
-    }
+      "new": "New workflow",
+      "rename": "Rename workflow",
+      "delete": "Delete workflow"
+    },
   }
 }

+ 128 - 0
src/locales/zh-TW/newtab.json

@@ -0,0 +1,128 @@
+{
+  "home": {
+    "viewAll": "檢視全部"
+  },
+  "settings": {
+    "language": {
+      "label": "語言",
+      "helpTranslate": "找不到你的語言嗎? 請協助我們翻譯。",
+      "reloadPage": "重整頁面使其生效"
+    }
+  },
+  "workflow": {
+    "import": "匯入工作流",
+    "new": "新增工作流",
+    "delete": "刪除工作流",
+    "name": "工作流名稱",
+    "rename": "重新命名工作流",
+    "add": "建立工作流",
+    "clickToEnable": "點擊啟用",
+    "dataColumns": {
+      "title": "資料欄位",
+      "placeholder": "搜尋或建立欄位",
+      "column": {
+        "name": "欄位名稱",
+        "type": "資料型別"
+      }
+    },
+    "sidebar": {
+      "workflowIcon": "工作流圖示"
+    },
+    "editor": {
+      "zoomIn": "放大",
+      "zoomOut": "縮小",
+      "resetZoom": "重設縮放",
+      "duplicate": "複製"
+    },
+    "settings": {
+      "onError": {
+        "title": "工作流發生錯誤",
+        "items": {
+          "keepRunning": "繼續執行",
+          "stopWorkflow": "停止工作流"
+        }
+      },
+      "timeout": {
+        "title": "工作流超時 (毫秒)"
+      }
+    }
+  },
+  "collection": {
+    "description": "依序執行工作流",
+    "new": "新增集合",
+    "delete": "刪除集合",
+    "add": "建立集合",
+    "rename": "重新命名集合",
+    "flow": "工作流",
+    "dragDropText": "拖曳工作流或區塊至此",
+    "options": {
+      "atOnce": {
+        "title": "立即執行集合中的所有工作流",
+        "description": "使用此選項不會執行區塊"
+      }
+    },
+    "globalData": {
+      "note": "這將覆蓋工作流的全域資料"
+    }
+  },
+  "log": {
+    "goBack": "返回 \"{name}\" 紀錄",
+    "startedDate": "開始日期",
+    "duration": "期間",
+    "selectAll": "全選",
+    "deselectAll": "取消全選",
+    "deleteSelected": "刪除選取的紀錄",
+    "types": {
+      "stop": "工作流停止",
+      "finish": "工作流完成"
+    },
+    "messages": {
+      "workflow-disabled": "工作流已禁用",
+      "stop-timeout": "工作流因逾時而停止",
+      "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."
+    },
+    "description": {
+      "text": "{status} on {date} in {duration}",
+      "status": {
+        "success": "成功",
+        "error": "失敗",
+        "stopped": "停止"
+      }
+    },
+    "delete": {
+      "title": "刪除紀錄",
+      "description": "確定要刪除所有選取的紀錄嗎?"
+    },
+    "exportData": {
+      "title": "匯出資料",
+      "types": {
+        "json": "JSON",
+        "csv": "CSV",
+        "plain-text": "純文字"
+      }
+    },
+    "filter": {
+      "title": "篩選",
+      "byStatus": "依 status",
+      "byDate": {
+        "title": "依 date",
+        "items": {
+          "lastDay": "最後1天",
+          "last7Days": "最後7天",
+          "last30Days": "最後30天"
+        }
+      }
+    }
+  },
+  "components": {
+    "pagination": {
+      "text1": "顯示",
+      "text2": "項, 總共 {count} 項",
+      "nextPage": "下一頁",
+      "currentPage": "目前頁數",
+      "prevPage": "上一頁",
+      "of": "of {page}"
+    }
+  }
+}

+ 13 - 0
src/locales/zh-TW/popup.json

@@ -0,0 +1,13 @@
+{
+  "home": {
+    "elementSelector": {
+      "name": "元素選擇器",
+      "noAccess": "無權訪問這個網站"
+    },
+    "workflow": {
+      "new": "新增工作流",
+      "rename": "重新命名工作流",
+      "delete": "刪除工作流"
+    }
+  }
+}

+ 6 - 0
src/manifest.json

@@ -32,5 +32,11 @@
     "storage",
     "unlimitedStorage",
     "<all_urls>"
+  ],
+  "web_accessible_resources": [
+    "/elementSelector.css",
+    "/Inter-roman-latin.var.woff2",
+    "/locales/*",
+    "elementSelector.bundle.js"
   ]
 }

+ 1 - 1
src/utils/shared.js

@@ -531,5 +531,5 @@ export const contentTypes = [
 export const supportLocales = [
   { id: 'en', name: 'English' },
   { id: 'zh', name: '简体中文' },
-  { id: 'zh-TW', name: '繁體中文'}
+  { id: 'zh-tw', name: '繁體中文' },
 ];

+ 8 - 1
webpack.config.js

@@ -43,9 +43,16 @@ const options = {
     background: path.join(__dirname, 'src', 'background', 'index.js'),
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
     shortcut: path.join(__dirname, 'src', 'content', 'shortcut.js'),
+    elementSelector: path.join(
+      __dirname,
+      'src',
+      'content',
+      'element-selector',
+      'main.js'
+    ),
   },
   chromeExtensionBoilerplate: {
-    notHotReload: ['contentScript', 'shortcut'],
+    notHotReload: ['contentScript', 'shortcut', 'elementSelector'],
   },
   output: {
     path: path.resolve(__dirname, 'build'),