TiddlyWiki / TiddlyWiki5

A self-contained JavaScript wiki for the browser, Node.js, AWS Lambda etc.
https://tiddlywiki.com/
Other
7.97k stars 1.18k forks source link

How to support drag and drop reordering of lists #1486

Closed tobibeer closed 1 year ago

tobibeer commented 9 years ago

For now, this is a "placeholder" issue, so we can have one spot for drafting a design...

My idea would be to extend the ListWidget so as to provide drag-sorting functionality. Even if that would require additional DOM elements — which I don't think it does — it cannot be said to create compatibility issues precisely because, beforehand, drag-sorting would not have existed as an option.

tobibeer commented 9 years ago

From https://github.com/Jermolene/TiddlyWiki5/issues/592#issuecomment-73621400:

@buggyj: I sure hope to see your taglist dropadapter actually extending the core $list widget rather than being a "standalone" thing. Any chance of seeing that happening? —@tobibeer

@tobibeer: I am not sure that adding drag and drop to the list widget is a good idea, it is already a very complicated thing, and I fear it would become a bit of a 'monster'. Also for drag and drop there needs to be something to add drop handlers to, in the taglist widget I add divs and render elements inside the divs which is at odds with the design of the list widget.

Maybe we could have a separate 'arrangelist' widget (without the story functionality)? —@buggyj

Complicated or not, I don't quite see how the two can be untied. Let alone, some may even desire the story to be a drag-sortable thing.

As for the design, not sure we actually need those additional div's. Why would those drag- / drop-handlers not go onto the LIs?

My gut feeling suggests that not making the list widget this "little" more complicated will make a lot of other things a whole lot more complicated in turn... not for the better. After all, we are indeed talking about the very same list(s), imho.

Pfiver commented 9 years ago

Not that I have much knowledge about the architecture/design of the tiddlywiki application, but as an "end user" (who has recently "played" with the (html5) drag-drop-api a little and thinks that it is quiet simple to use) it would seem natural to me that the functionality is added to the standard list widget.

(I am looking very much forward to seeing a JIRA/greenhopper scum/kanban board clone.)

sukima commented 9 years ago

Oh kanban board... That's an idea.

tobibeer commented 8 years ago

This looks like a very promising foundation:

A possible syntax, if we're to extend the list widget:

<$list sortable/>

...and with options...

<$list sortable="""{SORTABLEOPTIONS}"""/>

This could specify via json the options to be applied, e.g. css selectors of sortable elements, all with meaningful defaults.

As for what to sort, the default would be to list the tiddlers tagging to the current tiddler as sorted via its list field, and then ways to either specify a different tiddler and/or a different list-field.

The list widget could possibly pass all specified options to the sortable component as-is while consuming any that are TiddlyWiki specific, such as tiddler:"foo" or field:baz... the necessary functions for dragsorting obviously hardcoded into the widget.

This way, even the filter parameter of the list widget might be associated with a list-field of a given tiddler in which to persist ordering information.

Obviously, any sorting would not be handled via some sort[] filter but rather by the sortable via some list-field.

pmario commented 8 years ago

looks good, is MIT licensed and small. Should be worth a try.

twMat commented 8 years ago

+1

tobibeer commented 8 years ago

@Jermolene, what would be the proper way to integrate Sortable in TiddlyWiki.

Would the necessary dependency, come packaged in a plugin, even if the core might have the codebase that leverages it?

Jermolene commented 8 years ago

Hi @tobibeer

what would be the proper way to integrate Sortable in TiddlyWiki.

Pretty difficult to do it robustly: consider what needs to happen if the tiddler containing the sortable is refreshed during the drag. It's pretty much the same problem as restoring the text focus after a refresh.

tobibeer commented 8 years ago

It's pretty much the same problem as restoring the text focus after a refresh.

Does that mean we need to solve that first and, if so, do we have any approach to that yet?

Jermolene commented 8 years ago

Hi @tobibeer

Does that mean we need to solve that first and, if so, do we have any approach to that yet?

I think we need to evolve a "native" way of handling drag and drop. If one ignores performance concerns, then one could imagine a fairly simple approach that starts with the principle of keeping the state within the tiddler store.

One might model an in-progress drag as a reference to template that is rendered as the dragged object, and text references that give the current coordinates of the object. The system would recognise touches/drags on an appropriately configured widget and continually update the coordinates as it is dragged.

To make that work, we'd have to engineer the refresh cycle to be fast enough to call it on a mouse/touch move event. That's not impossible to imagine, but would certainly require some creativity.

tobibeer commented 8 years ago

I really think the above takes the idea of "states" much too far, for no discernible reason, even further than it already is. Drag and drop should be done entirely outside the scope of the store, which only needs to know of dnd happening maybe at dragstart and then dragend.

Everything in between should be "basic" dom manipulation and never anything else. To be doing more than that would be equivalent to remembering that the last time we used MS Word we were about to drag something from A to B. Why would we ever do that? That dragging operation is aborted.

If for some reason a change to the store would try and steal the underlying representation via refresh, then that refresh needs to halt until after any dragend.

Jermolene commented 8 years ago

I really think the above takes the idea of "states" much too far, for no discernible reason, even further than it already is. Drag and drop should be done entirely outside the scope of the store, which only needs to know of dnd happening maybe at dragstart and then dragend.

But @tobibeer TiddlyWiki uses the tiddler store for all UI state; that's how it is designed. Why would one invent some new mechanism to handle the same thing?

Everything in between should be "basic" dom manipulation and never anything else.

"Should"? As I say, the boat has already sailed: it's too late to make TiddlyWiki 5 use the DOM for state. But there's plenty of DOM specific code - have a look at how modals or popups are implemented, for example.

To be doing more than that would be equivalent to remembering that the last time we used MS Word we were about to drag something from A to B. Why would we ever do that? That dragging operation is aborted.

I'm not sure that I follow the metaphor here?

If for some reason a change to the store would try and steal the underlying representation via refresh, then that refresh needs to halt until after any dragend.

How does one halt a refresh cycle? What are the semantics?

tobibeer commented 8 years ago

Why would one invent some new mechanism to handle the same thing?

As I said, it takes the idea too far. Why would you want to have such information persisted, like "we are currently dragging at x and y"?

So, with respect to...

it's too late to make TiddlyWiki 5 use the DOM for state.

...the question to me is: What is considered "state" and what isn't?

Surely, not everything that can be persisted is worthy of being persisted, as state. For example, we could also persist the current pointer position. But why?

If I included jQuery in TiddlyWiki, the first thing I'd do was DOM-manipulation ...and only doing any store-stuff when actually needed. I would not commit everything I do as something persisted in the store. All I do is ui-enhancements. For example, if I don't need to or want to remember a player state, e.g. paused or playing, I just don't store it. Simple.

there's plenty of DOM specific code

So, of course there is. After all, you want your state to correspond to your representation to correspond to your state... to the level of detail that any state needs to be in sync with representation. We're not going to move the pointer to the last position where it was when we last visited TiddlyWiki, are we? :D

How does one halt a refresh cycle?

The same way changes are accumulated first before a refresh is fired. Perhaps it's not the refresh that is halted but whichever function calls for it.

Jermolene commented 8 years ago

@tobibeer I think you are basically trying to re-design TiddlyWiki, which is fair enough and interesting in a way. But the problem is that we've already got a TiddlyWiki that keeps it's state in the DOM, in TiddlyWiki Classic. Working on TiddlyWiki Classic I got to take the DOM-based idea as far as I could, and that's why I've ended up with the current design for TiddlyWiki5.

As it happens, the development of TiddlyWiki has mirrored the direction of the mainstream web development community, which is in the process of shifting away from jQuery towards new frameworks like Angular and React, that themselves are much more like TiddlyWiki.

Surely, not everything that can be persisted is worthy of being persisted, as state. For example, we could also persist the current pointer position. But why?

Well, if we wanted to do certain effects it might be useful.

I don't understand why you are so against storing state in tiddlers. Is it that you think it's complicated?

As it happens, in the case of drag and drop, since only one drag can happen at once, we can actually track a lot of the state globally.

tobibeer commented 8 years ago

I'm very much for leveraging new technologies ...where that makes sense.

I don't understand why you are so against storing state in tiddlers. Is it that you think it's complicated?

I am not against using the state mechanism in general and I don't think its complicated nor should it be.

However, for anything to be persisted as state, I believe it needs two prerequisites...

I'd gladly accept persisting information like "where are we currently dragging, say, that tiddlylink" as a state, if you were so kind to explain the benefit of doing so.

To me, what needs persisting indeed is the information that the user is currently dragging something and what, so that TiddlyWiki doesn't steal away the representation during a dragging operation and maybe show a little drag-handle, etc...

Jermolene commented 8 years ago

However, for anything to be persisted as state, I believe it needs two prerequisites...

  • a benefit of persisting that information
  • statefulness actually being a requirement to progress with very important issues, like reordering lists
    • and I mean during the action, rather than its end-result

The criteria are actually much simpler: state needs to be stored in the tiddler store if that state needs to survive the widget being destroyed and recreated during the refresh cycle. That's a hard and fast rule; it's kind of one of the laws of physics of the universe we've built.

tobibeer commented 8 years ago

The criteria are actually much simpler: state needs to be stored in the tiddler store if that state needs to survive the widget being destroyed and recreated during the refresh cycle.

Allow me to ask again: Which (stateful) information would that be during a dnd operation? Sure, we may need some global temp tiddler created on dragstart referencing or even duplicating the tiddler being dragged.

But I don't see any need to do something with that state tiddler while dragging. I don't think I need to remember the coordinates of where I was just hovering. What is important is to know where I dropped something ...when I consciously did so.

Should the store steal away that wiki representation a few millisecond before I got to lift my finger form my pointing device so as to end the drag operation and so I end up dropping it at the wrong end, then that wiki and its ui are doing it wrong. It should never change during dnd in any stateful manner, only on dragend and beyond. Otherwise, please explain why it should.

Jermolene commented 8 years ago

Hi @tobibeer

The first thing is to understand how and why a refresh cycle might be triggered. One good example is a clock: a tiddler that is updated every 1 second with the current time; one could then imagine how easy it would be to build clocks that dynamically update. With a little date arithmetic, we could make timers, too.

Anyhow, that clock is surely going to keep ticking while the drag operation is taking place.

Allow me to ask again: Which (stateful) information would that be during a dnd operation?

I don't know at this point. But it's clear from the start that there are only two options for state data: the tiddler store, and global state in JS associated with the drag. That's pretty much the same situation as with the existing modals, popups and notification handling.

tobibeer commented 8 years ago

I imagine what you say is that, in general, you want any clocks to keep ticking while a user drags something around.

For a given clockety widget that doesn't change position that might be ok, for a story river or other list that dynamically renders representations of tiddlers, perhaps lazily loaded and added to lists it seems a little fidgety during a dnd operation to find that the lists and list items where you try to drop something keep on changing under your nose.

Imagine some live feed where items get added dynamically, e.g. a twitter or facebook feed. If that thing was to update while a user wants to drop something onto a given feed item, and assuming that to be a valid dnd operation, then that sounds problematic.

But then, let's accept, heck, maybe even the item we currently believe to be dragging around no longer exists. Still, I don't see a need to update a state tiddler during dragging... even if the entire wiki chose to self-destruct (and maybe rebuilt ;-) its entire representation during my doing so.

The only thing that counts — state-wise — appears to be the moment I drop something and then, of course, the moment I started dragging it.

Jermolene commented 8 years ago

@tobibeer mostly you are arguing about the validity of the example that I gave.

Obviously it would be a problem if drag targets move around during the drag. So it would if links moved around before one clicked them. But that's a UI issue, it's not really germane to the question of how to handle refreshes during a drag operation.

There a lot of places in the TW design where the mechanisms support thing that would make a poor user interface, but that doesn't invalidate the presence of those mechanisms.

Still, I don't see a need to update a state tiddler during dragging... even if the entire wiki chose to self-destruct (and maybe rebuilt ;-) its entire representation during my doing so.

I think you missed the part where I said that I didn't know which bits of state would be stored where. I've not really tried to design the mechanism yet.

The only thing that counts — state-wise — appears to be the moment I drop something and then, of course, the moment I started dragging it.

Maybe you are forgetting the need for drag targets to give feedback as a drag passes over them.

tobibeer commented 8 years ago

Maybe you are forgetting the need for drag targets to give feedback as a drag passes over them.

Did not forget that. They would need to be specifically dnd enabled, i.e. they need handlers, dropzones and templates that render the appropriate content upon dragenter and destroy that content upon dragleave, by default using the item-template of that list.

For a flat list, dragging over a list-item would possibly push it down and show the content of the tiddler reference being dragged instead in an ad-hoc list-item.

For nested lists, with items being (possible) sub-items, dragging over a list item might use some different item-template, i.e. one with some placeholder / dropzone that allowed for the dragged link to eventually become a nested item.

that doesn't invalidate the presence of those mechanisms

Was not intending to invalidate mechanisms, only asking about when to use which... and when not to.

I think you missed the part where I said that I didn't know which bits of state would be stored where. I've not really tried to design the mechanism yet.

I did not miss that part. To me, one aspect of this issue is to come to terms with the mechanism, so I was discussing the details of that rather than questioning the general usefulness of states

t5a commented 8 years ago

I thought I previously saw this done by BJ. What happened to http://tobibeer.github.io/tb5/#Reorder%20ToC%20Via%20Drag%20And%20Drop ?

tobibeer commented 8 years ago

Hi @Infurnoape, thanks for the reminder. BJ's solution shows one thing above all, that we are able to and possibly well advised to go for a "native" solution w/o external dependencies such as the above proposed library that solely work on DOM based manipulation.

Anyhow, while BJs dragsorting works, it has some constraints I'd hope not to see in a core implementation. The first being that it all anchors around links and buttons. A list item can be something freely designed and so I'd hope for it to be dragable, whatever its form, not via representation of links or buttons disabling the default behaviour.

However, I quite like the smart sublist rendering. It takes some getting used to, but then it requires much less space than any nested lists would. This could possibly even work for more than two levels.

Anyhow, I imagine wrapping drag-enabled list-items in a, well, <$draggable/> widget, so I can grab it wherever I'd like or maybe declare some handle="true" attribute for it, so as to constrain drag-initiation of a draggable to a given area ...via css ...and maybe a dropzone:"no" attribute so as to constrain dnd to only drag from this list but not to it.

Essentially, when a dropped item hovers a draggable, I want it to render as-if a list-item, maybe with a mild translucency to have a ghost-like appearance. This means, that we need to remember any scope variables of the list-item and, I'm not sure we can actually achieve that for any items that have not been on the list during initial rendering, as it can be the outcome of some <$list variable="foo"/> ...so how to get foo for any new items? Not trivial.

So the dropzone for any item would, by default, be the item itself and then there'd have to be another one below, so I can place a dragged item after the last item. This would make for any draggables being siblings consuming the drop-after-zone of the previous list-item, so as to only have a very last drop-zone for dropping an item after the last list item.

I imagine there also to be slight differences in how to optimize items that are stacked vertically, horizontally or inline / floating. So, a draggable may need to have that as an attribute to behave slightly differently depending on the case, e.g. mode="vertical".

buggyj commented 8 years ago

@tobibeer

Anyhow, while BJs dragsorting works, it has some constraints I'd hope not to see in a core implementation. The first being that it all anchors around links and buttons. A list item can be something freely designed and so I'd hope for it to be dragable, whatever its form, not via representation of links or buttons disabling the default behaviour.

The drag functionality is determined by link widget that has a 'draggable' attribute. - When dragging links to a tiddler, the tiddler is copied into the system. The taglist widget has a 'nodrop' attribute.

Limiting the list type to tags ensures consistency - on dropping a tiddler the list field is updated. As the list field of a tag only re-orders the tagged tiddlers it is not necessary to remove the tiddler from the list field if the tiddler is deleted - I cannot see how this would work with general lists.

tobibeer commented 8 years ago

The above mentioned widget could have an attribute specifying the list to be modified, e.g.:

buggyj commented 8 years ago

The list field of a tiddler that is being used as a tag determines the ordering of the tiddlers that carry that tag - i.e. the tag and the list are tied together. In this sense the taglist widget is a special case. The list items of the taglist widget are obtained thru filtering and so are guaranteed to exist, if you delete a tiddler that is on the list it will disappear from the list (by the refresh mechanism). List fields are just sequences of strings, they are not lists of tiddlers. This means that a "list-by-field" widget used with a list field of a tag will have different semantics to the taglist widget and so should be avoided.

It is quite reasonable to have a "list-by-field" widget (for uses other than re-ordering tags) I was going to make one, but I have not needed one yet.

tobibeer commented 8 years ago

Yes, the taglist widget is constrained to the core list field, working tags. So, it's not the general-purpose dnd I'd hope we arrive at through this exercise. But it does provide the groundworks.

warpsprung commented 5 years ago

Currently lists of Tiddlers that are tagged with the current Tiddler can easily be generated by e.g. <<list-links filter:"[tag<currentTiddler>] +[sort[title]]">> The example tiddler TaskManagementExampleDraggable is nice but the ability to mention <currentTiddler> is missing

pmario commented 2 years ago

@Jermolene ... I think this one can be closed. We do have drag and drop sorting in lists now

pmario commented 1 year ago

@Jermolene ... Bump. ... I think this one can be closed.

Jermolene commented 1 year ago

Thanks @pmario this was indeed fixed with the introduction of the "list-link-draggable" and related macros introduced in 5ed7ade44fce0e1fec02fc03e47fdbaa50fcabaf