diff --git a/README.md b/README.md index cc2699a..77a19f5 100755 --- a/README.md +++ b/README.md @@ -3,11 +3,7 @@ # ACF SVG Icon Picker Field -Forked from [houke/acf-icon-picker](https://github.com/houke/acf-icon-picker) (which is no longer in active dev), updated to work with ACF v6.3 and above. - -## Description - -Add the svg icons you want to be available in your theme to an acf folder inside an img folder in your theme. The field returns the name of the svg. +Add an ACF field to your theme that lets users easily select SVG icons from a specified folder. The field returns the SVG's name. ## Compatibility @@ -18,7 +14,7 @@ This ACF field type is compatible with: ## Screenshots -![SVG Icon Picker](/screenshots/example.png) +![SVG Icon Picker Popup](/screenshots/example-popup.jpg) ## Installation @@ -27,10 +23,10 @@ Run `composer require smithfield-studio/acf-svg-icon-picker` and activate the pl ### Manually 1. Copy the `acf-svg-icon-picker` folder into your `wp-content/plugins` folder -2. Activate the Icon Selector plugin via the plugins admin page -3. Create a new field via ACF and select the Icon Selector type +2. Activate the plugin +3. Create a new ACF field and select the SVG Icon Picker type -## Switch from ACF Icon Picker to ACF SVG Icon Picker +## Switch from the legacy 'ACF Icon Picker' to 'ACF SVG Icon Picker' If you're coming from the original ACF Icon Picker plugin, you can switch to this plugin by following these steps: 1. Deactivate the old *ACF Icon Picker plugin* @@ -99,6 +95,9 @@ $fields->addField('my_icon', 'svg_icon_picker', [ ]) ``` +## Originally Forked from [houke/acf-icon-picker](https://github.com/houke/acf-icon-picker) +Updated to work with ACF v6.3 and above. + ## Changelog * 3.1.0 - Changed name of field to `svg_icon_picker` to avoid conflicts with vanilla ACF Icon Picker field. diff --git a/assets/css/input.css b/assets/css/input.css index d448baf..5f4b3de 100755 --- a/assets/css/input.css +++ b/assets/css/input.css @@ -1,134 +1,148 @@ -.acf-svg-icon-picker__popup * { - box-sizing: border-box; -} - -.acf-svg-icon-picker__svg, -.acf-svg-icon-picker__popup-svg { - position: relative; - width: 50px; - height: 50px; -} - -.acf-svg-icon-picker__popup-svg { - margin: 0 auto; -} - -.acf-svg-icon-picker__popup-svg img, -.acf-svg-icon-picker__svg img { - position: absolute; - top: 50%; - left: 50%; - display: block; - max-width: 50px; - max-height: 50px; - transform: translate(-50%, -50%); +:root { + --acfsip-border-radius: 4px; + --acfsip-spacing: 10px; } -.acf-svg-icon-picker__img { - width: 60px; - cursor: pointer; +/* Icon Selector Button */ +.acf-svg-icon-picker__selector { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + border-radius: 50px; + background-color: #eee; + width: 50px; + height: 50px; + color: #aaa; + font-weight: 300; + font-size: 20px; + user-select: none; } -.acf-svg-icon-picker__popup { - overflow: auto; - width: 500px; - height: 400px; - padding: 20px; - background-color: #fff; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); +.acf-svg-icon-picker__selector:hover { + background-color: #ddd; } -.acf-svg-icon-picker__popup__title { - margin: 0 0 10px; - float: left; +.acf-svg-icon-picker__icon { + padding: 15px; + width: 100%; + text-align: center; } -.acf-svg-icon-picker__popup__close { - display: inline-block; - margin: 0 0 10px; - float: right; +.acf-svg-icon-picker__icon img { + width: 100%; + height: 100%; } -.acf-svg-icon-picker__popup ul { - position: relative; - margin: 0; +.acf-svg-icon-picker__remove { + display: none; + appearance: none; + cursor: pointer; + margin-top: 5px; + border: 1px solid #e1e1e1; + border-radius: var(--acfsip-border-radius); + background-color: #f4f4f4; + padding: 4px; + font-size: 11px; + line-height: 1; } -.acf-svg-icon-picker__popup ul::before, -.acf-svg-icon-picker__popup ul::after { - display: table; - width: 100%; - content: ""; +.acf-svg-icon-picker__remove:hover { + background-color: indianred; + color: white; } -.acf-svg-icon-picker__popup ul li { - position: absolute; - width: 25%; - padding: 10px; - cursor: pointer; - float: left; +.acf-svg-icon-picker__remove--active { + display: inline-block; } -.acf-svg-icon-picker__popup ul li:nth-child(4n+1) { - clear: left; +/* Popup */ +.acf-svg-icon-picker__popup * { + box-sizing: border-box; + margin: 0; } -.acf-svg-icon-picker__popup ul li:hover { - background-color: #eee; +.acf-svg-icon-picker__popup-overlay { + display: grid; + position: fixed; + place-items: center; + z-index: 99999; + inset: 0; + background-color: rgba(0, 0, 0, 0.8); } -.acf-svg-icon-picker__popup ul li span { - display: block; - margin: 10px auto 0; - color: #222; - font-size: 12px; - text-align: center; - text-transform: capitalize; +.acf-svg-icon-picker__popup { + display: flex; + flex-direction: column; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + border-radius: var(--acfsip-border-radius); + background-color: #fff; + width: min(500px, 98vw); + height: 400px; + overflow: auto; + overscroll-behavior: contain; +} + +.acf-svg-icon-picker__popup-header { + display: flex; + position: sticky; + top: 0; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: var(--acfsip-spacing); + z-index: 1; + background-color: #f4f4f4; + padding: 20px; + line-height: 1; +} + +.acf-svg-icon-picker__popup-close { + padding: 0; + border: none; + background-color: transparent; } -.acf-svg-icon-picker__popup-holder { - position: fixed; - z-index: 99999; - inset: 0; - display: grid; - background-color: rgba(0, 0, 0, 0.8); - place-items: center; +.acf-svg-icon-picker__popup ul { + display: grid; + position: relative; + grid-template-columns: repeat(var(--acfsip-columns, 4), calc(25% - var(--acfsip-spacing))); + gap: var(--acfsip-spacing); + padding: 20px; } -.acf-svg-icon-picker__svg--span { - display: inline-block; - width: 50px; - height: 50px; - border-radius: 50px; - background-color: #eee; - color: #aaa; - font-size: 20px; - font-weight: 300; - line-height: 50px; - text-align: center; +.acf-svg-icon-picker__popup ul li { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + border-radius: var(--acfsip-border-radius); + padding: var(--acfsip-spacing); } -.acf-svg-icon-picker__svg--span:hover { - background-color: #ddd; +.acf-svg-icon-picker__popup ul li:hover { + background-color: #eee; } -.acf-svg-icon-picker__remove { - display: none; - padding: 2px; - border: 1px solid #e1e1e1; - margin-top: 5px; - background-color: #f4f4f4; - cursor: pointer; - font-size: 11px; - line-height: 1; +.acf-svg-icon-picker__popup ul li img { + max-height: 50px; + max-width: 100%; } -.acf-svg-icon-picker__remove--active { - display: inline-block; +.acf-svg-icon-picker__popup ul li span { + display: block; + margin: 10px auto 0; + color: #222; + font-size: 12px; + line-height: 1.4; + text-align: center; + hyphens: auto; + text-transform: capitalize; } .acf-svg-icon-picker__filter { - width: 100%; - padding: 12px; - border-radius: 4px; -} \ No newline at end of file + border-radius: var(--acfsip-border-radius); + padding: 12px; + width: 100%; +} diff --git a/assets/js/input.js b/assets/js/input.js index fa3e9a9..78109cb 100755 --- a/assets/js/input.js +++ b/assets/js/input.js @@ -1,207 +1,46 @@ (function ($) { - var active_item; - var item_width = 125; - var item_height = 116 + 6; - var recycled_items = []; - - jQuery(document).on('click', 'li[data-svg]', function () { - var val = jQuery(this).attr('data-svg'); - active_item.find('input').val(val); - active_item.find('.acf-svg-icon-picker__svg').html( - '' - ); - jQuery('.acf-svg-icon-picker__popup-holder').trigger('close'); - jQuery('.acf-svg-icon-picker__popup-holder').remove(); - jQuery('.acf-svg-icon-picker__img input').trigger('change'); - - active_item - .parents('.acf-svg-icon-picker') - .find('.acf-svg-icon-picker__remove') - .addClass('acf-svg-icon-picker__remove--active'); - }); + let active_item; + let isOpen = false; function initialize_field($el) { - $el.find('.acf-svg-icon-picker__img').on('click', function (e) { + $el.find('.acf-svg-icon-picker__selector').on('click', function (e) { e.preventDefault(); - var is_open = true; active_item = $(this); - // Remove any existing popups to prevent duplicates - jQuery(".acf-svg-icon-picker__popup-holder").remove(); - - if (acfSvgIconPicker.svgs.length == 0) { - var list = '

' + acfSvgIconPicker.no_icons_msg + '

'; - } else { - var list = ``; - } - - jQuery('body').append( - `
-
- close -

ACF Icon Picker - Choose icon

- - ${list} -
-
` - ); - - jQuery('.acf-svg-icon-picker__popup-holder').on('close', function () { - is_open = false; - }); - - var $list = $('#icons-list'); - var margin = 200; // number of px to show above and below. - var columns = 4; - var svgs = acfSvgIconPicker.svgs; - - function setListHeight() { - var total_lines = Math.ceil(svgs.length / columns); - $list.height(total_lines * item_height); + if (isOpen) { + return; } - function removeAllItems() { - $('[data-acf-icon-index]').each(function (i, el) { - var $el = $(el); - recycled_items.push($el); - $el.remove(); - }); - } - - function render() { - if (!is_open) return; - - var scroll_top = $('.acf-svg-icon-picker__popup').scrollTop(); - var scroll_min = scroll_top - item_height - margin; - var scroll_max = scroll_top + $('.acf-svg-icon-picker__popup').height() + margin; - // Get the index of the first and last element from array we will show. - var index_min = Math.ceil(scroll_min / item_height) * columns; - var index_max = Math.ceil(scroll_max / item_height) * columns; - - // remove unneeded items and add them to recycled items. - $('[data-acf-icon-index]').each(function (i, el) { - var $el = $(el); - var index = $el.attr('data-acf-icon-index'); - var name = $el.attr('data-svg'); - // Check if we have the element in the resulting array. - var elementExist = function () { - return svgs.find(function (svg) { - return svg.name === name; - }); - } - - if (index < index_min || index > index_max || !elementExist()) { - recycled_items.push($el); - $el.remove(); - } - }); - - for (var i = index_min; i < index_max; i++) { - if (i < 0 || i >= svgs.length) continue; - var svg = svgs[i]; - // Calculate the position of the item. - var y = Math.floor(i / columns) * item_height; - var x = i % columns * item_width; - - // If we already have the element visible we can continue - var $el = $(`[data-acf-icon-index="${i}"][data-svg="${svg.name}"]`); - // If item already exist we can skip. - if ($el.length) continue; - - if (recycled_items.length) { - // If there are recycled items reuse one. - $el = recycled_items.pop(); - } - else { - // Or create a new element. - $el = $(`
  • -
    - -
    - -
  • `); - } - - // We use attr instead of data since we want to use css selector. - $el.attr({ - 'data-svg': svg.name, - 'data-acf-icon-index': i - }).css({ - transform: `translate(${x}px, ${y}px)` - }); - const filename = svg['name'].split('.'); - const name = filename[0].replace(/[-_]/g, ' '); - $el.find('.icons-list__name').text(name); - $el.find('img').attr('src', `${acfSvgIconPicker.path}${svg['icon']}`); - $list.append($el); - } - - requestAnimationFrame(render); - } - if (svgs.length) { - setListHeight(); - render(); - } - - const iconsFilter = document.querySelector('#filterIcons'); + renderPopup(); - function filterIcons(wordToMatch) { - const normalizedWord = wordToMatch.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); - return acfSvgIconPicker.svgs.filter(icon => { - var name = icon.name.replace(/[-_]/g, ' ').normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); - const regex = new RegExp(normalizedWord, 'gi'); - return name.match(regex); - }); + if (acfSvgIconPicker.svgs.length > 0) { + renderIconsList(); } - function displayResults() { - svgs = filterIcons($(this).val()); - removeAllItems(); - setListHeight(); - } - - function debounce(func, wait) { - var timeout; - return function (...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(this, args), wait); - }; - } - - iconsFilter.focus(); - - iconsFilter.addEventListener('keyup', debounce(displayResults, 300)); + setupFilter(); // Closing - jQuery('.acf-svg-icon-picker__popup__close').on('click', function (e) { - e.stopPropagation(); - is_open = false; - jQuery('.acf-svg-icon-picker__popup-holder').remove(); - }); + document + .querySelector('.acf-svg-icon-picker__popup-close') + .addEventListener('click', function (e) { + document.querySelector('.acf-svg-icon-picker__popup-overlay').remove(); + isOpen = false; + }); }); // show the remove button if there is an icon selected - const $input = $el.find('input') + const $input = $el.find('input'); if ($input.length && $input.val().length != 0) { - $el - .find('.acf-svg-icon-picker__remove') - .addClass('acf-svg-icon-picker__remove--active'); + $el.find('.acf-svg-icon-picker__remove').addClass('acf-svg-icon-picker__remove--active'); } $el.find('.acf-svg-icon-picker__remove').on('click', function (e) { e.preventDefault(); - var parent = $(this).parents('.acf-svg-icon-picker'); + const parent = $(this).parents('.acf-svg-icon-picker'); parent.find('input').val(''); - parent - .find('.acf-svg-icon-picker__svg') - .html('+'); + parent.find('.acf-svg-icon-picker__icon').html('+'); - jQuery('.acf-svg-icon-picker__img input').trigger('change'); + jQuery('.acf-svg-icon-picker__selector input').trigger('change'); parent .find('.acf-svg-icon-picker__remove') @@ -209,6 +48,60 @@ }); } + function renderIconsList(svgs = acfSvgIconPicker.svgs) { + let popupContents = ''; + + if (acfSvgIconPicker.svgs.length === 0) { + popupContents = `

    ${acfSvgIconPicker.msgs.no_icons}

    `; + } else { + const iconsList = svgs + .map((svg) => { + const filename = svg['name'].split('.'); + const name = filename[0].replace(/[-_]/g, ' '); + const src = `${acfSvgIconPicker.path}${svg['icon']}`; + + return ` +
  • + ${name} + ${name} +
  • + `; + }) + .join(''); + + popupContents = ``; + } + + document.querySelector('.acf-svg-icon-picker__popup-contents').innerHTML = popupContents; + } + + function renderPopup() { + const popup = ` +
    +
    +
    +

    ${acfSvgIconPicker.msgs.title}

    + + +
    +
    + +
    +
    +
    + `; + + jQuery('body').append(popup); + isOpen = true; + + jQuery('.acf-svg-icon-picker__popup-overlay').on('close', function () { + jQuery('.acf-svg-icon-picker__popup-overlay').remove(); + isOpen = false; + }); + } + if (typeof acf.add_action !== 'undefined') { acf.add_action('ready append', function ($el) { acf.get_fields({ type: 'svg_icon_picker' }, $el).each(function () { @@ -217,12 +110,63 @@ }); } + function setupFilter() { + const iconsFilter = document.querySelector('#filterIcons'); + + function filterIcons(wordToMatch) { + const normalizedWord = wordToMatch + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); + return acfSvgIconPicker.svgs.filter((icon) => { + const name = icon.name + .replace(/[-_]/g, ' ') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); + const regex = new RegExp(normalizedWord, 'gi'); + return name.match(regex); + }); + } + + function displayResults() { + svgs = filterIcons($(this).val()); + renderIconsList(svgs); + } + + function debounce(func, wait) { + let timeout; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + + iconsFilter.focus(); + iconsFilter.addEventListener('keyup', debounce(displayResults, 300)); + } + + jQuery(document).on('click', 'li[data-svg]', function () { + const val = jQuery(this).attr('data-svg'); + const src = jQuery(this).find('img').attr('src'); + active_item.find('input').val(val); + active_item.find('.acf-svg-icon-picker__icon').html(``); + jQuery('.acf-svg-icon-picker__popup-overlay').trigger('close'); + jQuery('.acf-svg-icon-picker__popup-overlay').remove(); + jQuery('.acf-svg-icon-picker__selector input').trigger('change'); + + active_item + .parents('.acf-svg-icon-picker') + .find('.acf-svg-icon-picker__remove') + .addClass('acf-svg-icon-picker__remove--active'); + }); + // Use MutationObserver to detect changes in the DOM const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { $(mutation.addedNodes) - .find(".acf-svg-icon-picker") + .find('.acf-svg-icon-picker') .each(function () { initialize_field($(this)); }); diff --git a/class-acf-field-svg-icon-picker.php b/class-acf-field-svg-icon-picker.php index f7fdad5..1a7e99e 100755 --- a/class-acf-field-svg-icon-picker.php +++ b/class-acf-field-svg-icon-picker.php @@ -108,20 +108,16 @@ public function render_field( $field ) { ?>
    -
    -
    - ' - : '+'; - ?> -
    - +
    +
    + ' : '+'; ?> +
    +
    - - - +
    $this->url, - 'svgs' => $this->svgs, - /* translators: %s: path_suffix */ - 'no_icons_msg' => sprintf( esc_html__( 'To add icons, add your svg files in the /%s folder in your theme.', 'acf-svg-icon-picker' ), $this->path_suffix ), + 'path' => $this->url, + 'svgs' => $this->svgs, + 'columns' => 4, + 'msgs' => array( + 'title' => esc_html__( 'Select an icon', 'acf-svg-icon-picker' ), + 'close' => esc_html__( 'close', 'acf-svg-icon-picker' ), + 'filter' => esc_html__( 'Start typing to filter icons', 'acf-svg-icon-picker' ), + /* translators: %s: path_suffix */ + 'no_icons' => sprintf( esc_html__( 'To add icons, add your svg files in the /%s folder in your theme.', 'acf-svg-icon-picker' ), $this->path_suffix ), + ), ) ); diff --git a/screenshots/example-popup.jpg b/screenshots/example-popup.jpg new file mode 100644 index 0000000..943b204 Binary files /dev/null and b/screenshots/example-popup.jpg differ diff --git a/screenshots/example.png b/screenshots/example.png deleted file mode 100644 index 9da3366..0000000 Binary files a/screenshots/example.png and /dev/null differ