<?php
|
namespace JVBase\managers;
|
|
use JVBase\utility\Features;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class IconsManager
|
{
|
protected static ?IconsManager $instance = null;
|
protected CacheManager $cache;
|
protected string $style = 'regular';
|
protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin'];
|
// Custom icons registered via filter
|
protected array $customIcons = [];
|
protected array $usedIcons = [];
|
protected array $map = [];
|
|
/**
|
* Get singleton instance
|
*/
|
public static function getInstance(): IconsManager
|
{
|
if (self::$instance === null) {
|
self::$instance = new self();
|
}
|
return self::$instance;
|
}
|
private function __construct()
|
{
|
$this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
|
$this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
|
? JVB_SITE['icons']
|
: 'regular';
|
|
$this->addMap();
|
|
// Allow custom icon registration
|
$this->customIcons = apply_filters('jvbRegisterCustomIcons', [
|
'syncing' => JVB_DIR .'/assets/icons/cloud-sync-thin.svg',
|
'alphabetical' => JVB_DIR.'/assets/icons/alphabetical.svg'
|
]);
|
|
|
$this->usedIcons = get_option(BASE.'used_icons', []);
|
$this->includeIcons();
|
// Register hooks only once
|
$this->registerHooks();
|
}
|
|
/**
|
* Include icons via filter (for JS usage, etc.)
|
*/
|
protected function includeIcons():array
|
{
|
$icons = get_option(BASE.'includeIcons');
|
// $icons = false;
|
if (!$icons) {
|
$icons = $this->addIncludeIcons();
|
}
|
$include = apply_filters('jvbIncludeIcons', []);
|
$add = array_filter($include, function($addIt) use ($icons) {
|
return !in_array($addIt, $icons);
|
});
|
error_log('Adding icons: '.print_r($add, true));
|
if (!empty($add)) {
|
$this->addIncludeIcons($add);
|
}
|
|
return $icons;
|
}
|
|
protected function addIncludeIcons(array $add = []) {
|
$icons = get_option(BASE.'includeIcons');
|
if (!$icons) {
|
$icons = [
|
'check-circle',
|
'close-circle',
|
'cloud-slash',
|
'exclamation-mark',
|
'cloud-arrow-down',
|
'cloud-arrow-up',
|
'cloud-check',
|
'cloud-slash',
|
'cloud-warning',
|
'syncing',
|
'cloud-x',
|
'arrows-clockwise',
|
'share-fat',
|
'trash',
|
'star',
|
['name' => 'star-half', 'style' => 'fill'],
|
['name' => 'star', 'style' => 'fill'],
|
//FORMATTING
|
'copy',
|
'paragraph',
|
'text-h-one',
|
'text-h-two',
|
'text-h-three',
|
'text-h-four',
|
'text-h-five',
|
'text-h-six',
|
['name' =>'text-b', 'style' => 'fill'],
|
'text-italic',
|
'text-underline',
|
'text-strikethrough',
|
'list-dashes',
|
'list-numbers',
|
'text-align-left',
|
'text-align-center',
|
'text-align-right',
|
// 'text-align-justify',
|
'link',
|
//FILE ICONS
|
'file-pdf',
|
'file-csv',
|
'file-doc',
|
'file-txt',
|
'file-xls',
|
];
|
$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
|
foreach ($check as $constant) {
|
foreach ($constant as $key => $value) {
|
if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) {
|
$icons[] = $value['icon'];
|
}
|
}
|
}
|
$icons = apply_filters('jvbIncludeIcons', $icons);
|
|
update_option(BASE.'includeIcons', $icons);
|
update_option(BASE.'icons_needs_update', true);
|
}
|
$add = apply_filters('jvbIncludeIcons', $add);
|
$add = array_filter($add, function($addIt) use ($icons) {
|
return !in_array($addIt, $icons);
|
});
|
if (!empty($add)) {
|
$icons = array_merge($add, $icons);
|
update_option(BASE.'usedIcons', $icons);
|
update_option(BASE.'icons_needs_update', true);
|
}
|
return $icons;
|
}
|
|
protected function addMap():void
|
{
|
$map = get_option(BASE.'iconMap');
|
if (!$map) {
|
$map = [];
|
if (Features::forSite()->has('referrals')){
|
$map['referrals'] = 'hand-heart';
|
}
|
if (Features::forSite()->has('dashboard')){
|
$map['dash'] = 'door';
|
}
|
if (Features::forSite()->has('magicLink')){
|
$map['magicLink'] = 'magic-wand';
|
}
|
if (Features::hasAnyIntegration()) {
|
$map['integrations'] = 'plugs-connected';
|
}
|
update_option(BASE.'iconMap', $map);
|
}
|
|
$this->map = apply_filters('jvbMapIcons', $map);
|
}
|
|
/**
|
* Register WordPress hooks
|
*/
|
protected function registerHooks(): void
|
{
|
add_action('init', [$this, 'checkCSS']);
|
add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
|
}
|
|
public function checkCSS():void
|
{
|
// update_option(BASE.'icons_needs_update', true);
|
if (get_option(BASE.'icons_needs_update', false)) {
|
error_log('Regenerating CSS');
|
delete_option(BASE.'icons_needs_update');
|
$this->regenerateCSS();
|
}
|
}
|
|
protected function regenerateCSS(): void
|
{
|
error_log('[IconsManager]:regenerateCSS');
|
$css = $this->generateIconCSS();
|
$css_path = JVB_DIR.'/assets/css/icons.css';
|
|
if (file_put_contents($css_path, $css) !== false) {
|
CacheManager::updateTimestamp('icons');
|
} else {
|
error_log('[IconsManager]Could not write css.');
|
}
|
}
|
|
/**
|
* Prevent cloning
|
*/
|
private function __clone() {}
|
|
/**
|
* Prevent unserialization
|
*/
|
public function __wakeup()
|
{
|
throw new \Exception("Cannot unserialize singleton");
|
}
|
|
/**
|
* Get an icon element
|
*
|
* @param string $name Icon name (e.g., 'heart', 'calendar')
|
* @param array $options Options array:
|
* - 'style' => 'regular'|'bold'|'fill'|etc.
|
* - 'label' => 'Accessible label' (for standalone icons)
|
* - 'decorative' => true (for icons next to text)
|
* - 'class' => 'additional classes'
|
* - 'size' => 24 (for custom sizing via inline style)
|
* @return string HTML icon element
|
*/
|
public function getIcon(string $name, array $options = []): string
|
{
|
if (!array_key_exists('style', $options)) {
|
$options['style'] = $this->style;
|
}
|
$name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
|
|
// Validate icon exists
|
if (!$this->iconExists($name, $options['style'])) {
|
error_log('[IconsManager] Icon not found: ' . $name);
|
return '';
|
}
|
|
$style = $options['style'] ?? $this->style;
|
|
// Track icon usage
|
$this->trackIconUsage($name, $style);
|
|
// Build classes
|
$classes = ['icon', 'icon-' . $name];
|
if (!empty($options['class'])) {
|
$classes[] = $options['class'];
|
}
|
|
|
$attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"'];
|
$attrs[] = 'aria-hidden="true"';
|
|
|
|
return '<i ' . implode(' ', $attrs) . '></i>';
|
}
|
|
/**
|
* Track icon usage for CSS generation
|
*/
|
protected function trackIconUsage(string $name, string $style): void
|
{
|
if (!array_key_exists($style, $this->usedIcons)) {
|
$this->usedIcons[$style] = [];
|
}
|
|
if (!in_array($name, $this->usedIcons[$style])) {
|
$this->usedIcons[$style][] = $name;
|
update_option(BASE.'used_icons', $this->usedIcons);
|
// Flag for regeneration on next init
|
update_option(BASE.'icons_needs_update', true);
|
|
// Clear cache
|
$this->cache->delete('icon_styles_css');
|
}
|
}
|
|
/**
|
* Check if icon file exists
|
*/
|
protected function iconExists(string $name, ?string $style = null): bool
|
{
|
if (!$style) {
|
$style = $this->style;
|
}
|
// Check custom icons first
|
if (array_key_exists($name, $this->customIcons)) {
|
return file_exists($this->customIcons[$name]);
|
}
|
|
// Check standard icons
|
$filepath = $this->buildFilePath($name, $style);
|
return file_exists($filepath);
|
}
|
|
/**
|
* Build file path for icon
|
*/
|
protected function buildFilePath(string $name, ?string $style = null): string
|
{
|
if (!$style) {
|
$style = $this->style;
|
}
|
// Custom icons (absolute path provided)
|
if (array_key_exists($name, $this->customIcons)) {
|
return $this->customIcons[$name];
|
}
|
|
// Standard SVG icons in /assets/icons/
|
if (str_ends_with($name, '.svg')) {
|
return JVB_DIR . '/assets/icons/' . $name;
|
}
|
$name = ($style === 'regular') ? $name : $name . '-' . $style;
|
|
// Phosphor icons with style variants
|
return JVB_DIR . '/assets/phosphor-icons/' . $style . '/' . $name . '.svg';
|
}
|
|
/**
|
* Get raw SVG content for CSS mask-image
|
*/
|
protected function getRawSvg(string $name, ?string $style = null): ?string
|
{
|
if (!$style) {
|
$style = $this->style;
|
}
|
$filepath = $this->buildFilePath($name, $style);
|
|
if (!file_exists($filepath)) {
|
return null;
|
}
|
|
$svg = file_get_contents($filepath);
|
if ($svg === false) {
|
return null;
|
}
|
|
// Clean up SVG for CSS usage
|
$svg = preg_replace("/([\n\t]+)/", ' ', $svg);
|
$svg = preg_replace('/>\s*</', '><', $svg);
|
$svg = trim($svg);
|
|
return $svg;
|
}
|
|
|
/**
|
* Enqueue icon styles via REST endpoint
|
*/
|
public function enqueueIconStyles(): void
|
{
|
$timestamp = CacheManager::getTimestamp('icons');
|
|
wp_enqueue_style(
|
'jvb-icons',
|
JVB_URL.'assets/css/icons.css',
|
[],
|
$timestamp
|
);
|
}
|
|
/**
|
* Generate CSS from icon list
|
*/
|
protected function generateIconCSS(): string
|
{
|
$css = '';
|
foreach ($this->usedIcons as $style => $icons) {
|
$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
|
foreach ($icons as $icon) {
|
|
$svg = $this->getEncodedSVG($icon, $style);
|
if ($svg !== '') {
|
$css .= ".icon-{$icon}{$styleClass}{";
|
$css .= "--icon:url('data:image/svg+xml;base64,{$svg}');";
|
$css .= "}";
|
}
|
}
|
}
|
return $this->minifyCss($css);
|
}
|
|
protected function minifyCSS(string $css): string
|
{
|
// Remove comments
|
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
// Remove whitespace
|
$css = preg_replace('/\s+/', ' ', $css);
|
// Remove spaces around specific characters
|
$css = preg_replace('/\s*([:;{}])\s*/', '$1', $css);
|
|
return trim($css);
|
}
|
public function getCSSIcon(string $icon, ?string $style=null):string
|
{
|
if (!$style) {
|
$style = $this->style;
|
}
|
$svg = $this->getEncodedSVG($icon, $style);
|
if ($svg !== '') {
|
return "data:image/svg+xml;base64,{$svg}";
|
}
|
return '';
|
}
|
public function getEncodedSVG(string $icon, ?string $style = null):string
|
{
|
if (!$style) {
|
$style = $this->style;
|
}
|
return $this->cache->remember($style.$icon,
|
function () use ($icon, $style) {
|
$svg = $this->getRawSvg($icon, $style);
|
if ($svg) {
|
return base64_encode($svg);
|
}
|
return '';
|
});
|
|
}
|
|
/**
|
* Clear icon cache (useful for development/debugging)
|
*/
|
public function clearIconCache(): void
|
{
|
delete_option(BASE . 'icon_usage_list'); // Clear DB option
|
$this->cache->delete('icon_styles_css');
|
CacheManager::updateTimestamp('icons');
|
}
|
}
|