简体   繁体   中英

Flow Builder (visual editor)

I want to build a Flow Builder for my clients to give them an entirely way to construct data inside my dashboard editor, inspired with manychat flow builder

I have a simple app which has UI editor I have a need to build a workflow tool, allowing people to drag nodes onto a canvas, connect outlets of nodes to inputs of other nodes... Zoom.etc.

I am inspired with a manychat editor which u can see it here https://manychat.com/

在此处输入图片说明

More how it look in manychat在此处输入图片说明

Looking for suggestions on how to get started... Curious if there are frameworks/libraries anyone would recommend making this easier or just confirmation that I should just start whipping out Javascript to handle the drags/drops/line drawing/etc.

I have found this library named rete.js

So far this is what I have.

js

var numSocket = new Rete.Socket('Number value');

var VueNumControl = {
  props: ['readonly', 'emitter', 'ikey', 'getData', 'putData'],
  template: '<input type="number" :readonly="readonly" :value="value" @input="change($event)" @dblclick.stop="" @pointermove.stop=""/>',
  data() {
    return {
      value: 0,
    }
  },
  methods: {
    change(e){
      this.value = +e.target.value;
      this.update();
    },
    update() {
      if (this.ikey)
        this.putData(this.ikey, this.value)
      this.emitter.trigger('process');
    }
  },
  mounted() {
    this.value = this.getData(this.ikey);
  }
}

class NumControl extends Rete.Control {

  constructor(emitter, key, readonly) {
    super(key);
    this.component = VueNumControl;
    this.props = { emitter, ikey: key, readonly };
  }

  setValue(val) {
    this.vueContext.value = val;
  }
}

class NumComponent extends Rete.Component {

    constructor(){
        super("Number");
    }

    builder(node) {
        var out1 = new Rete.Output('num', "Number", numSocket);

        return node.addControl(new NumControl(this.editor, 'num')).addOutput(out1);
    }

    worker(node, inputs, outputs) {
        outputs['num'] = node.data.num;
    }
}

class AddComponent extends Rete.Component {
    constructor(){
        super("Add");
    }

    builder(node) {
        var inp1 = new Rete.Input('num1',"Number", numSocket);
        var inp2 = new Rete.Input('num2', "Number2", numSocket);
        var out = new Rete.Output('num', "Number", numSocket);

        inp1.addControl(new NumControl(this.editor, 'num1'))
        inp2.addControl(new NumControl(this.editor, 'num2'))

        return node
            .addInput(inp1)
            .addInput(inp2)
            .addControl(new NumControl(this.editor, 'preview', true))
            .addOutput(out);
    }

    worker(node, inputs, outputs) {
        var n1 = inputs['num1'].length?inputs['num1'][0]:node.data.num1;
        var n2 = inputs['num2'].length?inputs['num2'][0]:node.data.num2;
        var sum = n1 + n2;

        this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(sum);
        outputs['num'] = sum;
    }
}

(async () => {
    var container = document.querySelector('#rete');
    var components = [new NumComponent(), new AddComponent()];

    var editor = new Rete.NodeEditor('demo@0.1.0', container);
    editor.use(ConnectionPlugin.default);
    editor.use(VueRenderPlugin.default);    
    editor.use(ContextMenuPlugin.default);
    editor.use(AreaPlugin);
    editor.use(CommentPlugin.default);
    editor.use(HistoryPlugin);
    editor.use(ConnectionMasteryPlugin.default);

    var engine = new Rete.Engine('demo@0.1.0');

    components.map(c => {
        editor.register(c);
        engine.register(c);
    });

    var n1 = await components[0].createNode({num: 2});
    var n2 = await components[0].createNode({num: 0});
    var add = await components[1].createNode();

    n1.position = [80, 200];
    n2.position = [80, 400];
    add.position = [500, 240];


    editor.addNode(n1);
    editor.addNode(n2);
    editor.addNode(add);

    editor.connect(n1.outputs.get('num'), add.inputs.get('num1'));
    editor.connect(n2.outputs.get('num'), add.inputs.get('num2'));


    editor.on('process nodecreated noderemoved connectioncreated connectionremoved', async () => {
      console.log('process');
        await engine.abort();
        await engine.process(editor.toJSON());
    });

    editor.view.resize();
    AreaPlugin.zoomAt(editor);
    editor.trigger('process');
})();

Here is HTML

<div id="rete"></div>

<a target="_blank" href="https://github.com/retejs/rete"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/652c5b9acfaddf3a9c326fa6bde407b87f7be0f4/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"></a>

Here is codepen code pen demo

Unfortunately, this does not give what I want,

What do I need to do to get what I need? or is there any library which I can try?

ManyChat uses PIXI.js to build the flow builder. I found out myself when reading through their bundle codes (who the hell read compiled codes, yes I did :))

Seems like they use React-PIXI - a react wrapper of PIXI.js (I'm not sure about this)

From that observation I can build my own one using VueJS (in a project of my previous company)

You should take a look at PIXI. Some important notices for you (all I collect from Manychat's idea and during my implementation):

  • Remember to double your canvas width and height property, then use CSS to style your canvas with half size of the width and height. Doing this will make your canvas has high quality. Example:
<canvas width="400" height="600" style="width: 200px; height: 300px;"></canvas>
  • One of the hard part is making the arrow to connect between 2 blocks, I read compiled code of Manychat and found out, not remember exactly what it is but basically they split the arrow to 2 symmetric parts and start drawing them to their center, you can read it and find out yourself (if you lucky, LOL), it's in main.js
  • Another hard part is reflecting Vue data into canvas, canvas is not DOM, any change you'll have to re-draw the whole canvas, so do this carefully
  • don't render multiple images, it's cost
  • for Object snapping, you can learn from idea of KonvaJS (they have an example for that)
  • For pin, pan and Zoom you can search on Google examples using PIXI.
  • When clicking an block, the camera will smoothly focus that block with some animations (zoom in then out, centralize the block with the camera,...), seem Many chat uses tween.js
  • Beware of computing data in event listeners (mousemove, mousedrag,...), it can easily destroy performance

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM