0 Icon Picker (v18+) Shared by Jeff Richmond, The Well Community Church 3 days ago 18.1 CMS, Web Intermediate Rock v18+ Only This is an updated version of the recipe that takes advantage of features introduced in Rock v18. If you are running an earlier version of Rock, see the original Icon Picker recipe. Description While building our website in Rock, 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 script that pulls the icons for Tabler, Font Awesome, and our own custom icon font, and adds a searchable icon picker to nearly every Icon CSS Class field in Rock. v18 Improvements This updated recipe has been simplified significantly. The original involved an icon importer workflow that used C# to extract icon information from each of the icon font files and added them to a couple custom defined types. That workflow and the defined types are no longer necessary since Rock now stores icon definitions in JSON files that are referenced by a single system defined type. The only thing required now is the icon picker script on every page. If you have a custom icon font, then one additional step is required to generate the special JSON file, but that is covered in my Custom Icon Font recipe. Updating to v18+ If you are updating from the original version of this recipe, then you can perform some additional clean-up steps: Delete the Import Icon Fonts Workflow Delete the Icon Font Families defined type Delete the Font Icons defined type Disclaimer I have not tested any of this with FontAwesome Pro since we don't have a Pro license. According to Korigan Payne, he was able to get Pro working with the previous version of the recipe by editing one of the Icon Font Families defined values, but I'm not sure how that translates to Rock's new JSON icon definitions though. Either way, since FontAwesome is going away at some point, this shouldn't be a concern for too much longer. How-To Create the Icon Picker Shortcode Create a new Lava Shortcode or update your existing shortcode with the following details: Name: Icon Picker Tag Name: iconpicker Tag Type: Inline Description: Adds an icon picker dropdown to nearly every Icon CSS Class fields on the page. Enabled Lava Commands: None Documentation: <p><strong>Usage:</strong></p> <pre>{[ iconpicker ]}</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: {%- assign iconLibraryDefTypeID = 186 %} {%- assign faBrandsPath = '/Assets/Icons/Libraries/fa-brand-icons.json' %} //----------------------------------- {%- assign inputSelector = 'input[id*="Icon"]:not(.picker-obsidian input), label:contains(Icon) ~ .control-wrapper input:not(.picker-obsidian input)' %} {%- assign loadingIndicator = '<li class="icon-dropdown-loading"><i class="ti ti-progress icon-dropdown-spin"></i> Loading…</li>' %} {%- 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('ti ','').replace('ti-','').replace('fa ','').replace('fas ','').replace('fab ','').replace('far ','').replace('fa-','').replace('w ','').replace('w-',''); 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); $Input.parent() .addClass('icon-input-wrapper') .click(function(e) { e.stopPropagation(); }); $Dropdown.html('{{ loadingIndicator }}'); $Dropdown.stop(true, true).show(); // Fetch active icon library defined values to get JSON file URLs var libraryApiURL = '{{ 'Global' | Attribute:'InternalApplicationRoot' }}api/DefinedValues?$filter=DefinedTypeId%20eq%20{{ iconLibraryDefTypeID }}%20and%20IsActive%20eq%20true&$select=Description&$orderby=Order'; $.getJSON(libraryApiURL, function(libraries) { if (DEBUGICONS === true) console.log('START: Load Icon Libraries (' + libraries.length + ')'); // Fetch each library's JSON file and collect all icons var fetchPromises = libraries.map(function(library) { var jsonPath = library.Description; if (DEBUGICONS === true) console.log(' > Fetching: ' + jsonPath); return fetch(jsonPath) .then(function(response) { if (!response.ok) { console.warn('Icon library fetch failed: ' + jsonPath + ' (' + response.status + ')'); return null; } return response.json(); }) .catch(function(err) { console.warn('Icon library fetch error: ' + jsonPath, err); return null; }); }); var brandsFetch = fetch('{{ faBrandsPath }}') .then(function(response) { if (!response.ok) { console.warn('FA brands fetch failed (' + response.status + ')'); return []; } return response.json(); }) .catch(function(err) { console.warn('FA brands fetch error:', err); return []; }); Promise.all(fetchPromises).then(function(results) { brandsFetch.then(function(brandsData) { var fabNames = new Set(Array.isArray(brandsData) ? brandsData : []); if (DEBUGICONS === true) console.log('START: Build Icon Array (' + fabNames.size + ' brand icons)'); var allIcons = []; results.forEach(function(libraryData) { if (!libraryData || !Array.isArray(libraryData.Icons)) return; var prefix = libraryData.StyleClassPrefix ? libraryData.StyleClassPrefix + ' ' : ''; libraryData.Icons.forEach(function(icon) { var iconName = icon.StyleClass.replace(/^fa-/, ''); var cssPrefix = (prefix.trim() === 'fa' && fabNames.has(iconName)) ? 'fab ' : prefix; allIcons.push({ cssClass: cssPrefix + icon.StyleClass, name: icon.Title, searchTerms: (Array.isArray(icon.SearchTerms) ? icon.SearchTerms.join(' ') : '').replace(/\n/g, ' ') }); }); }); if (DEBUGICONS === true) console.log('DONE: Icon Array Built (' + allIcons.length + ')'); $Dropdown.data('icons', allIcons); $Dropdown.on('click', 'a', function(e) { if (DEBUGICONS === true) console.log('Icon Click'); e.preventDefault(); e.stopPropagation(); $Input.val($(this).data('class')); HideIconDropdown($Input); return false; }); $Input.data('initializing', false).data('initialized', true); if (DEBUGICONS === true) console.log('DONE: Initialize'); if (showDropdown === true) ShowIconDropdown($Input); }); }); }); } function ShowIconDropdown($Input) { // Still loading - dropdown is already visible with the loading indicator if ($Input.data('initializing') === true) return; 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('DONE: 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-',''); if ($Input.data('prev-term') === '' || $Input.data('prev-term') !== term) { if (DEBUGICONS === true) console.log(' > Refresh Icons'); $Input.data('prev-term', term); var count = RenderIcons($Dropdown, term); 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'); } function RenderIcons($Dropdown, term) { var icons = $Dropdown.data('icons'); // Icons haven't loaded yet - leave the loading indicator in place if (!icons) return 0; var matches = term === '' ? icons : icons.filter(function(icon) { return icon.searchTerms.includes(term); }); var html = ''; matches.forEach(function(icon) { html += '<li><a data-class="' + icon.cssClass + '" data-terms="' + icon.searchTerms + '" title="' + icon.name + '"><i class="fa-lg ' + icon.cssClass + '"></i></a></li>'; }); $Dropdown.html(html); return matches.length; } }); {%- endjavascript %} {%- stylesheet id:'icon-picker-css' cacheduration:'3600' %} .icon-input-wrapper { position: relative; } .icon-dropdown { max-width: 300px; max-height: 250px; min-width: 0; overflow: auto; padding: 3px; text-align: left; } .icon-dropdown li { display: inline-block; margin: 1px; } .icon-dropdown > li.icon-dropdown-loading { display: block; padding: 8px 12px; color: var(--color-interface-medium-strong); font-size: .9em; white-space: nowrap; } @keyframes icon-dropdown-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .icon-dropdown-spin { font-size: 1.4em; display: inline-block; animation: icon-dropdown-spin 1s linear infinite; } .icon-dropdown > li > a { display: inline-block; width: 2em; height: auto; padding: 3px; border-radius: 3px; text-align: center; line-height: 1.25em; cursor: pointer; } .icon-dropdown > li > a:hover, .icon-dropdown > li > a:focus { background-color: var(--color-interface-medium-strongest); color: var(--color-interface-soft); } .icon-dropdown > li > a i { line-height: inherit; } {%- endstylesheet %} Add the Shortcode to the Internal Site Add an HTML block to the Footer zone of your Internal Rock Website. Make sure to add the block at the Site level, not the Layout or Page level, so it appears on every page. 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 from all of your icon libraries. 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 2022-09-15 - Initial version 2023-05-08 - Updated shortcode markup to use stylesheet and javascript commands to prevent duplicate styles/scripts 2024-09-13 - Added note about running the workflow after importing it. Removed unnecessary Lava command requirements on the shortcode. Updated section about adding shortcode to the site to be more specific. 2026-06-19 - Overhauled for Rock v18+