TMS Web Core and interact.js - Drag and Resize Elements

This is a post describing how to add the interact.js library to a TMS Web Core project to give the user the ability to drag and resize elements on the page with very little coding effort.

Motivation: I'm setting up a traditional dashboard type of module with widgets that can be added or removed, resized and reordered, that sort of thing. Most of the widgets are setup as a TWebPanel that has a little header and rounded corners and the rest of it. While it is possible in straight CSS to add the "resize" property to a div, dragging is a little different and getting notified about either event is a little different again. Interact.js kind of wraps up everything in a nice little package.

To get started, as usual it is just an extra line in Project.html.

<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>

Then you have to define what elements to work with. Two steps. The first is super-convenient in that it just uses a class to find the elements. So adding the resize-drag class to your component's ElementClassName makes it resizable and draggable. I'm using TWebPanel components in the demo project, but other components work just as well. Then, in the WebFormCreate function you can add the code to setup the functionality. This is what I've used in the demo. It is a bit stripped down from what is on their website, but mostly the same. Some comments on that in a moment.

  asm
    function dragMoveListener (event) {
      var target = event.target
      var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
      var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
      target.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
      target.setAttribute('data-x', x)
      target.setAttribute('data-y', y)
    };
    window.dragMoveListener = dragMoveListener;
    interact('.resize-drag')
      .resizable({
        edges: { left: true, right: true, bottom: true, top: true },
        margin:20, // size of resizing boundary interaction area
        listeners: {
          move (event) {
            var target = event.target
            var x = (parseFloat(target.getAttribute('data-x')) || 0)
            var y = (parseFloat(target.getAttribute('data-y')) || 0)
            target.style.width = event.rect.width + 'px'
            target.style.height = event.rect.height + 'px'
            x += event.deltaRect.left
            y += event.deltaRect.top
            target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
            target.setAttribute('data-x', x)
            target.setAttribute('data-y', y)
            pas.Unit1.Form1.ResizeDrag(target.id);
          }
        }
      })
      .draggable({
        listeners: { move: window.dragMoveListener }
      });
  end;

That's pretty much all there is to it. Off and running! I've attached a demo project and a screen shot. The demo uses a TWebScrollbox for the main area that holds the widgets as well as the wdiget body elements so that scrollbars appear/disappear automatically as the widgets are resized and if they are moved off the edge of the main area.

The trickiest part is for Delphi to know that the element has changed, particularly size, so that it can do its thing with the object alignment and so on. Might not be something others have to worry about but I've setup my components in a particular way that needs this. Also provides the opportunity to do other things at this point, such as trigger a reflow or something if the intent is to automatically rearrange widgets when one is adjusted.

To do this there's the call pas.Unit1.Form1.ResizeDrag(target.id);. Probably a dozen ways to do this, but I generally have these things collected up in a different module which is why I wrote it that way. This function just looks at the JS object and adjusts the Delphi object to have the same size and position. Does the trick to trigger the realignment of the child components but could be generalized a bit further and might not even be needed by some. Might also be added to the drag listeners but wasn't needed in this case.

Some additional notes.

  1. The sample code on the front page of their website for their resize example also supports dragging but it needs the code from the draggable example to work. Also, their sample code tries to display the size of the element after resizing, but has the comical side-effect of removing all the child elements of the element that you are resizing. As a result, the code sample above addresses these and trims out things that I don't need.
  2. The margin value in the interact definition is used to set how wide the perimeter border is when moving between dragging and resizing. The default is a pretty generous resize border.
  3. There are options that can be applied so as to not interfere with the underlying components. For example, in the demo it isn't easy to select text in the TWebMemo because it just drags the component instead. Conversely, you can add the CSS property pointer-events: none to any elements where it is ok for interact.js to take over but where you don't want the "disabled" look.
  4. As with many js libraries there are a lot of other useful options. Setting limits on the resize dimentions/aspect ratio, inverting, inertia, and so on. Best to check out the site for more details if you're interested in more complex interactions.

Here's the demo project. Lots of Bootstrap and Fontawesome to make it look pretty. The widgets can be dragged from anywhere but if you want to see the cursor for dragging try moving just outside of the TWebMemo area. Lots and lots of refinements are possible of course, this was just put together in a few minutes.

InteractTest.zip (1.6 MB)

That's about it for now. I've just gotten started with it, but happy to try and answer any questions.

3 Likes

Here's a video of a work-in-progress... a dashboard module. Idea being that you can add various "widgets" to a dashboard, with multiple dashboards supported, and the ability to rearrange the widgets using the interact.js library to deal with dragging and resizing elements. Super fun to play around with. Lots of fiddling and little supporting touches were needed of course. Such as "bring to front" kind of functionality, being able to save positions, grid alignment and that sort of thing but all quite manageable.

It was a bit of an adventure to sort out one aspect though, as when Interact.js moves your elements, the CSS "position" actually stays the same. It moves your elements using translation offsets :expressionless: but I managed to get it all sorted out in the end.

Interact.JS Dashboard Example

1 Like