ACF Iconpicker
and fontawesome
With the release of ACF 6.3 there is a new field type “icon picker”.
Thanks to the developers there are some hooks so we can add own tabs or in this specific case a tab for FontAwesome :)
Because our own WordPress System (PAWS) is very customized, I can’t just release the whole sourcecode.
But I hope with the keyconcepts below every dev should be able to adapt it into their own system.
Some !important infos before you continue reading:
This post is only for developers. It’s not supposed to be a “download & work”-solution or something like that!
This is an early (working) beta and not optimized at all. Feel free to optimize and change the code as much as you want to :)
This solution works with both FontAwesome and FontAwesome PRO but you need to have the folder with all the svgs locally in your project.
As mentioned above, this solution works with ACF 6.3+ and FontAwesome 6.5+ (probably also 6.x, but not tested)
Step 1 - adding the tab
Basically we need two hooks - one to add the tab itself and the other one to display the content of the tab.
Adding the tab to ACF:
<?php
namespace <your namespace>;
class Acf {
public static function AcfIconpickerTabs($tabs) {
$tabs['fontawesome'] = 'FontAwesome';
return $tabs;
}
}
add_filter('acf/fields/icon_picker/tabs', ['\\' . __NAMESPACE__ . '\\Acf', 'AcfIconpickerTabs']);
Display the base-layout of the FontAwesome tab:
<?php
namespace <your namespace>;
class Acf {
public static function AcfIconpickerTabFAContent($field) {
$restEndpoint = \PAWS\Helper\Rest::GetRestUrl(
namespace: '<your namespace>',
endpoint: '/fontawesome/',
ignoreLanguage: true,
);
echo '<div class="acf-fontawesome-search-wrap" data-endpoint="' . $restEndpoint . '">';
acf_text_input(
array(
'class' => 'acf-fontawesome-search-input',
'placeholder' => esc_html__( 'Search fontawesome...', 'acf' ),
'type' => 'search',
)
);
echo '</div>';
echo '<div class="acf-fontawesome-list"><span class="loading">Loading</span></div>';
}
}
add_action('acf/fields/icon_picker/tab/fontawesome', ['\\' . __NAMESPACE__ . '\\Acf', 'AcfIconpickerTabFAContent']);
As you can see, we need a REST-endpoint to search for our icons.
Step 2 - REST-ENDPOINT
The code for this endpoint is very specific for our own system but here is what you need to know:
The endpoint is accepting two arguments as GET-parameters:
q=<searchtext>
This is the search term the user types into the field.current=<currentIcon>
This is the current icon code which should be something like “light|cat.svg”
Then there must be an array called $icons. This array has the following structure:
$icons = [];
$icons[‘light’] = [
‘name’ => ‘Light’,
‘items’ => [$items]
];
While the $items array has the following structure:
$items = [];
$items[‘cat’] = [
‘text’ => ‘Cat’,
‘id’ => ‘light|cat.svg’,
‘icon’ => ‘<base64 encoded svg content for this icon>’
The result of adding all icons for every type of fontawesome icons to the $icons array should result in a very big array which contains all the icons available from FontAwesome.
Next, the REST-endpoint can be build:
<?php
namespace <your namespace>;
class Fontawesome {
public static function Search($request): array {
$icons = <your code to get all the fontawesome icons here>
$searchTerm = strtolower($request['q']);
$current = isset($request['current']) ? strtolower($request['current']) : false;
$currentResult = [];
$result = [];
if (!empty($searchTerm)) {
foreach ($icons as $groupKey => $group) {
if ($groupKey == 'favorites') {
$children = [];
foreach ($group['items'] as $item) {
$item['active'] = false;
$children[] = $item;
}
$result[] = [
'text' => $group['name'],
'active' => false,
'children' => $children,
];
} else {
$children = [];
foreach ($group['items'] as $item) {
if (str_contains(strtolower($item['text']), $searchTerm)) {
$item['active'] = false;
$children[] = $item;
}
}
if ($children !== []) {
$result[] = [
'text' => $group['name'],
'active' => false,
'children' => $children,
];
}
}
foreach ($group['items'] as $item) {
if ($item['id'] == $current) {
$item['active'] = true;
$currentResult[] = [
'text' => 'Selected',
'active' => true,
'children' => [$item],
];
$children[] = $item;
}
}
}
}
else if (!empty($current)) {
foreach ($icons as $groupKey => $group) {
foreach ($group['items'] as $item) {
if ($item['id'] == $current) {
$item['active'] = true;
$currentResult[] = [
'text' => 'Selected',
'active' => true,
'children' => [$item],
];
break(2);
}
}
}
}
return [
'items' => array_merge($currentResult, $result)
];
}
}
add_action('rest_api_init', function (): void {
\PAWS\Helper\Rest::RegisterRestRoute(
namespace: '<your namespace>',
endpoint: '/fontawesome/',
args: [
'methods' => 'GET',
'callback' => ['\\' . __NAMESPACE__ . '\\Fontawesome', 'Search'],
'permission_callback' => '__return_true',
],
ignoreLanguage: true,
);
});
The endpoint searches trough all the icons for matches and returns them. Also if there is a previously selected icon it will be display first and also marked active.
Now we have done the backend part of the FontAwesome selector. Let’s add some javascript so the endpoint is actually used :-)
Step 3 - the <script>
First, we need to do the initial search when the icon picker field is display:
import { nossIconpicker } from './acfActions/iconPicker';
if (typeof acf !== 'undefined') {
acf.addAction('new_field/type=icon_picker', nossIconpicker);
}
Once displayed, we need to watch if the user searches for an icon or clicks one:
import { handleFontawesomeSearch, handleFontawesomeSelect} from './eventHandlers/handleFontawesome';
window.addEventListener(`DOMContentLoaded`, () => {
document.addEventListener(`input`, handleFontawesomeSearch);
document.addEventListener(`click`, handleFontawesomeSelect);
});
The code for the initial ACF-action is located in the file /acfActions/iconPicker.js:
export const nossIconpicker = (field) => {
let target = field.$el.find('.acf-fontawesome-search-input')[0];
let endpoint = target.closest('[data-endpoint]').dataset.endpoint + '?q=' + target.value;
if (target.closest('.acf-icon-picker').querySelector('[data-hidden-type="type"]').value == 'fontawesome') {
endpoint += '¤t=' + target.closest('.acf-icon-picker').querySelector('[data-hidden-type="value"]').value;
}
let resultContainer = target.closest('.acf-icon-picker-fontawesome-tabs').querySelector('.acf-fontawesome-list');
fetch(endpoint, {
method: 'GET',
})
.then(function (response) {
return response.json();
})
.then(function (data) {
resultContainer.innerHTML = '<span class="loading">Loading</span>';
if (data.items.length === 0) {
resultContainer.innerHTML = '<span class="notfound">No results found</span>';
}
else {
let tmpHTML = '';
for (let groups = 0; groups < data.items.length; groups++) {
tmpHTML += '<label>' + data.items[groups].text + '</label>';
let iconList = '';
for (let icons = 0; icons < data.items[groups].children.length; icons++) {
iconList += '<img class="acf-fontawesome-icon';
if (data.items[groups].children[icons].active) {
iconList += ' active';
}
iconList += '" src="data:image/svg+xml;base64, ' + data.items[groups].children[icons].icon + '" title="' + data.items[groups].children[icons].text + '" data-value="' + data.items[groups].children[icons].id + '" />';
}
tmpHTML += '<div class="results">' + iconList + '</div>';
if (data.items[groups].active) {
tmpHTML = '<div class="selected">' + tmpHTML + '</div>';
}
}
resultContainer.innerHTML = tmpHTML;
}
});
};
In this file, we send the current value to our endpoint, receive the result and display it.
There are two more functions needed for the icon picker to work. First, almost the same function again to search while the user types and the second function handles the user clicks and sets the correct values for the icon picker field. Here is the file /eventHandlers/handleFontawesome.js:
export function handleFontawesomeSearch(e) {
let target = e.target;
if (!target.classList.contains('acf-fontawesome-search-input')) {
return;
}
e.preventDefault();
clearTimeout(window.handleFontawesomeTimer);
window.handleFontawesomeTimer = setTimeout(() => {
let endpoint = target.closest('[data-endpoint]').dataset.endpoint + '?q=' + target.value;
if (target.closest('.acf-icon-picker').querySelector('[data-hidden-type="type"]').value == 'fontawesome') {
endpoint += '¤t=' + target.closest('.acf-icon-picker').querySelector('[data-hidden-type="value"]').value;
}
let resultContainer = target.closest('.acf-icon-picker-fontawesome-tabs').querySelector('.acf-fontawesome-list');
console.log(resultContainer);
fetch(endpoint, {
method: 'GET',
})
.then(function (response) {
return response.json();
})
.then(function (data) {
resultContainer.innerHTML = '<span class="loading">Loading</span>';
if (data.items.length === 0) {
resultContainer.innerHTML = '<span class="notfound">No results found</span>';
}
else {
let tmpHTML = '';
for (let groups = 0; groups < data.items.length; groups++) {
tmpHTML += '<label>' + data.items[groups].text + '</label>';
let iconList = '';
for (let icons = 0; icons < data.items[groups].children.length; icons++) {
iconList += '<img class="acf-fontawesome-icon'
if (data.items[groups].children[icons].active) {
iconList += ' active';
}
iconList += '" src="data:image/svg+xml;base64, ' + data.items[groups].children[icons].icon + '" title="' + data.items[groups].children[icons].text + '" data-value="' + data.items[groups].children[icons].id + '" />';
}
tmpHTML += '<div class="results">' + iconList + '</div>';
if (data.items[groups].active) {
tmpHTML = '<div class="selected">' + tmpHTML + '</div>';
}
}
resultContainer.innerHTML = tmpHTML;
}
});
}, 250)
}
export function handleFontawesomeSelect(e) {
let target = e.target;
if (!target.classList.contains('acf-fontawesome-icon')) {
return;
}
e.preventDefault();
let activeIcons = target.closest('.acf-icon-picker-fontawesome-tabs').querySelectorAll('.acf-fontawesome-icon.active');
if (activeIcons.length > 0) {
for (let icons = 0; icons < activeIcons.length; icons++) {
activeIcons[icons].classList.remove('active');
}
}
target.closest('.acf-icon-picker').querySelector('[data-hidden-type="type"]').value = 'fontawesome';
target.closest('.acf-icon-picker').querySelector('[data-hidden-type="value"]').value = target.dataset.value;
target.classList.add('active');
}
The last thing to do? Add some stylings!
Step 4 - Have some Styles
The code has some styles as you’ve seen them in the screenshot above.
Note: It’s not pure css, it’s SCSS below :)
.acf-icon-picker-fontawesome-tabs {
.acf-fontawesome-search-wrap {
position: relative;
z-index: 1;
&:after {
position: absolute;
content: '';
top: 50%;
left: 8px;
width: 16px;
height: 16px;
transform: translateY(-50%);
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path class="fa-secondary" opacity=".4" d="M208 64a144 144 0 1 1 0 288 144 144 0 1 1 0-288zm0 352A208 208 0 1 0 208 0a208 208 0 1 0 0 416z"/><path class="fa-primary" d="M330.7 376L457.4 502.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L376 330.7C363.3 348 348 363.3 330.7 376z"/></svg>');
background-size: contain;
}
.acf-fontawesome-search-input {
padding-left: 30px;
border-radius: 0;
}
}
.acf-fontawesome-list {
height: 135px;
overflow: hidden;
overflow-y: auto;
margin-bottom: 0;
background-color: #f9f9f9;
border: 1px solid #8c8f94;
border-top: none;
border-radius: 0 0 6px 6px;
padding: 8px;
.notfound {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
font-style: italic;
}
.loading {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
font-style: italic;
&:after {
content: '.';
animation: loading 2s linear infinite;
}
@keyframes loading {
0% { content: '.' }
33% { content: '..' }
66% { content: '...'}
}
}
label {
display: block;
font-weight: bold;
padding: 4px;
border-bottom: 1px solid #aaa;
margin-bottom: 4px;
}
.results {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: start;
img {
padding: 4px;
width: 28px;
height: 28px;
object-fit: contain;
border: 1px solid transparent;
transition: $transition-base;
border-radius: 4px;
&:hover {
border: 1px solid #aaa;
background-color: #fff;
}
&.active {
border: 1px solid #198754;
background-color: rgba(#198754, 0.15);
}
}
}
.selected {
display: flex;
border-bottom: 1px solid #aaa;
margin-bottom: 4px;
padding: 4px;
justify-content: space-between;
label {
padding: 0;
border-bottom: 0;
margin-bottom: 0;
align-content: center;
}
}
}
}
Now everything should work and you should have your own FontAwesome icon picker directly integrated into the ACF icon picker.
Step 5 - The Result
To properly handle the result, you have to set the return format to “Array”. By doing this you can extract the fontawesome infos like that:
$icon = get_field(‘<your icon picker fieldname>’);
if (is_array($icon)) {
if ($icon[‘type’] == ‘fontawesome’) {
var_dump($icon[‘value’]);
}
}
The result display should be something like “light|cat.svg”. The only thing you’ve to do now is get the correct svg-image from the FontAwesome Folder and display it either as an img-tag or directly as inline-svg. Have fun!
The End!
If you have any questions, feel free to write an email to christian@nosidestory.ch - please add some useful subject because if you just write “I need help” the mail might get lost in the cativerse :D
2024-05-29 / Christian Stampfli