alyssaxuu / flowy

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

Disable action if minor move #50

Open fcnyp opened 4 years ago

fcnyp commented 4 years ago

Would like to see an optional feature to disable block deletes if the drag/release action is minimal. I'm finding that when clicking a block I often move it by a few pixels and cause it to be deleted (Likely because my mouse sensitive is set to super high and I'm using a trackball =)). Would be nice to see some error tolerance here.

yellow1912 commented 4 years ago

This may help you:

https://github.com/alyssaxuu/flowy/issues/48

alyssaxuu commented 4 years ago

Would like to see an optional feature to disable block deletes if the drag/release action is minimal. I'm finding that when clicking a block I often move it by a few pixels and cause it to be deleted (Likely because my mouse sensitive is set to super high and I'm using a trackball =)). Would be nice to see some error tolerance here.

You can use the on rearrange method to prevent the block from being deleted & instead re-attach to the previous parent. Does that help?

fcnyp commented 4 years ago

I believe, per the comments in #48 if I were to disable deletes by drag all together and add a trash can icon it would flow a little better. What would the method be for deleting a block (and its children) by its id?

alyssaxuu commented 4 years ago

I believe, per the comments in #48 if I were to disable deletes by drag all together and add a trash can icon it would flow a little better. What would the method be for deleting a block (and its children) by its id?

Yeah, you can certainly do that with the on rearrange method, just return false to delete the blocks currently being dragged.

As per deleting blocks by ID, I'm still working on a method! It shouldn't be hard to implement at all, I just want to make sure it's clean and it makes sense.

fcnyp commented 4 years ago

I added a new method:

        flowy.getBlocks = function() {
            return blocks;
        };

Using that in combination with Lodash I'm accessing different blocks.

    const BLOCKS = flowy.getBlocks();
    const BLOCK_OBJECT = _.find(BLOCKS, {'id': parseInt(BLOCK_ID)});

Food for thought, provides an easy way to access blocks and I could see a similar delete method being used in combination with getBlocks where you pass in the block object or the ID

alyssaxuu commented 4 years ago

I added a new method:

        flowy.getBlocks = function() {
          return blocks;
        };

Using that in combination with Lodash I'm accessing different blocks.

    const BLOCKS = flowy.getBlocks();
    const BLOCK_OBJECT = _.find(BLOCKS, {'id': parseInt(BLOCK_ID)});

Food for thought, provides an easy way to access blocks and I could see a similar delete method being used in combination with getBlocks where you pass in the block object or the ID

Hmm, I'm just thinking though, not sure if it's necessary when there's the flowy.output method which returns all the blocks data, including the entire block array. Unless you think it would be better to have a dedicated method only for the array?

fcnyp commented 4 years ago

Different use case based on my understanding. Output is great for import/export (i.e., JSON). I'm using it to access attributes in the array and to manipulate the attributes, this way my custom attributes live in the import/export data. My plan was to add a sister method to update the attributes (e.g., setBlock).

fcnyp commented 4 years ago

This should work to remove a block and its children, I would error check me =)

Would be nice to add some easing, as it seems jumpy and may need a bit more error handling, I think that's covered though.

flowy.deleteBlock = function(id) {
    if (!Number.isInteger(id)) {
        id = parseInt(id);
    }
    for (var i=0; i < blocks.length; i++) {
        if (blocks[i].id === id) {
            removeBlockEl(blocks[i].id);
            blocks.splice(i, 1);
            removeChildren(id);
            break;
        }
    }

        if (blocks.length > 1) {
            rearrangeMe();
        }

    function removeChildren(parentId) {
        let children = [];
        for (var i = blocks.length - 1; i >= 0; i--) {
            if (blocks[i].parent === parentId) {
                children.push(blocks[i].id);
                removeBlockEl(blocks[i].id);
                blocks.splice(i, 1);
            }
        }
        for (var i=0; i < children.length; i++) {
            removeChildren(children[i]);
        }
    }
    function removeBlockEl(id) {
        document.querySelector(".blockid[value='" + id + "']").parentNode.remove();
        document.querySelector(".arrowid[value='" + id + "']").parentNode.remove();
    }
};
alyssaxuu commented 4 years ago

This should work to remove a block and its children, I would error check me =)

Would be nice to add some easing, as it seems jumpy and may need a bit more error handling, I think that's covered though.

flowy.deleteBlock = function(id) {
  if (!Number.isInteger(id)) {
      id = parseInt(id);
  }
  for (var i=0; i < blocks.length; i++) {
      if (blocks[i].id === id) {
          console.log(blocks[i])
          removeBlockEl(blocks[i].id);
          blocks.splice(i, 1);
          removeChildren(id);
          break;
      }
  }

        if (blocks.length > 1) {
            rearrangeMe();
        }

  function removeChildren(parentId) {
      let children = [];
      for (var i = blocks.length - 1; i >= 0; i--) {
          if (blocks[i].parent === parentId) {
              children.push(blocks[i].id);
              removeBlockEl(blocks[i].id);
              blocks.splice(i, 1);
          }
      }
      for (var i=0; i < children.length; i++) {
          removeChildren(children[i]);
      }
  }
  function removeBlockEl(id) {
      document.querySelector(".blockid[value='" + id + "']").parentNode.remove();
      document.querySelector(".arrowid[value='" + id + "']").parentNode.remove();
  }
};

Yeah that's good. I am currently trying to make a method to individually remove blocks (so not just a block + children, also just a block inside the tree) so I might focus on that and then use that same method to allow for removing trees as well. The idea would be for the children of the block you want to delete to be attached to the parent of said block, it might not make the most sense, but I guess it may be good to have as an option.

fcnyp commented 4 years ago

I see, that would be good to have as well. You could use this method and pass in an optional second param that would perform that action and knock both out

If false just skip the el remove and splice and rewrite the block attributes to point to the new parent.

fcnyp commented 4 years ago

Updated correcting several errors. Still does not rearrange correctly.

flowy.deleteBlock = function(id, removeChildren) {
    //Track orginal parent
    let newParentId;

    if (!Number.isInteger(id)) {
        id = parseInt(id);
    }

    for (var i=0; i < blocks.length; i++) {
        if (blocks[i].id === id) {
            newParentId = blocks[i].parent;
            removeBlockEls(blocks[i].id);
            blocks.splice(i, 1);
            modifyChildBlocks(id);
            break;
        }
    }

       rearrangeMe();

    function modifyChildBlocks(parentId) {
        let children = [];
        for (var i = blocks.length - 1; i >= 0; i--) {
            if (blocks[i].parent === parentId) {
                children.push(blocks[i].id);
                if (removeChildren === true) {
                    removeBlockEl(blocks[i].id);
                    blocks.splice(i, 1);
                } else {
                    blocks[i].parent = newParentId;
                    rearrange = true;
                    document.querySelector(".arrowid[value='" + blocks[i].id + "']").parentNode.remove();
                    blockstemp = [];
                                blockstemp.push(blocks.filter(a => a.id == blocks[i].id)[0]);
                    snap(document.querySelector(".blockid[value='" + blocks[i].id + "']").parentNode, newParentId, blocks.map(a => a.id));
                    rearrange = false;
                }
            }
        }

        //Only call repeat of function if removing children
        if (removeChildren === true) {
            for (var i=0; i < children.length; i++) {
                modifyChildBlocks(children[i]);
            }
        }
    }
    function removeBlockEls(id) {
        document.querySelector(".blockid[value='" + id + "']").parentNode.remove();
        document.querySelector(".arrowid[value='" + id + "']").parentNode.remove();
    }
};
pilootchoum commented 4 years ago

Updated correcting several errors. Still does not rearrange correctly.

flowy.deleteBlock = function(id, removeChildren) {
  //Track orginal parent
  let newParentId;

  if (!Number.isInteger(id)) {
      id = parseInt(id);
  }

  for (var i=0; i < blocks.length; i++) {
      if (blocks[i].id === id) {
          newParentId = blocks[i].parent;
          removeBlockEls(blocks[i].id);
          blocks.splice(i, 1);
          modifyChildBlocks(id);
          break;
      }
  }

       rearrangeMe();

  function modifyChildBlocks(parentId) {
      let children = [];
      for (var i = blocks.length - 1; i >= 0; i--) {
          if (blocks[i].parent === parentId) {
              children.push(blocks[i].id);
              if (removeChildren === true) {
                  removeBlockEl(blocks[i].id);
                  blocks.splice(i, 1);
              } else {
                  blocks[i].parent = newParentId;
                  rearrange = true;
                  document.querySelector(".arrowid[value='" + blocks[i].id + "']").parentNode.remove();
                  blockstemp = [];
                                blockstemp.push(blocks.filter(a => a.id == blocks[i].id)[0]);
                  snap(document.querySelector(".blockid[value='" + blocks[i].id + "']").parentNode, newParentId, blocks.map(a => a.id));
                  rearrange = false;
              }
          }
      }

      //Only call repeat of function if removing children
      if (removeChildren === true) {
          for (var i=0; i < children.length; i++) {
              modifyChildBlocks(children[i]);
          }
      }
  }
  function removeBlockEls(id) {
      document.querySelector(".blockid[value='" + id + "']").parentNode.remove();
      document.querySelector(".arrowid[value='" + id + "']").parentNode.remove();
  }
};

Hello, did you find why this is not reaarranging? I am quite struggling but would really love to have this feature as well,

Btw @alyssaxuu when you say " It shouldn't be hard to implement at all,", do you think of a simple piece of code that would allow at least to delete a block and all of its children? Sorry to ask, but I am quite in an hurry on my project ;)

alyssaxuu commented 4 years ago

Updated correcting several errors. Still does not rearrange correctly.

flowy.deleteBlock = function(id, removeChildren) {
    //Track orginal parent
    let newParentId;

    if (!Number.isInteger(id)) {
        id = parseInt(id);
    }

    for (var i=0; i < blocks.length; i++) {
        if (blocks[i].id === id) {
            newParentId = blocks[i].parent;
            removeBlockEls(blocks[i].id);
            blocks.splice(i, 1);
            modifyChildBlocks(id);
            break;
        }
    }

       rearrangeMe();

    function modifyChildBlocks(parentId) {
        let children = [];
        for (var i = blocks.length - 1; i >= 0; i--) {
            if (blocks[i].parent === parentId) {
                children.push(blocks[i].id);
                if (removeChildren === true) {
                    removeBlockEl(blocks[i].id);
                    blocks.splice(i, 1);
                } else {
                    blocks[i].parent = newParentId;
                    rearrange = true;
                    document.querySelector(".arrowid[value='" + blocks[i].id + "']").parentNode.remove();
                    blockstemp = [];
                                blockstemp.push(blocks.filter(a => a.id == blocks[i].id)[0]);
                    snap(document.querySelector(".blockid[value='" + blocks[i].id + "']").parentNode, newParentId, blocks.map(a => a.id));
                    rearrange = false;
                }
            }
        }

        //Only call repeat of function if removing children
        if (removeChildren === true) {
            for (var i=0; i < children.length; i++) {
                modifyChildBlocks(children[i]);
            }
        }
    }
    function removeBlockEls(id) {
        document.querySelector(".blockid[value='" + id + "']").parentNode.remove();
        document.querySelector(".arrowid[value='" + id + "']").parentNode.remove();
    }
};

Hello, did you find why this is not reaarranging? I am quite struggling but would really love to have this feature as well,

Unfortunately it's hard to say - I would have to create the method myself to be able to see why the code isn't working. It could be due to the childwidth property in the blocks array not being reset, or possibly the fact that the "i" argument in snap(drag, i, blocko) is not in fact a block ID, but rather a position in the "blocko" array (so I know this may be a bit counterintuitive, but the way I originally approached it was to have blocko be an array of all the block IDs, so blocko = blocks.map(a => a.id);).

In that regard, you can try the following:

  1. Create blocko array with blocko = blocks.map(a => a.id); (inside modifyChildBlocks(parentId))
  2. For loop (so the one inside modifyChildBlocks(parentId)) iterating for each value of the blocko array, so instead of using blocks[i], you would use, for example, blocks.filter(a => a.id == blocko[i])[0] (this is super counterintuitive and I should definitely make it better it in the future)
  3. Call snap(drag, i, blocko) when ready to snap using the same parameters

Aside from that, another possible issue you might encounter is that you might accidentally remove the "blue pulse" that indicates that a block can be attached, since it automatically becomes a child of whichever block is about to become a parent. To avoid that you can just use this before you remove the block div:

canvas_div.appendChild(document.querySelector(".indicator"));

Hope that helps. I am currently working on somewhat of an "overhaul" for the library but I am not going to release new features & enhancements progressively, I would rather wait until everything is polished and release everything at once.

Btw @alyssaxuu when you say " It shouldn't be hard to implement at all,", do you think of a simple piece of code that would allow at least to delete a block and all of its children? Sorry to ask, but I am quite in an hurry on my project ;)

I don't currently have something ready in that regard, but the way I would approach it would be to look at how the code works for when you rearrange blocks (so, under flowy.moveBlock, look for if (dragblock) { }). The idea, if you wanted to implement this quickly (albeit in a "dirty" way), would be to do the following:

  1. Make the drag variable equal to the parent block div you want to delete
  2. Reuse the code under if (dragblock) { } within flowy.moveBlock
  3. Delete the tree using the following (note that the indicator is moved to the canvas div before the tree is deleted):
blockstemp = [];
canvas_div.appendChild(document.querySelector(".indicator"));
drag.parentNode.removeChild(drag);

In theory that should work as it would act the same way as if you were manually moving a block. Let me know if it works!

pilootchoum commented 4 years ago

Thx a lot, will try asap and come back to you. Could you detail why is childwith property needed?

alyssaxuu commented 4 years ago

Thx a lot, will try asap and come back to you. Could you detail why is childwith property needed?

All right!

The childwidth property is simply used to calculate the total width of the children under a parent, combined (including the offset between siblings). I use this in order to make sure, when building the tree, that it is both centered and also spaced out properly.

image

Basically to understand it better, here's an image. The space between the blue lines is the "childwidth" amount. Thanks to that, I can quickly arrange the sibling of that tree on the right with the correct spacing (red line).

If the childwidth isn't reset it might just cause problems for that reason in terms of alignment and whatnot.

pilootchoum commented 4 years ago

Thank you very much.

I got it working for a DeleteBlock WITH children only using the following piece of code :

` flowy.deleteBlocks = function () { blocks = []; canvas_div.innerHTML = "

"; };

flowy.deleteBlock = function (id) {
  let newParentId;

  if (!Number.isInteger(id)) {
    id = parseInt(id);
  }

  for (var i = 0; i < blocks.length; i++) {
    if (blocks[i].id === id) {
      newParentId = blocks[i].parent;
      canvas_div.appendChild(document.querySelector(".indicator"));
      removeBlockEls(blocks[i].id);
      blocks.splice(i, 1);
      modifyChildBlocks(id);
      break;
    }
  }

  if (blocks.length > 1) {
    rearrangeMe();
  }

  function modifyChildBlocks(parentId) {
    let children = [];
    let blocko = blocks.map((a) => a.id);
    for (var i = blocko.length - 1; i >= 0; i--) {
      let currentBlock = blocks.filter((a) => a.id == blocko[i])[0];
      if (currentBlock.parent === parentId) {
        children.push(currentBlock.id);
        removeBlockEls(currentBlock.id);
        blocks.splice(i, 1);
      }
    }

    for (var i = 0; i < children.length; i++) {
      modifyChildBlocks(children[i]);
    }
  }
  function removeBlockEls(id) {
    document
      .querySelector(".blockid[value='" + id + "']")
      .parentNode.remove();
    document
      .querySelector(".arrowid[value='" + id + "']")
      .parentNode.remove();
  }
};`

I started having a look for not removing children, but couldnt get it to work so far.

robhoward commented 3 years ago

Well, I got this to work. I've been slowly going through and updating/refactoring the code. In this case I used some of the above logic for deleteBlock.

However, I've bypassed the snap function in flowy and have implemented my own draw() method. This method redraws (or moves) block, arrows, etc. Trying to get this to also support zoom in/out using style.transform, but it's slow going. The plan right now is to get insert working and then move on to zoom in/out.

Unfortunately my version of flowy is likely impossible to merge back in. If there is any interest, I may consider publishing this fork but only if there are some other people interested in helping maintain it.

jackbanditdude commented 3 years ago

Well, I got this to work. I've been slowly going through and updating/refactoring the code. In this case I used some of the above logic for deleteBlock.

However, I've bypassed the snap function in flowy and have implemented my own draw() method. This method redraws (or moves) block, arrows, etc. Trying to get this to also support zoom in/out using style.transform, but it's slow going. The plan right now is to get insert working and then move on to zoom in/out.

Unfortunately my version of flowy is likely impossible to merge back in. If there is any interest, I may consider publishing this fork but only if there are some other people interested in helping maintain it.

I'm interested, but I'd like to see how you did it before committing my time. I've accepted at this point that I too have gone too far off the beaten path to ever cleanly merge back with the main branch, but I'd like us to not all reinvent the wheel when we have the same needs.

robhoward commented 3 years ago

I'm happy to share what I've done. I also now have zoom in/out working along with insert/remove for single blocks.

The Yes/No is specific to my use case where the only conditional splitting we do is on a logic check, e.g. "Has replied to SMS".

Flowy is also pretty de-coupled from the UX. I marry the two up separately along with an abstraction for supporting auto-loading blocks. But I'm likely going to start folding the UX/flowy more tightly together as there is just about no way for me to merge back in at this point.

    //

┌──────────────────────────────────────────────────────────────────── // │ Delete a block by id // └──────────────────────────────────────────────────────────────────── flowy.deleteBlock = function (id, removeChildren = true) { let newParentId;

        if (!Number.isInteger(id)) {
            id = parseInt(id);
        }

        // how many children?
        if (directChildrenCount(id) > 1)
            removeChildren = true;

        // Loop through blocks and remove the selected block
        for (var i = 0; i < blocks.length; i++) {

            // found the block to remove
            if (blocks[i].id === id) {

                // hold a reference to the blocks parent id
                newParentId = blocks[i].parent;

                // Set the indicator

canvas_div.appendChild(document.querySelector(".indicator"));

                // Remove the block
                removeBlockEls(blocks[i].id);

                // Remove from blocks array
                blocks.splice(i, 1);

                // update child blocks
                modifyChildBlocks(id, newParentId);

                break;
            }
        }

        // Ensure there are no yes/no blocks hanging around
        checkForTrailingYesNo(newParentId);

        if (blocks.length > 1) {
            flowy.draw();
        }

        // Updates the child block of the block getting removed
        function modifyChildBlocks(parentId, newParentId) {
            let children = [];
            let blocko = blocks.map((a) => a.id);
            for (var i = blocko.length - 1; i >= 0; i--) {
                let currentBlock = blocks.filter((a) => a.id ==

blocko[i])[0]; if (currentBlock.parent === parentId) { children.push(currentBlock.id);

                    // update parent id?
                    if (newParentId != undefined)
                        currentBlock.parent = newParentId;

                    if (removeChildren) {
                        removeBlockEls(currentBlock.id);
                        blocks.splice(i, 1);
                    }
                }
            }

            // Remove all children blocks?
            if (removeChildren) {
                for (var i = 0; i < children.length; i++) {
                    modifyChildBlocks(children[i]);
                }
            }
        }

        function removeBlockEls(id) {

document.querySelector(.blockid[value='${id}']).parentNode.remove(); document.querySelector(.arrowid[value ='${id}']).parentNode.remove(); }

        // Check that we didn't leave a trailing yes/no block
        function checkForTrailingYesNo(id) {

            for (var i = 0; i < blocks.length; i++) {

                if (blocks[i].parent === id) {
                    let blockElem =

document.querySelector(.blockid[value='${blocks[i].id}']); let val = blockElem.parentElement.querySelector('.blockelemtype').value;

                    if (val == "Yes" || val == "No") {
                        flowy.deleteBlock(blocks[i].id, false);
                    }
                }
            }
        }
    };

Founder, DailyStory

DailyStory Marketing Automationdailystory.com | +1-833-914-1105

On Thu, Apr 1, 2021 at 12:32 AM Christopher Boyd @.***> wrote:

Well, I got this to work. I've been slowly going through and updating/refactoring the code. In this case I used some of the above logic for deleteBlock.

However, I've bypassed the snap function in flowy and have implemented my own draw() method. This method redraws (or moves) block, arrows, etc. Trying to get this to also support zoom in/out using style.transform, but it's slow going. The plan right now is to get insert working and then move on to zoom in/out.

Unfortunately my version of flowy is likely impossible to merge back in. If there is any interest, I may consider publishing this fork but only if there are some other people interested in helping maintain it.

I'm interested, but I'd like to see how you did it before committing my time. I've accepted at this point that I too have gone too far off the beaten path to ever cleanly merge back with the main branch, but I'd like us to not all reinvent the wheel when we have the same needs.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/alyssaxuu/flowy/issues/50#issuecomment-811652855, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHHFGADARQ2IL6K5Y3XJDLTGQAQRANCNFSM4MKNZAAA .

sabatale commented 2 years ago

@robhoward @pilootchoum

Probably a dumb question but how to get the id on the fly (not through output)? Is there an event to get the id when clicking an active block that I missed?

Or do I need to get it from the var doneTouch?

var doneTouch = function (event) {
    if (event.type === "mouseup" && aclick && !noinfo) {
      if (!rightcard && event.target.closest(".block") && !event.target.closest(".block").classList.contains("dragging")) {
            tempblock = event.target.closest(".block");

            // Look here
            console.log(tempblock.getElementsByTagName('input')[1].value)

            rightcard = true;
            document.getElementById("properties").classList.add("expanded");
            document.getElementById("propwrap").classList.add("itson");
            tempblock.classList.add("selectedblock");
       }
    }
}

pilootchoum code works for me, thanks! Also, Rob, you seem to be missing the directChildrenCount(id) function.

robhoward commented 2 years ago

This should work -- I've changed the code so much as this point from the original source that I'm not 100% sure though:

document.querySelector('.selectedblock').querySelector('.blockid').value

loralll0 commented 2 years ago

Hello, I've tried with the code from @robhoward, but I have an error at directChildrenCount(id). Can you help me with that?

robhoward commented 2 years ago

All that function does is determine how many children the current block has.

Here is the function if it helps at all: ` // ┌──────────────────────────────────────────────────────────────────── // │ Returns the count of children for the block id // │
// │ @id - id of block to get children count // └──────────────────────────────────────────────────────────────────── directChildrenCount: function(id) {

    let children = 0;

    // count how many blocks are a child
    for (var i = 0; i < this.blocks.length; i++) {

        if (this.blocks[i].parent === id)
            children++
    }

    return children;
},`
SejanasJoshua commented 2 years ago

Hello, can you please share the draw() code.