|
@@ -0,0 +1,331 @@
|
|
|
+<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 left-0"
|
|
|
+ style="z-index: 9999999999; font-family: Inter, sans-serif"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ ref="cardEl"
|
|
|
+ :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
|
|
|
+ class="
|
|
|
+ absolute
|
|
|
+ root-card
|
|
|
+ bg-white
|
|
|
+ shadow-xl
|
|
|
+ z-50
|
|
|
+ p-4
|
|
|
+ pointer-events-auto
|
|
|
+ rounded-lg
|
|
|
+ w-80
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="
|
|
|
+ absolute
|
|
|
+ p-2
|
|
|
+ drag-button
|
|
|
+ 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 items-center">
|
|
|
+ <p class="ml-1 text-lg font-semibold">Automa</p>
|
|
|
+ <div class="flex-grow"></div>
|
|
|
+ <ui-button icon class="mr-2" @click="state.hide = !state.hide">
|
|
|
+ <v-remixicon :name="state.hide ? 'riEyeOffLine' : 'riEyeLine'" />
|
|
|
+ </ui-button>
|
|
|
+ <ui-button icon @click="destroy">
|
|
|
+ <v-remixicon name="riCloseLine" />
|
|
|
+ </ui-button>
|
|
|
+ </div>
|
|
|
+ <app-selector
|
|
|
+ :selector="state.elSelector"
|
|
|
+ :selected-count="state.selectedElements.length"
|
|
|
+ @child="selectChildElement"
|
|
|
+ @parent="selectParentElement"
|
|
|
+ @change="updateSelectedElements"
|
|
|
+ />
|
|
|
+ <template v-if="!state.hide && state.selectedElements.length > 0">
|
|
|
+ <ui-tabs v-model="state.activeTab" class="mt-2" fill>
|
|
|
+ <ui-tab value="attributes"> Attributes </ui-tab>
|
|
|
+ <ui-tab value="blocks"> Blocks </ui-tab>
|
|
|
+ </ui-tabs>
|
|
|
+ <ui-tab-panels
|
|
|
+ v-model="state.activeTab"
|
|
|
+ class="overflow-y-auto scroll"
|
|
|
+ style="max-height: calc(100vh - 15rem)"
|
|
|
+ >
|
|
|
+ <ui-tab-panel value="attributes">
|
|
|
+ <app-element-attributes
|
|
|
+ :elements="state.selectedElements"
|
|
|
+ @highlight="
|
|
|
+ state.selectedElements[$event.index].highlight =
|
|
|
+ $event.highlight
|
|
|
+ "
|
|
|
+ />
|
|
|
+ </ui-tab-panel>
|
|
|
+ <ui-tab-panel value="blocks">
|
|
|
+ <app-blocks
|
|
|
+ :elements="state.selectedElements"
|
|
|
+ @update="updateCardSize"
|
|
|
+ />
|
|
|
+ </ui-tab-panel>
|
|
|
+ </ui-tab-panels>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <svg
|
|
|
+ v-if="!state.hide"
|
|
|
+ class="h-full w-full absolute top-0 pointer-events-none left-0 z-10"
|
|
|
+ >
|
|
|
+ <rect
|
|
|
+ v-bind="hoverElementRect"
|
|
|
+ stroke-width="2"
|
|
|
+ stroke="#fbbf24"
|
|
|
+ fill="rgba(251, 191, 36, 0.2)"
|
|
|
+ ></rect>
|
|
|
+ <rect
|
|
|
+ v-for="(item, index) in state.selectedElements"
|
|
|
+ v-bind="item"
|
|
|
+ :key="index"
|
|
|
+ :stroke="item.highlight ? '#2563EB' : '#f87171'"
|
|
|
+ :fill="
|
|
|
+ item.highlight ? 'rgb(37, 99, 235, 0.2)' : 'rgba(248, 113, 113, 0.2)'
|
|
|
+ "
|
|
|
+ stroke-width="2"
|
|
|
+ ></rect>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script setup>
|
|
|
+import { reactive, ref, watch, inject, nextTick } from 'vue';
|
|
|
+import { finder } from '@medv/finder';
|
|
|
+import { debounce } from '@/utils/helper';
|
|
|
+import AppBlocks from './AppBlocks.vue';
|
|
|
+import AppSelector from './AppSelector.vue';
|
|
|
+import AppElementAttributes from './AppElementAttributes.vue';
|
|
|
+
|
|
|
+const selectedElement = {
|
|
|
+ path: [],
|
|
|
+ pathIndex: 0,
|
|
|
+};
|
|
|
+let lastScrollPosY = window.scrollY;
|
|
|
+let lastScrollPosX = window.scrollX;
|
|
|
+
|
|
|
+const rootElement = inject('rootElement');
|
|
|
+
|
|
|
+const cardEl = ref('cardEl');
|
|
|
+const state = reactive({
|
|
|
+ hide: false,
|
|
|
+ elSelector: '',
|
|
|
+ isDragging: false,
|
|
|
+ activeTab: 'blocks',
|
|
|
+ selectedElements: [],
|
|
|
+});
|
|
|
+const hoverElementRect = reactive({
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ height: 0,
|
|
|
+ width: 0,
|
|
|
+});
|
|
|
+const cardRect = reactive({
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ height: 0,
|
|
|
+ width: 0,
|
|
|
+});
|
|
|
+
|
|
|
+function getElementRect(target) {
|
|
|
+ if (!target) return {};
|
|
|
+
|
|
|
+ const { x, y, height, width } = target.getBoundingClientRect();
|
|
|
+
|
|
|
+ return {
|
|
|
+ width: width + 4,
|
|
|
+ height: height + 4,
|
|
|
+ x: x - 2,
|
|
|
+ y: y - 2,
|
|
|
+ };
|
|
|
+}
|
|
|
+function updateSelectedElements(selector) {
|
|
|
+ state.elSelector = selector;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const elements = document.querySelectorAll(selector);
|
|
|
+
|
|
|
+ state.selectedElements = Array.from(elements).map((element) => {
|
|
|
+ const attributes = Array.from(element.attributes).map(
|
|
|
+ ({ name, value }) => ({ name, value })
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ element,
|
|
|
+ attributes,
|
|
|
+ highlight: false,
|
|
|
+ ...getElementRect(element),
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ state.selectedElements = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+function handleMouseMove({ clientX, clientY, target }) {
|
|
|
+ if (state.isDragging) {
|
|
|
+ const height = window.innerHeight;
|
|
|
+ const width = document.documentElement.clientWidth;
|
|
|
+
|
|
|
+ if (clientY < 10) clientY = 0;
|
|
|
+ else if (cardRect.height + clientY > height)
|
|
|
+ clientY = height - cardRect.height;
|
|
|
+
|
|
|
+ if (clientX < 10) clientX = 0;
|
|
|
+ else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
|
|
|
+
|
|
|
+ cardRect.x = clientX;
|
|
|
+ cardRect.y = clientY;
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state.hide || rootElement === target) return;
|
|
|
+
|
|
|
+ Object.assign(hoverElementRect, getElementRect(target));
|
|
|
+}
|
|
|
+function handleClick(event) {
|
|
|
+ if (event.target === rootElement || state.hide) return;
|
|
|
+
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+
|
|
|
+ const attributes = Array.from(event.target.attributes).map(
|
|
|
+ ({ name, value }) => ({ name, value })
|
|
|
+ );
|
|
|
+ state.selectedElements = [
|
|
|
+ {
|
|
|
+ ...getElementRect(event.target),
|
|
|
+ attributes,
|
|
|
+ element: event.target,
|
|
|
+ highlight: false,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ state.elSelector = finder(event.target);
|
|
|
+
|
|
|
+ selectedElement.index = 0;
|
|
|
+ selectedElement.path = event.path;
|
|
|
+}
|
|
|
+function selectChildElement() {
|
|
|
+ if (selectedElement.path.length === 0 || state.hide) return;
|
|
|
+
|
|
|
+ const currentEl = selectedElement.path[selectedElement.pathIndex];
|
|
|
+ let childElement = currentEl;
|
|
|
+
|
|
|
+ if (selectedElement.pathIndex <= 0) {
|
|
|
+ const childEl = Array.from(currentEl.children).find(
|
|
|
+ (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (currentEl.childElementCount === 0 || currentEl === childEl) return;
|
|
|
+
|
|
|
+ childElement = childEl;
|
|
|
+ selectedElement.path.unshift(childEl);
|
|
|
+ } else {
|
|
|
+ selectedElement.pathIndex -= 1;
|
|
|
+ childElement = selectedElement.path[selectedElement.pathIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ updateSelectedElements(finder(childElement));
|
|
|
+}
|
|
|
+function selectParentElement() {
|
|
|
+ if (selectedElement.path.length === 0 || state.hide) return;
|
|
|
+
|
|
|
+ const parentElement = selectedElement.path[selectedElement.pathIndex];
|
|
|
+
|
|
|
+ if (parentElement.tagName === 'HTML') return;
|
|
|
+
|
|
|
+ selectedElement.pathIndex += 1;
|
|
|
+
|
|
|
+ updateSelectedElements(finder(parentElement));
|
|
|
+}
|
|
|
+function handleMouseUp() {
|
|
|
+ if (state.isDragging) state.isDragging = false;
|
|
|
+}
|
|
|
+function updateCardSize() {
|
|
|
+ setTimeout(() => {
|
|
|
+ cardRect.height = cardEl.value.getBoundingClientRect().height;
|
|
|
+ }, 250);
|
|
|
+}
|
|
|
+const handleScroll = debounce(() => {
|
|
|
+ if (state.hide) return;
|
|
|
+
|
|
|
+ const yPos = window.scrollY - lastScrollPosY;
|
|
|
+ const xPos = window.scrollX - lastScrollPosX;
|
|
|
+
|
|
|
+ state.selectedElements.forEach((_, index) => {
|
|
|
+ state.selectedElements[index].x -= xPos;
|
|
|
+ state.selectedElements[index].y -= yPos;
|
|
|
+ });
|
|
|
+
|
|
|
+ hoverElementRect.x -= xPos;
|
|
|
+ hoverElementRect.y -= yPos;
|
|
|
+
|
|
|
+ lastScrollPosX = window.scrollX;
|
|
|
+ lastScrollPosY = window.scrollY;
|
|
|
+}, 100);
|
|
|
+function destroy() {
|
|
|
+ window.removeEventListener('scroll', handleScroll);
|
|
|
+ window.removeEventListener('mouseup', handleMouseUp);
|
|
|
+ window.removeEventListener('mousemove', handleMouseMove);
|
|
|
+ document.removeEventListener('click', handleClick, true);
|
|
|
+
|
|
|
+ const automaElements = document.querySelectorAll('automa-element-selector');
|
|
|
+ automaElements.forEach((element) => {
|
|
|
+ element.remove();
|
|
|
+ });
|
|
|
+
|
|
|
+ rootElement.remove();
|
|
|
+}
|
|
|
+
|
|
|
+window.addEventListener('scroll', handleScroll);
|
|
|
+window.addEventListener('mouseup', handleMouseUp);
|
|
|
+window.addEventListener('mousemove', handleMouseMove);
|
|
|
+document.addEventListener('click', handleClick, true);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => state.isDragging,
|
|
|
+ (value) => {
|
|
|
+ document.body.toggleAttribute('automa-isDragging', value);
|
|
|
+ }
|
|
|
+);
|
|
|
+watch(() => [state.elSelector, state.activeTab, state.hide], updateCardSize);
|
|
|
+
|
|
|
+nextTick(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ const { height, width } = cardEl.value.getBoundingClientRect();
|
|
|
+
|
|
|
+ cardRect.x = window.innerWidth - (width + 35);
|
|
|
+ cardRect.y = 20;
|
|
|
+ cardRect.width = width;
|
|
|
+ cardRect.height = height;
|
|
|
+ }, 250);
|
|
|
+});
|
|
|
+</script>
|
|
|
+<style>
|
|
|
+.drag-button {
|
|
|
+ transform: scale(0);
|
|
|
+ transition: transform 200ms ease-in-out;
|
|
|
+}
|
|
|
+.root-card:hover .drag-button {
|
|
|
+ transform: scale(1);
|
|
|
+}
|
|
|
+</style>
|