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



TMS Software Delphi  Components

In the last two outings, we covered some big JS libraries.  Bootstrap and FontAwesome 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, 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 JS functionality involving Observers to get this to work. Not so 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 setup, 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 where 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 JS library to your project should be easy and predictable.  And today is no exception.  While not as wildly popular as Bootstrap or FontAwesome, 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.


BigText is also one of these JS 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 JS 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 other 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 JS library. The Interact.js homepage lists plenty of code examples to help out here.  What we're doing initially is just loading the JS 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 the 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.

So 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 JS 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.  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 the 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 behaviour, showing the width×height or even more detailed information is potentially useful. Think about resizing something in your favourite 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 setup 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 JS 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 setup a few to provide more examples of the things that you can do with it, and give each such configuration its own class.  So you can just add that class to an element to get the Interact.js behaviour 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 article here, 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)

So 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 JS 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 behaviour 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 added 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 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 by default.
  • 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.

Andrew Simard.



Masiha Zemarai


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