123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <template>
- <div
- id="drawflow"
- class="parent-drawflow relative"
- @drop="dropHandler"
- @dragover.prevent="handleDragOver"
- >
- <slot v-bind="{ editor }"></slot>
- <ui-popover
- v-model="contextMenu.show"
- :options="contextMenu.position"
- padding="p-3"
- >
- <ui-list class="space-y-1 w-52">
- <ui-list-item
- v-for="item in contextMenu.items"
- :key="item.id"
- v-close-popover
- class="cursor-pointer justify-between"
- @click="contextMenuHandler[item.event]"
- >
- <span>
- {{ item.name }}
- </span>
- <span
- v-if="item.shortcut"
- class="text-sm capitalize text-gray-600 dark:text-gray-200"
- >
- {{ item.shortcut }}
- </span>
- </ui-list-item>
- </ui-list>
- </ui-popover>
- </div>
- </template>
- <script>
- /* eslint-disable camelcase */
- import {
- onMounted,
- shallowRef,
- reactive,
- getCurrentInstance,
- watch,
- onBeforeUnmount,
- } from 'vue';
- import { useRoute } from 'vue-router';
- import { useI18n } from 'vue-i18n';
- import { compare } from 'compare-versions';
- import defu from 'defu';
- import emitter from '@/lib/mitt';
- import { useShortcut, getShortcut } from '@/composable/shortcut';
- import { tasks } from '@/utils/shared';
- import { parseJSON } from '@/utils/helper';
- import { useGroupTooltip } from '@/composable/groupTooltip';
- import drawflow from '@/lib/drawflow';
- export default {
- props: {
- data: {
- type: [Object, String],
- default: null,
- },
- isShared: {
- type: Boolean,
- default: false,
- },
- version: {
- type: String,
- default: '',
- },
- },
- emits: ['load', 'deleteBlock', 'update', 'save'],
- setup(props, { emit }) {
- useGroupTooltip();
- const { t } = useI18n();
- const route = useRoute();
- const contextMenuItems = {
- block: [
- {
- id: 'duplicate',
- name: t('workflow.editor.duplicate'),
- icon: 'riFileCopyLine',
- event: 'duplicateBlock',
- shortcut: getShortcut('editor:duplicate-block').readable,
- },
- {
- id: 'delete',
- name: t('common.delete'),
- icon: 'riDeleteBin7Line',
- event: 'deleteBlock',
- shortcut: 'Del',
- },
- ],
- };
- const editor = shallowRef(null);
- const contextMenu = reactive({
- items: [],
- data: null,
- show: false,
- position: {},
- });
- const workflowId = route.params.id;
- const prevSelectedEl = {
- output: null,
- connection: null,
- };
- const isOutputEl = (el) => el.classList.contains('output');
- const isConnectionEl = (el) =>
- el.matches('path.main-path') ||
- el.parentElement.classList.contains('connection');
- function toggleHoverClass({ target, name, active, classes }) {
- const prev = prevSelectedEl[name];
- if (active) {
- if (prev === target) return;
- target.classList.toggle(classes, true);
- } else if (prev) {
- prev.classList.toggle(classes, false);
- }
- prevSelectedEl[name] = target;
- }
- function handleDragOver({ target }) {
- toggleHoverClass({
- target,
- name: 'connection',
- classes: 'selected',
- active: isConnectionEl(target),
- });
- toggleHoverClass({
- target,
- name: 'output',
- classes: 'ring-4',
- active: isOutputEl(target),
- });
- }
- function dropHandler({ dataTransfer, clientX, clientY, target }) {
- const block = JSON.parse(dataTransfer.getData('block') || null);
- if (!block || block.fromBlockBasic) return;
- const isTriggerExists =
- block.id === 'trigger' &&
- editor.value.getNodesFromName('trigger').length !== 0;
- if (isTriggerExists) return;
- const xPosition =
- clientX *
- (editor.value.precanvas.clientWidth /
- (editor.value.precanvas.clientWidth * editor.value.zoom)) -
- editor.value.precanvas.getBoundingClientRect().x *
- (editor.value.precanvas.clientWidth /
- (editor.value.precanvas.clientWidth * editor.value.zoom));
- const yPosition =
- clientY *
- (editor.value.precanvas.clientHeight /
- (editor.value.precanvas.clientHeight * editor.value.zoom)) -
- editor.value.precanvas.getBoundingClientRect().y *
- (editor.value.precanvas.clientHeight /
- (editor.value.precanvas.clientHeight * editor.value.zoom));
- const blockId = editor.value.addNode(
- block.id,
- block.inputs,
- block.outputs,
- xPosition + 25,
- yPosition - 25,
- block.id,
- block.data,
- block.component,
- 'vue'
- );
- if (block.fromGroup) {
- const blockEl = document.getElementById(`node-${blockId}`);
- blockEl.setAttribute('group-item-id', block.itemId);
- }
- if (isConnectionEl(target)) {
- target.classList.remove('selected');
- const classes = target.parentElement.classList.toString();
- const result = {};
- const items = [
- { str: 'node_in_', key: 'inputId' },
- { str: 'input_', key: 'inputClass' },
- { str: 'node_out_', key: 'outputId' },
- { str: 'output_', key: 'outputClass' },
- ];
- items.forEach(({ key, str }) => {
- result[key] = classes
- .match(new RegExp(`${str}[^\\s]*`))[0]
- ?.replace(/node_in_node-|node_out_node-/, '');
- });
- try {
- editor.value.removeSingleConnection(
- result.outputId,
- result.inputId,
- result.outputClass,
- result.inputClass
- );
- editor.value.addConnection(
- result.outputId,
- blockId,
- result.outputClass,
- 'input_1'
- );
- editor.value.addConnection(
- blockId,
- result.inputId,
- 'output_1',
- result.inputClass
- );
- } catch (error) {
- // Do nothing
- }
- } else if (isOutputEl(target)) {
- prevSelectedEl.output?.classList.remove('ring-4');
- const targetBlockId = target
- .closest('.drawflow-node')
- .id.replace(/node-/, '');
- const outputClass = target.classList[1];
- const blockData = editor.value.getNodeFromId(targetBlockId);
- const { connections } = blockData.outputs[outputClass];
- if (connections[0]) {
- const { output, node } = connections[0];
- editor.value.removeSingleConnection(
- targetBlockId,
- node,
- outputClass,
- output
- );
- }
- editor.value.addConnection(
- targetBlockId,
- blockId,
- outputClass,
- 'input_1'
- );
- }
- emitter.emit('editor:data-changed');
- }
- function isInputAllowed(allowedInputs, input) {
- if (typeof allowedInputs === 'boolean') return allowedInputs;
- return allowedInputs.some((item) => {
- if (item.startsWith('#')) {
- return tasks[input].category === item.substr(1);
- }
- return item === input;
- });
- }
- function deleteBlock() {
- editor.value.removeNodeId(contextMenu.data);
- }
- function duplicateBlock(id) {
- const { name, pos_x, pos_y, data, html } = editor.value.getNodeFromId(
- id || contextMenu.data.substr(5)
- );
- if (name === 'trigger') return;
- const { outputs, inputs } = tasks[name];
- editor.value.addNode(
- name,
- inputs,
- outputs,
- pos_x + 50,
- pos_y + 100,
- name,
- data,
- html,
- 'vue'
- );
- }
- function checkWorkflowData() {
- if (!editor.value) return;
- editor.value.editor_mode = props.isShared ? 'fixed' : 'edit';
- editor.value.container.classList.toggle('is-shared', props.isShared);
- }
- function refreshConnection() {
- const nodes = document.querySelectorAll('#drawflow .drawflow-node');
- nodes.forEach((node) => {
- if (!node.id) return;
- editor.value.updateConnectionNodes(node.id);
- });
- }
- useShortcut('editor:duplicate-block', () => {
- const selectedElement = document.querySelector('.drawflow-node.selected');
- if (!selectedElement) return;
- duplicateBlock(selectedElement.id.substr(5));
- });
- watch(() => props.isShared, checkWorkflowData);
- onMounted(() => {
- const context = getCurrentInstance().appContext.app._context;
- const element = document.querySelector('#drawflow');
- editor.value = drawflow(element, { context, options: { reroute: true } });
- const editorStates =
- parseJSON(localStorage.getItem('editor-states'), {}) || {};
- const editorState = editorStates[workflowId];
- if (editorState) {
- editor.value.zoom = editorState.zoom;
- editor.value.canvas_x = editorState.canvas_x;
- editor.value.canvas_y = editorState.canvas_y;
- }
- editor.value.start();
- emit('load', editor.value);
- if (props.data) {
- let data =
- typeof props.data === 'string'
- ? parseJSON(props.data, null)
- : props.data;
- if (!data) return;
- const currentExtVersion = chrome.runtime.getManifest().version;
- const isOldWorkflow = compare(
- currentExtVersion,
- props.version || '0.0.0',
- '>'
- );
- if (isOldWorkflow) {
- const newDrawflowData = Object.entries(
- data.drawflow.Home.data
- ).reduce((obj, [key, value]) => {
- obj[key] = {
- ...value,
- html: tasks[value.name].component,
- data: defu({}, value.data, tasks[value.name].data),
- };
- return obj;
- }, {});
- data = {
- drawflow: { Home: { data: newDrawflowData } },
- };
- emit('update', { version: currentExtVersion });
- }
- editor.value.import(data);
- if (isOldWorkflow) {
- setTimeout(() => {
- emit('save');
- }, 200);
- }
- } else if (!props.isShared) {
- editor.value.addNode(
- 'trigger',
- 0,
- 1,
- 50,
- 300,
- 'trigger',
- tasks.trigger.data,
- 'BlockBasic',
- 'vue'
- );
- }
- editor.value.on('nodeRemoved', (id) => {
- emit('deleteBlock', id);
- });
- editor.value.on(
- 'connectionCreated',
- ({ output_id, input_id, output_class, input_class }) => {
- const { outputs } = editor.value.getNodeFromId(output_id);
- const { name: inputName } = editor.value.getNodeFromId(input_id);
- const { allowedInputs, maxConnection } = tasks[inputName];
- const isAllowed = isInputAllowed(allowedInputs, inputName);
- const isMaxConnections =
- outputs[output_class]?.connections.length > maxConnection;
- if (!isAllowed || isMaxConnections) {
- editor.value.removeSingleConnection(
- output_id,
- input_id,
- output_class,
- input_class
- );
- }
- emitter.emit('editor:data-changed');
- }
- );
- editor.value.on('connectionRemoved', () => {
- emitter.emit('editor:data-changed');
- });
- editor.value.on('contextmenu', ({ clientY, clientX, target }) => {
- const isBlock = target.closest('.drawflow .drawflow-node');
- if (isBlock) {
- const virtualEl = {
- getReferenceClientRect: () => ({
- width: 0,
- height: 0,
- top: clientY,
- right: clientX,
- bottom: clientY,
- left: clientX,
- }),
- };
- contextMenu.data = isBlock.id;
- contextMenu.position = virtualEl;
- contextMenu.items = contextMenuItems.block;
- contextMenu.show = true;
- }
- });
- checkWorkflowData();
- setTimeout(() => {
- editor.value.zoom_refresh();
- refreshConnection();
- }, 500);
- });
- onBeforeUnmount(() => {
- const editorStates =
- parseJSON(localStorage.getItem('editor-states'), {}) || {};
- editorStates[workflowId] = {
- zoom: editor.value.zoom,
- canvas_x: editor.value.canvas_x,
- canvas_y: editor.value.canvas_y,
- };
- localStorage.setItem('editor-states', JSON.stringify(editorStates));
- });
- return {
- t,
- editor,
- contextMenu,
- dropHandler,
- handleDragOver,
- contextMenuHandler: {
- deleteBlock,
- duplicateBlock: () => duplicateBlock(),
- },
- };
- },
- };
- </script>
- <style>
- #drawflow {
- background-image: url('@/assets/images/tile.png');
- background-size: 35px;
- }
- .dark #drawflow {
- background-image: url('@/assets/images/tile-white.png');
- }
- .drawflow .drawflow-node {
- @apply dark:bg-gray-800;
- }
- </style>
|