svelte-dnd-action icon indicating copy to clipboard operation
svelte-dnd-action copied to clipboard

How to implement a target zone with static contents?

Open elonen opened this issue 2 years ago • 11 comments

I'm trying to implement a basic files-and-folders scenario, with customizable folder "icons", something like this:

image

The folders are supposed to be moveable and draggable like files, and to accept drops. I don't want to show any dynamic items inside their HTML elements (much less have users drag them). The trouble is, I do need them to have some extra HTML children (like the "A" in example pic above) for decoration that can't be done with CSS tricks (unlike the simple A example).

Using dragDisabled: true option on folders almost does it (you can drag them around and drop other items in them), but the trouble is that their decorations get mangled. As README puts it:

The dndzone should not have children that don't originate in items

What this means is, the following example gets "decoration stuff" elements hidden (style="visibility: hidden"), among other things, as user hovers stuff above it, due to how shadow elements work:

<script>
 ...
 let dummy_dnd_items = [];
 </script>
 
<div class="myFolder" use:dndzone="{{items: dummy_dnd_items, dragDisabled: true}}">
  <div>
     ...lots of decoration stuff here
     <span>Folder label</span>
   </div>
   <div> ...more decoration etc..</div>
...

First I tried making an empty handleConsider() that doesn't apply any changes that the library suggests, but that results in the dreaded getBoundingClientRect problems in handleFinalize(). Apparently the library expects shadow elements to be there.

I've tried to work around it with a monstrosity like this...

<div class="myFolder" use:dndzone="{{items: dummy_dnd_items, dragDisabled: true}}">
    {#each dummy_dnd_items as item, i}
    {#if i == 0}
      {#each [1,2] as _x}
        {#if x == 0 || item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
          <div>
             ...lots of decoration stuff here
             ...
 ...

...but that lead to chaos with element size calculations in browser, and is pretty much unreadable anyway.

Is there some (clean) way to achieve this?

If not, maybe one could mark static target children with some class like dnd-dont-touch-this-its-not-a-zone-item so you could actually have children that don't come from items?

elonen avatar Mar 03 '23 15:03 elonen

~~It sounds like PR #335 would allow this as well, you just wouldn't render the dummy_dnd_items for the folder div.~~ (Looks like it had issues of its own.)

elonen avatar Mar 03 '23 15:03 elonen

Ok, I finally figured out a method that works:

<script>
let dummy_dnd_items: any = [{'id': 'dummy'}];

function handleConsider(e: CustomEvent<DndEvent>) {
    // Move shadow item to the end of the list
    // so first item is never hidden
    dummy_dnd_items = e.detail.items
        .filter(it => !it[SHADOW_ITEM_MARKER_PROPERTY_NAME])
        .concat(e.detail.items.filter(it => it[SHADOW_ITEM_MARKER_PROPERTY_NAME]));
}

function handleFinalize(e: CustomEvent<DndEvent>) {
    // <Handle drop logic here>, then
    // clear the dummy item list again
    dummy_dnd_items = [{'id': 'dummy'}];
}
</script>


<div use:dndzone="{{items: dummy_dnd_items, morphDisabled: true, dragDisabled: true}}">
  {#each dummy_dnd_items as item, i}
    {#if i == 0}
    <div>
      ... static decoration stuff ...
    </div>
    {:else}<span/>{/if}   <!-- without these invisible items, getBoundingClientRect error at finalize -->
  {/each}
</div>

elonen avatar Mar 03 '23 19:03 elonen

cool workaround but why not place that icon under the same parent as the dndzone and position:absolute it to your liking rather than making it a child of the dndzone?

isaacHagoel avatar Mar 04 '23 10:03 isaacHagoel

I want the folders to change style (zoom and brighten) on dnd actions, which the drop zone action supports nicely, and have the decorations be included.

elonen avatar Mar 04 '23 14:03 elonen

Bug with the workaround: works ok for mouse, but for keyboard navigation, the static decorations divs apparently get automatically assigned tabindexes from the library (even though dragDisabled: true) => bogus tab focus targets inside the folder element, and weird behavior when reordering by keyboard.

Maybe if the tabindex wasn't overridden if it's already set to -1..? A way to define static items that the library ignores would be cleaner though. Or perhaps - for this use case - just a way to declare a dropzone targetOnly (=zone that can finalize() drops but doesn't have any managed items in it).

elonen avatar Mar 04 '23 18:03 elonen

Feel free to make a repl and i will check if the tab index behaviour is a bug or better - help you get the absolute positioning solution working including the styling that you need. Ignoring some of the items was considered in the past but it adds a lot of complxity and edge cases and I can remember a case in which the desired behaviour couldn't have been achieved without it.

On Sun, Mar 5, 2023, 05:12 elonen @.***> wrote:

Bug with the workaround: works ok for mouse, but for keyboard navigation, the static decorations divs apparently get automatically assigned tabindexes from the library (even though dragDisabled: true) => bogus tab focus targets inside the folder element, and weird behavior when reordering by keyboard.

Maybe if the tabindex wasn't overridden if it's already set to -1..? A way to define static items that the library ignores would be cleaner though.

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

isaacHagoel avatar Mar 04 '23 20:03 isaacHagoel

Thank you. Here's the repl: https://svelte.dev/repl/82a7534845b44337b9cdaffdea034b6e?version=3.55.1

elonen avatar Mar 05 '23 10:03 elonen

Thanks, just before I spend time on this, have you tried the zoneTabIndex option?

On Sun, Mar 5, 2023 at 9:05 PM elonen @.***> wrote:

Thank you. Here's the repl: https://svelte.dev/repl/82a7534845b44337b9cdaffdea034b6e?version=3.55.1

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

isaacHagoel avatar Mar 05 '23 20:03 isaacHagoel

Great suggestion, thanks! It's almost there now. I managed to get the decorations out of Folder items with some CSS trickery. Combining that with zoneTabIndex: -1 and a finalize() variation that completely clears items after the drop very nearly does it.

Unfortunately, keyboard dropping now throws an exception. It feels like a dangling reference inside the library. Here's a new REPL with instructions on how to reproduce it: https://svelte.dev/repl/0efc86c2581340b0ac5b448b0fc407b9?version=3.55.1

To avoid this undefined.id exception, it probably should exit the keyboard drag state if dragged element is destroyed by a some finalize() (or even consider()).

elonen avatar Mar 05 '23 22:03 elonen

Success! Version 3 seems to work perfectly with both mouse and keyboard: https://svelte.dev/repl/0c13450eff4347318dfc684342f86b6e?version=3.55.1

The code is surprisingly terse, but I had to use info.trigger and info.source in a rather groovy way to clear the item list at an appropriate state depending on the used input device:

function onSink(e) {
	console.log("Sunk #" + e.detail.items[0].id + " into #" + id)
	items = [];
}
	
function consider(e) {
	if (e.detail.info.trigger == TRIGGERS.DRAG_STOPPED && e.detail.info.source == SOURCES.KEYBOARD) {
		// On keyboard drag, DRAG_STOPPED on consider() is the _real_ "finalize" state
		onSink(e);
	} else {
		items = e.detail.items;
	}
}
	
function finalize(e) {
	if (e.detail.info.source == SOURCES.KEYBOARD) {
		// On keyboard, dragged item can be taken back out by another (shift-)tab key hit,
		// so we have to keep in `items` for now:		
		items = e.detail.items;
	} else {
		// On pointer, finalize() is actually final. Sink the item.
		onSink(e);
	}
}

The exception mentioned in previous comment might be worth fixing anyway, though.

elonen avatar Mar 06 '23 00:03 elonen

I'm fine with the solution for now, so if/when you're done looking into this @isaacHagoel, the issue can be closed. Thank you so much for the help!

elonen avatar Mar 08 '23 09:03 elonen