App.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <template>
  2. <div
  3. :class="{
  4. 'select-none': state.isDragging,
  5. 'bg-black bg-opacity-30': !state.hide,
  6. }"
  7. class="root fixed h-full w-full pointer-events-none top-0 text-black left-0"
  8. style="z-index: 99999999"
  9. >
  10. <div
  11. ref="cardEl"
  12. :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
  13. style="width: 320px"
  14. class="relative root-card bg-white shadow-xl z-50 pointer-events-auto rounded-lg"
  15. >
  16. <div
  17. class="absolute p-2 drag-button z-50 shadow-xl bg-white p-1 cursor-move rounded-lg"
  18. style="top: -15px; left: -15px"
  19. >
  20. <v-remixicon
  21. name="riDragMoveLine"
  22. @mousedown="state.isDragging = true"
  23. />
  24. </div>
  25. <div class="flex px-4 pt-4 items-center">
  26. <ui-tabs
  27. v-if="false"
  28. v-model="mainActiveTab"
  29. type="fill"
  30. class="main-tab"
  31. >
  32. <ui-tab value="selector"> Selector </ui-tab>
  33. <ui-tab value="workflow"> Workflow </ui-tab>
  34. </ui-tabs>
  35. <p class="text-lg font-semibold">Automa</p>
  36. <div class="flex-grow"></div>
  37. <button
  38. class="mr-2 hoverable p-1 rounded-md transition"
  39. @click.stop.prevent="state.hide = !state.hide"
  40. >
  41. <v-remixicon :name="state.hide ? 'riEyeOffLine' : 'riEyeLine'" />
  42. </button>
  43. <button
  44. class="hoverable p-1 rounded-md transition"
  45. @click.stop.prevent="destroy"
  46. >
  47. <v-remixicon name="riCloseLine" />
  48. </button>
  49. </div>
  50. <div class="p-4">
  51. <selector-query
  52. v-model:selectorType="state.selectorType"
  53. v-model:selectList="state.selectList"
  54. :selector="state.elSelector"
  55. :settings-active="state.showSettings"
  56. :selected-count="state.selectedElements.length"
  57. @settings="state.showSettings = $event"
  58. @selector="updateSelector"
  59. @parent="selectElementPath('up')"
  60. @child="selectElementPath('down')"
  61. />
  62. <selector-elements-detail
  63. v-if="
  64. !state.showSettings &&
  65. !state.hide &&
  66. state.selectedElements.length > 0
  67. "
  68. v-model:active-tab="state.activeTab"
  69. v-bind="{
  70. elSelector: state.elSelector,
  71. selectElements: state.selectElements,
  72. selectedElements: state.selectedElements,
  73. }"
  74. @highlight="toggleHighlightElement"
  75. @execute="state.isExecuting = $event"
  76. />
  77. <div v-if="state.showSettings && !state.hide" class="mt-4">
  78. <p class="font-semibold mb-4">Selector settings</p>
  79. <ul class="space-y-4">
  80. <li>
  81. <label class="flex items-center space-x-2">
  82. <ui-switch v-model="selectorSettings.idName" />
  83. <p>Include element id</p>
  84. </label>
  85. </li>
  86. <li>
  87. <label class="flex items-center space-x-2">
  88. <ui-switch v-model="selectorSettings.tagName" />
  89. <p>Include tag name</p>
  90. </label>
  91. </li>
  92. <li>
  93. <label class="flex items-center space-x-2">
  94. <ui-switch v-model="selectorSettings.className" />
  95. <p>Include class name</p>
  96. </label>
  97. </li>
  98. <li>
  99. <label class="flex items-center space-x-2">
  100. <ui-switch v-model="selectorSettings.attr" />
  101. <p>Include attributes</p>
  102. </label>
  103. <template v-if="selectorSettings.attr">
  104. <label
  105. class="ml-1 text-sm text-gray-600 mt-2 block"
  106. for="automa-attribute-names"
  107. >
  108. Attribute names
  109. </label>
  110. <ui-textarea
  111. id="automa-attribute-names"
  112. v-model="selectorSettings.attrNames"
  113. label="Attribute name"
  114. placeholder="data-testid, aria-label, type"
  115. />
  116. <span class="text-sm">
  117. Use commas to separate the attribute
  118. </span>
  119. </template>
  120. </li>
  121. </ul>
  122. </div>
  123. </div>
  124. </div>
  125. </div>
  126. <shared-element-selector
  127. :hide="state.hide"
  128. :disabled="state.hide"
  129. :list="state.selectList"
  130. :selector-type="state.selectorType"
  131. :selected-els="state.selectedElements"
  132. :selector-settings="getSelectorOptions(selectorSettings)"
  133. with-attributes
  134. @selected="onElementsSelected"
  135. />
  136. </template>
  137. <script setup>
  138. import {
  139. reactive,
  140. ref,
  141. watch,
  142. inject,
  143. onMounted,
  144. onBeforeUnmount,
  145. toRaw,
  146. } from 'vue';
  147. import browser from 'webextension-polyfill';
  148. import { debounce } from '@/utils/helper';
  149. import findSelector from '@/lib/findSelector';
  150. import FindElement from '@/utils/FindElement';
  151. import SelectorQuery from '@/components/content/selector/SelectorQuery.vue';
  152. import SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';
  153. import SelectorElementsDetail from '@/components/content/selector/SelectorElementsDetail.vue';
  154. import getSelectorOptions from './getSelectorOptions';
  155. import { getElementRect } from '../utils';
  156. const originalFontSize = document.documentElement.style.fontSize;
  157. const selectedElement = {
  158. path: [],
  159. pathIndex: 0,
  160. cache: new WeakMap(),
  161. };
  162. const rootElement = inject('rootElement');
  163. const cardEl = ref('cardEl');
  164. const mainActiveTab = ref('selector');
  165. const state = reactive({
  166. hide: false,
  167. elSelector: '',
  168. isDragging: false,
  169. selectList: false,
  170. isExecuting: false,
  171. selectElements: [],
  172. showSettings: false,
  173. selectorType: 'css',
  174. selectedElements: [],
  175. activeTab: 'attributes',
  176. });
  177. const cardRect = reactive({
  178. x: 0,
  179. y: 0,
  180. height: 0,
  181. width: 0,
  182. });
  183. const selectorSettings = reactive({
  184. idName: true,
  185. tagName: true,
  186. attr: true,
  187. className: true,
  188. attrNames: 'data-testid',
  189. });
  190. const cardElementObserver = new ResizeObserver(([entry]) => {
  191. const { height, width } = entry.contentRect;
  192. cardRect.width = width;
  193. cardRect.height = height;
  194. });
  195. const updateSelector = debounce((selector) => {
  196. let frameSelector;
  197. let elSelector = selector;
  198. if (selector.includes('|>')) {
  199. [frameSelector, elSelector] = selector.split(/\|>(.+)/);
  200. }
  201. const selectorType = state.selectorType === 'css' ? 'cssSelector' : 'xpath';
  202. try {
  203. if (frameSelector) {
  204. const frame = FindElement[selectorType]({
  205. selector: frameSelector,
  206. multiple: false,
  207. });
  208. if (!['IFRAME', 'FRAME'].includes(frame.tagName)) return;
  209. const { top, left } = frame.getBoundingClientRect();
  210. frame.contentWindow.postMessage(
  211. {
  212. selectorType,
  213. selector: elSelector,
  214. type: 'automa:find-element',
  215. frameRect: { top, left },
  216. },
  217. '*'
  218. );
  219. return;
  220. }
  221. const elements = FindElement[selectorType]({
  222. selector: elSelector,
  223. multiple: true,
  224. });
  225. state.selectedElements = Array.from(elements || []).map((el) =>
  226. getElementRect(el, true)
  227. );
  228. } catch (error) {
  229. console.error(error);
  230. state.selectedElements = [];
  231. }
  232. }, 200);
  233. function toggleHighlightElement({ index, highlight }) {
  234. state.selectedElements[index].highlight = highlight;
  235. }
  236. function onElementsSelected({ selector, elements, path, selectElements }) {
  237. if (path) {
  238. selectedElement.path = path;
  239. selectedElement.pathIndex = 0;
  240. }
  241. state.elSelector = selector;
  242. state.selectedElements = elements || [];
  243. state.selectElements = selectElements || [];
  244. }
  245. function onMousemove({ clientX, clientY }) {
  246. if (!state.isDragging) return;
  247. const height = window.innerHeight;
  248. const width = document.documentElement.clientWidth;
  249. if (clientY < 10) clientY = 10;
  250. else if (cardRect.height + clientY > height)
  251. clientY = height - cardRect.height;
  252. if (clientX < 10) clientX = 10;
  253. else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
  254. cardRect.x = clientX;
  255. cardRect.y = clientY;
  256. }
  257. function selectElementPath(type) {
  258. let pathIndex =
  259. type === 'up'
  260. ? selectedElement.pathIndex + 1
  261. : selectedElement.pathIndex - 1;
  262. let element = selectedElement.path[pathIndex];
  263. if ((type === 'up' && !element) || element?.tagName === 'BODY') return;
  264. if (type === 'down' && !element) {
  265. const previousElement = selectedElement.path[selectedElement.pathIndex];
  266. const childEl = Array.from(previousElement.children).find(
  267. (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
  268. );
  269. if (!childEl) return;
  270. element = childEl;
  271. selectedElement.path.unshift(childEl);
  272. pathIndex = 0;
  273. }
  274. selectedElement.pathIndex = pathIndex;
  275. state.selectedElements = [getElementRect(element, true)];
  276. state.elSelector = selectedElement.cache.has(element)
  277. ? selectedElement.cache.get(element)
  278. : findSelector(element, getSelectorOptions(selectorSettings));
  279. }
  280. function onMouseup() {
  281. if (state.isDragging) state.isDragging = false;
  282. }
  283. function onMessage({ data }) {
  284. if (data.type !== 'automa:selected-elements') return;
  285. state.selectedElements = data.elements;
  286. }
  287. function destroy() {
  288. rootElement.style.display = 'none';
  289. Object.assign(state, {
  290. hide: true,
  291. activeTab: '',
  292. elSelector: '',
  293. isDragging: false,
  294. isExecuting: false,
  295. hoveredElements: [],
  296. selectedElements: [],
  297. });
  298. const prevSelectedList = document.querySelectorAll('[automa-el-list]');
  299. prevSelectedList.forEach((element) => {
  300. element.removeAttribute('automa-el-list');
  301. });
  302. document.documentElement.style.fontSize = originalFontSize;
  303. }
  304. function attachListeners() {
  305. cardElementObserver.observe(cardEl.value);
  306. window.addEventListener('message', onMessage);
  307. window.addEventListener('mouseup', onMouseup);
  308. window.addEventListener('mousemove', onMousemove);
  309. }
  310. function detachListeners() {
  311. cardElementObserver.disconnect();
  312. window.removeEventListener('message', onMessage);
  313. window.removeEventListener('mouseup', onMouseup);
  314. window.removeEventListener('mousemove', onMousemove);
  315. }
  316. watch(
  317. () => state.isDragging,
  318. (value) => {
  319. document.body.toggleAttribute('automa-isDragging', value);
  320. }
  321. );
  322. watch(
  323. selectorSettings,
  324. (settings) => {
  325. browser.storage.local.set({
  326. selectorSettings: toRaw(settings),
  327. });
  328. },
  329. { deep: true }
  330. );
  331. onMounted(() => {
  332. browser.storage.local.get('selectorSettings').then((storage) => {
  333. const settings = storage.selectorSettings || {};
  334. Object.assign(selectorSettings, settings);
  335. });
  336. setTimeout(() => {
  337. const { height, width } = cardEl.value.getBoundingClientRect();
  338. cardRect.x = window.innerWidth - (width + 35);
  339. cardRect.y = 20;
  340. cardRect.width = width;
  341. cardRect.height = height;
  342. }, 500);
  343. attachListeners();
  344. });
  345. onBeforeUnmount(() => {
  346. detachListeners();
  347. });
  348. </script>
  349. <style>
  350. .root {
  351. font-size: 16px;
  352. z-index: 99999;
  353. line-height: 1.5 !important;
  354. font-family: 'Inter var', sans-serif;
  355. font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
  356. }
  357. .root-card:hover .drag-button {
  358. transform: scale(1);
  359. }
  360. .drag-button {
  361. transform: scale(0);
  362. transition: transform 200ms ease-in-out;
  363. }
  364. .main-tab {
  365. background-color: transparent !important;
  366. padding: 0 !important;
  367. }
  368. .main-tab .ui-tab.is-active.fill {
  369. @apply bg-accent text-white !important;
  370. }
  371. </style>