Sfoglia il codice sorgente

feat: add element selector

Ahmad Kholid 3 anni fa
parent
commit
169c98738e

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
   "dependencies": {
     "@medv/finder": "^2.1.0",
     "@vuex-orm/core": "^0.36.4",
+    "@webcomponents/custom-elements": "^1.5.0",
     "css-selector-generator": "^3.4.4",
     "dayjs": "^1.10.7",
     "drawflow": "^0.0.49",

+ 2 - 2
src/background/blocks-handler.js

@@ -109,7 +109,7 @@ export function interactionHandler(block) {
       return;
     }
 
-    this._connectedTab.postMessage(block);
+    this._connectedTab.postMessage({ isBlock: true, ...block });
     this._listener({
       name: 'tab-message',
       id: block.name,
@@ -171,7 +171,7 @@ export function elementExists(block) {
   return new Promise((resolve) => {
     if (!this._connectedTab) return;
 
-    this._connectedTab.postMessage(block);
+    this._connectedTab.postMessage({ isBlock: true, ...block });
     this._listener({
       name: 'tab-message',
       id: block.name,

+ 2 - 1
src/background/index.js

@@ -25,8 +25,9 @@ function getWorkflow(workflowId) {
 }
 function executeWorkflow(workflow) {
   try {
+    /* to-do handle running workflow & validate if a tab is using by workflow */
     console.log(executingWorkflow[workflow.id]);
-    if (executingWorkflow[workflow.id]) return false;
+    if (executingWorkflow[workflow.id]) return true;
 
     const engine = new WorkflowEngine(workflow);
     console.log('execute');

+ 258 - 0
src/content/element-selector/ElementSelector.ce.vue

@@ -0,0 +1,258 @@
+<template>
+  <div
+    :style="{
+      transform: `translate(${element.hovered.x}px, ${element.hovered.y}px)`,
+      height: element.hovered.height + 'px',
+      width: element.hovered.width + 'px',
+    }"
+    class="indicator pointer-events-auto"
+  ></div>
+  <div
+    v-if="element.selector"
+    :style="{
+      transform: `translate(${element.selected.x}px, ${element.selected.y}px)`,
+      height: element.selected.height + 'px',
+      width: element.selected.width + 'px',
+    }"
+    class="indicator selected"
+  ></div>
+  <div class="card">
+    <div class="selector">
+      <v-remix-icon
+        style="cursor: pointer"
+        title="Copy selector"
+        :path="riFileCopyLine"
+        @click="copySelector"
+      />
+      <input :value="element.selector" />
+    </div>
+    <template v-if="element.selector">
+      <button
+        title="Select parent element (press P)"
+        @click="selectParentElement"
+      >
+        <v-remix-icon :path="riArrowDownLine" rotate="180" />
+      </button>
+      <button
+        title="Select parent element (press C)"
+        @click="selectChildElement"
+      >
+        <v-remix-icon :path="riArrowDownLine" />
+      </button>
+    </template>
+    <button class="primary" @click="destroy">Close</button>
+  </div>
+</template>
+<script setup>
+import { reactive } from 'vue';
+import { finder } from '@medv/finder';
+import { VRemixIcon } from 'v-remixicon';
+import { riFileCopyLine, riArrowDownLine } from 'v-remixicon/icons';
+
+const element = reactive({
+  hovered: {},
+  selected: {},
+  selector: '',
+});
+
+let targetEl = null;
+let selectedEl = null;
+let pathIndex = 0;
+let selectedPath = [];
+
+function getElementRect(target) {
+  if (!target) return {};
+
+  const { x, y, height, width } = target.getBoundingClientRect();
+
+  return {
+    width,
+    height,
+    x: x - 2,
+    y: y - 2,
+  };
+}
+function handleMouseMove({ target }) {
+  if (targetEl === target) return;
+
+  targetEl = target;
+  element.hovered = getElementRect(target);
+}
+function copySelector() {
+  navigator.clipboard
+    .writeText(element.selector)
+    .then(() => {
+      console.log('Selector copied');
+    })
+    .catch((error) => {
+      console.error(error);
+    });
+}
+function selectChildElement() {
+  if (selectedPath.length === 0) return;
+
+  const currentEl = selectedPath[pathIndex];
+  let activeEl = currentEl;
+
+  if (pathIndex <= 0) {
+    const childEl = Array.from(currentEl.children).find(
+      (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
+    );
+
+    if (currentEl.childElementCount === 0 || currentEl === childEl) return;
+
+    activeEl = childEl;
+    selectedPath.unshift(childEl);
+  } else {
+    pathIndex -= 1;
+    activeEl = selectedPath[pathIndex];
+  }
+
+  element.selected = getElementRect(activeEl);
+  element.selector = finder(activeEl);
+  selectedEl = activeEl;
+  console.log(pathIndex, selectedPath);
+}
+function selectParentElement() {
+  if (selectedEl.tagName === 'HTML' || selectedPath.length === 0) return;
+
+  pathIndex += 1;
+  const activeEl = selectedPath[pathIndex];
+
+  element.selected = getElementRect(activeEl);
+  element.selector = finder(activeEl);
+  selectedEl = activeEl;
+  console.log(pathIndex, selectedPath);
+}
+function handleClick(event) {
+  event.preventDefault();
+  event.stopPropagation();
+
+  selectedPath = event.path;
+  element.selected = getElementRect(targetEl);
+  element.selector = finder(targetEl);
+
+  selectedEl = targetEl;
+}
+function handleKeyup({ code }) {
+  const shortcuts = {
+    /* eslint-disable-next-line */
+    Escape: destroy,
+    KeyC: selectChildElement,
+    KeyP: selectParentElement,
+  };
+
+  if (shortcuts[code]) shortcuts[code]();
+  console.log(code);
+}
+function handleScroll() {
+  const { x: hoveredX, y: hoveredY } = getElementRect(targetEl);
+  const { x: selectedX, y: selectedY } = getElementRect(selectedEl);
+
+  element.hovered.x = hoveredX;
+  element.hovered.y = hoveredY;
+  element.selected.x = selectedX;
+  element.selected.y = selectedY;
+}
+function destroy() {
+  const root = document.querySelector('element-selector');
+
+  window.removeEventListener('keyup', handleKeyup);
+  window.removeEventListener('scroll', handleScroll);
+  document.body.removeEventListener('click', handleClick);
+  window.removeEventListener('mousemove', handleMouseMove);
+
+  root.remove();
+}
+
+window.addEventListener('keyup', handleKeyup);
+window.addEventListener('scroll', handleScroll);
+document.body.addEventListener('click', handleClick);
+window.addEventListener('mousemove', handleMouseMove);
+</script>
+<style>
+:host {
+  position: fixed;
+  height: 100%;
+  width: 100%;
+  top: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.2);
+  pointer-events: none;
+  z-index: 99999;
+  color: #18181b;
+  font-size: 16px;
+  box-sizing: border-box;
+  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+    'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
+    'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+}
+
+:host * {
+  font-size: 16px;
+}
+
+svg {
+  display: inline-block;
+}
+
+button {
+  border: none;
+  background-color: transparent;
+  color: inherit;
+  border-radius: 8px;
+  height: 38px;
+  padding: 0 10px;
+  background-color: #e4e4e7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 6px;
+  cursor: pointer;
+}
+button.primary {
+  background-color: #18181b;
+  color: white;
+}
+
+.selector {
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  padding-left: 12px;
+  background-color: #e4e4e7;
+}
+input {
+  border: none;
+  color: inherit;
+  background-color: transparent;
+  padding: 10px 12px 10px 6px;
+  width: 150px;
+}
+input:focus {
+  outline: none;
+}
+
+.card {
+  position: absolute;
+  display: flex;
+  align-items: center;
+  bottom: 12px;
+  left: 12px;
+  background-color: white;
+  border-radius: 8px;
+  padding: 12px;
+  color: #1f2937;
+  pointer-events: all;
+}
+
+.indicator {
+  background-color: rgba(251, 191, 36, 0.2);
+  border: 2px solid #fbbf24;
+  position: absolute;
+}
+.indicator.selected {
+  background-color: rgba(248, 113, 113, 0.2);
+  border-color: #f87171;
+}
+</style>

+ 19 - 0
src/content/element-selector/index.js

@@ -0,0 +1,19 @@
+import '@webcomponents/custom-elements';
+import { defineCustomElement } from 'vue';
+import ElementSelector from './ElementSelector.ce.vue';
+
+export default function () {
+  const isElementExists = document.querySelector('element-selector');
+
+  if (isElementExists) return;
+  if (!customElements.get('element-selector')) {
+    window.customElements.define(
+      'element-selector',
+      defineCustomElement(ElementSelector)
+    );
+  }
+
+  document.documentElement.appendChild(
+    document.createElement('element-selector')
+  );
+}

+ 12 - 2
src/content/index.js

@@ -1,9 +1,8 @@
 import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
+import elementSelector from './element-selector';
 import * as blocksHandler from './blocks-handler';
 
-console.log('===Content Script===');
-
 function onConnectListener() {
   browser.runtime.onConnect.addListener((port) => {
     console.log('Connect');
@@ -21,5 +20,16 @@ function onConnectListener() {
   });
 }
 
+browser.runtime.onMessage.addListener(({ type }) => {
+  return new Promise((resolve) => {
+    if (type === 'content-script-exists') {
+      resolve(true);
+    } else if (type === 'select-element') {
+      elementSelector();
+      resolve(true);
+    }
+  });
+});
+
 if (document.readyState === 'complete') onConnectListener();
 else window.addEventListener('load', onConnectListener);

+ 39 - 2
src/popup/pages/Home.vue

@@ -9,7 +9,15 @@
       class="flex-1 search-input"
       placeholder="Search..."
     ></ui-input>
-    <ui-button icon title="dashboard" class="ml-3" @click="openDashboard">
+    <ui-button
+      icon
+      title="Element selector"
+      class="ml-3"
+      @click="selectElement"
+    >
+      <v-remixicon name="riFocus3Line" />
+    </ui-button>
+    <ui-button icon title="Dashboard" class="ml-3" @click="openDashboard">
       <v-remixicon name="riHome5Line" />
     </ui-button>
   </div>
@@ -18,9 +26,38 @@
   </div>
 </template>
 <script setup>
+import browser from 'webextension-polyfill';
 import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
 
 function openDashboard() {
-  window.open(chrome.runtime.getURL('/newtab.html'), '_blank');
+  const newTabURL = chrome.runtime.getURL('/newtab.html');
+
+  browser.tabs.query({ url: newTabURL }).then(([tab]) => {
+    if (tab) browser.tabs.update(tab.id, { active: true });
+    else browser.tabs.create({ url: newTabURL, active: true });
+  });
+}
+async function selectElement() {
+  const [tab] = await browser.tabs.query({ active: true });
+
+  try {
+    await browser.tabs.sendMessage(tab.id, {
+      type: 'content-script-exists',
+    });
+
+    browser.tabs.sendMessage(tab.id, {
+      type: 'select-element',
+    });
+  } catch (error) {
+    if (error.message.includes('Could not establish connection.')) {
+      await browser.tabs.executeScript(tab.id, {
+        file: './contentScript.bundle.js',
+      });
+
+      selectElement();
+    }
+
+    console.error(error);
+  }
 }
 </script>

+ 5 - 0
yarn.lock

@@ -1236,6 +1236,11 @@
     "@webassemblyjs/ast" "1.11.1"
     "@xtuc/long" "4.2.2"
 
+"@webcomponents/custom-elements@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.5.0.tgz#7d07ff4979312dda167cc0a2b7586e76dc1cf6ab"
+  integrity sha512-c+7jPQCs9h/BYVcZ2Kna/3tsl3A/9EyXfvWjp5RiTDm1OpTcbZaCa1z4RNcTe/hUtXaqn64JjNW1yrWT+rZ8gg==
+
 "@webpack-cli/configtest@^1.0.4":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"