Browse Source

feat: take a screenshot of an element

Ahmad Kholid 3 years ago
parent
commit
68bbba8ea3

+ 11 - 5
src/background/workflow-engine/blocks-handler/handler-take-screenshot.js

@@ -1,6 +1,6 @@
 import browser from 'webextension-polyfill';
 import { fileSaver } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
+import { getBlockConnection, waitTabLoaded } from '../helper';
 
 async function saveImage({ filename, uri, ext }) {
   const hasDownloadAccess = await browser.permissions.contains({
@@ -65,16 +65,20 @@ async function takeScreenshot({ data, outputs, name }) {
         currentWindow: true,
       });
 
-      if (this.windowId)
+      if (this.windowId) {
         await browser.windows.update(this.windowId, { focused: true });
-      await browser.tabs.update(this.activeTab.id, { active: true });
+      }
 
-      await new Promise((resolve) => setTimeout(resolve, 500));
+      await browser.tabs.update(this.activeTab.id, { active: true });
+      await waitTabLoaded(this.activeTab.id);
 
-      screenshot = await (data.fullPage
+      screenshot = await (data.fullPage ||
+      ['element', 'fullpage'].includes(data.type)
         ? this._sendMessageToTab({
             name,
             options,
+            type: data.type,
+            selector: data.selector,
             tabId: this.activeTab.id,
           })
         : browser.tabs.captureVisibleTab(options));
@@ -95,6 +99,8 @@ async function takeScreenshot({ data, outputs, name }) {
   } catch (error) {
     error.nextBlockId = nextBlockId;
 
+    if (data.type === 'element') error.data = { selector: data.selector };
+
     throw error;
   }
 }

+ 55 - 22
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -1,26 +1,46 @@
 <template>
-  <template v-if="data.ext === 'jpeg'">
-    <p class="text-sm text-gray-600 dark:text-gray-200 ml-2">Image quality</p>
-    <div class="bg-box-transparent px-4 mb-4 py-2 rounded-lg flex items-center">
-      <input
-        :value="data.quality"
-        :title="t('workflow.blocks.take-screenshot.imageQuality')"
-        class="focus:outline-none flex-1"
-        type="range"
-        min="0"
-        max="100"
-        @change="updateQuality"
-      />
-      <span class="w-12 text-right">{{ data.quality }}%</span>
-    </div>
-  </template>
   <div class="take-screenshot">
-    <ui-checkbox
-      :model-value="data.fullPage"
-      @change="updateData({ fullPage: $event })"
+    <ui-textarea
+      :model-value="data.description"
+      :placeholder="t('common.description')"
+      class="w-full"
+      @change="updateData({ description: $event })"
+    />
+    <ui-select
+      :model-value="data.type"
+      :label="t('workflow.blocks.take-screenshot.types.title')"
+      class="w-full mt-2"
+      @change="updateData({ type: $event })"
     >
-      {{ t('workflow.blocks.take-screenshot.fullPage') }}
-    </ui-checkbox>
+      <option v-for="type in types" :key="type" :value="type">
+        {{ t(`workflow.blocks.take-screenshot.types.${type}`) }}
+      </option>
+    </ui-select>
+    <ui-input
+      v-if="data.type === 'element'"
+      :model-value="data.selector"
+      :label="t(`workflow.blocks.base.findElement.options.cssSelector`)"
+      class="mt-2 w-full"
+      placeholder=".element"
+      @change="updateData({ selector: $event })"
+    />
+    <template v-if="data.ext === 'jpeg'">
+      <p class="text-sm text-gray-600 dark:text-gray-200 ml-2 mt-4">
+        {{ t('workflow.blocks.take-screenshot.imageQuality') }}
+      </p>
+      <div class="bg-box-transparent px-4 py-2 rounded-lg flex items-center">
+        <input
+          :value="data.quality"
+          :title="t('workflow.blocks.take-screenshot.imageQuality')"
+          class="focus:outline-none flex-1"
+          type="range"
+          min="0"
+          max="100"
+          @change="updateQuality"
+        />
+        <span class="w-12 text-right">{{ data.quality }}%</span>
+      </div>
+    </template>
     <ui-checkbox
       :model-value="data.saveToComputer"
       class="mt-4"
@@ -89,6 +109,7 @@
   </div>
 </template>
 <script setup>
+/* eslint-disable no-unused-expressions */
 import { inject, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { objectHasKey } from '@/utils/helper';
@@ -105,6 +126,8 @@ const emit = defineEmits(['update:data']);
 const { t } = useI18n();
 const workflow = inject('workflow');
 
+const types = ['page', 'fullpage', 'element'];
+
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
@@ -118,8 +141,18 @@ function updateQuality({ target }) {
 }
 
 onMounted(() => {
-  if (objectHasKey(props.data, 'saveToComputer')) return;
+  if (!objectHasKey(props.data, 'saveToComputer')) {
+    updateData({ saveToComputer: true, saveToColumn: false });
+  }
+
+  if (!objectHasKey(props.data, 'type')) {
+    const type = 'page';
+
+    if (props.data.fullPage) {
+      type === 'fullpage';
+    }
 
-  updateData({ saveToComputer: true, saveToColumn: false });
+    updateData({ type, fullPage: false });
+  }
 });
 </script>

+ 58 - 18
src/content/blocks-handler/handler-take-screenshot.js

@@ -1,5 +1,6 @@
 /* eslint-disable no-await-in-loop */
 import { sendMessage } from '@/utils/message';
+import { sleep } from '@/utils/helper';
 
 function findScrollableElement(
   element = document.documentElement,
@@ -39,39 +40,78 @@ function injectStyle() {
 
   return style;
 }
-
-const loadAsyncImg = (src) =>
-  new Promise((resolve) => {
+function canvasToBase64(canvas, { format, quality }) {
+  return canvas.toDataURL(`image/${format}`, quality / 100);
+}
+function loadAsyncImg(src) {
+  return new Promise((resolve) => {
     const image = new Image();
     image.onload = () => {
       resolve(image);
     };
     image.src = src;
   });
+}
+async function takeScreenshot(tabId, options) {
+  await sendMessage('set:active-tab', tabId, 'background');
+  const imageUrl = await sendMessage(
+    'get:tab-screenshot',
+    options,
+    'background'
+  );
+
+  return imageUrl;
+}
+async function captureElementScreenshot({ selector, tabId, options }) {
+  const element = document.querySelector(selector);
 
-export default async function ({ tabId, options }) {
-  document.body.classList.add('is-screenshotting');
+  if (!element) {
+    const error = new Error('element-not-found');
+
+    throw error;
+  }
+
+  element.scrollIntoView();
+
+  await sleep(500);
+
+  const imageUrl = await takeScreenshot(tabId, options);
+  const image = await loadAsyncImg(imageUrl);
 
   const canvas = document.createElement('canvas');
   const context = canvas.getContext('2d');
-  const maxCanvasSize = 32767;
+  const { height, width, x, y } = element.getBoundingClientRect();
 
-  const scrollElement = document.querySelector('.automa-scrollable-el');
-  let scrollableElement = scrollElement || findScrollableElement();
+  canvas.width = width;
+  canvas.height = height;
+
+  context.drawImage(image, x, y, width, height, 0, 0, width, height);
 
-  const takeScreenshot = async () => {
-    await sendMessage('set:active-tab', tabId, 'background');
-    const imageUrl = await sendMessage(
-      'get:tab-screenshot',
+  return canvasToBase64(canvas, options);
+}
+
+export default async function ({ tabId, options, type, selector }) {
+  if (type === 'element') {
+    const imageUrl = await captureElementScreenshot({
+      tabId,
       options,
-      'background'
-    );
+      selector,
+    });
 
     return imageUrl;
-  };
+  }
+
+  document.body.classList.add('is-screenshotting');
+
+  const canvas = document.createElement('canvas');
+  const context = canvas.getContext('2d');
+  const maxCanvasSize = 32767;
+
+  const scrollElement = document.querySelector('.automa-scrollable-el');
+  let scrollableElement = scrollElement || findScrollableElement();
 
   if (!scrollableElement) {
-    const imageUrl = await takeScreenshot();
+    const imageUrl = await takeScreenshot(tabId, options);
 
     return imageUrl;
   }
@@ -102,7 +142,7 @@ export default async function ({ tabId, options }) {
   if (scrollableElement.tagName === 'HTML') scrollableElement = window;
 
   while (scrollPosition <= originalScrollHeight) {
-    const imageUrl = await takeScreenshot();
+    const imageUrl = await takeScreenshot(tabId, options);
 
     if (scrollPosition > 0 && !document.body.classList.contains('hide-fixed')) {
       document.body.classList.add('hide-fixed');
@@ -139,5 +179,5 @@ export default async function ({ tabId, options }) {
 
   scrollableElement.scrollTo(0, originalYPosition);
 
-  return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
+  return canvasToBase64(canvas, options);
 }

+ 7 - 1
src/locales/en/blocks.json

@@ -528,7 +528,13 @@
         "description": "Take a screenshot of current active tab",
         "imageQuality": "Image quality",
         "saveToColumn": "Insert screenshot to table",
-        "saveToComputer": "Save screenshot to computer"
+        "saveToComputer": "Save screenshot to computer",
+        "types": {
+          "title": "Take a screenshot of",
+          "page": "Page",
+          "fullpage": "Full page",
+          "element": "An element"
+        }
       },
       "switch-to": {
         "name": "Switch frame",

+ 2 - 0
src/utils/shared.js

@@ -212,12 +212,14 @@ export const tasks = {
     refDataKeys: ['fileName'],
     autocomplete: ['variableName'],
     data: {
+      description: '',
       disableBlock: false,
       fileName: '',
       ext: 'png',
       quality: 100,
       dataColumn: '',
       variableName: '',
+      selector: '',
       fullPage: false,
       saveToColumn: false,
       saveToComputer: true,