Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Interact.js + BigText.js

Bookmarks: 

Tuesday, April 26, 2022

Photo of Andrew Simard

In the last two outings, we covered some big JavaScript libraries. Bootstrap and Font Awesome are wildly popular and with good reason. And getting started using them in your TMS WEB Core project couldn't be simpler. This time out, we're going to explore a pair of much smaller JS libraries. And we'll have to get a little more hands-on using JavaScript in our code to best take advantage of them. Fortunately, there are plenty of code examples to draw from. First, we'll look at Interact.js which is used to add the ability to drag and resize HTML elements on the page. And then we'll pair this with BigText.js which is used to automatically scale text within an element to match its size.   


Motivation.

Using standard HTML and CSS, it is possible to create elements on a page that can be resized simply by using the CSS resize property. It takes values of none, horizontal, vertical, and both. And it works as expected, adding the familiar small diagonal lines to the bottom-right corner of the element, indicating that it can be resized. It even respects standard CSS properties like min-width, max-width, min-height, and max-height to limit how far the element can be resized. But that's about the extent of the feature. And as developers, we'd like to have a few more options available.

  • Styling options. You get the diagonal lines in the corner. And that's exactly all that you get. There's not even an HTML element for it. It's just 'there', wherever it decides 'there' is.
  • Aspect Ratio. While it is possible to force elements to have an aspect ratio, it isn't necessarily the easiest thing to do. There's a new aspect-ratio CSS property that may help simplify this if you happen to know that your visitors use a browser that supports it, but that's often not the case.
  • UI. What if you want to start your resize from a different corner? Or an edge? Not happening with the standard CSS resize.
  • Event Handling. If your app needs to know about the change (perhaps to save the new size for later), then you have to dig into more obscure JavaScript functionality involving Observers to get this to work. Not so much fun.
  • Dragging. The CSS resize property allows only for resizing. Not really anything readily available to deal with dragging elements.

Fortunately, Interact.js addresses all of these areas quite readily. And, once set up, you can add drag and resize functionality to any of your elements simply by adding CSS classes to them! 

Once you've got an element that is resizable, the next challenge is how to deal with its contents. Often, it doesn't matter - longer bits of text or other elements can be set to wrap automatically, and this is likely what will happen by default anyway.  And even the dimensions of the elements contained within or alongside the recently dragged and/or resized element can be set to automatically resize or flow in different directions, based on almost any kind of arrangement you can envision. 

Sometimes, though, it would be better if the text itself could be automatically scaled to fit the new dimensions. This is why the event handling aspect is important, as knowing when the element has been resized can then trigger this scaling function. While it is not a big stretch to calculate a new font size for an element based on its width property, BigText.js does this, and more, for you.


YAJSL.

Hopefully by now, adding yet another JavaScript library to your project should be easy and predictable. And today is no exception. While not as wildly popular as Bootstrap or Font Awesome, they are still available via CDN links. So you can add them via the JavaScript Library Manager or just link to them directly in your Project.html file.

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

BigText is also one of these JavaScript Libraries that is dependent on JQuery. If you didn't already use JQuery in your project, it would probably be a good idea to seek out a similar JavaScript library that didn't depend on JQuery as the extra overhead is likely unwarranted in this case. However, we'll be using other parts of JQuery in the Sample Project soon, so I don't have any particular reason to not include JQuery. In this case, just be sure that JQuery is loaded before BigText. This can be done by changing the order in the JavaScript Library Manager or by editing the Project.html file directly. We'll delve into JQuery next time, but for now, it is just sufficient that JQuery is included. If it isn't, BigText will issue an error that you'll see in the JavaScript console.

This is also a good time for a reminder about the pros and cons of using CDNs or other external sources. While a critical error in a new Bootstrap release would likely be fixed in a matter of hours, these projects may potentially take a little longer to get sorted, should a problem arise. If your project is of a nature that cannot withstand any kind of downtime, then by all means download the relevant JS files and include them in your project directly. 

Note that I don't mean to imply anything negative at all about the reliability or quality of smaller JS libraries as compared to larger ones. Some small JS libraries have outstanding support structures in place, and some larger ones are, well, abysmal in this regard. Also, a reminder that whenever you use these kinds of JS libraries in your projects, it is a good idea to subscribe to their GitHub issues queue or another comparable notification service so that you can be made aware of any changes or other ongoing development concerns.


Something New.

First, let's cover Interact.js. This time out we've got to do more than simply add the JavaScript library. The Interact.js homepage lists plenty of code examples to help out here. What we're doing initially is just loading the JavaScript library and establishing a link between it and one or more CSS classes that it will use to find elements to act upon.

For now, we're just interested in the ability to drag and resize elements. There are other things Interact.js can do as well, so be sure to check out their homepage for more information. To start with, we're going to use their code as-is. This needs to be copied and pasted somewhere. The WebFormCreate procedure is a good place to add this if you're adding this to a single form. Putting it in its own procedure is not a terrible idea either, to help keep things tidy. 

If you are planning on using this in many forms in your project, or in forms that are loaded dynamically, adding this instead to a datamodule or other more persistent form that is loaded up first is a good idea so that you're not loading and unloading the library unnecessarily. If you only need this in one form, however, you might as well just load it in that form.

If we copy and paste the code for the draggable and resize functions, we'll end up with the following.


procedure TForm1.WebFormCreate(Sender: TObject);
begin
  // Interact.js Draggable Example
  asm
    // target elements with the "draggable" class
    interact('.draggable')
      .draggable({
        // enable inertial throwing
        inertia: true,
        // keep the element within the area of it's parent
        modifiers: [
          interact.modifiers.restrictRect({
            restriction: 'parent',
            endOnly: true
          })
        ],
        // enable autoScroll
        autoScroll: true,
        listeners: {
          // call this function on every dragmove event
          move: dragMoveListener,
          // call this function on every dragend event
          onend (event) {
            var textEl = event.target.querySelector('p')
            textEl && (textEl.textContent =
              'moved a distance of ' +
              (Math.sqrt(Math.pow(event.pageX - event.x0, 2) +
                         Math.pow(event.pageY - event.y0, 2) | 0))
                .toFixed(2) + 'px')
          }
        }
      })
    function dragMoveListener (event) {
      var target = event.target
      // keep the dragged position in the data-x/data-y attributes
      var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
      var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
      // translate the element
      target.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
      // update the posiion attributes
      target.setAttribute('data-x', x)
      target.setAttribute('data-y', y)
    }
    // this function is used later in the resizing and gesture demos
    window.dragMoveListener = dragMoveListener
  end;
  // Interact.js Resizable Example
  asm
    interact('.resize-drag')
      .resizable({
        // resize from all edges and corners
        edges: { left: true, right: true, bottom: true, top: true },
        listeners: {
          move (event) {
            var target = event.target
            var x = (parseFloat(target.getAttribute('data-x')) || 0)
            var y = (parseFloat(target.getAttribute('data-y')) || 0)
            // update the element's style
            target.style.width = event.rect.width + 'px'
            target.style.height = event.rect.height + 'px'
            // translate when resizing from top or left edges
            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)
            target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
          }
        },
        modifiers: [
          // keep the edges inside the parent
          interact.modifiers.restrictEdges({
            outer: 'parent'
          }),
          // minimum size
          interact.modifiers.restrictSize({
            min: { width: 100, height: 50 }
          })
        ],
        inertia: true
      })
      .draggable({
        listeners: { move: window.dragMoveListener },
        inertia: true,
        modifiers: [
          interact.modifiers.restrictRect({
            restriction: 'parent',
            endOnly: true
          })
        ]
      })
  end;
end;


NOTE: In every other JavaScript library we'll cover, copying and pasting code in this fashion has worked without issue.  Remarkable really. But in this one instance, we've got a small problem. Around about line 20 of their code, they have an inline function defined called end (event). This had to be changed to onend (event) because the Delphi compiler was rather unhappy about an extra end floating around seemingly unexpectedly. No amount of {} or {$IFNDEF WIN32} {$ENDIF} seemed to help. Ultimately we're not going to need that block of code anyway, so not a problem. This is just one of the caveats of copying and pasting and mixing code from different languages.

With that minor code change in place, we're already operational.  Add a TWebButton to a form, and then add resize-drag to the ElementClassName property.  Your button should be both draggable and resizable!


TMS Software Delphi  Components
Interact.js in Action.

Adding the same resize-drag class to any other element will make it both draggable and resizable, too. So we're off to a fantastic start. But there's a lot of room for improvement here. The first thing you might notice, for example, is that if you add the resize-drag class to an element that has any children, those will be replaced by the block of text showing the dimensions of the resize. This is, well, rather undesirable most of the time. So let's find a different place to put that resize information so it doesn't mess up the rest of our work. Also, we can create a great deal more flexibility by adding more classes with different Interact.js default settings.


A Little More Class, Please.

With the code from Interact.js in place, we have two classes to start with. When we want to drag but not resize, we have draggable. And when we want to do both, we have resize-drag. But it would be a bit more flexible if we had more classes available. For example, it would be good to have just a resizable class, for those times when you want to only resize and not drag. If you can resize all four sides, then this is technically equivalent to draggable but we're not going to worry too much about that.

When the user is dragging or resizing an element, it may be helpful to know what the new location or the new dimensions are. Similar to its default behavior, showing the width×height or even more detailed information is potentially useful. Think about resizing something in your favorite drawing app, for example. The dimensions might be shown in a status bar at the bottom of the window so you can more easily make more precise adjustments.

Sometimes this is even in addition to showing the values directly on the object you're interacting with. In each case, there may be more or less information presented, in part based on the space available. Here, we'll set up three different classes to show versions of the information to be displayed in varying levels of detail.

  • interact-simple will show what it does now - width×height.
  • interact-standard will show a bit more - x, y, w, and h.
  • interact-full will show a bit more again - x, y, w, h and Δx, Δy, Δw, Δh

Whenever the element is dragged or resized, we'll then call the JavaScript function interactInfo to find elements with these classes and then update them to show the relevant information. This could be implemented in a Delphi function just as easily. So to make this work, we just have to add these classes to other elements on the page.  Those elements might be attached to the element we're interacting with or might be in the status bar or elsewhere.  It doesn't really matter. These display elements can then be styled and positioned separately, shown or hidden when needed, etc. Here's what interact-full looks like in action.


TMS Software Delphi  Components
Example of interact-full Display.

Interact.js has many options that can be set when it is initialized. It is likely that most of the time you'd only need a small number of different configurations for your particular project. Here, we're going to set up a few to provide more examples of the things that you can do with it, and give each such configuration its own class. You can just add that class to an element to get the Interact.js behavior you're after.  

Interact.js provides the ability to stipulate which sides are available to use for resizing an element. So if you've got a block of text on a fixed-width page, maybe the only way it can be resized is vertically down, so you'd want the other three sides turned off. We'll create a class for this and call it resize-bottom. Perhaps that element could also be draggable so we'll also add resize-drag-bottom. Other equivalent classes could be added for other edges or combinations of edges easily enough by copying and pasting the code, picking a new class name, and enabling or disabling the relevant sides.

Another aspect we talked about initially was the aspect ratio of the resized element. Interact.js can do this as well.  You could find yourself in a situation where an element's width always had to be twice its height. Or if your project has lots of video clips, maybe keeping to a 16:9 aspect ratio would make some sense. So we'll add a class called resize-drag-aspect-16x9. Creating other aspect ratios is just a matter of copying and pasting a block of code and updating the class name and the aspect ratio desired. Pretty easy.

Finally, while it makes sense often to display the resizing information on the element being resized or in a status bar, sometimes that isn't necessary. And we'd like to make things as performant as we can, by not updating that information if we don't need to. This involves making another copy of the same code and just stripping out the calls to interactInfo. To distinguish these from the regular variant, we'll just tack -quiet on to the end of the class name.  Here is the new section in the Sample Project for Inject.js.


TMS Software Delphi  Components
Interact.js Classes in Sample Project v3.

The resulting code is a bit long for our blog post, but really it just involves copying and pasting a block and updating the class name and whatever is different about that class. This also involves removing interactInfo or the .draggable stanza that is at the bottom of the block, depending on the class being implemented. Nothing too difficult at all.   

In the source code, you'll see I've stripped out most of the comments and whitespace to make it easier to copy and paste this block in particular. You can find all of this in the InitJSLibraries procedure which is called from WebFormCreate. Other pieces of Interact.js could be integrated in a similar way so be sure to check out their website for details. Particularly if you're looking for things like pinch-to-zoom or other mobile-specific types of interactions.


The Same But Bigger (or Smaller).

That's it for Interact.js. But once you've resized an element, you might be thinking about how it would be convenient if the text within was resized as well. This is where we're going to use the second JavaScript library, BigText. This is much simpler to use as there is no initialization required. It does use JQuery which we've not covered yet, so we'll tread lightly in that department. The basic usage is simply making a BigText function call on an element when its text needs to be resized. The only real (not really) tricky bit is in finding the element to apply the function to. In pure JavaScript, you use calls like document.getElementById('elementid') to find an element. In JQuery they've got a shorthand for this, which is $('#elementid').  So all we really need to do is this.

asm
  $('#elementid').bigtext();
end;

There are some aspects of BigText behavior that can be fine-tuned. For our purposes, it is super convenient to use classes for this kind of thing, so we'll create a few to control the minimum and maximum font sizes as examples.  The bigtext class will be used to find all the elements to invoke the function against. And then bigtext-min-8, bigtext-min-10, bigtext-min-12, bigtext-min-14 will be used for setting the minimum font sizes. And bigtext-max-24, bigtext-max-36, bigtext-max-48 and bigtext-max-60 will be used for setting the maximum font sizes. 

I've added the function applyBigText to the InitJSLibraries procedure to do this, and I've added it to the various Interact.js bits so that it is called automatically whenever an element is resized. There are other instances where an element can be resized, such as when the page is resized, so it might need to be called in those instances as well.

Note that BigText only works for certain types of elements. Check their website for details and for other features.  For example, there is the capability to apply BigText to child elements of a particular type, like all <p> tags, that kind of thing. For our purposes, resizing the text on a TWebPanel is plenty for now. Here's what we have for new classes for BigText, and what it actually looks like.


TMS Software Delphi  Components
BigText Classes in Sample Project v3.

TMS Software Delphi  Components
BigText in Action.

Sample Project v3.

Adding Interact.js and BigText to the mix, Sample Project v3 is a bit more capable now. Here's a rundown of the significant changes to the project.

  • Procedure InitJSLibraries added, covers adding support for BigText and Interact.js classes.
  • A new TWebPanel button added to the top-left menu. More components to come.
  • The WorkArea at the top of the window is now resizable using resize-bottom-quiet. No more little diagonals in the middle of nowhere.
  • The resize-drag class has been added by default to each of the main elements.
  • The interact-full class has been added to an element that is at the bottom of the WorkArea.
  • This element fades in/out using JQuery to give a nicer effect. Will cover that next time.
  • Minor changes to the themes.


Enough JS Already.

That about wraps up what I have for you today. More actual JavaScript coding than is normal I think, but mostly copy and paste so not too painful. Next time out we'll be taking a look at JQuery. Which is also just more JavaScript coding, but more pleasant. Mostly.  I cheated a bit by including some JQuery in the project already, but we'll cover all that and more. Until then, I'd be happy to hear any thoughts on Interact.js, BigText, or how you've tackled similar problems in your projects.

Update: Be sure to also check out this post where we had a more in-depth look at using the InteractJS library to create more complex projects.

Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.



Andrew Simard


Bookmarks: 

This blog post has received 6 comments.


1. Wednesday, April 27, 2022 at 10:00:33 AM

Great Stuff, thanks Andrew :)

Morango Jose


2. Wednesday, April 27, 2022 at 9:18:54 PM

You''re very welcome! Always nice to get feedback. Sometimes it feels like I''m talking to myself :)

Simard Andrew


3. Thursday, April 28, 2022 at 4:46:39 PM

Andrew, I aıways learn something new from your forum posts and blog posts.

Borbor Mehmet Emin


4. Thursday, April 28, 2022 at 7:09:14 PM

That is very kind of you to say, thanks! I also continually learn new things from the blog and forum posts of others The hope is that my posts will help encourage conversations around these topics.

Simard Andrew


5. Thursday, April 28, 2022 at 7:44:56 PM

Loving these blog posts - keep them coming!

Winstanley Tim


6. Thursday, April 28, 2022 at 9:24:21 PM

Will do, thanks!!

Simard Andrew




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post