alyssaxuu / flowy

The minimal javascript library to create flowcharts ✨
MIT License
11.47k stars 998 forks source link

Multiple parents, how to #102

Open fcnyp opened 4 years ago

fcnyp commented 4 years ago

Seen that several people have requested the ability to add multiple parents. I understand this is outside the scope of the library and why alyssaxuu has made that choice.

I am looking to add this feature locally and am querying for information on how to.

Outside the scope of the ask:

  1. How to select a parent. This is a UX decision and something people should handle on their own.
  2. How to remove parent relationships. Again this is UX.
  3. How to store the relationships.

My thought would be to expose a function with the following inputs: block (original child), second parent id. I have the arrow being drawn, the shape is right but the height is off. I am also concerned that it may not draw right under all circumstances.

const snapParent = function(drag, i) {
    let blocko = blocks.map(a => a.id);

    let arrowhelp = blocks.filter(a => a.id == parseInt(drag.querySelector(".blockid").value))[0];
    let arrowx = arrowhelp.x - blocks.filter(a => a.id == blocko[i])[0].x + 20;

    let arrowy = parseFloat(arrowhelp.y - (arrowhelp.height / 2) - (blocks.filter(block => block.id == blocko[i])[0].y + (blocks.filter(block => block.id == blocko[i])[0].height / 2)) + canvas_div.scrollTop);
    canvas_div.innerHTML += `<div class="arrowblock"><input type="hidden" class="arrowid" value="${drag.querySelector(".blockid").value}.${i}">
        <svg preserveaspectratio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} 0L${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} ${(paddingy / 2)}L5 ${(paddingy / 2)}L5 ${arrowy}" stroke="#C5CCD0" stroke-width="2px"/>
            <path d="M0 ${(arrowy - 5)}H10L5 ${arrowy}L0 ${(arrowy - 5)}Z" fill="#C5CCD0"/>
        </svg>
    </div>`;

    //El horizontal position
    flowyScope.querySelector(`.arrowid[value="${drag.querySelector(".blockid").value}.${i}"]`).parentNode.style.left = (arrowhelp.x - 5) - (canvas_div.getBoundingClientRect().left + window.scrollX) + canvas_div.scrollLeft + "px";

    //El virtical position
    flowyScope.querySelector(`.arrowid[value="${parseInt(drag.querySelector(".blockid").value)}.${i}"]`).parentNode.style.top = 
        blocks.filter(a => a.id == blocko[i])[0].y + "px";
}
fcnyp commented 4 years ago

Updated.

fcnyp commented 4 years ago

@alyssaxuu Any assistance you can provide would be great! The above seems to work but it doesn't account for two things.

  1. The arrow placement doesn't account for canvas scroll position. If your scrolled down its off.
  2. Arrows are not routed correctly around child blocks are multiple levels down (would assume this would be the same in reverse). (e.g., I have 4 rows. Root, row 2 with 2 nodes, row 3 with 1 node and row 4 with 1 node. I try to connect row 4 node to row 2 node and the arrow goes straight through row 3 node).
queenie0708 commented 4 years ago

also need multi parents or at least 2 parents

fcnyp commented 4 years ago

See update. Scroll position addressed. Only one issue left - Arrows are not routed around blocks if the arrow must pass through multiple levels between the parent and child. Not sure how to solve this one, looking for any suggestions/direction.

snapParent = function(drag, i) {
    let blocko = blocks.map(a => a.id);

    let arrowhelp = blocks.filter(a => a.id == parseInt(drag.querySelector(".blockid").value))[0];

    let arrowy = parseFloat(arrowhelp.y - (arrowhelp.height / 2) - (blocks.filter(block => block.id == blocko[i])[0].y + (blocks.filter(block => block.id == blocko[i])[0].height / 2)));

    canvas_div.innerHTML += `<div class="arrowblock"><input type="hidden" class="arrowid" value="${drag.querySelector(".blockid").value}.${i}">
        <svg preserveaspectratio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} 0L${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} ${(paddingy / 2)}L5 ${(paddingy / 2)}L5 ${arrowy}" stroke="#C5CCD0" stroke-width="2px"/>
            <path d="M0 ${(arrowy - 5)}H10L5 ${arrowy}L0 ${(arrowy - 5)}Z" fill="#C5CCD0"/>
        </svg>
    </div>`;

    let arrowParent = flowyScope.querySelector(`.arrowid[value="${parseInt(drag.querySelector(".blockid").value)}.${i}"]`).parentNode;
    arrowParent.style.left = (arrowhelp.x - 5) - (canvas_div.getBoundingClientRect().left + window.scrollX) + canvas_div.scrollLeft + "px";

    arrowParent.style.top = (blocks.filter(a => a.id == blocko[i])[0].y + 12) + "px";
};

@alyssaxuu Maybe we can make this a method (and a modification that deletes any parent/children if delete) people can call? They would be responsible for their own UX/UI implementation.

ebihimself commented 4 years ago

How this method is getting called? By the way for better user experience, I assume if the indicator becomes active on the time of mouse hover and lets the client grab the line and drop it on the children would be more intuitive. But need to decide how we want to centralize and align the children below their parent.

Divyahb commented 4 years ago

@fcnyp A good solution! Did you manage to solve this issue? And how and where is this snapParent supposed to be called. Thanks in advance

alyssaxuu commented 4 years ago

See update. Scroll position addressed. Only one issue left - Arrows are not routed around blocks if the arrow must pass through multiple levels between the parent and child. Not sure how to solve this one, looking for any suggestions/direction.

snapParent = function(drag, i) {
  let blocko = blocks.map(a => a.id);

  let arrowhelp = blocks.filter(a => a.id == parseInt(drag.querySelector(".blockid").value))[0];

  let arrowy = parseFloat(arrowhelp.y - (arrowhelp.height / 2) - (blocks.filter(block => block.id == blocko[i])[0].y + (blocks.filter(block => block.id == blocko[i])[0].height / 2)));

  canvas_div.innerHTML += `<div class="arrowblock"><input type="hidden" class="arrowid" value="${drag.querySelector(".blockid").value}.${i}">
      <svg preserveaspectratio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} 0L${(blocks.filter(a => a.id == blocko[i])[0].x - arrowhelp.x + 5)} ${(paddingy / 2)}L5 ${(paddingy / 2)}L5 ${arrowy}" stroke="#C5CCD0" stroke-width="2px"/>
          <path d="M0 ${(arrowy - 5)}H10L5 ${arrowy}L0 ${(arrowy - 5)}Z" fill="#C5CCD0"/>
      </svg>
  </div>`;

  let arrowParent = flowyScope.querySelector(`.arrowid[value="${parseInt(drag.querySelector(".blockid").value)}.${i}"]`).parentNode;
  arrowParent.style.left = (arrowhelp.x - 5) - (canvas_div.getBoundingClientRect().left + window.scrollX) + canvas_div.scrollLeft + "px";

  arrowParent.style.top = (blocks.filter(a => a.id == blocko[i])[0].y + 12) + "px";
};

@alyssaxuu Maybe we can make this a method (and a modification that deletes any parent/children if delete) people can call? They would be responsible for their own UX/UI implementation.

I see - so basically this function draws an arrow between the blocks then? As per the arrows not being routed to avoid blocks, I assume that is happening because you want to allow for blocks to be attached in different levels? Seems interesting, although with the multiple level functionality it would be very convoluted from a UX/UX perspective, don't really know how it could work while looking clean. Same level though would be something I might consider adding into the library, that said.

cmoussalli commented 3 years ago

As per my understanding, we still don't have a solution for fork's join -multiple parent-, Am I correct?

douglasrono commented 2 years ago

Hello. I also thought that this needs some improvement

simkimsia commented 2 years ago

@fcnyp

They would be responsible for their own UX/UI implementation.

Do you have an eventual choice on how to set the UX/UI for selecting 2nd parent, storing relations, delete the connection? I like to emulate your example