Procházet zdrojové kódy

feat: select and move multiple blocks

Ahmad Kholid před 3 roky
rodič
revize
0efb452583

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "@tiptap/extension-placeholder": "^2.0.0-beta.48",
     "@tiptap/starter-kit": "^2.0.0-beta.181",
     "@tiptap/vue-3": "^2.0.0-beta.90",
+    "@viselect/vanilla": "^3.0.0-beta.13",
     "@vuex-orm/core": "^0.36.4",
     "compare-versions": "^4.1.2",
     "crypto-js": "^4.1.1",

+ 3 - 1
src/assets/css/drawflow.css

@@ -1,3 +1,4 @@
+.drawflow-node.selected-list .menu,
 .drawflow-node.selected .menu,
 .drawflow-node .block-base:hover .menu {
   @apply translate-y-11;
@@ -34,7 +35,8 @@
   @apply rounded-lg transition ring-2 ring-transparent duration-200 shadow-lg;
 }
 
-.drawflow .drawflow-node.selected {
+.drawflow .drawflow-node.selected,
+.drawflow .drawflow-node.selected-list {
   @apply ring-accent;
 }
 

+ 172 - 0
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -79,6 +79,7 @@ import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { compare } from 'compare-versions';
 import defu from 'defu';
+import SelectionArea from '@viselect/vanilla';
 import emitter from '@/lib/mitt';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { tasks } from '@/utils/shared';
@@ -132,6 +133,11 @@ export default {
       ],
     };
 
+    let activeNode = null;
+    let hasDragged = false;
+    let isDragging = false;
+    let selectedElements = [];
+
     const editor = shallowRef(null);
     const contextMenu = reactive({
       items: [],
@@ -354,6 +360,60 @@ export default {
 
       localStorage.setItem('editor-states', JSON.stringify(editorStates));
     }
+    function initSelectArea() {
+      const selection = new SelectionArea({
+        container: '#drawflow',
+        startareas: ['#drawflow'],
+        boundaries: ['#drawflow'],
+        selectables: ['.drawflow-node'],
+        features: {
+          singleTap: {
+            allow: false,
+          },
+        },
+      });
+
+      selection.on('beforestart', ({ event }) => {
+        if (!event.ctrlKey) return false;
+
+        editor.value.editor_mode = 'fixed';
+        editor.value.editor_selected = false;
+
+        return true;
+      });
+      selection.on('move', () => {
+        hasDragged = true;
+      });
+      selection.on('stop', (event) => {
+        event.store.selected.forEach((el) => {
+          const isExists = selectedElements.some((item) =>
+            item.el.isEqualNode(el)
+          );
+
+          if (isExists) return;
+
+          el.classList.toggle('selected-list', true);
+
+          selectedElements.push({
+            el,
+            id: el.id.slice(5),
+            posY: parseInt(el.style.top, 10),
+            posX: parseInt(el.style.left, 10),
+          });
+        });
+
+        setTimeout(() => {
+          hasDragged = false;
+        }, 500);
+      });
+    }
+    function clearSelectedElements() {
+      selectedElements.forEach(({ el }) => {
+        el.classList.remove('selected-list');
+      });
+      selectedElements = [];
+      activeNode = null;
+    }
 
     useShortcut('editor:duplicate-block', () => {
       const selectedElement = document.querySelector('.drawflow-node.selected');
@@ -369,6 +429,84 @@ export default {
       const context = getCurrentInstance().appContext.app._context;
       const element = document.querySelector('#drawflow');
 
+      element.addEventListener('mousedown', ({ target }) => {
+        const nodeEl = target.closest('.drawflow-node');
+        if (!nodeEl) return;
+
+        if (nodeEl.classList.contains('selected-list')) {
+          activeNode = {
+            el: nodeEl,
+            id: nodeEl.id.slice(5),
+            posY: parseInt(nodeEl.style.top, 10),
+            posX: parseInt(nodeEl.style.left, 10),
+          };
+        }
+
+        isDragging = true;
+      });
+      element.addEventListener('mouseup', ({ target }) => {
+        editor.value.editor_mode = 'edit';
+
+        const isNodeEl = target.closest('.drawflow-node');
+        if (!isNodeEl) return;
+
+        const getPosition = (el) => {
+          return {
+            posY: parseInt(el.style.top, 10),
+            posX: parseInt(el.style.left, 10),
+          };
+        };
+
+        selectedElements.forEach(({ el }, index) => {
+          Object.assign(selectedElements[index], getPosition(el));
+        });
+
+        if (activeNode) Object.assign(activeNode, getPosition(activeNode.el));
+
+        isDragging = false;
+      });
+      element.addEventListener('click', ({ ctrlKey, target }) => {
+        const nodeEl = target.closest('.drawflow-node');
+        if (!nodeEl) {
+          if (!hasDragged) clearSelectedElements();
+          return;
+        }
+
+        const nodeProperties = {
+          el: nodeEl,
+          id: nodeEl.id.slice(5),
+          posY: parseInt(nodeEl.style.top, 10),
+          posX: parseInt(nodeEl.style.left, 10),
+        };
+
+        if (!ctrlKey && !hasDragged) {
+          clearSelectedElements();
+
+          activeNode = nodeProperties;
+          nodeEl.classList.add('selected-list');
+          selectedElements = [nodeProperties];
+          hasDragged = false;
+
+          return;
+        }
+        hasDragged = false;
+
+        if (!ctrlKey) return;
+
+        const nodeIndex = selectedElements.findIndex(({ el }) =>
+          nodeEl.isEqualNode(el)
+        );
+        if (nodeIndex !== -1) {
+          setTimeout(() => {
+            nodeEl.classList.remove('selected-list', 'selected');
+          }, 400);
+          selectedElements.splice(nodeIndex, 1);
+        } else {
+          nodeEl.classList.add('selected-list');
+          selectedElements.push(nodeProperties);
+        }
+      });
+
       editor.value = drawflow(element, {
         context,
         options: {
@@ -447,6 +585,33 @@ export default {
         );
       }
 
+      editor.value.on('mouseMove', () => {
+        if (!activeNode || !isDragging) return;
+
+        const xDistance =
+          parseInt(activeNode.el.style.left, 10) - activeNode.posX;
+        const yDistance =
+          parseInt(activeNode.el.style.top, 10) - activeNode.posY;
+
+        selectedElements.forEach(({ el, posX, posY }) => {
+          if (el.isEqualNode(activeNode.el)) return;
+
+          const nodeId = el.id.slice(5);
+          const node = editor.value.drawflow.drawflow.Home.data[nodeId];
+
+          const newPosX = posX + xDistance;
+          const newPosY = posY + yDistance;
+
+          node.pos_x = newPosX;
+          node.pos_y = newPosY;
+          el.style.top = `${newPosY}px`;
+          el.style.left = `${newPosX}px`;
+
+          editor.value.updateConnectionNodes(el.id);
+        });
+
+        hasDragged = true;
+      });
       editor.value.on('nodeRemoved', (id) => {
         emit('deleteBlock', id);
       });
@@ -499,6 +664,7 @@ export default {
       });
 
       checkWorkflowData();
+      initSelectArea();
 
       setTimeout(() => {
         editor.value.zoom_refresh();
@@ -525,6 +691,7 @@ export default {
 #drawflow {
   background-image: url('@/assets/images/tile.png');
   background-size: 35px;
+  user-select: none;
 }
 .dark #drawflow {
   background-image: url('@/assets/images/tile-white.png');
@@ -540,4 +707,9 @@ export default {
   border-right: 10px solid transparent;
   border-bottom: 10px solid transparent;
 }
+.selection-area {
+  background: rgba(46, 115, 252, 0.11);
+  border: 2px solid rgba(98, 155, 255, 0.81);
+  border-radius: 0.1em;
+}
 </style>

+ 12 - 5
src/content/element-selector/App.vue

@@ -189,9 +189,10 @@ const cardRect = reactive({
 });
 
 /* eslint-disable  no-use-before-define */
-const getElementSelector = (element, options = {}) =>
-  state.selectorType === 'css'
-    ? getCssSelector(element, {
+const getElementSelector = (element, options = {}) => {
+  if (state.selectorType === 'css') {
+    if (Array.isArray(element)) {
+      return getCssSelector(element, {
         root: document.body,
         blacklist: [
           '[focused]',
@@ -205,8 +206,14 @@ const getElementSelector = (element, options = {}) =>
         ],
         includeTag: true,
         ...options,
-      })
-    : generateXPath(element);
+      });
+    }
+
+    return finder(element);
+  }
+
+  return generateXPath(element);
+};
 
 function generateXPath(element) {
   if (!element) return null;

+ 5 - 0
yarn.lock

@@ -1652,6 +1652,11 @@
     "@types/prosemirror-state" "*"
     "@types/prosemirror-transform" "*"
 
+"@viselect/vanilla@^3.0.0-beta.13":
+  version "3.0.0-beta.13"
+  resolved "https://registry.yarnpkg.com/@viselect/vanilla/-/vanilla-3.0.0-beta.13.tgz#cb2ac109701ba25923a885e2ba691fb82302e243"
+  integrity sha512-ML6uLrIpAgtFMRDXc5NfC2K7LiD+IzcsmUfYxrUVvvJgKfxm/eZjhHBvNVbJfto9SHsD9o+KjxJR/iexFyRLVg==
+
 "@vue/compiler-core@3.2.19":
   version "3.2.19"
   resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.19.tgz#b537dd377ce51fdb64e9b30ebfbff7cd70a64cb9"