Description

While building our website, I got tired of going back and forth between Rock and the Font Awesome website to find the icons I wanted to use, so I built a system that catalogs the icons in the Font Awesome files as well as our own custom icon font, and then adds a searchable icon picker to nearly every Icon CSS Class field in Rock.

icon-picker-screenshot.jpg

Details

There are two main parts of this system. The first is the icon importer. Information about the installed icon fonts is stored in a defined type. Whenever an icon font is updated, either via a Rock update or a custom font update, there is an Import Icon Fonts workflow that should be run manually. The workflow finds the files associated with each font in the defined type and extracts the details of each individual icon into a second defined type.

The second part is the icon picker. A special shortcode with all of the necessary Lava, CSS and JavaScript for adding searchable icon picker functionality to (nearly) every Icon CSS Class field is inserted on every page on the site. When the user starts typing into one of the icon fields, the JavaScript searches the icon class names to display the matching icons. Clicking on an icon in the search results automatically inserts the corresponding CSS classes into the icon class field.

Disclaimers

This recipe relies heavily on things like file names, folder structure and even file contents, so there's a decent chance it could break at any time if any of those things change. However, we have been using it for over 2 years without any issues.

None of this has been tested with FontAwesome Pro. It may just work, but we don't have a Pro license, so I don't know for sure. If you have FontAwesome Pro and try this out, please let me know how it goes.

How-To

Create the Icon Font Families Defined Type

  • Name: Icon Font Families
  • Description: Details about currently installed icon font families
  • Attribute: Prefix
    • Name: Prefix
    • Description: The CSS class prefix for the icon font
    • Key: Prefix
    • Field Type: Text
    • Required: Yes
    • Show in Grid: Yes
    • Default Value: fa
  • Attribute: SVG Files
    • Name: SVG Files
    • Description: A comma separated list of SVG font file paths (relative to the /Assets/Fonts folder)
    • Key: SVGFiles
    • Field Type: Text
    • Required: Yes
    • Show in Grid: No

Add at least one value for the built in FontAwesome font, as well as values for each custom icon font that you have.

  • Value: Font Awesome
  • Prefix: fa
  • SVG Files: FontAwesome\fa-regular-400.svg,FontAwesome\fa-solid-900.svg,FontAwesome\fa-brands-400.svg

Create the Font Icons Defined Type

  • Name: Font Icons
  • Description: Individual icons from an icon font
  • Attribute: Font Family
    • Name: Font Family
    • Key: FontFamily
    • Field Type: Defined Value
    • Required: Yes
    • Show in Grid: Yes
    • Defined Type: Icon Font Families

Import and Edit the Import Icon Fonts Workflow Type

Download and import the Import Icon Fonts workflow type. Once it's imported, edit the workflow type. Find the Import Icons > Run Import Code action and change the Icon Font Family Defined Type ID and Font Icon Defined Type ID to match the defined types you just created.

Create the Icon Picker Shortcode

Create a new Lava Shortcode with the following details:

  • Name: Icon Picker
  • Tag Name: iconpicker
  • Tag Type: Inline
  • Description: Adds an icon picker dropdown to nearly any Icon CSS Class fields on the page.
  • Enabled Lava Commands: Cache, RockEntity

Documentation:

<p><strong>Usage:</strong></p>
<pre style="position: relative;">{[ iconpicker ]}<div class="open_grepper_editor" title="Edit &amp; Save To Grepper"></div><div class="open_grepper_editor" title="Edit &amp; Save To Grepper"></div></pre>
<p>No parameters.</p>
<p>This shortcode should only be used once per page to enable the icon search feature on all <code>label</code> or <code>input</code> elements that have the word <code>Icon</code> in their <code>id</code> or content. The output of this shortcode includes all necessary JavaScript and CSS for creating font icon dropdowns on the entire page.</p>

Shortcode Markup:

Be sure to edit the Font Icons defined type ID on the first line to match the defined type you created earlier.

{%- assign iconDefTypeID = 102 -%}
{%- stylesheet id:'icon-picker-css' cacheduration:'3600' compile:'less' -%}
    .icon-input-wrapper { position: relative; }
    .icon-dropdown
    {
        max-width: 300px;
        max-height: 250px;
        min-width: 0;
        overflow: auto;
        padding: 3px;
        text-align: left;
        li
        {
            display: inline-block;
            margin: 1px;
        }
        &>li>a
        {
            display: inline-block;
            width: 2em; height: auto;
            padding: 3px;
            border-radius: 3px;
            text-align: center;
            line-height: 1.25em;
            cursor: pointer;
            transition: color .25s ease-in-out;
            &:hover { background-color: #e8e8e8; }
            &:active { color: #777; }
            i { line-height: inherit; }
        }
    }
{%- endstylesheet -%}
{%- assign inputSelector = 'input[id*="Icon"], label:contains(Icon) ~ .control-wrapper input' -%}
{%- javascript id:'icon-picker-js' -%}
    const DEBUGICONS = false;

    $('document').ready(function()
    {
        if (DEBUGICONS === true) console.log('Initialize Icon Picker');
        $('body')
            .on('propertychange change click keyup input paste', '{{ inputSelector }}', function(e)
            {
                var $Input = $(this);
                var term = $Input.val().replace('fa ','').replace('fas ','').replace('fab ','').replace('fa-','');

                if ($Input.data('initialized') !== true)
                {
                    InitializeInput($Input, true);
                }
                else if ($Input.data('prev-term') === '' || $Input.data('prev-term') !== term)
                {
                    if (DEBUGICONS === true) console.log('Input Change');

                    var $Dropdown = $Input.data('dropdown');

                    RefreshIcons($Input, $Dropdown);
                }
            })
            .on('focusin', '{{ inputSelector }}', function(e)
            {
                if (DEBUGICONS === true) console.log('Focus');
                var $Input = $(this);

                ShowIconDropdown($Input);
            })
            .on('mouseup', '{{ inputSelector }}', function(e)
            {
                //only trigger on left click
                if (e.which === 1)
                {
                    if (DEBUGICONS === true) console.log('MouseUp');
                    var $Input = $(this);

                    ShowIconDropdown($Input);
                }
            })
            .click(function() { $('.icon-dropdown').filter(':visible').stop(true, true).slideUp('fast'); $('{{ inputSelector }}').data('prev-term', '') });

        function InitializeInput($Input, showDropdown = false)
        {
            if (DEBUGICONS === true) console.log('Already Initializing: ' + $Input.data('initializing'));
            if ($Input.data('initializing') === true) return;

            if (DEBUGICONS === true) console.log('START: Initialize');
            $Input.data('initializing', true);
            var $Dropdown = $('<ul class="icon-dropdown dropdown-menu" role="menu"></ul>');
            $Dropdown.hide().insertAfter($Input);

            $Input.data('dropdown', $Dropdown);

            var apiURL = '{{ 'Global' | Attribute:'InternalApplicationRoot' }}api/DefinedValues?$filter=DefinedTypeId%20eq%20{{ iconDefTypeID }}%20and%20IsActive%20eq%20true&$select=Value%2CDescription&$orderby=Order'
            $.getJSON(apiURL, function(data)
            {
                if (DEBUGICONS === true) console.log('START: Add Icons');
                $.each(data, function(i, icon)
                {
                    var cssClass = icon.Value;
                    var name = icon.Description;
                    $Dropdown.append('<li><a data-class="' + cssClass + '" title="' + name + '"><i class="fa-lg ' + cssClass + '"></i></a></li>');
                });
                if (DEBUGICONS === true) console.log('DONE: Icons Added (' + data.length + ')');

                $Dropdown.on('click', 'a', function(e)
                {
                    if (DEBUGICONS === true) console.log('Icon Click');
                    e.preventDefault();
                    e.stopPropagation();

                    $Input.val($(this).data('class'));

                    $Dropdown.find('.active').removeClass('active');
                    $(this).parent().addClass('active');

                    HideIconDropdown($Input);

                    return false;
                });

                $Input.parent()
                    .addClass('icon-input-wrapper')
                    .click(function(e) { e.stopPropagation(); });

                $Input.data('initializing', false).data('initialized', true);
                if (DEBUGICONS === true) console.log('DONE: Initialize');

                if (showDropdown === true) ShowIconDropdown($Input);
            });
        }

        function ShowIconDropdown($Input)
        {
            if ($Input.data('initialized') !== true)
            {
                InitializeInput($Input, true);
            }
            else
            {
                if (DEBUGICONS === true) console.log('START: Show Dropdown');
                var $Dropdown = $Input.data('dropdown');
                if (!$Dropdown.data('opening'))
                {
                    if (DEBUGICONS === true) console.log(' > Show Dropdown');
                    $Dropdown
                        .data('opening', true)
                        .stop(true, true)
                        .slideDown('fast', function() { $(this).data('opening', false) });
                    RefreshIcons($Input, $Dropdown);
                }
                if (DEBUGICONS === true) console.log('DONE: Show Dropdown');
            }
        }

        function HideIconDropdown($Input)
        {
            if (DEBUGICONS === true) console.log('START: Hide Dropdown');
            var $Dropdown = $Input.data('dropdown');
            if (!$Dropdown.data('closing'))
            {
                if (DEBUGICONS === true) console.log(' > Hide Dropdown');
                $Dropdown
                    .data('closing', true)
                    .stop(true, true)
                    .slideUp('fast', function() { $(this).data('closing', false) });
            }
            if (DEBUGICONS === true) console.log('START: Hide Dropdown');
        }

        function RefreshIcons($Input, $Dropdown)
        {
            if (DEBUGICONS === true) console.log('START: Refresh Icons');
            var term = $Input.val().replace('fa ','').replace('fas ','').replace('fab ','').replace('fa-','');
            var count = 0;

            if ($Input.data('prev-term') === '' || $Input.data('prev-term') !== term)
            {
                if (DEBUGICONS === true) console.log(' > Refresh Icons');
                $Input.data('prev-term', term);

                $Dropdown.find('.active').removeClass('active');
                $Dropdown.find('a:hidden').each(function()
                {
                    var $Icon = $(this);
                    var cssClass = $Icon.data('class');

                    if (term === '' || cssClass.includes(term))
                    {
                        $Icon
                            .addClass('visible')
                            .parent()
                                .stop(true, true)
                                .fadeIn('fast');
                    }
                });

                $Dropdown.find('a').filter(':visible, .visible').each(function()
                {
                    var $Icon = $(this);
                    var cssClass = $Icon.data('class');

                    if (term !== '' && !cssClass.includes(term))
                    {
                        $Icon
                            .removeClass('visible')
                            .parent()
                                .stop(true, true)
                                .fadeOut('fast');
                    }
                });

                var count = $Dropdown.find('a.visible').length;
                if (DEBUGICONS === true) console.log(' > Matches: ' + count);

                if ($Dropdown.is(':hidden') && (term === '' || count > 0))
                    ShowIconDropdown($Input);
                else if ($Dropdown.is(':visible') && count <= 0 && term !== '')
                    HideIconDropdown($Input);
            }
            if (DEBUGICONS === true) console.log('DONE: Refresh Icons');
        }
    });
{%- endjavascript -%}

Add the Shortcode to The Internal Site

Add an HTML block to the Footer zone of your Internal Rock Website, and set the content to {[ iconpicker ]}.

Go to Admin Tools > CMS Configuration > Pages and find a page on the Internal site that uses the Dialog layout. There should be one at Internal Homepage > System Dialogs > ZoneBlocks. Add an HTML block to the Main zone of the Layout, and set the content to {[ iconpicker ]}.

All done!

You should be able to start typing into nearly any Icon CSS Class field and see a list of matching icons pop up.

Follow Up

Please don't hesitate to leave a comment or hit me up on Rock Chat (@JeffRichmond) if you have questions or find any issues with this recipe.


Change Log

  • 2023-05-08 - Updated shortcode markup to use stylesheet and javascript commands to prevent duplicate styles/scripts
  • 2022-09-15 - Initial version