App.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <template>
  2. <div
  3. ref="rootEl"
  4. class="content rounded-lg bg-white shadow-xl fixed overflow-hidden text-black top-0 left-0"
  5. style="z-index: 99999999; font-size: 16px"
  6. :style="{
  7. transform: `translate(${draggingState.xPos}px, ${draggingState.yPos}px)`,
  8. }"
  9. >
  10. <div
  11. class="px-4 py-2 hoverable flex items-center transition select-none"
  12. :class="[draggingState.dragging ? 'cursor-grabbing' : 'cursor-grab']"
  13. @mouseup="toggleDragging(false, $event)"
  14. @mousedown="toggleDragging(true, $event)"
  15. >
  16. <span
  17. class="relative cursor-pointer rounded-full bg-red-400 flex items-center justify-center"
  18. style="height: 24px; width: 24px"
  19. title="Stop recording"
  20. @click="stopRecording"
  21. >
  22. <v-remixicon
  23. name="riRecordCircleLine"
  24. class="relative z-10"
  25. size="20"
  26. />
  27. <span
  28. class="absolute animate-ping bg-red-400 rounded-full"
  29. style="height: 80%; width: 80%; animation-duration: 1.3s"
  30. ></span>
  31. </span>
  32. <p class="font-semibold ml-2">Automa</p>
  33. <div class="flex-grow"></div>
  34. <v-remixicon name="mdiDragHorizontal" />
  35. </div>
  36. <div class="p-4">
  37. <template v-if="selectState.status === 'idle'">
  38. <button
  39. class="px-4 py-2 rounded-lg bg-input transition w-full"
  40. @click="startSelecting()"
  41. >
  42. Select element
  43. </button>
  44. <button
  45. class="px-4 py-2 rounded-lg bg-input transition w-full mt-2"
  46. @click="startSelecting(true)"
  47. >
  48. Select list element
  49. </button>
  50. </template>
  51. <div v-else-if="selectState.status === 'selecting'" class="leading-tight">
  52. <p v-if="selectState.selectedElements.length === 0">
  53. Select an element by clicking on it
  54. </p>
  55. <template v-else>
  56. <template v-if="selectState.list && !selectState.listId">
  57. <label for="list-id" class="ml-1" style="font-size: 14px">
  58. Element list id
  59. </label>
  60. <input
  61. id="list-id"
  62. v-model="tempListId"
  63. placeholder="listId"
  64. class="px-4 py-2 rounded-lg bg-input w-full"
  65. @keyup.enter="saveElementListId"
  66. />
  67. <button
  68. :class="{ 'opacity-75 pointer-events-none': !tempListId }"
  69. class="px-4 py-2 w-full bg-accent rounded-lg mt-2 text-white"
  70. @click="saveElementListId"
  71. >
  72. Save
  73. </button>
  74. </template>
  75. <template v-else>
  76. <div class="flex items-center space-x-2 w-full">
  77. <input
  78. :value="selectState.childSelector || selectState.parentSelector"
  79. class="px-4 py-2 rounded-lg bg-input w-full"
  80. readonly
  81. />
  82. <template
  83. v-if="
  84. !selectState.list && !selectState.childSelector.includes('|>')
  85. "
  86. >
  87. <button @click="selectElementPath('up')">
  88. <v-remixicon name="riArrowLeftLine" rotate="90" />
  89. </button>
  90. <button @click="selectElementPath('down')">
  91. <v-remixicon name="riArrowLeftLine" rotate="-90" />
  92. </button>
  93. </template>
  94. </div>
  95. <select
  96. v-model="addBlockState.activeBlock"
  97. class="px-4 py-2 rounded-lg bg-input w-full mt-2"
  98. >
  99. <option value="" disabled selected>Select what to do</option>
  100. <option
  101. v-for="block in addBlockState.blocks"
  102. :key="block"
  103. :value="block"
  104. >
  105. {{ tasks[block].name }}
  106. </option>
  107. </select>
  108. <template
  109. v-if="
  110. ['get-text', 'attribute-value'].includes(
  111. addBlockState.activeBlock
  112. )
  113. "
  114. >
  115. <select
  116. v-if="addBlockState.activeBlock === 'attribute-value'"
  117. v-model="addBlockState.activeAttr"
  118. class="px-4 py-2 rounded-lg bg-input mt-2 block w-full"
  119. >
  120. <option value="" selected disabled>Select attribute</option>
  121. <option
  122. v-for="(value, name) in addBlockState.attributes"
  123. :key="name"
  124. :value="name"
  125. >
  126. {{ name }}({{ value.slice(0, 64) }})
  127. </option>
  128. </select>
  129. <label
  130. for="variable-name"
  131. class="text-sm ml-2 text-gray-600 mt-2"
  132. >
  133. Assign to variable
  134. </label>
  135. <input
  136. id="variable-name"
  137. v-model="addBlockState.varName"
  138. placeholder="Variable name"
  139. class="px-4 py-2 w-full rounded-lg bg-input"
  140. />
  141. <label
  142. for="select-column"
  143. class="text-sm ml-2 text-gray-600 mt-2"
  144. >
  145. Insert to table
  146. </label>
  147. <select
  148. id="select-column"
  149. v-model="addBlockState.column"
  150. class="block w-full rounded-lg px-4 py-2 bg-input"
  151. >
  152. <option value="" selected>Select column [none]</option>
  153. <option
  154. v-for="column in addBlockState.workflowColumns"
  155. :key="column.id"
  156. :value="column.id"
  157. >
  158. {{ column.name }}
  159. </option>
  160. </select>
  161. </template>
  162. <button
  163. v-if="addBlockState.activeBlock"
  164. :class="{
  165. 'pointer-events-none opacity-75':
  166. addBlockState.activeBlock === 'attribute-value' &&
  167. !addBlockState.activeAttr,
  168. }"
  169. class="px-4 py-2 rounded-lg block w-full bg-accent text-white mt-4"
  170. @click="addFlowItem"
  171. >
  172. Save
  173. </button>
  174. </template>
  175. </template>
  176. <p class="mt-4" style="font-size: 14px">
  177. Press <kbd class="p-1 rounded-md bg-box-transparent">Esc</kbd> to
  178. cancel
  179. </p>
  180. </div>
  181. </div>
  182. </div>
  183. <shared-element-selector
  184. v-if="selectState.isSelecting"
  185. :selected-els="selectState.selectedElements"
  186. with-attributes
  187. only-in-list
  188. :list="selectState.list"
  189. :pause="
  190. selectState.selectedElements.length > 0 &&
  191. selectState.list &&
  192. !selectState.listId
  193. "
  194. @selected="onElementsSelected"
  195. />
  196. </template>
  197. <script setup>
  198. import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue';
  199. import browser from 'webextension-polyfill';
  200. import { toCamelCase } from '@/utils/helper';
  201. import { tasks } from '@/utils/shared';
  202. import findSelector from '@/lib/findSelector';
  203. import SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';
  204. import { getElementRect } from '../../utils';
  205. import addBlock from './addBlock';
  206. const mouseRelativePos = { x: 0, y: 0 };
  207. const elementsPath = {
  208. path: [],
  209. cache: new WeakMap(),
  210. };
  211. const rootEl = ref(null);
  212. const tempListId = ref('');
  213. const selectState = reactive({
  214. listId: '',
  215. list: false,
  216. pathIndex: 0,
  217. status: 'idle',
  218. isInList: false,
  219. listSelector: '',
  220. childSelector: '',
  221. isSelecting: false,
  222. selectedElements: [],
  223. });
  224. const draggingState = reactive({
  225. yPos: 20,
  226. dragging: false,
  227. xPos: window.innerWidth - 300,
  228. });
  229. const addBlockState = reactive({
  230. blocks: [],
  231. column: '',
  232. varName: '',
  233. attributes: [],
  234. activeAttr: '',
  235. activeBlock: '',
  236. workflowColumns: [],
  237. });
  238. const blocksList = {
  239. IMG: ['save-assets', 'attribute-value'],
  240. VIDEO: ['save-assets', 'attribute-value'],
  241. AUDIO: ['save-assets', 'attribute-value'],
  242. default: ['get-text', 'attribute-value'],
  243. };
  244. function stopRecording() {
  245. browser.runtime.sendMessage({
  246. type: 'background--recording:stop',
  247. });
  248. }
  249. function getElementBlocks(element) {
  250. if (!element) return;
  251. const elTag = element.tagName;
  252. const blocks = [...(blocksList[elTag] || blocksList.default)];
  253. const attrBlockIndex = blocks.indexOf('attribute-value');
  254. if (attrBlockIndex !== -1) {
  255. addBlockState.attributes = element.attributes;
  256. }
  257. addBlockState.blocks = blocks;
  258. }
  259. function onElementsSelected({ selector, elements, path }) {
  260. if (path) {
  261. elementsPath.path = path;
  262. selectState.pathIndex = 0;
  263. }
  264. getElementBlocks(elements[0]);
  265. selectState.selectedElements = elements;
  266. if (selectState.list) {
  267. if (!selectState.listSelector) {
  268. selectState.isInList = false;
  269. selectState.listSelector = selector;
  270. selectState.childSelector = selector;
  271. return;
  272. }
  273. selectState.isInList = true;
  274. selector = selector.replace(selectState.listSelector, '');
  275. }
  276. selectState.childSelector = selector;
  277. }
  278. function addFlowItem() {
  279. const saveData = Boolean(addBlockState.column);
  280. const assignVariable = Boolean(addBlockState.varName);
  281. const block = {
  282. id: addBlockState.activeBlock,
  283. data: {
  284. saveData,
  285. assignVariable,
  286. waitForSelector: true,
  287. dataColumn: addBlockState.column,
  288. variableName: addBlockState.varName,
  289. selector: selectState.list
  290. ? selectState.listSelector
  291. : selectState.childSelector,
  292. },
  293. };
  294. if (selectState.list) {
  295. if (selectState.isInList || selectState.listId) {
  296. const childSelector = selectState.isInList
  297. ? selectState.childSelector
  298. : '';
  299. block.data.selector = `{{loopData@${selectState.listId}}} ${childSelector}`;
  300. } else {
  301. block.data.multiple = true;
  302. }
  303. }
  304. if (addBlockState.activeBlock === 'attribute-value') {
  305. block.data.attributeName = addBlockState.activeAttr;
  306. }
  307. addBlock(block).then(() => {
  308. addBlockState.column = '';
  309. addBlockState.varName = '';
  310. addBlockState.activeAttr = '';
  311. });
  312. }
  313. function selectElementPath(type) {
  314. let pathIndex =
  315. type === 'up' ? selectState.pathIndex + 1 : selectState.pathIndex - 1;
  316. let element = elementsPath.path[pathIndex];
  317. if ((type === 'up' && !element) || element?.tagName === 'BODY') return;
  318. if (type === 'down' && !element) {
  319. const previousElement = elementsPath.path[selectState.pathIndex];
  320. const childEl = Array.from(previousElement.children).find(
  321. (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
  322. );
  323. if (!childEl) return;
  324. element = childEl;
  325. elementsPath.path.unshift(childEl);
  326. pathIndex = 0;
  327. }
  328. selectState.pathIndex = pathIndex;
  329. selectState.selectedElements = [getElementRect(element)];
  330. selectState.childSelector = elementsPath.cache.has(element)
  331. ? elementsPath.cache.get(element)
  332. : findSelector(element);
  333. }
  334. function clearSelectState() {
  335. if (selectState.list && selectState.listId) {
  336. addBlock({
  337. id: 'loop-breakpoint',
  338. description: selectState.listId,
  339. data: {
  340. loopId: selectState.listId,
  341. },
  342. });
  343. }
  344. selectState.listId = '';
  345. selectState.list = false;
  346. selectState.status = 'idle';
  347. selectState.listSelector = '';
  348. selectState.childSelector = '';
  349. selectState.parentSelector = '';
  350. selectState.isSelecting = false;
  351. selectState.selectedElements = [];
  352. const selectedList = document.querySelectorAll('[automa-el-list]');
  353. selectedList.forEach((element) => {
  354. element.removeAttribute('automa-el-list');
  355. });
  356. const frameElements = document.querySelectorAll('iframe, frame');
  357. frameElements.forEach((element) => {
  358. element.contentWindow.postMessage(
  359. {
  360. type: 'automa:reset-element-selector',
  361. },
  362. '*'
  363. );
  364. });
  365. document.body.removeAttribute('automa-selecting');
  366. }
  367. function saveElementListId() {
  368. if (!tempListId.value) return;
  369. selectState.listId = toCamelCase(tempListId.value);
  370. tempListId.value = '';
  371. addBlock({
  372. id: 'loop-data',
  373. description: selectState.listId,
  374. data: {
  375. loopThrough: 'elements',
  376. loopId: selectState.listId,
  377. elementSelector: selectState.listSelector,
  378. },
  379. });
  380. }
  381. function toggleDragging(value, event) {
  382. if (value) {
  383. const bounds = rootEl.value.getBoundingClientRect();
  384. const y = event.clientY - bounds.top;
  385. const x = event.clientX - bounds.left;
  386. mouseRelativePos.x = x;
  387. mouseRelativePos.y = y;
  388. } else {
  389. mouseRelativePos.x = 0;
  390. mouseRelativePos.y = 0;
  391. }
  392. draggingState.dragging = value;
  393. }
  394. function onKeyup({ key }) {
  395. if (key !== 'Escape') return;
  396. clearSelectState();
  397. window.removeEventListener('keyup', onKeyup);
  398. }
  399. function startSelecting(list = false) {
  400. selectState.list = list;
  401. selectState.isSelecting = true;
  402. selectState.status = 'selecting';
  403. document.body.setAttribute('automa-selecting', '');
  404. window.addEventListener('keyup', onKeyup);
  405. }
  406. function onMousemove({ clientX, clientY }) {
  407. if (!draggingState.dragging) return;
  408. draggingState.xPos = clientX - mouseRelativePos.x;
  409. draggingState.yPos = clientY - mouseRelativePos.y;
  410. }
  411. function attachListeners() {
  412. window.addEventListener('mousemove', onMousemove);
  413. }
  414. function detachListeners() {
  415. window.removeEventListener('keyup', onKeyup);
  416. window.removeEventListener('mousemove', onMousemove);
  417. }
  418. watch(
  419. () => selectState.selectedElements,
  420. () => {
  421. addBlockState.column = '';
  422. addBlockState.varName = '';
  423. addBlockState.activeBlock = '';
  424. }
  425. );
  426. onMounted(() => {
  427. attachListeners();
  428. browser.storage.local
  429. .get(['recording', 'workflows'])
  430. .then(({ recording, workflows }) => {
  431. const workflow = Object.values(workflows).find(
  432. ({ id }) => recording.workflowId === id
  433. );
  434. addBlockState.workflowColumns = workflow?.table || [];
  435. });
  436. });
  437. onBeforeUnmount(detachListeners);
  438. </script>