123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- <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 text-black left-0"
- style="z-index: 99999999"
- >
- <div
- ref="cardEl"
- :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
- style="width: 320px"
- class="relative root-card bg-white shadow-xl z-50 pointer-events-auto rounded-lg"
- >
- <div
- class="absolute p-2 drag-button z-50 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 px-4 pt-4 items-center">
- <ui-tabs
- v-if="false"
- v-model="mainActiveTab"
- type="fill"
- class="main-tab"
- >
- <ui-tab value="selector"> Selector </ui-tab>
- <ui-tab value="workflow"> Workflow </ui-tab>
- </ui-tabs>
- <p class="text-lg font-semibold">Automa</p>
- <div class="flex-grow"></div>
- <button
- class="mr-2 hoverable p-1 rounded-md transition"
- @click.stop.prevent="state.hide = !state.hide"
- >
- <v-remixicon :name="state.hide ? 'riEyeOffLine' : 'riEyeLine'" />
- </button>
- <button
- class="hoverable p-1 rounded-md transition"
- @click.stop.prevent="destroy"
- >
- <v-remixicon name="riCloseLine" />
- </button>
- </div>
- <div class="p-4">
- <selector-query
- v-model:selectorType="state.selectorType"
- v-model:selectList="state.selectList"
- :selector="state.elSelector"
- :settings-active="state.showSettings"
- :selected-count="state.selectedElements.length"
- @settings="state.showSettings = $event"
- @selector="updateSelector"
- @parent="selectElementPath('up')"
- @child="selectElementPath('down')"
- />
- <selector-elements-detail
- v-if="
- !state.showSettings &&
- !state.hide &&
- state.selectedElements.length > 0
- "
- v-model:active-tab="state.activeTab"
- v-bind="{
- elSelector: state.elSelector,
- selectElements: state.selectElements,
- selectedElements: state.selectedElements,
- }"
- @highlight="toggleHighlightElement"
- @execute="state.isExecuting = $event"
- />
- <div v-if="state.showSettings && !state.hide" class="mt-4">
- <p class="font-semibold mb-4">Selector settings</p>
- <ul class="space-y-4">
- <li>
- <label class="flex items-center space-x-2">
- <ui-switch v-model="selectorSettings.idName" />
- <p>Include element id</p>
- </label>
- </li>
- <li>
- <label class="flex items-center space-x-2">
- <ui-switch v-model="selectorSettings.tagName" />
- <p>Include tag name</p>
- </label>
- </li>
- <li>
- <label class="flex items-center space-x-2">
- <ui-switch v-model="selectorSettings.className" />
- <p>Include class name</p>
- </label>
- </li>
- <li>
- <label class="flex items-center space-x-2">
- <ui-switch v-model="selectorSettings.attr" />
- <p>Include attributes</p>
- </label>
- <template v-if="selectorSettings.attr">
- <label
- class="ml-1 text-sm text-gray-600 mt-2 block"
- for="automa-attribute-names"
- >
- Attribute names
- </label>
- <ui-textarea
- id="automa-attribute-names"
- v-model="selectorSettings.attrNames"
- label="Attribute name"
- placeholder="data-testid, aria-label, type"
- />
- <span class="text-sm">
- Use commas to separate the attribute
- </span>
- </template>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <shared-element-selector
- :hide="state.hide"
- :disabled="state.hide"
- :list="state.selectList"
- :selector-type="state.selectorType"
- :selected-els="state.selectedElements"
- :selector-settings="getSelectorOptions(selectorSettings)"
- with-attributes
- @selected="onElementsSelected"
- />
- </template>
- <script setup>
- import {
- reactive,
- ref,
- watch,
- inject,
- onMounted,
- onBeforeUnmount,
- toRaw,
- } from 'vue';
- import browser from 'webextension-polyfill';
- import { debounce } from '@/utils/helper';
- import findSelector from '@/lib/findSelector';
- import FindElement from '@/utils/FindElement';
- import SelectorQuery from '@/components/content/selector/SelectorQuery.vue';
- import SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';
- import SelectorElementsDetail from '@/components/content/selector/SelectorElementsDetail.vue';
- import getSelectorOptions from './getSelectorOptions';
- import { getElementRect } from '../utils';
- const originalFontSize = document.documentElement.style.fontSize;
- const selectedElement = {
- path: [],
- pathIndex: 0,
- cache: new WeakMap(),
- };
- const rootElement = inject('rootElement');
- const cardEl = ref('cardEl');
- const mainActiveTab = ref('selector');
- const state = reactive({
- hide: false,
- elSelector: '',
- isDragging: false,
- selectList: false,
- isExecuting: false,
- selectElements: [],
- showSettings: false,
- selectorType: 'css',
- selectedElements: [],
- activeTab: 'attributes',
- });
- const cardRect = reactive({
- x: 0,
- y: 0,
- height: 0,
- width: 0,
- });
- const selectorSettings = reactive({
- idName: true,
- tagName: true,
- attr: true,
- className: true,
- attrNames: 'data-testid',
- });
- const cardElementObserver = new ResizeObserver(([entry]) => {
- const { height, width } = entry.contentRect;
- cardRect.width = width;
- cardRect.height = height;
- });
- const updateSelector = debounce((selector) => {
- let frameSelector;
- let elSelector = selector;
- if (selector.includes('|>')) {
- [frameSelector, elSelector] = selector.split(/\|>(.+)/);
- }
- const selectorType = state.selectorType === 'css' ? 'cssSelector' : 'xpath';
- try {
- if (frameSelector) {
- const frame = FindElement[selectorType]({
- selector: frameSelector,
- multiple: false,
- });
- if (!['IFRAME', 'FRAME'].includes(frame.tagName)) return;
- const { top, left } = frame.getBoundingClientRect();
- frame.contentWindow.postMessage(
- {
- selectorType,
- selector: elSelector,
- type: 'automa:find-element',
- frameRect: { top, left },
- },
- '*'
- );
- return;
- }
- const elements = FindElement[selectorType]({
- selector: elSelector,
- multiple: true,
- });
- state.selectedElements = Array.from(elements || []).map((el) =>
- getElementRect(el, true)
- );
- } catch (error) {
- console.error(error);
- state.selectedElements = [];
- }
- }, 200);
- function toggleHighlightElement({ index, highlight }) {
- state.selectedElements[index].highlight = highlight;
- }
- function onElementsSelected({ selector, elements, path, selectElements }) {
- if (path) {
- selectedElement.path = path;
- selectedElement.pathIndex = 0;
- }
- state.elSelector = selector;
- state.selectedElements = elements || [];
- state.selectElements = selectElements || [];
- }
- function onMousemove({ clientX, clientY }) {
- if (!state.isDragging) return;
- const height = window.innerHeight;
- const width = document.documentElement.clientWidth;
- if (clientY < 10) clientY = 10;
- else if (cardRect.height + clientY > height)
- clientY = height - cardRect.height;
- if (clientX < 10) clientX = 10;
- else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
- cardRect.x = clientX;
- cardRect.y = clientY;
- }
- function selectElementPath(type) {
- let pathIndex =
- type === 'up'
- ? selectedElement.pathIndex + 1
- : selectedElement.pathIndex - 1;
- let element = selectedElement.path[pathIndex];
- if ((type === 'up' && !element) || element?.tagName === 'BODY') return;
- if (type === 'down' && !element) {
- const previousElement = selectedElement.path[selectedElement.pathIndex];
- const childEl = Array.from(previousElement.children).find(
- (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
- );
- if (!childEl) return;
- element = childEl;
- selectedElement.path.unshift(childEl);
- pathIndex = 0;
- }
- selectedElement.pathIndex = pathIndex;
- state.selectedElements = [getElementRect(element, true)];
- state.elSelector = selectedElement.cache.has(element)
- ? selectedElement.cache.get(element)
- : findSelector(element, getSelectorOptions(selectorSettings));
- }
- function onMouseup() {
- if (state.isDragging) state.isDragging = false;
- }
- function onMessage({ data }) {
- if (data.type !== 'automa:selected-elements') return;
- state.selectedElements = data.elements;
- }
- function destroy() {
- rootElement.style.display = 'none';
- Object.assign(state, {
- hide: true,
- activeTab: '',
- elSelector: '',
- isDragging: false,
- isExecuting: false,
- hoveredElements: [],
- selectedElements: [],
- });
- const prevSelectedList = document.querySelectorAll('[automa-el-list]');
- prevSelectedList.forEach((element) => {
- element.removeAttribute('automa-el-list');
- });
- document.documentElement.style.fontSize = originalFontSize;
- }
- function attachListeners() {
- cardElementObserver.observe(cardEl.value);
- window.addEventListener('message', onMessage);
- window.addEventListener('mouseup', onMouseup);
- window.addEventListener('mousemove', onMousemove);
- }
- function detachListeners() {
- cardElementObserver.disconnect();
- window.removeEventListener('message', onMessage);
- window.removeEventListener('mouseup', onMouseup);
- window.removeEventListener('mousemove', onMousemove);
- }
- watch(
- () => state.isDragging,
- (value) => {
- document.body.toggleAttribute('automa-isDragging', value);
- }
- );
- watch(
- selectorSettings,
- (settings) => {
- browser.storage.local.set({
- selectorSettings: toRaw(settings),
- });
- },
- { deep: true }
- );
- onMounted(() => {
- browser.storage.local.get('selectorSettings').then((storage) => {
- const settings = storage.selectorSettings || {};
- Object.assign(selectorSettings, settings);
- });
- setTimeout(() => {
- const { height, width } = cardEl.value.getBoundingClientRect();
- cardRect.x = window.innerWidth - (width + 35);
- cardRect.y = 20;
- cardRect.width = width;
- cardRect.height = height;
- }, 500);
- attachListeners();
- });
- onBeforeUnmount(() => {
- detachListeners();
- });
- </script>
- <style>
- .root {
- font-size: 16px;
- z-index: 99999;
- line-height: 1.5 !important;
- font-family: 'Inter var', sans-serif;
- font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
- }
- .root-card:hover .drag-button {
- transform: scale(1);
- }
- .drag-button {
- transform: scale(0);
- transition: transform 200ms ease-in-out;
- }
- .main-tab {
- background-color: transparent !important;
- padding: 0 !important;
- }
- .main-tab .ui-tab.is-active.fill {
- @apply bg-accent text-white !important;
- }
- </style>
|