isaacHagoel / svelte-dnd-action

An action based drag and drop container for Svelte
MIT License
1.81k stars 104 forks source link

Conditionally drop item into dndzone from a central zone? #591

Closed Gold-Blend closed 4 months ago

Gold-Blend commented 4 months ago

Sorry if this has been asked, had a look through the issues but couldn't see it.

I want to have 2 drop zones where each drop zone has a different type and there is a central zone that initially holds the items to be dragged to other zones. However, each zone will only accept certain items. I can't seem to find a way to do this. This is what I currently have which doesn't work for handleSort

function handleSort(e: { detail: { items: any[] } }) {
        let draggedWord: any = e.detail.items.filter((item) => item.isDndShadowItem) 
        if (draggedWord[0] && shadowWords.has(draggedWord[0].word)) {
            items = e.detail.items;
        }
    }

I've tried some other methods but it's no good. It's also awkward because when you drag an item over a dropzone handleSort is called multiple times and my var draggedWord is not guaranteed to be the same due to how it works under the hood.

isaacHagoel commented 4 months ago

The consider/finalize handlers are not the ideal place for the functionality you're describing. Instead you should be changing "dropFromOthersDisabled" dynamically (changing types mid drag is not supported currently, maybe I should add support for it). An outline for a solution could be as follows:

  1. create a store that tells the app which zone the current drag originates in.
  2. in the consider handlers check the "trigger" in the event (e.detail.info.trigger) and if it is TRIGGERS.DRAG_STARTED, set the store value. On the finalize handler clear the store.
  3. Use the store value to decide whether the "dropFromOthersDisabled" option needs to be true or false.

554 has a similar question.

Gold-Blend commented 4 months ago

Thanks for the reply.

So I've tried to implement the solution but I've realised need the value from the items from within the original dropzone. So, this is what I've implemented so far:

      function handleConsider(e: { detail: { info: any; items: any[]; }; }) {
        if (e.detail.info.trigger === TRIGGERS.DRAG_STARTED) {
            let [wordOrigin] = e.detail.items.filter((item) => item.isDndShadowItem)
            currentWordDragOrigin.set(wordOrigin.type);
        }
        items = e.detail.items;
    }

    function handleFinalize(e: { detail: { info: any; items: any[]; }; }) {
        currentWordOrigin = get(currentWordDragOrigin);
        if (currentWordOrigin  && currentWordOrigin  === origin) {
            dropFromOthersDisabled = false
        }
        items = e.detail.items;
        currentWordDragOrigin.set(null);
        dropFromOthersDisabled = true
      }

But you can see that the original dropzone is where origin comes from which won't match the dropzone that I'm trying to drop the item into. So I need to get the origin of the target dropzone, see if it matches a property on the item and then adjust the prop dropFromOthersDisabled but I don't think it's currently possible?

isaacHagoel commented 4 months ago

The item shouldn't matter. All of this happens at a zone level. I am flying somewhere today but will try to find time to make an example for you when I can.

On Fri, Jul 5, 2024, 23:14 Dalton Thomas @.***> wrote:

Thanks for the reply.

So I've tried to implement the solution but I've realised need the value from the items from within the original dropzone. So, this is what I've implemented so far:

  function handleConsider(e: { detail: { info: any; items: any[]; }; }) {
  if (e.detail.info.trigger === TRIGGERS.DRAG_STARTED) {
      let [wordOrigin] = e.detail.items.filter((item) => item.isDndShadowItem)
      currentWordDragOrigin.set(wordOrigin.type);
  }
  items = e.detail.items;

}

function handleFinalize(e: { detail: { info: any; items: any[]; }; }) { currentWordOrigin = get(currentWordDragOrigin); if (currentWordOrigin && currentWordOrigin === origin) { dropFromOthersDisabled = false } items = e.detail.items; currentWordDragOrigin.set(null); dropFromOthersDisabled = true }

But you can see that the original dropzone is where origin comes from which won't match the dropzone that I'm trying to drop the item into. So I need to get the origin of the target dropzone, see if it matches a property on the item and then adjust the prop dropFromOthersDisabled but I don't think it's currently possible?

— Reply to this email directly, view it on GitHub https://github.com/isaacHagoel/svelte-dnd-action/issues/591#issuecomment-2210858347, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4OZCYOPS7NGDQTO3W74NLZK2L2ZAVCNFSM6AAAAABKLRTROWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMJQHA2TQMZUG4 . You are receiving this because you commented.Message ID: @.***>

isaacHagoel commented 4 months ago

My idea was something like this https://svelte.dev/repl/2d0fa17497f947aa971709ab50bc752f?version=4.2.18

Gold-Blend commented 4 months ago

That's exactly what I was looking for, thank you!

I added this bit to understand where the word can be dropped into, as only the item itself knows:

function handleConsider(e: { detail: { info: any; items: any[]; }; }) {
        const { trigger } = e.detail.info;
        if (trigger === TRIGGERS.DRAG_STARTED) {
            let [draggedWordOrigin] = e.detail.items.filter((item) => item.isDndShadowItem)
            currentWordDragOrigin.set(draggedWordOrigin.type);
        }
        items = e.detail.items;
    }

Before I close this, would you say it's reliable to use isDndShadowItem this way?

Thanks again for your help.

isaacHagoel commented 4 months ago

Yes. It's reliable

On Sat, Jul 6, 2024, 18:11 Dalton Thomas @.***> wrote:

That's exactly what I was looking for, thank you!

I added this bit to understand where the word can be dropped into, as only the item itself knows:

function handleConsider(e: { detail: { info: any; items: any[]; }; }) { const { trigger } = e.detail.info; if (trigger === TRIGGERS.DRAG_STARTED) { let [draggedWordOrigin] = e.detail.items.filter((item) => item.isDndShadowItem) currentWordDragOrigin.set(draggedWordOrigin.type); } items = e.detail.items; }

Before I close this, would you say it's reliable to use isDndShadowItem this way?

Thanks again for your help.

— Reply to this email directly, view it on GitHub https://github.com/isaacHagoel/svelte-dnd-action/issues/591#issuecomment-2211702369, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4OZCZI3ZDZ3JG2374AWO3ZK6RE5AVCNFSM6AAAAABKLRTROWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMJRG4YDEMZWHE . You are receiving this because you commented.Message ID: @.***>

Gold-Blend commented 4 months ago

Great, thanks again and good work on the repo.

isaacHagoel commented 4 months ago

With that said, i am still not sure why not do this. It seems cleaner to me.

function handleConsider(e: { detail: { info: any; items: any[]; }; }) {
  const { trigger, id } = e.detail.info;
  if (trigger === TRIGGERS.DRAG_STARTED) {
    const draggedWordOrigin = items.find(item => item.id === id);
    currentWordDragOrigin.set(draggedWordOrigin.type);
  }
  items = e.detail.items;
}
Gold-Blend commented 4 months ago

Yeah, you're absolutely right this is way cleaner.

You've been really helpful, thanks.

isaacHagoel commented 4 months ago

my pleasure. let me know if you need anything else

Gold-Blend commented 4 months ago

Hi Isaac,

Didn't want to start a new issue unless you want me to, I don't mind.

I think it may be intentional, but flipDurationMs is directly tied to how long you must keep the item over the dz before it'll be accepted and perform the animation. I think I found the block that addresses this here:

// it is important that we don't have an interval that is faster than the flip duration because it can cause elements to jump bach and forth
    const setIntervalMs = Math.max(...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs));
    const observationIntervalMs = setIntervalMs === 0 ? DISABLED_OBSERVATION_INTERVAL_MS : Math.max(setIntervalMs, MIN_OBSERVATION_INTERVAL_MS); // if setIntervalMs is 0 it goes to 20, otherwise it is max between it and min observation.
    multiScroller = createMultiScroller(dropZones, () => currentMousePosition);
    observe(draggedEl, dropZones, observationIntervalMs * 1.07, multiScroller);

and I can see you left that comment. So is this not able to be decoupled otherwise it causes the elements to jump back and forth?

For context, I'm making a kid's phonics application and one part has a game where they must drag n drop the words in the right dz that get generated one at a time. If it's accepted, I want it to do a slower animation than 200ms, realistically about 800ms as it looks smoother for children. But, this means they have to keep the cursor there longer for it to be accepted which is pretty bad for UX.

isaacHagoel commented 4 months ago

Animation that is slower than the library refresh cycle can be interrupted mid way if the user moves the item to a new place before the animation finish playing. it is not a technical limitation of the lib. If the lib needs to wait for animations to complete it effectively means slower response time and if it doesn't, the UX is strange. You can pass 800 to svelte and 200 or any smaller number to the library and see for yourself. If you have an idea for a different solution let me know.

On Sun, Jul 7, 2024, 03:54 Dalton Thomas @.***> wrote:

Hi Isaac,

Didn't want to start a new issue unless you want me to, I don't mind.

I think it may be intentional, but flipDurationMs is directly tied to how long you must keep the item over the dz before it'll be accepted and perform the animation. I think I found the block that addresses this here:

// it is important that we don't have an interval that is faster than the flip duration because it can cause elements to jump bach and forth const setIntervalMs = Math.max(...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs)); const observationIntervalMs = setIntervalMs === 0 ? DISABLED_OBSERVATION_INTERVAL_MS : Math.max(setIntervalMs, MIN_OBSERVATION_INTERVAL_MS); // if setIntervalMs is 0 it goes to 20, otherwise it is max between it and min observation. multiScroller = createMultiScroller(dropZones, () => currentMousePosition); observe(draggedEl, dropZones, observationIntervalMs * 1.07, multiScroller);

and I can see you left that comment. So is this not able to be decoupled otherwise it causes the elements to jump back and forth?

For context, I'm making a kid's phonics application and one part has a game where they must drag n drop the words in the right dz that get generated one at a time. If it's accepted, I want it to do a slower animation than 200ms, realistically about 800ms as it looks smoother for children. But, this means they have to keep the cursor there longer for it to be accepted which is pretty bad for UX.

— Reply to this email directly, view it on GitHub https://github.com/isaacHagoel/svelte-dnd-action/issues/591#issuecomment-2211832236, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4OZC73JH4OYKWNPNELGJDZLAVL5AVCNFSM6AAAAABKLRTROWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMJRHAZTEMRTGY . You are receiving this because you commented.Message ID: @.***>

Gold-Blend commented 4 months ago

Ah okay, got you. Yeah I'll have a play around and let you know if I come up with anything