Re-sorting Web-based Tabs like it works in Google Chrome

Especially in case you have a lot of Tabs open inside your Browser, you are most likely using this feature on a daily basis. The drag&drop based re-sorting is fun to use and provides a great user experience.

Just to ensure that you have the same picture in mind:

Now, in case you look at most of today’s Web-based User Interfaces which are using Tabs, you will notice that there is no drag&drop based re-sorting in place.

This is actually a problem, since users are familiar with the feature and like it.

I picked up the challenge to implement the same UX for Web-based Apps, including vertical Tab Bars & inverted flexbox layouts.

This article is going to show some of the problems I encountered on the way as well as diving into the coding side.

Content

  1. Introduction
  2. What is neo.mjs?
  3. How to add animations to flexbox based Toolbars?
  4. How does the drag:move logic work?
  5. How does the Tab re-sorting work?
  6. Is using a toolbar.SortZone optional?
  7. Online Demo of the Video Example
  8. What are the next items on the neo.mjs roadmap?
  9. Final thoughts

1. Introduction

To get a quick overview up front, here is a short video showing the drag&drop based Tab re-sorting, including vertical HeaderToolbars, dynamically changing the layouts and theme:

I implemented the logic inside the open source based neo.mjs UI framework, but you could make it work for different scenarios as well.

2. What is neo.mjs?

neo.mjs is a very disruptive Javascript based next generation UI framework.
It enables you to create blazing fast multithreading frontends.

Image for post

You can find the MIT licensed repository here:

In case you want to join a lovely open source project: neo.mjs is looking for more contributors. The demand for neo.mjs based UIs is growing fast, so getting up to speed can make sense in case you are looking for new client projects anyway.

3. How to add animations to flexbox based Toolbars?

A TabHeader is just a class extension of container.Toolbar.
Obviously it is sticking to CSS3 and using display:flex, so the Tabs (Buttons) can have a flex style to specify their width or alternatively use a fixed width.

Image for post

If you think about it:
It is not possible to move items inside a flexbox layout with using animations.

So, how does it work?

The main thread is using either a Mouse- or a TouchSensor and will forward the custom drag:start, drag:move, and drag:end events to the App Worker.

You can optionally use the DragDrop main thread addon, if you like to. This ensures there is no overhead regarding the file size in case you don’t need DD for your App(s). Describing the addon in detail would go too far off this topic, so I am just posting the link to its source code (in case you are curious):

https://github.com/neomjs/neo/blob/dev/src/main/addon/DragDrop.mjs

What are are going to cover in detail is the newdraggable.toolbar.SortZone.

This class is extending draggable.toolbar.DragZone, which will subscribe to the mentioned drag related events.

https://github.com/neomjs/neo/blob/dev/src/draggable/toolbar/DragZone.mjs#L35

One way to enable animated Tab movements is to transform each Tab into an absolute positioned element. To do this, we need to get the related DomRects, which contains the related sizes & positions.

Element.getBoundingClientRect() helps here:

Since our code is running inside the App Worker scope, there is no direct access to the DOM. This is not a big deal though, since we can use the remotes API to easily fetch the data from the main thread:

We can pass an array of ids and will asynchronically get the related data.

Neo.main.DomAccess does not exist inside the App Worker scope either, but the remotes API will make exposed methods usable via Promises. This call will send a postMessage from the app worker to the main thread, fetch the data, send a postMessage back to the app worker and the promise is fulfilled.

Here is the full logic:

The important part here is line 20item.style=.
While this might look as an assignment, Component.style is a neo.mjs based class config. Changing the value of style will trigger a setter, comparing the new style object to the current one and sending the related delta updates to the main thread.

In case you have been following the other neo.mjs related blog posts, you will know that dynamically changing the DOM at runtime is one of the big strengths of this framework.

We are transforming all Toolbar items, which includes the one we are dragging. This item will get the style visibility: 'hidden', to keep the positioning and movement logic consistent.

4. How does the drag:move logic work?

At this point you are probably wondering how we can drag an Element which is hidden. We don’t. The answer is inside draggable.DragZone:

We are fetching the DomRect of the Element which we want to drag and are calling the createDragProxy() method.

This one is cloning the DOM tree of the Component which we want to drag and is creating a new DragProxyComponent.

For our use case, moveInMainThread equals true, so we don’t need to manually take care of the movement inside the App worker.A tick faster performance wise.

We can also pass a boundaryContainerId to the main thread DragDrop addon, which limits the movements to the matching DomRect.

5. How does the Tab re-sorting work?

Our App Worker is subscribed to the drag:move event, so we can easily add more logic which should happen.

The draggable.toolbar.SortZone logic looks like this:

It took me a couple of attempts to get the code into this shape.

It is covering the 4 use cases:

  1. flex-direction: row
  2. flex-direction: row, sort-direction: row-reverse
  3. flex-direction: column
  4. flex-direction: column, sort-direction: column-reverse

Keep in mind that when we reverse the sort-direction, the indexes start from the other side of the container, but we still need to compare the left or top values in a correct way.

The switchItems() logic is pretty straight forward:

We need an indexMap, since one drag operation can trigger multiple switchItems() calls and we only adjust the real items when drag:end triggers.

This also enables us to stop a drag operation at any point in time.
E.g. we could add a drag:cancel event when a user hits the ESC key and just remove the absolute positioning.

In case someone wants to add this feature, you are welcome to go for it 🙂

The updateItem() logic is trivial:

We are just using the style config setter once more.

Changing the left & top values will trigger an CSS3 based animation automatically:

Image for post

Obviously, onDragEnd() will remove the absolute positioning, restore fixed item sizes (in case they exist) and trigger the related Container.moveTo() logic.

You can take a look into the full SortZone source code here:

https://github.com/neomjs/neo/blob/dev/src/draggable/toolbar/SortZone.mjs

There are more edge cases & hidden gems waiting for you to get discovered.

6. Is using a toolbar.SortZone optional?

Yes, it definitely is. tab.header.Toolbar has a sortable config,
which will automatically trigger:

This logic is actually beautiful.

As you are hopefully aware of, the neo.mjs dev mode runs directly inside the Browser, without any builds or code transpilations.

So, we can use dynamic imports and the Browser will only load the related files in case they are needed.

This code also works very well with the webpack based related builds for the dist/production environment.

As mentioned inside my Cross Apps Split Chunks Blog Post:

we do have dynamic chunks in place. You can put multiple Apps on one Page with close to zero overhead. This also covers scenarios, where you have some base dependencies of a module you lazy load already imported into your App(s).

7. Online Demo of the Video Example

The example is not fully polished for mobile yet, but the drag&drop based logic works fine there.

https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/examples/tab/container/index.html

You can use the Chrome Dev Tools to switch to a Tablet view:

Image for post

Hint: make sure to reload the Page after switching to a mobile simulation.

The framework is using either the MouseSensor or the TouchSensor, not both at the same time.

I also added the Tab re-sorting the neo.mjs Docs App.
Yes, it does work for dynamically added Tabs as well:

https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/docs/index.html

The feature also got added to the Covid Dashboard as well as the SharedWorkers driven multi Browser Window App.

You can find all online examples here:

https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/apps/website/index.html#mainview=examples

8. What are the next items on the neo.mjs roadmap?

Right now, I am working on a client project to create a neo.mjs based UI builder. A bit similar to WebFlow, but it will create neo.mjs based Component trees.

It feels like a perpetuum mobile:

A neo.mjs based UI which enables its App users to create neo.mjs based UIs via drag&drop.

For the framework itself this means that I will be focussing on DragZones & SortZones for Trees next. Definitely an epic item.

Since the Browser support for public class fields is very nice at this point:

Image for post

I would like to add this into the neo.mjs framework soon-ish. We can move all “primitive” configs out of the static getConfig() logic.

This is the first really breaking change since the framework went public, so this will be neo.mjs v2.

The only thing which is holding me back is that webpack does not support public class fields yet. More precisely: they are using an acorn parser without the related Stage3 proposal.

So far I had no luck to override the npm dependency:

In case someone has a good idea, help on this one is appreciated!

9. Final thoughts

With the drag&drop based Tab re-sorting in place, you can easily add this feature to your App(s).

You are very welcome to build custom logic on top of it.

Once the Tree based drag&drop logic is in place, I also want to further enhance the Calendar Component and polish the drag&drop for Dialogs more.

Especially a demo for multi Browser Window based drag&drop will be stunning. On my todo list for one of the next Blog Posts.

Happy coding and stay safe during the pandemic!

Best regards,
Tobias

Author: admin

Leave a Reply

Your email address will not be published.