3
\$\begingroup\$

[This is my first question to Code Review and it might be a bit too much.]

I want to give users the means to draw a simple diagram in the browser, and because it is partly a self-education project about the low-level entities and relationships in the browser, I haven't used one of the bigger frameworks that might perhaps have helped more.

My goal here is to prepare a diagram and be able to submit (save) to a server, then have the server return the diagram and reconstitute the entities and links. At the moment, this is done with data-* attributes.

My questions:

  • There are a lot of global variables (draw.state, draw.docKeyListenerCount etc are contained only inside the 'onload'-equivalent function) that I address from different eventListeners, and I recall that shared mutable state is evil. I found this (and having the listeners at the top scope) necessary because the user can move the mouse faster than my script can update the location of objects.

  • To complete one action (e.g. attach a free link to an entity) involves a sequence of mousedown, mouseout, mouseover and mouseup events. It's very confusingly organised, and to add new actions I'll have to think really hard about where the fall in the flow of possible responses to each event.

  • Changes to the entity types or action possibilities would have to be added in on each of those (e.g. if I want to be able to drop an entity into another entity to represent a sub-component, how can I introduce that in a nice modular way).

    SVG.on(document, 'DOMContentLoaded', function() {
//=================
//set up drawing
//=================        
        var draw = SVG('diagram-0').size(1200,700).attr({"class": "graphics"});
        draw.state = 'rest';
        draw.docKeyListenerCount = 0;
        draw.fE = null;
        draw.originX = undefined;
        draw.originY = undefined;
        draw.clientX = undefined;
        draw.clientY = undefined;
        draw.newX = undefined;
        draw.newY = undefined;
        draw.labelTextbuffer = [];

        let tools = draw.nested().size(200, 700);
        draw.tools = tools;
        tools.attr({"class": "tools"});
        let diagram = draw.nested().size(1000, 700).move(200, 0);
        draw.diagram = diagram;  //expose diagram
        diagram.attr({"class": "diagram"});
        tools.toolarea = tools.rect(200, 700).attr({fill: "#ddd", strokeWidth: 1}).radius(5);
        tools.title = tools.text("Tools").move(0, 0);
        let entity_tool = moveableEntity(draw, 'new entity');
        entity_tool.addTo(tools);
        entity_tool.update = entityUpdate;
        entity_tool.update(25, 25);
        entity_tool.update();
        let link_tool = newLink(draw, 25, 80, 60, 80);
        link_tool.update = lineUpdate;
        link_tool.addTo(tools);
        link_tool.update();
        entity_tool.data('grouptype', 'entity');
        link_tool.data('grouptype', 'linker');

        diagram.workarea = diagram.rect(1000, 700).radius(5).attr({fill: "#ddd", strokeWidth: 3});
        diagram.title = diagram.text("Diagram").move(0, 0);

// ---------------
//   E V E N T S
// ---------------
        function ancestralGroup(svg_element) {
            if (svg_element.type === 'g') {
                return svg_element;}
            else if (svg_element.type === 'svg') {return svg_element;}
            else {
                let parent = svg_element.parent();
                return ancestralGroup(parent);                                          // Watch Out: Recursion
            };
        };

        function toolAct(e) {
            let item = SVG.get(e.target.id);
            let groupToCopy = ancestralGroup(item);
            console.log('at ', groupToCopy);
            if (groupToCopy.type === 'svg') { return; };   // user has clicked the background panel or a label
            let copied = groupToCopy.clone();
            console.log('copy made: ', copied.node)
            diagram.add(copied); };

        tools.on('click', toolAct);

        diagram.on('mouseout', function (e) {
            e.preventDefault();
            if ((draw.state==='move')&&(draw.fE.type === 'circle')) {
                let rings = ((10 - draw.fE.collocates.length*2) > 2) ? (10 - draw.fE.collocates.length*2) : 2;
                draw.fE.attr({fill: "none", r: rings});
                let thisring = rings;
                for (const collocate of draw.fE.collocates) {
                    thisring = thisring + 2;
                    collocate.attr({fill: "none", r: thisring})
                };
            };
        });

        diagram.on('mouseover', function (e) {
            e.preventDefault();
            if (draw.state === 'move') {
            let attachment = SVG.get(e.target.id);
                if (attachment.type !== 'circle') {return;}
                if ((draw.fE === attachment)||(draw.fE.collocates.indexOf(attachment)) > -1) {return;}
                else {
                    draw.fE.collocates.push(attachment);
                    attachment.collocates.push(draw.fE);
                    };
           };
           return;
        });

        diagram.on('mouseup', function (e) {
            if (draw.state !== 'edit') {
               draw.state = 'rest';
               if (draw.docKeyListenerCount === 1) {
                   SVG.off(document, 'keydown', edit);
                   draw.docKeyListenerCount = 0;};
               if (draw.fE) {
                   if (draw.fE.attr("fill") === 'none') { unsticky(draw.fE);}
                   else {};
                   draw.fE = undefined;
               };
            }
            else {

            };
        });

        diagram.on('mousemove', function (e) {
              if (draw.state === 'move') {
                  draw.newX = draw.originX + (e.clientX - draw.clickX);
                  draw.newY = draw.originY + (e.clientY - draw.clickY);
                  let parentGroup = ancestralGroup(draw.fE);
                 // console.log('MOUSEMOVE - draw.fE was: ', draw.fE, draw.state, parentGroup);
                  if (draw.fE.type === 'circle') {   // then its a moveablePoint
                      draw.fE.cx(draw.newX).cy(draw.newY);
                      console.log('an immovable circle?', draw.fE.cx(), draw.fE.cy());
                      parentGroup.update();
                      if (draw.fE.collocates) { update_collocates(draw.fE); }}
                  else if ((draw.fE.type === 'rect')||(draw.fE.type=== 'tspan')) {   // then it's an entity
                        parentGroup.from.cx(draw.newX).cy(draw.newY);
                        if (parentGroup.from.collocates) { update_collocates(parentGroup.from); }
                        parentGroup.to.cx((draw.newX + parentGroup.panel.width())).cy((draw.newY + parentGroup.panel.height()));
                        if (parentGroup.to.collocates) { update_collocates(parentGroup.to); }
                        parentGroup.label.x(draw.newX).y(draw.newY);
                        parentGroup.panel.x(draw.newX).y(draw.newY);
                        parentGroup.update();
                  }  
                }
                else {};  // if draw.state not 'move', do nothing
              });

        diagram.on('mousedown', function (e) {
            draw.fE = SVG.get(e.target.id);  // reaches out to drawing scope
            let parentGroup = ancestralGroup(draw.fE);
            if (!parentGroup.to) { //  this should be a 'take up  group' function
                let children = parentGroup.children();
                if (parentGroup.data('grouptype') === 'entity') {
                    parentGroup.update = entityUpdate;
                }
                else if (parentGroup.data('grouptype') === 'linker') {
                    parentGroup.update = lineUpdate;
                };
                children.forEach(function (child) {
                        let partIndicator = child.data('part');
                        switch (partIndicator) {
                            case 'to':
                                parentGroup.to = child;
                                parentGroup.to.collocates = [];
                                break;
                            case 'from':
                                parentGroup.from = child;
                                parentGroup.from.collocates = [];
                                break;
                            case 'straight':
                                parentGroup.straight = child;
                                break;
                            case 'panel':
                                parentGroup.panel = child;
                                break;
                            case 'label':
                                parentGroup.label = child;
                                break; }
                   console.log('parent', parentGroup);
                    }
                );
            };
            if (draw.fE.type === 'tspan') {
                draw.state = 'edit';
                draw.labelTextbuffer = draw.fE.text().split("");
                if (draw.docKeyListenerCount <= 0) {
                    SVG.on(document, 'keydown', edit);
                    draw.docKeyListenerCount = 1; };
                return;
                }
            else {if ((draw.fE.type !== 'tspan')&&(draw.state === 'edit')) {
                    draw.state = 'rest';
                    for (let i=0; i < draw.docKeyListenerCount; i++) {
                        SVG.off(document, 'keydown', edit);
                        draw.docKeyListenerCount--;
                    console.log('key listener off', draw.docKeyListenerCount)};
                  };
            };
            if (e.button === 0) {
                draw.state = 'move';
                draw.originX = ((draw.fE.type === 'circle') ? draw.fE.cx() : draw.fE.x());
                draw.originY = ((draw.fE.type === 'circle') ? draw.fE.cy() : draw.fE.y());
                draw.clickX = e.clientX;
                draw.clickY = e.clientY;
            }
            else {  // to detach
                draw.state = 'detach';
                if ((draw.fE.collocates)&&(draw.fE.collocates.length > 0)) {
                    for (const collocate of draw.fE.collocates) {
                        let idx = collocate.collocates.indexOf(draw.fE);
                        if (idx > -1) {collocate.collocates.splice(idx,1)};
                    }
                    draw.fE.collocates.length = 0;
                    draw.fE.attr({fill: 'red'});
                }
            draw.fE.dx(30);
            parentGroup.update();
            };
            return;
        });

        function edit(e) {
            e.preventDefault();
            if (e.key === "Escape") { draw.state = 'rest'; draw.fE = undefined; return;}
            if (draw.state === 'edit') {
                if (e.key === "Backspace"){
                    draw.labelTextbuffer.pop();
                    draw.fE.node.innerHTML = draw.labelTextbuffer.join('');}
                else if ((e.key === "Shift")||(e.key === "Alt")||(e.key=== "Control")||(e.key === "Delete")) {  return;}
                else if ((e.key !== "Enter")&&(e.key !== "Escape")) {
                    draw.labelTextbuffer.push(e.key);
                    draw.fE.node.innerHTML = draw.labelTextbuffer.join('');
                    }
                else {  // e.key === Enter or Escape
                    draw.labelTextbuffer = [];
                    draw.focusGroup = null;
                    draw.state = 'rest';
                    SVG.off(document, 'keydown', edit);
                    draw.docKeyListenerCount = 0; }
            };
        };

    });  //  close of the "onload" function
// ---------------
//  SVG Objects
//  --------------
    function moveableEntity(draw, label='new E') {
        let newE = draw.group();
        newE.attr({"class": "entity"});
        newE.height = 25;
        newE.width = 90;
        newE.panel = newE.rect(newE.width, newE.height);
        newE.panel.data('part', 'panel');
        newE.from = newE.put(moveablePoint(draw).cx(0).cy(0));
        newE.from.data('part', 'from');
        newE.to = newE.put(moveablePoint(draw).cx(newE.width).cy(newE.height));
        newE.to.data('part', 'to');
        newE.label = newE.text(label).attr({"font-size":"8"});
        newE.label.data('part', 'label');
        return newE;
    };

    function moveablePoint(draw) {
        let pt = draw.circle(6);
        pt.attr({"class": "moveablePoint"});
        // pt.attr({class:"handle", 'pointer-events':"all"});
        pt.collocates = [];
    return pt;
    }; // return type: SVG circle with event handlers and event emitters

    function newLink(diagram, x1=10, y1=10, x2=50, y2=10) {
     // return type: SVG group
        let linker = diagram.group().attr({class: "graphics"});
        linker.attr({"class": "linker"});
        linker.straight = linker.put(diagram.line());
        linker.straight.data('part', 'straight');
        linker.straight.marker('end', 10,10, function(add){
          add.polygon('0,2 0,8 10,5');});       
        linker.from = linker.put(moveablePoint(diagram).cx(x1).cy(y1));
        linker.from.data('part', 'from');
        linker.to = linker.put(moveablePoint(diagram).cx(x2).cy(y2));
        linker.to.data('part', 'to');
        return linker;
    }

//  Update functions
    function entityUpdate(x1=undefined, y1 = undefined) {
        let x2 = this.to.cx();
        let y2 = this.to.cy();
        let txtBBox = this.label.bbox();

        if ((x1===undefined)||(y1===undefined)) {  // if the points have moved, adjust the box
            x1 = this.from.cx();
            y1 = this.from.cy();
            this.panel.x(x1).y(y1);
            this.label.x(x1).y(y1);
            this.panel.width(this.to.cx()-this.from.cx());
            this.panel.height(this.to.cy()-this.from.cy());
            this.to.cx(x2).cy(y2);
        }
        else {   // if the box is moved, adjust the points
            this.panel.x(x1).y(y1);
            this.label.x(x1).y(y1);
            this.from.cx(x1).cy(y1);
            let width = (this.panel.width() < txtBBox.width) ? txtBBox.width : this.panel.width();  // size to fit width
            this.panel.width(width);
            this.to.cx(x1+this.panel.width()).cy(y1+this.panel.height());
        };
    }

    function lineUpdate() {  // return line from 'from' to 'to'.
        return this.straight.plot(this.from.cx(), this.from.cy(),this.to.cx(),this.to.cy())
    };

    function sticky(draw) {
          if (draw.state === 'grab') {
              let rings = ((10 - draw.fE.collocates.length*2) > 2) ? (10 - draw.fE.collocates.length*2) : 2;
              draw.fE.attr({fill: "none", r: rings});
              let thisring = rings;
              for (const collocate of draw.fE.collocates) {
                  thisring = thisring + 2;
                  collocate.attr({fill: "none", r: thisring})
              };
              draw.state = 'move';
          };
      }

    function unsticky(focusElement) {
           focusElement.attr({fill: "#0e84b5", r: "3"});
           focusElement.parent().update();
           for (const collocate of focusElement.collocates) 
             {collocate.attr({fill: "#0e84b5", r: "3"});}; 
    }

    function update_collocates(amoveablepoint) {
        let x = amoveablepoint.cx();
        let y = amoveablepoint.cy();
        for (const joint of amoveablepoint.collocates) {
             if (joint.type === 'circle') {
                  joint.cx(x).cy(y);
                  joint.parent().update(); }
             else {
                  joint.parent().update();
                 };
             }
    }

I tried to make a jsfiddle but got a lot of errors. This script just expects a <div> labeled "diagram-0", and there are some styles, but nothing essential.

I also tried to make a jsfiddle but it only rendered the button. I looked at the console and there were a number of errors that made little sense to me.

Finally, its friday afternoon here, so I may be slow to respond.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.