background

This article is an extension of the workflow editor development based on VUE and Jsplumb

Business implementation

  • undo
  • Initialize data
  • Automatic arrangement
  • Empty data

undo

For the implementation of undo, it mainly needs a buffer memory to store the data structure after each operation, so that when the undo button is clicked again, the data structure can be taken out from the buffer memory to render the page.

Arrays are used to store data structures after operations.

let MEMORY_LIST = [];
Copy the code

Only 10 operations are cached here.

$_updateMemoryList() { <! -- Format data structure -->const tempItem = this.formatData();

    // max store is 10
    if (MEMORY_LIST.length > 10) {
        MEMORY_LIST.shift();
    }
    MEMORY_LIST.push(tempItem);
    // Update button operation status.
    this.$_updateCanUndoBtn();
}
Copy the code

The $_UPDATemoryList method is then called during various operations, such as adding or dragging nodes

When I hit the undo button,

handleUndo() {
    if (!this.canUndo) {
        return;
    }

    if (MEMORY_LIST.length > 0) {
        const tempItem = MEMORY_LIST.pop();
        this.$_doClear();
        this.$options.jsPlumb.reset();
        this.$nextTick(() = > {
            this.updateFlow(tempItem, this.$_plumbRepaintEverything); })}this.$_updateCanUndoBtn();
},
Copy the code

Read the data structure directly from buffer memory and re-render the flowchart. Update button status.

Initialize data

For initializing rendered data structures

{
    positions: {"key": {left:'xx'.top:'xx'}},steps:[
        {
            elementId:'startNode'.stepId:'uuid'.nextStep:'uuid'
        },
        {
            "elementId": "switchNode"."stepId": "c89829f5-8595-458c-b040-4ff84d27befc"."nextSteps": [{"nextStep": "d611c32f-b6c0-4b97-80d9-47b783bd93ad"}, {"nextStep": "7bd4fc3d-c3b9-4b19-81dc-e49cd1e7b5c5"}, {"nextStep": "85c30556-75fa-441c-9a1d-0dced21755a5",}],}, {"elementId": "stopNode"."stepId": "d611c32f-b6c0-4b97-80d9-47b783bd93ad"."nextStep": null]}},Copy the code

Through this data structure, the render method updateFlow is then executed

updateFlow(editItem, callback) {
    let positions = JSON.parse(editItem.positions);
    let steps = editItem.steps;
    let flowList = [];

    steps.forEach((step) = > {
        let flowItem = this.getFlowItemById(step.elementId);

        if(! flowItem) {return;
        }
        flowItem.next = [];
        flowItem.prev = [];
        flowItem.uuid = step.stepId;

        let position = positions[step.stepId];

        if (position) {
            flowItem.left = position.left;
            flowItem.top = position.top;
        }

        if (step.nextStep) {
            flowItem.next = [step.nextStep];
        } else if (step.nextSteps) {
            flowItem.next = step.nextSteps.map((nextStep) = > {
                return nextStep.nextStep;
            });
        }

        if(flowItem.type ! == FLOW_ITEM_TYPE.endNode) {//
            if (this.isIfFlowItem(flowItem.type)) {
                let formData = clone(step.nextSteps[0]);
                formData.stepName = step.stepName;
                flowItem.formData = this.getFlowItemFormData(formData);
                // else
                if (formData.isDefault) {
                    flowItem.nextElseId = formData.nextStep;
                    flowItem.nextIfId = step.nextSteps[1].nextStep;
                } else {
                    flowItem.nextIfId = formData.nextStep;
                    flowItem.nextElseId = step.nextSteps[1].nextStep;
                }
                if (step.stepJson) {
                    let stepOtherObj = JSON.parse(step.stepJson); flowItem.formData.ifNodeTitle = stepOtherObj.ifNodeTitle; }}else if (this.isExpandFlowItem(flowItem.type)) {
                let ruleGroupList = step.nextSteps;
                let formData = {};
                formData.stepName = step.stepName;
                formData.ruleGroupList = ruleGroupList;
                flowItem.formData = formData;
            } else {
                flowItem.formData = this.getFlowItemFormData(step);
            }
        }

        flowList.push(flowItem);
    });

    // update
    flowList.forEach((item) = > {
        if (item.next.length > 0) {
            item.next.forEach((id) = > {
                let nextItem = _.find(flowList, (tempItem) = > {
                    return tempItem.uuid === id;
                });

                if (nextItem) {
                    if (nextItem.prev.indexOf(item.uuid) === -1) { nextItem.prev.push(item.uuid); }}}); }});this.flowList = flowList;

    this.$nextTick(() = > {
        //
        flowList.forEach((item) = > {
            this.$options.jsPlumb.draggable(item.uuid, {});
        });

        this.$nextTick(() = > {
            flowList.forEach((item) = > {
                item.next.forEach((id, index) = > {
                    let nextFlowItem = this.getFlow(id);
                    if (!this.isTempFlowItem(nextFlowItem)) {
                        this.addFlowItemConnect(item.uuid, id);
                    } else {
                        this.draggableFlowConnect(item.uuid, id, true);
                    }
                    //
                    if (this.isIfFlowItem(item.type)) {
                        let isIf = item.nextIfId === nextFlowItem.uuid;
                        this.createFlowItemLabel(item.uuid, id, isIf ? 'is' : 'no');
                    } else if (this.isExpandFlowItem(item.type)) {
                        let name = this.getExpandFlowItemName(item, id);
                        this.createFlowItemLabel(item.uuid, id, name); }}); });this.$nextTick(() = >{ callback && callback(); })}); }); },Copy the code

Automatic arrangement

For the realization of automatic arrangement, mainly based on the realization of a compact tree layout algorithm, write very good, thank the giant, let me stand on his shoulder.

The general idea is:

  1. We go from the top down, and we start with the minimum distance between adjacent nodesFLOW_LEFT_STEP_LENGTHMake the first layout of the tree. Initialize the location of the root node. Start at the root. The coordinate of the root node is set manually, and then the child nodes of the root node are hung under the root node, and the child nodes are distributed in the root nodeFLOW_STEP_LENGTHBelow the height, the distance between the child nodes isFLOW_LEFT_STEP_LENGTHAnd the distribution is symmetric with respect to the root node. Do this recursively until all the nodes are laid out.
  2. Then, we need a hashTree to save the tree into a hierarchical linear table. We convert the tree to hashTree. The tree in the renderings corresponds to the hashTree as follows:
/**
 * layer [
 *   0  [ node(0) ], 
 *   1  [ node(1), node(2), node(3), node(4), node(5) ],
 *   2  [ node(6), node(7), node(8), node(9), node(10), node(11), node(12), node(13), node(14), node(15), node(16), node(17), node(18), node(19), node(20) ],
 *   3  [ node(21), node(22), node(23), node(24), node(25), node(26), node(27), node(28) ]
 * ]
 */
Copy the code
  1. The hashTree is traversed from the lowest level down to the top level, detecting adjacent nodes. Suppose n1 and n2 are a pair of adjacent nodes, and the subscript of n1 in the linear table is less than n2. Detect whether N1 and N2 overlap. If overlap occurs, the left side does not move and the whole adjusts to the right. But instead of n2, the adjustment is “an ancestor of N2 that is a sibling of an ancestor of N1.”
  2. Every time you move a node, its parent loses symmetry, so you have to adjust it. But we leave the parent node alone and restore symmetry by moving the children to the left.
  3. Each time symmetry is restored, some of the child nodes overlap again, so go back to the bottom layer and start scanning again.
  4. Repeat steps 3,4,5 until all overlap is removed and the layout is complete.

Initialize the location of the start node.

 this.tempLayerMap = [];
const startNode = _.find(this.flowList, (flowItem) => {
    return this.isStartFlowItem(flowItem);
});

// init start flow item
startNode.top = FLOW_START_STEP_TOP;
startNode.left = this.getFlowItemInitLeft();
Copy the code

Initialize layer 1 data.

this.tempLayerMap[0] = [startNode];

Copy the code

Recursively descends the subtree layout

this.$_layoutChild(startNode, 1);
Copy the code

In this method, we iterate through all the child nodes and initialize the top and left positions.

$_layoutChild method

$_layoutChild(pre, layer) {
    const nextList = pre.next;
    const nextListLength = nextList.length;

    if (this.tempLayerMap[layer] === undefined) {
        this.tempLayerMap[layer] = [];
    }

    nextList.forEach((nextFlowUUid, index) = > {
        const flowItem = this.getFlow(nextFlowUUid);
        // Initialize top
        flowItem.top = pre.top + FLOW_STEP_LENGTH;
        const startLeft = pre.left - (FLOW_LEFT_STEP_LENGTH * (nextListLength - 1)) / 2
        // initialize left
        flowItem.left = startLeft + FLOW_LEFT_STEP_LENGTH * index;
        this.tempLayerMap[layer].push(flowItem);
        if (flowItem.next && flowItem.next.length > 0) {
            this.$_layoutChild(flowItem, layer + 1); }})},Copy the code

Then all the child nodes are adjusted for ascending overlap

$_adjustChild()
Copy the code

$_adjustChild() {
    let flowList = null; <! -- From the bottom -->for (let i = this.tempLayerMap.length - 1; i >= 0; i--) {
        flowList = this.tempLayerMap[i];
        flowList.forEach((flowItem, index) = > {
            const leftFlowItem = flowList[index - 1]; <! -- Overlap -->if(leftFlowItem && flowItem.left - leftFlowItem.left < FLOW_LEFT_STEP_LENGTH) { <! ---->const parentFlowItem = this.$_findCommonParentNode(leftFlowItem, flowItem);
                const leftOffset = Math.abs(flowItem.left - leftFlowItem.left) + FLOW_LEFT_STEP_LENGTH; <! Change the left value of each node -->this.$_translateXTree(parentFlowItem, leftOffset);
                const prevFlowItem = this.getFlow(parentFlowItem.prev[0]); <! -- Center all child nodes -->this.$_centerChild(prevFlowItem); <! After moving, the lower layer nodes may overlap again, so scan again from the bottom layer --> I =this.tempLayerMap.length; }}}}),Copy the code

Clear data (redo)

Because it involves clearing every node information, node-bound events, and events bound to jsPlumb, it cannot simply clear vUe-bound list data,

So what I do is I find the start node, and then I go through the layers, removing the node information and the binding jsPlumb information.

$_doClear(needInit) { const startNode = _.find(this.flowList, (flowItem) => { return this.isStartFlowItem(flowItem); }); if (startNode) { this.deleteNextFlowItem(startNode.uuid); if (needInit) { this.$nextTick(() => { this.initFlow(); }}}}),Copy the code

The project address

Making: github.com/bosscheng/v…

Demo: bosscheng. Making. IO/vue – draggab…