Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Icon Pickers: Easy Access to 200,000+ Icons

Bookmarks: 

Tuesday, May 23, 2023

Photo of Andrew Simard
In our last Basics post, we covered how to add images and icons to our TMS WEB Core projects, which you can read about here. Most of that effort was related to using these kinds of elements in our projects directly, as part of the user interface, for example. But what if we want the users of our apps to have access to icons? Either for customizing the interface for their own purposes, or for when they're creating their own content, using an HTML editor for example, and would like to include icons in that content. There are a handful of icon picker JavaScript libraries that we can use, or we can create one ourselves.


Motivation.

Using icons in our projects helps to provide visual cues. For example, we can use an icon on a button to make it more compact and language-independent, as compared to using a text label. And if we have any kind of user-customizable features in our project, having the user select an icon for that feature may make some sense. In the case of user-generated content (blog posts, problem reports, messages, and so on could be included here) sometimes it helps to offer the ability to include icons in the content, like emoji perhaps, or other icons entirely.

Text messaging would be pretty bland without icons. But this is far from a standard practice. While using a mobile device, there's an expectation that anything that can be entered from the mobile keyboard is a candidate for including icons. Often this kind of support isn't available on a desktop device, and standards around this are certainly different.  

Complicating things further, there are many icon sets available. When designing user interfaces, it isn't a bad idea to pick an icon set and use it exclusively. This results in a smaller set of icons being available, but also a reduced project size and potentially a more consistent-looking user interface. Reasonable trade-offs for developers. 

When selecting an icon set for a user interface, we're making a choice based on what our project might need - not really related at all to what the user might be needing. For example, we might choose an icon set that has icons that have a consistent set of arrows and pointers that look good and work well together from a design perspective.

But if our app is used to host a discussion forum related to exotic animals, giving the users access to arrows and pointers might be sub-optimal. They'd be better served by having access to whatever icon set has the broadest number of animal icons. Or if we don't know what their discussions will be about, having the largest icon set possible might be a better choice.

The effort needed to provide access to a set of icons can also vary, depending on the choice of icon set, the number of available icons, licensing related to the icons, as well as the environment that our project is running within. Do we have access to a server? How much of an initial download of the icons can we reasonably support?  Just like most aspects of modern web application development, we have choices here, allowing us to craft the best solution possible for the users of our projects.

The Setup.

To help test the various options we'll be exploring, we'll need to have an example of a user interface where these kinds of icons might be used. Ultimately, we're after a button in our UI that we can click on to either see a list of icons or to perform a search for an icon. Once we select an icon, we'd like it to be added to whatever we were working on when we clicked the button. 

One type of application is where we're just selecting icons for buttons. Useful. Another would be a WYSIWYG HTML editor where we're looking to add icons to the content we're editing. We had a look at such editors - Summernote and SunEditor - back in this post. We also had a more in-depth look at SunEditor in this post. For today's adventure, we're going with SunEditor again, mostly because we've got a working example to start with. The editor used isn't particularly important here. Everything we're doing would likely work just as well anywhere. All we're after is retrieving an icon in some format (anything from just its name to an SVG rendering of the icon) and then inserting it into the editor.

To start with, then, we'll create a basic project that has just a SunEditor instance on the page with its usual toolbar and a bit of rudimentary monochromatic styling. Basically, just the editor part of the DocumentEditor post mentioned above, and without any of the fancy highlighting that we were working on. This involves a bit of work adding in the various properties to layout the page as we'd like, defining the order and grouping of the icons, and so on. Not particularly relevant to our topic, but here's what it looks like to start with. 

TMS Software Delphi  Components
SunEditor Example.

A little more tricky is where to stick our "icon button". SunEditor has an extensive plugin system we could use.  Or..... we could just insert a button directly into the SunEditor toolbar. One of the benefits of working on pure JavaScript/HTML/CSS projects is that you can kind of brute force your way through certain kinds of problems without anyone being any the wiser. Manipulating the DOM in this way perhaps doesn't fall under the umbrella of "best practices" but rather under "make it work". 

After our SunEditor instance has been initialized in WebFormCreate, we can add a bit of code to add a button.  Using the same styling and structure of the existing buttons, we can sneak in our own imposter. And we can add a "click" event listener that calls a Delphi function that we can then use to show our own icon picker. In fact, we're going to cover four different approaches to implementing an icon picker, so let's add four icons.

    // Manually insert a button
    function AddIconPickerIcon(picker,icon) {
      var iconButton = document.createElement('li');
      iconButton.innerHTML = '<button type="button" class="se-btn se-tooltip" aria-label="Icons">'+
                              '<span><i class="'+icon+'"></i></span>'+
                              '<span class="se-tooltip-inner"><span class="se-tooltip-text">'+picker+' Icons</span></span>'+
                            '</button>';

      // Add it to the last menu block
      divHeader.firstElementChild.firstElementChild.lastElementChild.firstElementChild.appendChild(iconButton);

      // Move it to the first icon in that block
      var firstel = divHeader.firstElementChild.firstElementChild.lastElementChild.firstElementChild.firstElementChild;
      firstel.parentElement.insertBefore(iconButton,firstel);

      // Call Delphi function when we click on it
      iconButton.addEventListener('click',(e) => {
        pas.Unit1.Form1.SelectIcon(picker)
      });
    };

    // Add these icons
    AddIconPickerIcon('Font Awesome','fa-solid fa-flag');
    AddIconPickerIcon('XData', 'fa-solid fa-dragon');
    AddIconPickerIcon('Local', 'fa-solid fa-star')
    AddIconPickerIcon('Bootstrap', 'fa-brands fa-bootstrap');


All we're up to here is just creating a new element using the same styling as the existing toolbar buttons and then adding it to the same <ul> element. As there are multiple toolbar groups, we've got to navigate the DOM elements to find the group we're after. And once we've added the icon, we have to do a bit of work to shift the order so that it is first in the group rather than last.

To set the order of the icons we're adding, we create them in the reverse order. The name of the icon picker as well as the icon used are passed as a parameter to a function that we've created, to save having to repeat it. Also note that our application is using the Font Awesome 6 Free icon set itself, initially at least, so the icons we're adding (and the rest of the icons in the other toolbars) are from this set.

TMS Software Delphi  Components
Icons Placed.

The Delphi method SelectIcon is then called, with the parameter indicating which icon picker is to be used. Each of the approaches we're going to cover will then present a user interface for selecting an icon, followed by a call to insert the icon into the document that we're currently editing. And, ideally, in a way that allows us to make minor alterations to the size and color of the icons.

Approach #1: Bootstrap

Our first approach is intended to require the least amount of development work, which coincidentally also gives us the least flexibility, as one would expect. There are numerous GitHub repositories that can be used to incorporate an icon picker into a NodeJS project, but far fewer that can be used directly via a browser. Many of the projects are specific to a JavaScript framework (like Vue or React), and many are also restricted to a specific set of icons.

This isn't all that surprising, given that a big part of a typical icon picker involves sourcing the icons themselves.  And as there really isn't a standard to follow, they all tend to do things a little differently. This is a key issue we'll be facing in each of our approaches. Where does the icon data come from in the first place, and how do we get it into our project so that we're able to run searches against it?

Fortunately, there's a relatively current project that gives us access to the set of Bootstrap Icons, along with a CDN so that we can add it to our project. Just like we're fond of doing with other JavaScript libraries. This one is called Iconpicker for Bootstrap 5. To get started, we can add it to our project using any of a number of CDNs. JSDelivr tends to get used the most in these blog posts, so let's use it here as well. We'll also need to load the actual Bootstrap Icons into our project.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/codethereal-iconpicker@1.2.1/dist/iconpicker.min.js"></script>

Then, we have to initialize this JavaScript library and add it to our interface. For the interface, for consistency between our different approaches, let's add a pop-up mechanism of sorts. We'll overlay a shade element and then place whatever picker control we're using on top of that, to implement a variation of a modal component.

We don't have to do it this way - we could make something more tailored to our particular HTML editor or another UI element of some kind, but this is a way to make sure that what we're implementing is completely separate and distinct - the developer (that'd be you!) can then tear out whatever isn't needed to bring the interface more inline with the rest of the project.

For the initialization, we're adapting the code example from GitHub. This goes and retrieves a JSON file from the CDN that contains the icon data. This is pretty small compared to some of the alternatives we'll be looking at. It's a little less than 30 KB, but it just contains the names of the 1,300 or so icons. Not really all that complicated.

  asm
    // Initialize Boostrap Icon Picker
    const response = await fetch('https://cdn.jsdelivr.net/npm/codethereal-iconpicker@1.2.1/dist/iconsets/bootstrap5.json')
    const result = await response.json()
    this.IconPicker = new Iconpicker(document.querySelector(".iconpicker"), {
      icons: result,
      showSelectedIn: document.querySelector(".selected-icon"),
      searchable: true,
      selectedClass: "selected",
      containerClass: "my-picker",
      hideOnSelect: false,
      fade: false
    });
  end;


This assumes that we've already defined an element on the page that includes the class 'iconpicker'. In addition to the shade element, we'll need to add this input control (aka TWebEdit) with a class value of 'iconpicker' for it to connect to. We can also set a search term. For example, if we type a word in our editor and then highlight it, we could use that as the initial search term. 

Let's drop a TWebEdit component on the form, called editBootstrap, with the ElementClassName property set to include 'iconpicker'. From the SelectIcon method, we'll then check for this picker choice ('Bootstrap') and then call SelectBootstrapIcon.

procedure TForm1.SelectIcon(Picker:String);
begin
  divShade.Visible := True;
  if Picker = 'Bootstrap' then SelectBootstrapIcon;
end;

procedure TForm1.SelectBootstrapIcon;
begin
  divBootstrap.ElementHandle.classList.replace('d-none','d-flex');
  editBootstrap.Visible := True;
  asm
    var selection = pas.Unit1.Form1.Editor.core.getSelection();
    var selected = selection.baseNode.textContent.substr(selection.baseOffset,selection.extentOffset - selection.baseOffset);
    var search = selected.split(' ').slice(0,2).join(' ').trim();

    this.IconPicker.set(search);
    editBootstrap.dispatchEvent(new Event('keyup'))

    document.getElementsByClassName('iconpicker-dropdown')[0].style.setProperty('visibility','visible','important');

    editBootstrap.style.top = (parseInt(divBootstrap.offsetTop) - 7)+'px';
    editBootstrap.style.left = (parseInt(divBootstrap.offsetLeft) - 175)+'px';
    editBootstrap.style.width = (parseInt(divBootstrap.offsetWidth) - 60)+'px';
  end;
end;

This is doing a few things, like figuring out a search term from the underlying HTML editor, and then passing it to the Bootstrap Icon Editor so it can perform the search. For the display, it automatically creates the element containing the icons. There are options for this to be automatically shown or hidden when the input has focus, but we're more interested in a static display. We've also got some buttons of our own to accept or cancel the selection. The layout is more complicated than it looks largely due to wanting things to look a certain way, in the same monochromatic style as the rest of the HTML editor. This can of course be adjusted in any number of ways.


#divShade {
  position: absolute;
  top: 0px;
  left: 0px;
  right: 0px;
  bottom: 0px;
  background: black;
  opacity: 0.5;
  z-index: 999;
}

#divBootstrap {
  position: absolute;
  width: 350px;
  left: 50%;
  top: 25%;
  transform: translate(-50%,-25%);
  z-index: 1000;
}

#btnBootstrapOK, #btnBootstrapCancel { border: 2px solid black; border-radius: 0.375rem; }
#divBootstrap > input { border: 2px solid black; }
#editBootstrap { position: absolute; z-index: 1001; border: 2px solid black; }
.iconpicker-dropdown { position: absolute; width: 350px; height:350px; left: 50%; top: 50%; transform: translate(-50%,-48%); z-index: 1000; border: none; overflow: hidden !important; visibility: hidden !important; opacity: 1 !important; border-radius: 0.375rem; border: 2px solid black; overflow: hidden; }
.iconpicker-dropdown > ul { border-radius: 0.375rem; top: 0px !important; overflow: auto !important; width: 100% !important; height: 100% !important; }

When we're done with the Icon Picker, we can select to either accept or cancel the icon selection using one of the buttons. If we are canceling, we'll have to hide everything we just unveiled. If we're accepting, we'll need to formulate a bit of HTML to insert into our document, and then perform the same hiding steps.


procedure TForm1.btnBootstrapCancelClick(Sender: TObject);
begin
  divBootstrap.ElementHandle.classList.replace('d-flex','d-none');
  editBootstrap.Visible := False;
  asm
    document.getElementsByClassName('iconpicker-dropdown')[0].style.setProperty('visibility','hidden','important');
  end;
  divShade.Visible := False;
end;

procedure TForm1.btnBootstrapOKClick(Sender: TObject);
var
  NewIcon: String;
begin

  NewIcon := '<i class="__se__tag_icon '+editBootstrap.Text+'"></i> ';
  asm
    var Editor = pas.Unit1.Form1.Editor;
    Editor.insertHTML(NewIcon,true);
  end;

  btnBootstrapCancelClick(Sender);
end;

The trickiest part here is that in this particular HTML editor (SunEditor) we need to add an extra class (__se__tag_icon) to the element so that it plays nice with the rest of the editor. Otherwise, we're just inserting a bit of HTML that conforms to the usual conventions for using web fonts to display icons - the <i> tag. Because we're using a web font in this case, the icon displayed will take on the same size and color as the text that surrounds it.  Here's an example where we've got a bit of text, we've highlighted a term, and then replaced the term with an icon.

TMS Software Delphi  Components
Bootstrap Icon Picker in Action.

This works pretty well. The JavaScript library also includes support for FontAwesome 4 icons using the same approach, though it is likely a one-or-the-other scenario. As Font Awesome 6 has been out for some time, this is less interesting than it might have been a few years ago. And this is the biggest shortcoming of this library - not a lot of icons to choose from. Bootstrap Icons are a great example of icons that might be perfect for UI design, but not anyone's top choice for embellishing general content.

Approach #2: Local Icons

The UI for an icon picker isn't all that complicated. An <input> field of some kind to enter a search term, and a list of icons in another <div> element. We can put this together pretty quickly ourselves without using another JavaScript library. And we can add a few more features to make it a little more flexible. For example, we might want to search across multiple icon libraries. And we might also want to change the size of the icons separately from the text that the icon is surrounded by. Maybe even pick a different color while we're at it.  

The biggest problem, then, is finding a set of metadata for the icons that we'd like to use. Turns out there's a GitHub project that lists some 180,000+ icons, grouped by icon set, that includes not only the metadata but the actual SVG icon data as well: Iconify icon Sets in JSON Format. This is a collection of JSON files, each containing a list of icons from a particular source, the SVG needed to render them, as well as other information about the source such as licensing information. 

Sounds pretty ideal, right? Well sure, and we'll be trying it out momentarily to see how great it really is (spoiler alert: pretty great!). But it has two fairly major shortcomings, which we'll address in the last two approaches.  First, some of these JSON files are rather large - we'd not want to download any more than a small number to our client at any given time. And second, this only helps us with free icons. No support here for the likes of Font Awesome Pro icons or any other commercial icon library.

But if we look past those two issues, we've got an excellent way to get started with our own locally hosted icons.  In the GitHub repository, we can access the JSON files and just download and include them in our project directly.  The ideal scenario would be to have a local GitHub clone of the repository, and then just copy the files we're interested in into our own project. Their repository claims to receive updates three times a week, so we wouldn't want to go too long without getting fresh copies. For now though, let's just copy a few of these JSON files into our project folder, into an "iconsets" subfolder.  

Selecting the "Bootstrap Icons", "Font Awesome 6 Brands", "Font Awesome 6 Solid", "Font Awesome 6 Regular", "Noto" and "Material Design Icons" gets us roughly 30 MB of JSON files (Noto is 24 MB, but they're worth it). This is quite a lot of overhead for our project to download. But not necessarily out of the question - these do get cached after all. We'll have to load these up into our application in order to perform searches. 

At the same time, we can get a bit of information about them to be able to produce an accurate list in our UI.  Speaking of which, let's set up a TWebCheckListBox to hold the list of libraries that we're making available, called listLibraries. We'll also need to have a place where the selected libraries, or a summary of them, can be shown.  Let's call it editLibraries. All that's left is to load up the libraries. Here's what we've got in that department.

  asm
   // Load up our Local icon sets
    var IconSetList = [
      'bi.json',
      'fa6-brands.json',
      'fa6-regular.json',
      'fa6-solid.json',
      'mdi.json',
      'noto'
    ];
    this.IconSets = [];

    for (var i = 0; i < IconSetList.length; i++) {
      var iconsetfile = await fetch('iconsets/'+IconSetList[i]);
      var iconsetjson = await iconsetfile.json();
      var iconcount = Object.keys(iconsetjson.icons).length
      console.log('['+IconSetList[i]+'] '+iconsetjson.info.name+': '+iconcount+' icons');
      this.IconSets.push(iconsetjson);
      this.IconSetNames.push(iconsetjson.info.name+': '+iconcount+' icons');
      this.IconSetCount.push(iconcount);
    };

  end;

  // Add libraries to listLibraries TWebCheckListBox and summary to editLibraries
  c := 0;
  for i := 0 to Length(IconsetNames)-1 do
  begin
    listLibraries.Items.Add(IconSetNames[i]);
    listLibraries.Checked[i] := True;
    c := c + IconSetCount[i];
  end;
  editLibraries.Text := IntToStr(Length(IconSetNames))+' Icon Libraries Loaded: '+FloatToStrF(c,ffNumber,5,0)+' icons';


We've got a couple of dynamic arrays that we're populating - IconSetNames and IconSetCount - to keep track of what is in the libraries. But the bulk of the work is in getting the libraries loaded into IconSets - essentially a larger JSON object that contains all the icon libraries combined. This is what we'll be searching and where we'll be getting the SVG icon data from.

One small administrative item here. We're using the JSON directly downloaded from the GitHub project. This data has many users, so it's a pretty safe bet there are no errors in the JSON. When adding these files to Delphi, however, it may be that they get altered - particularly as there are some super-long lines in these files. If you encounter errors loading the JSON, this is likely the culprit. The workaround is to just copy them again, or at least make sure that the copy that is part of the deployed application isn't somehow corrupted by Delphi in the process. But if all goes according to plan, the output to the browser console should look something like this.

[bi.json] Bootstrap Icons: 1959 icons
[fa6-brands.json] Font Awesome Brands: 467 icons
[fa6-regular.json] Font Awesome Regular: 164 icons
[fa6-solid.json] Font Awesome Solid: 1395 icons
[mdi.json] Material Design Icons: 7489 icons
[noto.json] Noto Emoji: 3526 icons

Note that in this approach, the JSON contains the icon metadata as well as the icons themselves (the SVG content). We could reduce the size of the JSON downloaded to the client initially by spitting out the metadata, dramatically reducing the size of the initial JSON needed to perform searches. Then we could retrieve the SVG content later, maybe even splitting that out into separate smaller files. Lots of options, but we'll try something different in the next approach.

With our icon data loaded up, we can then turn our attention to the UI. We've populated our TWebCheckListBox with a list of libraries. We've got a place to enter a search term. So how do we actually perform a search? We don't have much to go on aside from the name of the icon. Not really any categories or other elements to search. And if you search for "arrow" you're likely to get too many hits to be useful, so we'll need additional terms.

This code takes up to three terms and then applies them separately to filter the matches. It does take time to display the matches, so we'll limit it to 1,000 results, or 10,000 results if you explicitly click the search button.  The idea is that we want to call this as we're typing, so we want it to be as fast as we can but still return a useful number of results. This is one of the benefits of a local approach after all - we have all the data, so try and make the most of it. 

procedure TForm1.btnSearchClick(Sender: TObject);
var
  i: Integer;
  Search: String;
  SearchSets: Array of Integer;
  IconSize:Integer;
  MaxResults: Integer;
begin
  // Icon names are always lower case, so search with that
  Search := LowerCase(Trim(editSearch.text));

  // Limit returns while typing
  if (Sender = btnSearch)
  then MaxResults := 10000
  else MaxResults := 1000;

  // Icon Size from slider - determines font-size
  IconSize := WebTrackbar1.Position * 2;

  // Figure out which libraries we're searching through
  for i := 0 to listLibraries.Count - 1 do
  begin
    if listLibraries.Checked[i] = True
    then SearchSets := SearchSets + [i];
  end;

  // Must have something to search for and somewhere to search
  if (Search <> '') and (Length(SearchSets) > 0) then
  begin
    asm

      // Build a new results array
      var results = [];

      // Search for at most three terms
      var searchterms = Search.split(' ').slice(0,3);

      // Search each icon set in order
      for (var i = 0; i < SearchSets.length; i++) {
        var icons = this.IconSets[SearchSets[i]].icons;
        for (var icon in icons) {
          if (searchterms.length == 1) {
            if (icon.includes(searchterms[0])) {
              results.push({library:SearchSets[i],name:icon,icon:icons[icon]});
            }
          } else if (searchterms.length == 2) {
            if (icon.includes(searchterms[0]) && icon.includes(searchterms[1])) {
              results.push({library:SearchSets[i],name:icon,icon:icons[icon]});
            }
          } else {
            if (icon.includes(searchterms[0]) && icon.includes(searchterms[1]) && icon.includes(searchterms[2])) {
              results.push({library:SearchSets[i],name:icon,icon:icons[icon]});
            }
          }
        }
      }

      // Sort results by icon name
      results = results.sort((a, b) => {
        if (a.name < b.name) {
          return -1;
        }
      });

      // Limit results (after sort so we don't just get one library showing)
      results = results.slice(0,MaxResults);

      // Update count
      divData.innerHTML = '<div class="d-flex justify-content-between align-items-center"><div>Results: '+results.length+'</div></div>';
      this.IconResults = results.length;

      // Clear existing results
      divIconList.replaceChildren();

  end;
end;

The actual search in this case isn't very fancy - just a brute force pass through every icon to see if it matches. If it does, it is added to an array of results. This happens pretty quickly though, fast enough to keep up with typing or even with resizing the icons in the display with a slider. May not be as speedy on a mobile device though - something to test. If performance is a problem, limiting the size of the result array is likely the simplest approach.

When it comes to displaying the results, we'll have a little bit of work to do. The SVG data contained within this set of JSON is referred to as "IconifyJSON" as it is just a subset of what is needed to build an actual SVG file. This is deliberate, as it likely reduces the size of the JSON considerably. But it means we have to do a few things ourselves. The most difficult of which is to determine the size of the SVG. 

Each icon set has its own "default" size specified at the outset, but then icons individually can override this when needed. So we'll need to come up with some values for the "viewBox" attribute of the SVG. And we'll need to create the SVG itself as well - the JSON just includes the contents, not the generic header. While we're at it, we'll also add some extra bits to the <div> element - attributes that describe the icon library and related license, so we can display it when we select an icon. Here's how the presentation is created.

  asm
     // Create icons for display
      var display = '';
      for (i = 0; i < results.length; i++) {

        // Each library has its default width and height, and then overrides at the icon level
        var iconheight = results[i].icon.height || this.IconSets[results[i].library].height || 16;
        var iconwidth = results[i].icon.width || this.IconSets[results[i].library].width || iconheight;

        var displayicon = '<div style="font-size:'+IconSize+'px;" class="SearchIcon" '+
                               'icon-library="'+this.IconSets[results[i].library].info.name+'" '+
                               'icon-license="'+this.IconSets[results[i].library].info.license.title+'">'+
                             '<svg width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" '+
                               'viewBox="0 0 '+iconwidth+' '+iconheight+'">'+
                                 results[i].icon.body+
                             '</svg>'+
                             '<div style="font-size:8px; text-align:center; width:100%;">'+
                               results[i].name+
                             '</div>'+
                           '</div>';

        display += displayicon;
      }
      divIconList.innerHTML = display;
  end;

What happens when we click on an icon? Well, we don't really want to create an addEventListener call to each button. So instead, we'll attach one to the container and then use it to handle whatever we've clicked on. As a click could be on an SVG element, the label below it, an internal part of an SVG element, or outside of any of the icons, we've got to do a bit of work to sort out a reference to the icon itself. Then we can get those attributes we added to update our display and get a copy of the SVG that we can add to our document. We'll also disable/enable the button used to add the icon to our document (btnOK). We only need to set this up once, so this is handled in WebFormCreate.

  asm
   divIconList.addEventListener('click', (e) => {

      // Remove current highlight
      this.IconSVG = '';
      var selected = divIconList.querySelectorAll('.Selected');
      selected.forEach(el => {
        el.classList.remove('Selected');
      });

      // What was clicked on? Could be the label, the SVG, part of the SVG or the background
      var el = null;
      if (e.target.getAttribute('icon-library') !== null) {
        var el = e.target;
      } else if (e.target.parentElement.getAttribute('icon-library') !== null) {
        var el = e.target.parentElement;
      } else if (e.target.parentElement.parentElement.getAttribute('icon-library') !== null) {
        var el = e.target.parentElement.parentElement;
      }

      // Got something to work with
      if (el !== null) {
        this.IconSVG = el.firstElementChild.outerHTML;
        el.classList.add('Selected');
        btnOK.removeAttribute('disabled');
        pas.Unit1.Form1.btnOK.FEnabled = true;

        divData.innerHTML = '<div class="d-flex justify-content-between align-itens-center">'+
            '<div>Results: '+this.IconResults+'</div>'+
            '<div>'+el.getAttribute('icon-library')+'</div>'+
            '<div>'+el.getAttribute('icon-license')+'</div>'+
          '</div>';
      } else {
        pas.Unit1.Form1.btnOK.FEnabled = false;
        btnOK.setAttribute('disabled','');
      }
    });
  end;


All that's left then is to add the icon to our document. We can do this in the same way as we did with the Bootstrap approach. Here we're also embedding the SVG in a <span> element where we can set the font-size based on the slider position.

procedure TForm1.btnOKClick(Sender: TObject);
var
  IconSize: String;
begin
  IconSize := IntToStr(WebTrackBar1.Position)+'px';
  asm
    var Editor = pas.Unit1.Form1.Editor;
    Editor.insertHTML('<span style="font-size:'+IconSize+'" class="__se__tag_icon">'+this.IconSVG+'</span> ',true);
    Editor.core.history.push(false);
  end;
  btnCancelClick(Sender);
end;

Putting it all together, we get the following result.

TMS Software Delphi  Components
Local Icon Picker in Action.


In the Bootstrap Icon Picker, the icon inherited the same size and color as the surrounding text. In this local approach, we explicitly set the size of the icon based on the slider. We could also have the slider return a multiple of 1em, or any number of other approaches to have a way to easily resize the icons, both for display in the icon picker and for when it is inserted into the document.

Regardless, this picker works pretty well when you have a set of icons that you want to make available to the user.  "Noto" is an excellent set for this purpose, offering all the usual emoji-style icons with plenty of colors. Quite a stark departure from the Bootstrap Icons, but as mentioned earlier, these might very well have different purposes.  Would be a very strange UI that relied entirely on emojis for example. The download size can be an impediment, however. Just Noto alone is 24 MB. So that might not work for many applications.

Approach #3: Remote via XData

To get around that download size, and to potentially increase the number of icons up to the 180,000 mark, we can use another approach. This time, we'll use the same UI, but instead of loading the icon files into our application directly, we'll use an XData server to perform the search and return the icons. There are limitations here too, however. We'll want to limit the number of icons returned in the search, as if we're downloading 1,000 icons each time, that won't be particularly speedy.  

The first thing we'll need to do is add something to the XData startup to load the JSON that we'd like it to use. In this case, we're a little less concerned about the size because it is just loaded directly from disk, not over a network. And it will consume a bit of RAM to have this loaded, no question, so best to consider whether all of the libraries are required. To get them into the system, we can clone the GitHub project and just copy the icon sets we want into our own folder. When the XData server starts, it can then just scan this directory and load whatever it finds. It isn't out of the question to have this directory automatically updated by updating the GitHub clone and just pointing XData at that directly.

  // Load up Icon Sets
  if (AppConfiguration.GetValue('Icons') <> nil)
  then AppIconsFolder := (AppConfiguration.GetValue('Icons') as TJSONString).Value
  else AppIconsFolder := GetCurrentDir+'/icon-sets';
  if RightStr(AppIconsFolder,1) <> '/'
  then AppIconsFolder := AppIconsFolder + '/';
  IconFiles := TDirectory.GetFiles(AppIconsFolder,'*.json',TsearchOption.soAllDirectories);

  AppIcons := TJSONArray.Create;
  IconSets := TJSONArray.Create;
  IconCount := 0;
  IconTotal := 0;

  if length(IconFiles) = 0 then
  begin
    mmInfo.Lines.Add('...No Icon Sets Loaded: None Found.');
  end
  else
  begin
    mmInfo.Lines.Add('...Loading '+IntToStr(Length(IconFiles))+' Icon Sets:');
    IconFile := TStringList.Create;

    for i := 0 to Length(IconFiles)-1 do
    begin
      // Load JSON File
      IconFile.LoadFromFile(IconFiles[i], TEncoding.UTF8);
      IconJSON := TJSONObject.ParseJSONValue(IconFile.Text) as TJSONObject;
      AppIcons.Add(IconJSON);

      // Get Icon Count information
      IconCount := (IconJSON.GetValue('icons') as TJSONObject).Count;
      IconTotal := IconTotal + IconCount;

      // Log what we're doing
      mmInfo.Lines.Add('        ['+TPath.GetFileName(IconFiles[i])+'] '+
        ((IconJSON.GetValue('info') as TJSONObject).GetValue('name') as TJSONString).Value+' - '+
        IntToStr(IconCount)+' Icons');

      // Sort out the default width and height.  This is either from the width and height properties
      // found in the root of the JSON object, or in the info element, or perhaps not at all in the
      // the case of the width property, in which case we'll assume it is the same as the height.
      // We're doing this now as we're not passing back this information to the client, just the
      // name, license, and icons, so the client will need this to properly generate the SVG data.
      IconHeight := 0;
      IconWidth := 0;
      if IconJSON.GetValue('height') <> nil
      then IconHeight := (IconJSON.GetValue('height') as TJSONNumber).AsInt
      else if (IconJSON.GetValue('info') as TJSONObject).GetValue('height') <> nil
           then IconHeight := ((IconJSON.GetValue('info') as TJSONObject).GetValue('height') as TJSONNumber).AsInt;
      if IconJSON.GetValue('width') <> nil
      then IconWidth := (IconJSON.GetValue('width') as TJSONNumber).AsInt
      else if (IconJSON.GetValue('info') as TJSONObject).GetValue('width') <> nil
           then IconWidth := ((IconJSON.GetValue('info') as TJSONObject).GetValue('width') as TJSONNumber).AsInt;
      if IconWidth = 0 then IconWidth := IconHeight;

      // Here we're building the JSON that we'll pass to the client telling them what icon sets are
      // available, along with the other data they will need that is at the icon-set level
      IconSets.add(TJSONObject.ParseJSONValue('{'+
        '"name":"'+((IconJSON.GetValue('info') as TJSONObject).GetValue('name') as TJSONString).Value+'",'+
        '"license":"'+(((IconJSON.GetValue('info') as TJSONObject).GetValue('license') as TJSONObject).GetValue('title') as TJSONString).Value+'",'+
        '"width":'+IntToStr(IconWidth)+','+
        '"height":'+IntToStr(IconHeight)+','+
        '"count":'+IntToStr(IconCount)+
        '}') as TJSONObject);

      Application.ProcessMessages;
    end;
    IconFile.Free;
    IconJSON.Free;
  end;
  mmInfo.Lines.Add('        Icons Loaded: '+FloatToStrF(IconTotal,ffNumber,10,0));

  // We don't need to do anything else with this, so we'll store it as a string and
  // then return just that when asked for this ata.
  AppIconSets := IconSets.ToString;

Lots of code, to be sure, and lots of JSON parsing to extract the information we need about each icon set. This isn't much different than what we did earlier, but using Delphi's TJSONObject coding instead of JavaScript. It only takes a few seconds to run through all 150+ JSON files when the XData application first starts. The big question for anyone interested in using this is whether they need all the icon sets, or just some of them. 

Loading them all will result in about 700 MB of RAM being allocated, just from loading the 300+ MB of JSON.  Much less of an issue for an XData server than for a TMS WEB Core client, perhaps. Might also make sense to implement this in a separate XData server, given that it is likely that these can be used across multiple projects without necessarily having any interaction with anything else in any of the projects. Including it in several XData servers running on the same system might be a bit wasteful. 

...Memory Usage: 36.2 MB
...Loading 157 Icon Sets:
        [academicons.json] Academicons - 151 Icons
        [akar-icons.json] Akar Icons - 442 Icons
        [ant-design.json] Ant Design Icons - 789 Icons
...
        [zmdi.json] Material Design Iconic Font - 777 Icons
        [zondicons.json] Zondicons - 297 Icons
        Icons Loaded: 183,315
...Memory Usage: 736.6 MB


The next thing we'll need to do is set up some endpoints. This should work in any particular XData application, but we'll be adding to our TMS XData Template Demo Data project for this, just because it is already pretty well configured. We don't necessarily need to have any special authorization to use these endpoints, but it is trivial to add an [authorize] attribute to the interface to ensure only logged-in users have access.

We'll need two endpoints. One to return a list of the icon libraries that we are making available through this interface. And one to perform the search and return the icons that were found. For the first endpoint, all we really need to do is return the JSON that we created when the XData server first started. It isn't going to change unless the underlying icon JSON changes. Probably sufficiently infrequent and unimportant enough to just let the data get refreshed whenever the XData application is restarted. Here's the first endpoint.

function TSystemService.AvailableIconSets: TStream;
begin
  // Returning JSON, so flag it as such
  TXDataOperationContext.Current.Response.Headers.SetValue('content-type', 'application/json');

  Result := TStringStream.Create(MainForm.AppIconSets);
end;


And we can test it out in Swagger. This just shows the JSON that we constructed earlier.


TMS Software Delphi  Components

Available Icon Sets via Swagger.

For the second endpoint, we have to perform the same kind of search we performed earlier. And we'll be generating a "results" array in a similar format as well. This is a bit of an exercise in converting from JavaScript JSON handling to TJSONObject JSON handling. Not a lot different, ultimately, but it involves more typing, certainly.

function TSystemService.SearchIconSets(SearchTerms, SearchSets: String; Results: Integer): TStream;
var
  IconsFound: TJSONArray;
  IconSet: TJSONObject;
  IconSetList: TStringList;
  i: integer;
  j: integer;
  k: integer;
  IconName: String;
  Icon: TJSONArray;
  IconCount: Integer;
  Terms:TStringList;
  Matched: Boolean;
begin
  // Returning JSON, so flag it as such
  TXDataOperationContext.Current.Response.Headers.SetValue('content-type', 'application/json');

  // JSON Array we'll be returning
  IconsFound := TJSONArray.Create;
  IconCount := 0;

  // If all, will just iterate through the sets
  // Otherwise, we'll build a list and only iterate through the contents of that list
  if SearchSets = 'all' then
  begin
    k := Mainform.AppIcons.Count;
  end
  else
  begin
    IconSetList := TStringList.Create;
    IconSetList.CommaText := SearchSets;
    k := IconSetList.Count;
  end;

  // Sort out Search Terms
  Terms := TStringList.Create;
  Terms.CommaText := StringReplace(Trim(SearchTerms),' ',',',[rfReplaceAll]);

  i := 0;
  while (i < k) and (IconCount < Results) and (Terms.Count > 0) do
  begin

    // Load up an Icon Set to Search
    if SearchSets = 'all'
    then IconSet := (MainForm.AppIcons.Items[i] as TJSONObject).GetValue('icons') as TJSONObject
    else IconSet := (MainForm.AppIcons.Items[StrToInt(IconSetList[i])] as TJSONObject).GetValue('icons') as TJSONObject;

    // Search all the icons in the Set
    for j := 0 to IconSet.Count-1 do
    begin

      if (IconCount < Results) then
      begin

        IconName := (Iconset.Pairs[j].JSONString as TJSONString).Value;

        // See if there is a match using the number of terms we have
        Matched := False;
        if Terms.Count = 1
        then Matched := (Pos(Terms[0], IconName) > 0)
        else if Terms.Count = 2
             then Matched := (Pos(Terms[0], IconName) > 0) and (Pos(Terms[1], IconName) > 0)
             else Matched := (Pos(Terms[0], IconName) > 0) and (Pos(Terms[1], IconName) > 0) and (Pos(Terms[2], IconName) > 0);

        // Got a match
        if Matched then
        begin
          Icon := TJSONArray.Create;
          Icon.Add(IconName);

          // Need to know what set it is in so we get lookup default width, height, license, set name
          if SearchSets = 'all'
          then Icon.Add(i)
          else Icon.Add(IconSetList[i]);

          // Add in the icon data - the SVG and width/height overrides
          Icon.Add(IconSet.GetValue(IconName) as TJSONObject);

          // Save to our set that we're returning
          IconsFound.Add(Icon);
          IconCount := IconCount + 1;
        end;
      end;
    end;

    i := i + 1;
  end;

  // Return the array of results
  Result := TStringStream.Create(IconsFound.ToString);
end;


Not so bad, taken in small bites. But it gets the job done. Our endpoint work is complete. We can pass it a search string, space-delimited, which then gets converted into a set of up to three terms to search with, as we did previously. We can pass in either 'all' or a comma-separated list of icon libraries to search, where the numeric values correspond to their position in the original array we generated, listing all the icon sets. And we can limit the number of results returned. This can help speed the search by stopping earlier and also reduces the amount of JSON coming back. With 180,000+ icons potentially being searched, this can take some time if there are thousands of results to be transferred back.

TMS Software Delphi  Components
Searching with Swagger.

Using Swagger, we can test out how well this all works. And, as there are no network performance considerations (well, not when using the swagger interface on the same system as the XData app is running, that is), this actually works pretty quickly. Not as fast as the local copy perhaps, but we're also not using JavaScript, so even searching across 300 MB of JSON doesn't take long at all. Well under a second in many cases.  

To provide access to these endpoints, let's re-use our existing UI. Normally, we would not need both the local and remote variations here - the remote version can serve up everything so there would be no need for the local version, for example. Or if there were only a few smaller libraries needed, there would be no reason to have a remote version. Regardless, we'll reconfigure a few things so that we can use both (and also our fourth approach shortly) with the same UI by re-initializing it when needed to show the appropriate set of icon libraries, and then perform the searches in the appropriate places.

We'll need to get the list of libraries from our first endpoint. Taking a page from the Bootstrap control, we can use a fetch() call from JavaScript. One of about a dozen ways (or more!) to do this, but let's give this one a try. Once we have our list, we can populate the UI as we did previously.

procedure TForm1.InitializeXData;
var
  i: Integer;
  c: Integer;
begin
  // This intializes the custom icon editor to use the "remote" approach.
  asm
    this.IconSets = [];
    this.IconSetNames = [];
    this.IconSetCount = [];

    // Load up our Local icon sets
    var response = await fetch('http://localhost:12345/tms/xdata/SystemService/AvailableIconSets');
    var IconSetList = await response.json()

    // Original list is soprted by filename.  Lets sort it by library name instead (case-insensitive)
    IconSetList = IconSetList.sort((a, b) => {
      if (a.name.toLowerCase() < b.name.toLowerCase()) {
        return -1;
      }
    });

    // Get count data from this list
    for (var i = 0; i < IconSetList.length; i++) {
      var iconcount = IconSetList[i].count
      this.IconSetNames.push(IconSetList[i].name+': '+iconcount+' icons');
      this.IconSetCount.push(iconcount);
    };
  end;

  // Populate the listLibraries control
  c := 0;
  for i := 0 to Length(IconsetNames)-1 do
  begin
    listLibraries.Items.Add(IconSetNames[i]);
    listLibraries.Checked[i] := True;
    c := c + IconSetCount[i];
  end;
  editLibraries.Text := IntToStr(Length(IconSetNames))+' Icon Libraries Loaded: '+FloatToStrF(c,ffNumber,5,0)+' icons';
  IconPickerMode := 'XData';
end;


We also sorted the list as the default sort order was by filename - not very accurate. It is also a case-insensitive sort if that's of interest. Note that the order is important, so we added an extra 'library' element to the JSON so we can get the original library order when we want to make requests for specific libraries.

Now, when we perform a search, we'll instead have to send a request to XData and then populate the icon list with the results. We can use the same fetch() mechanism as before. This follows mostly the same logic as previously, just that the lookup data for the name, license, width, and height is in a different place. We end up with the same icons being loaded onto the page though, so we don't have to change anything after this. Here's what we've got.

procedure TForm1.btnSearchClickXData(Sender: TObject);
var
  i: Integer;
  Search: String;
  SearchSets: Array of Integer;
  IconSize:Integer;
  MaxResults: Integer;
  IconLib: Integer;
  SearchLib: String;
begin
  // Icon names are always lower case, so search with that
  Search := LowerCase(Trim(editSearch.text));

  // Limit returns while typing
  if (Sender = btnSearch)
  then MaxResults := 2500
  else MaxResults := 250;

  // Icon Size from slider - determines font-size
  IconSize := WebTrackbar1.Position * 2;

  // Figure out which libraries we're searching through
  for i := 0 to listLibraries.Count - 1 do
  begin
    if listLibraries.Checked[i] = True then
    begin
      IconLib := -1;
      asm
        IconLib = this.IconSetList[i].library;
      end;
      SearchSets := SearchSets + [IconLib];
    end;
  end;
  if Length(SearchSets) = listLibraries.Count
  then SearchLib := 'all';


  // Must have something to search for and somewhere to search
  if (Search <> '') and (Length(SearchSets) > 0) then
  begin
    asm

      if (SearchSets.length !== this.IconSetList.length) {
        SearchLib = SearchSets.join(',')
      }
      console.log(SearchLib);

      // Build a new results array
      var results = [];

      // Search for at most three terms
      var searchterms = Search.split(' ').slice(0,3).join(' ');

      var response = await fetch('http://localhost:12345/tms/xdata/SystemService/SearchIconSets'+
        '?SearchTerms='+encodeURIComponent(searchterms)+
        '&SearchSets='+SearchLib+
        '&Results='+MaxResults);
      var results = await response.json();

      // Sort results by icon name
      results = results.sort((a, b) => {
        if (a[0] < b[0]) {
          return -1;
        }
      });

      // Update count
      divData.innerHTML = '<div class="d-flex justify-content-between align-items-center"><div>Results: '+results.length+'</div></div>';
      this.IconResults = results.length;

      // Clear existing results
      divIconList.replaceChildren();

      // Create icons for display
      var display = '';
      for (i = 0; i < results.length; i++) {

        // Figure out which library we're using - note that it is now sorted differently
        var lib = this.IconSetList.find( o => o.library == results[i][1]);

        // Each library has its default width and height, and then overrides at the icon level
        var iconheight = results[i][2].height || lib.height;
        var iconwidth = results[i][2].width || lib.width;

        var displayicon = '<div style="font-size:'+IconSize+'px;" class="SearchIcon" '+
                               'icon-library="'+lib.name+'" '+
                               'icon-license="'+lib.license+'">'+
                             '<svg width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '+
                               'viewBox="0 0 '+iconwidth+' '+iconheight+'">'+
                                 results[i][2].body+
                             '</svg>'+
                             '<div style="font-size:8px; text-align:center; width:100%;">'+
                               results[i][0]+
                             '</div>'+
                           '</div>';

        display += displayicon;
      }
      divIconList.innerHTML = display;
    end;

  end;
end;

Note that we also dropped the number of icons being returned by default from 1000 to 250.  It is still pretty snappy though, enough to be able to perform searches while typing. Here's the update with XData performing the searches.


TMS Software Delphi  Components
XData Icon Picker in Action.


Adding the selected icons back into the document is the same as previously, as they were added to the list with the same properties, including the SVG content. SunEditor seems to struggle with some of the more complex SVG icons, such as those from the Fluent icon sets, but that's something to sort out with SunEditor - they display fine in our icon picker. 

Otherwise, this works almost as fast as the local version, even when searching through 180,000+ icons. But that's connecting to XData on the same system - may be different with a slower network connection, so we'll leave the icon limits in place and maybe adjust them downward if the client performance isn't as fast as we'd like. We could also not perform searches while typing, waiting instead for the user to tap the icon or hit enter or something like that, so we're only making a request of XData when necessary. Lots of options here.

Approach #4: Font Awesome Icons

So while that works pretty well, and opens the door to 180,000+ icons, well, there's another 25,000+ icons that are not included. I'm referring of course to the Font Awesome Pro icons, but if you've got an icon library that is not free or is otherwise somehow not included in the above, then we've got a problem.

In the case of Font Awesome Pro, for example, I'm rather fond of their Duotone icons, but they currently have eight different families to choose from. And while the Font Awesome Free icons are included in those 180,000+ icons already, there's no way to augment that with the pro versions. What to do? Well, it turns out they have their own API service where we can do essentially what we did with XData - send a request and get back a list of icons.  Using their example, we can perform a search for "coff" using the following.

curl -H "Content-Type: application/json" -d '{ "query": "query { search(version:\"6.4.0\", query:\"coff\", first: 2) { id, familyStylesByLicense { pro { family, style } } } } " }' https://api.fontawesome.com


And we'll get back a list of all of the matching icons. Note that they perform a more interesting search than just matching strings, which we'll see in the demo. Using the above request, limited to 2 icons, it will also return a list of all the families and styles associated with the icon.

{
  "data": {
    "search": [
      {
        "familyStylesByLicense": {
          "pro": [
            {
              "family": "sharp",
              "style": "solid"
            },
            {
              "family": "sharp",
              "style": "regular"
            },
            {
              "family": "sharp",
              "style": "light"
            },
            {
              "family": "duotone",
              "style": "solid"
            },
            {
              "family": "classic",
              "style": "thin"
            },
            {
              "family": "classic",
              "style": "solid"
            },
            {
              "family": "classic",
              "style": "regular"
            },
            {
              "family": "classic",
              "style": "light"
            }
          ]
        },
        "id": "coffin-cross"
      },
      {
        "familyStylesByLicense": {
          "pro": [
            {
              "family": "sharp",
              "style": "solid"
            },
            {
              "family": "sharp",
              "style": "regular"
            },
            {
              "family": "sharp",
              "style": "light"
            },
            {
              "family": "duotone",
              "style": "solid"
            },
            {
              "family": "classic",
              "style": "thin"
            },
            {
              "family": "classic",
              "style": "solid"
            },
            {
              "family": "classic",
              "style": "regular"
            },
            {
              "family": "classic",
              "style": "light"
            }
          ]
        },
        "id": "coffin"
      }
    ]
  }
}

Back in our project, we can reuse the same icon picker interface again. This time out, we'll know the icon sets in advance, so we don't need to look them up. We'll assume that we're using the Pro version, and have a Font Awesome Pro library linked in our Project.html. If we don't then most of the icons (the non-free icons) will not be displayed. 

As it turns out, the information returned by the API is incomplete. All the icons are available in all the families, with the exception of the Brands icons (467 icons that represent specific organizations). So while we can get this family and style information, it turns out that we don't really need it.

For font sizes, we can also use the Font Awesome classes for this. The Font Awesome Kit that I'm using converts <i> elements automatically into SVG elements, but it does sometimes take a moment or two for these icons to appear. To help ease the burden on these queries, the request isn't sent until the icon is clicked or enter is keyed in. It works without it, but it isn't as fast as our other approaches, so it is a bit less pleasant. This delayed sending mechanism works pretty well. Also, in the other approaches, there's a 1:1 relationship between the icons retrieved and those displayed. Here, there are eight different styles that correspond to each icon, so it is an 8:1 relationship. This doesn't matter much, other than the way we count them has to reflect this.

Similarly, we'll still filter the results by the libraries chosen, but this is done after we've retrieved the icon list from Font Awesome, rather than sending them the libraries we'd be interested in. This makes some sense now that each family has all of the icons defined - it wasn't always that way. Hopefully, they keep up with the different variations each time new icons are added in the future. Another benefit here is that we don't really need to do anything when their icons change - the API will return the latest icons available. Handy.  

Less handy is CORS. In this case, we have no control over the remote site (https://api.fontawesome.com) and it doesn't allow for CORS to work the way we'd like. So what I've done to get around it is create another XData endpoint to use as a proxy.  This just takes whatever is sent to it, sends it to Font Awesome, and sends back whatever their response was. Could probably make this more generic by also passing in the URL, but this is kind of a specific case. Here's what the endpoint looks like.

function TSystemService.SearchFontAwesome(Query: String): TStream;
var
  Client: TNetHTTPClient;
  QStream: TStringStream;
  Response: String;
begin
  QStream := TSTringStream.Create(Query);
  Client := TNetHTTPClient.Create(nil);
  Client.Asynchronous := False;
  Client.ContentType := 'application/json';
  Client.SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12];
  Response := Client.Post('https://api.fontawesome.com',QStream).ContentAsString;
  Result := TStringStream.Create(Response);
  Client.Free;
  QStream.Free;
end;

The idea is to do this as quickly as possible, as this is essentially overhead on our query. We could also expand this to cache requests. That might not make sense in this case, as every query is likely to be different, but it may well make sense in other cases, particularly if you have to pay for access to the API. In the case of Font Awesome, we don't even need an API key, so we're just trying to do this as efficiently as possible. With this in place, we can write our search function.


procedure TForm1.btnSearchClickFontAwesome(Sender: TObject);
var
  i: Integer;
  Search: String;
  SearchSets: Array of Integer;
  IconSize: String;
  MaxResults: Integer;
  IconLib: Integer;
  SearchLib: String;
begin
  // Icon names are always lower case, so search with that
  Search := LowerCase(Trim(editSearch.text));

  // Limit returns while typing
  if (Sender = btnSearch)
  then MaxResults := 2500
  else MaxResults := 100;

  // Icon Size from slider - determines font-size
  if      WebTrackbar1.Position <= 10 then IconSize := ' fa-2xs'
  else if WebTrackbar1.Position <= 12 then IconSize := ' fa-xs'
  else if WebTrackbar1.Position <= 14 then IconSize := ' fa-sm'
  else if WebTrackbar1.Position <= 16 then IconSize := ''
  else if WebTrackbar1.Position <= 18 then IconSize := ' fa-lg'
  else if WebTrackbar1.Position <= 20 then IconSize := ' fa-xl'
  else if WebTrackbar1.Position <= 22 then IconSize := ' fa-2xl'
  else if WebTrackbar1.Position <= 24 then IconSize := ' fa-2x'
  else if WebTrackbar1.Position <= 26 then IconSize := ' fa-3x'
  else if WebTrackbar1.Position <= 28 then IconSize := ' fa-4x'
  else if WebTrackbar1.Position <= 30 then IconSize := ' fa-5x'
  else if WebTrackbar1.Position <= 32 then IconSize := ' fa-6x';

  // Figure out which libraries we're searching through
  // Note that in this case, we'll filter the results that we get
  // back rather than filtering the request
  for i := 0 to listLibraries.Count - 1 do
  begin
    if listLibraries.Checked[i] = True then
    begin
      SearchSets := SearchSets + [i];
    end;
  end;
  if Length(SearchSets) = listLibraries.Count
  then SearchLib := 'all';

  // Must have something to search for and somewhere to search
  if (Search <> '') and (Length(SearchSets) > 0) then
  begin
    asm

      if (SearchSets.length !== this.IconSetList.length) {
        SearchLib = SearchSets.join(',')
      }

      // Build a new results array
      var results = [];

      // Search for at most three terms
      var searchterms = Search.split(' ').slice(0,3).join(' ');
      var request = JSON.stringify({query:'query{search(version:"latest",query:"'+searchterms+'",first:'+MaxResults+') {id,familyStylesByLicense{pro{family,style}}}}'});
      var response = await fetch('http://localhost:12345/tms/xdata/SystemService/SearchFontAwesome?Query='+encodeURIComponent(request));
      var results = await response.json();

      // Don't sort results by icon name
      results = results.data.search;

      // Clear existing results
      divIconList.replaceChildren();

      // Create icons for display
      var display = '';
      var count = 0;
      for (var i = 0; i < results.length; i++) {

        // Figure out which library we're using based on family and style values
        var fs = results[i].familyStylesByLicense.pro
        for (var j = 0; j < fs.length; j++ ) {

          if (fs[j].style == 'brands') {
            if ((SearchLib == 'all') || (SearchLib.indexOf(j) !== -1)) {
              var lib = this.IconSetList[0];
              var displayicon = '<div style="font-size:'+IconSize+'px;" class="SearchIcon" '+
                                   'icon-library="'+lib.name+'" '+
                                   'icon-license="'+lib.license+'">'+
                                 '<i class="fa-brands fa-'+results[i].id+IconSize+'"></i>'+
                                 '<div style="font-size:8px; text-align:center; width:100%;">'+
                                   results[i].id+
                                 '</div>'+
                               '</div>';
              display += displayicon;
              count += 1;
            }
          } else {
            for (var j = 1; j < 9; j++) {
              if ((SearchLib == 'all') || (SearchLib.indexOf(j) !== -1)) {
                var lib = this.IconSetList[j];
                var displayicon = '<div style="font-size:'+IconSize+'px;" class="SearchIcon" '+
                                     'icon-library="'+lib.name+'" '+
                                     'icon-license="'+lib.license+'">'+
                                   '<i class="'+lib.prefix+' fa-'+results[i].id+IconSize+'"></i>'+
                                   '<div style="font-size:8px; text-align:center; width:100%;">'+
                                     results[i].id+
                                   '</div>'+
                                 '</div>';
                display += displayicon;
                count += 1;
              }
            }
          }
        }
      }
      divIconList.innerHTML = display;
      // Update count
      divData.innerHTML = '<div class="d-flex justify-content-between align-items-center"><div>Results: '+count+'</div></div>';
      this.IconResults = count;

    end;
  end;
end;


Our previous addEventListener function still works here, even though we're displaying icons in a completely different way. This is because we've provided it with the attributes it needs, and it doesn't really matter what the actual icon format is - it works the same way. Here's what it looks like.

TMS Software Delphi  Components
Font Awesome Icon Picker in Action.

Just for fun, InteractJS was added to the project so that we could drag and resize the icon picker window. As we laid it out with this in mind, we didn't really have to do much to make that work. 

All Done.

Well, perhaps a little more coding than we'd like, but we've got a system in place to conveniently and quickly access more than 200,000 icons in total. Normally you'd likely only use one of these approaches in a particular app, but here we can see that all four working at the same time is also entirely possible. And we could assign icons to things other than an HTML editor. Here are some other ideas.

  • Having a set of icons available for users to select an avatar.
  • Adding custom icons to tags for document categories in a filing system.
  • Customizable pins on a map

There are likely many other uses for this kind of thing. As always, questions, comments, and feedback are most welcome. Is there an application for this we've not covered? Is there an icon set that is somehow not included among these 200,000 icons that we should be adding to the project? Please let us know!

Download Sample Code


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





Andrew Simard


Bookmarks: 

This blog post has not received any comments yet.



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