| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\utility\Features; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | |
| | | class IconsManager |
| | | { |
| | | protected static ?IconsManager $instance = null; |
| | | protected CacheManager $cache; |
| | | // Static array holding all source instances |
| | | protected static array $instances = []; |
| | | |
| | | // Static storage for all custom icons across sources |
| | | protected static array $customIconsRegistry = []; |
| | | |
| | | // Instance-specific properties |
| | | protected string $source; |
| | | protected array $icons = []; // Icons for THIS source [style => [names]] |
| | | protected Cache $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 $customIcons = []; // Custom icons for THIS source |
| | | protected array $map = []; |
| | | protected const MAX_VERSIONS = 5; |
| | | |
| | | /** |
| | | * Get singleton instance |
| | | * Factory method - get or create instance for a source |
| | | */ |
| | | public static function getInstance(): IconsManager |
| | | public static function for(string $source = 'icons'): IconsManager |
| | | { |
| | | if (self::$instance === null) { |
| | | self::$instance = new self(); |
| | | if (!isset(self::$instances[$source])) { |
| | | self::$instances[$source] = new self($source); |
| | | } |
| | | return self::$instance; |
| | | return self::$instances[$source]; |
| | | } |
| | | private function __construct() |
| | | |
| | | /** |
| | | * Constructor now takes source parameter |
| | | */ |
| | | private function __construct(string $source) |
| | | { |
| | | $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->source = $source; |
| | | $this->cache = Cache::for('icons_' . $source, WEEK_IN_SECONDS); |
| | | $this->style = Site::icon(); |
| | | |
| | | $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' |
| | | ]); |
| | | // Register custom icons only once for all sources |
| | | if ($source === 'icons') { |
| | | $this->registerCustomIcons(); |
| | | } |
| | | |
| | | // Load custom icons for THIS source |
| | | $this->loadCustomIconsForSource(); |
| | | |
| | | // Load stored icons for this source |
| | | $this->loadStoredIcons(); |
| | | |
| | | if (empty($this->icons)) { |
| | | $this->includeIcons(); |
| | | } |
| | | |
| | | // Register global hooks only once (first instance) |
| | | if (count(self::$instances) === 1) { |
| | | $this->registerGlobalHooks(); |
| | | } |
| | | |
| | | // Register instance's hooks (every instance) |
| | | $this->registerInstanceHooks(); |
| | | } |
| | | |
| | | |
| | | $this->usedIcons = get_option(BASE.'used_icons', []); |
| | | $this->includeIcons(); |
| | | // Register hooks only once |
| | | $this->registerHooks(); |
| | | |
| | | /** |
| | | * Register all custom icons (runs once) |
| | | */ |
| | | protected function registerCustomIcons(): void |
| | | { |
| | | $icons = array_merge(apply_filters('jvbRegisterCustomIcons', []), ['syncing' => JVB_DIR . '/assets/icons/cloud-sync-thin.svg', |
| | | 'alphabetical' => JVB_DIR . '/assets/icons/alphabetical.svg']); |
| | | |
| | | // Process and store in static property so all instances can access |
| | | self::$customIconsRegistry = $this->processCustomIconsArray($icons); |
| | | } |
| | | |
| | | /** |
| | | * Include icons via filter (for JS usage, etc.) |
| | | * Process custom icons array into source-grouped format |
| | | */ |
| | | protected function includeIcons():array |
| | | protected function processCustomIconsArray(array $icons): 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); |
| | | $out = []; |
| | | foreach ($icons as $name => $source) { |
| | | if (!file_exists($source)) { |
| | | error_log('[IconsManager] No file exists for custom Icon: '.$name); |
| | | continue; |
| | | } |
| | | $out[$name] = $source; |
| | | } |
| | | |
| | | return $icons; |
| | | return $out; |
| | | } |
| | | |
| | | protected function addIncludeIcons(array $add = []) { |
| | | $icons = get_option(BASE.'includeIcons'); |
| | | if (!$icons) { |
| | | $icons = [ |
| | | /** |
| | | * Load custom icons for this instance's source |
| | | */ |
| | | protected function loadCustomIconsForSource(): void |
| | | { |
| | | $this->customIcons = self::$customIconsRegistry; |
| | | // foreach ($this->customIcons as $name => $path) { |
| | | // if (!isset($this->icons[$this->style])) { |
| | | // $this->icons[$this->style] = []; |
| | | // } |
| | | // if (!in_array($name, $this->icons[$this->style])) { |
| | | // $this->icons[$this->style][] = $name; |
| | | // } |
| | | // } |
| | | } |
| | | |
| | | /** |
| | | * Load previously stored icons for this source |
| | | */ |
| | | protected function loadStoredIcons(): void |
| | | { |
| | | $allIcons = get_option(BASE.'usedIcons', []); |
| | | $storedIcons = $allIcons[$this->source] ?? []; |
| | | |
| | | // Merge stored icons with any existing icons (like custom icons) |
| | | foreach ($storedIcons as $style => $names) { |
| | | if (!isset($this->icons[$style])) { |
| | | $this->icons[$style] = []; |
| | | } |
| | | $this->icons[$style] = array_unique(array_merge($this->icons[$style], $names)); |
| | | } |
| | | } |
| | | |
| | | protected function includeIcons():void |
| | | { |
| | | $defaults = [ |
| | | 'icons' => [ |
| | | 'google-logo', |
| | | 'apple-logo', |
| | | 'check-circle', |
| | | 'close-circle', |
| | | 'faders-horizontal', |
| | | 'cloud-slash', |
| | | 'exclamation-mark', |
| | | 'cloud-arrow-down', |
| | | 'caret-down', |
| | | 'cloud-arrow-up', |
| | | 'cloud-check', |
| | | 'cloud-slash', |
| | |
| | | 'share-fat', |
| | | 'trash', |
| | | 'star', |
| | | 'alphabetical', |
| | | ['name' => 'star-half', 'style' => 'fill'], |
| | | ['name' => 'star', 'style' => 'fill'], |
| | | //FORMATTING |
| | | ], |
| | | 'forms' => [ |
| | | 'copy', |
| | | 'paragraph', |
| | | 'text-h-one', |
| | |
| | | '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); |
| | | ], |
| | | // 'dash' => [ |
| | | // |
| | | // ] |
| | | ]; |
| | | |
| | | update_option(BASE.'includeIcons', $icons); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | |
| | | // Add icons from content/taxonomy/user configs (like old behavior) |
| | | $configIcons = $this->getIconsFromConfigs(); |
| | | if (!empty($configIcons)) { |
| | | $defaults['icons'] = array_merge($defaults['icons'], $configIcons); |
| | | } |
| | | $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); |
| | | |
| | | // Allow filtering per source (extensibility) |
| | | $icons = apply_filters("jvbIncludeIcons_{$this->source}", $defaults[$this->source] ?? []); |
| | | |
| | | // Also allow filtering all sources at once |
| | | $allIcons = apply_filters('jvbIncludeIcons', $defaults); |
| | | if (isset($allIcons[$this->source])) { |
| | | $icons = array_merge($icons, $allIcons[$this->source]); |
| | | } |
| | | return $icons; |
| | | if (!empty($icons)) { |
| | | $this->include($icons); |
| | | } |
| | | } |
| | | |
| | | protected function addMap():void |
| | | /** |
| | | * Get icons from Registrar instances |
| | | * |
| | | */ |
| | | protected function getIconsFromConfigs(): array |
| | | { |
| | | $icons = []; |
| | | $registered = Registrar::getRegistered(); |
| | | |
| | | foreach ($registered as $type) { |
| | | $registrar = Registrar::getInstance($type); |
| | | $icons[] = $registrar->getIcon(); |
| | | } |
| | | |
| | | |
| | | return array_unique(array_filter($icons)); |
| | | } |
| | | |
| | | /** |
| | | * Public method to include icons in this source |
| | | */ |
| | | public function include(array $icons): self |
| | | { |
| | | $processed = $this->processIconArray($icons); |
| | | $changed = false; |
| | | |
| | | foreach ($processed as $style => $names) { |
| | | if (!isset($this->icons[$style])) { |
| | | $this->icons[$style] = []; |
| | | } |
| | | |
| | | foreach ($names as $name) { |
| | | // Skip if already in this source |
| | | if (in_array($name, $this->icons[$style])) { |
| | | continue; |
| | | } |
| | | |
| | | // Skip if already in main 'icons' source |
| | | if ($this->iconExistsInMainSource($name, $style)) { |
| | | error_log("[IconsManager] Skipping '{$name}' in '{$this->source}' - already in 'icons' source"); |
| | | continue; |
| | | } |
| | | |
| | | $this->icons[$style][] = $name; |
| | | $changed = true; |
| | | } |
| | | } |
| | | |
| | | // Only save if something actually changed |
| | | if ($changed) { |
| | | $this->saveIcons(); |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Process icon array into [style => [names]] format |
| | | */ |
| | | protected function processIconArray(array $icons): array |
| | | { |
| | | $out = []; |
| | | |
| | | foreach ($icons as $icon) { |
| | | if (is_array($icon) && isset($icon['style'])) { |
| | | $style = $icon['style']; |
| | | $name = $icon['name']; |
| | | } else { |
| | | $style = $this->style; |
| | | $name = is_array($icon) ? $icon['name'] : $icon; |
| | | } |
| | | |
| | | if (!isset($out[$style])) { |
| | | $out[$style] = []; |
| | | } |
| | | |
| | | if (!in_array($name, $out[$style])) { |
| | | $out[$style][] = $name; |
| | | } |
| | | } |
| | | |
| | | return $out; |
| | | } |
| | | |
| | | /** |
| | | * Save all icons across all instances |
| | | */ |
| | | protected function saveIcons(): void |
| | | { |
| | | $allIcons = []; |
| | | foreach (self::$instances as $source => $instance) { |
| | | $allIcons[$source] = $instance->icons; |
| | | } |
| | | |
| | | update_option(BASE.'usedIcons', $allIcons); |
| | | |
| | | // Track WHICH source needs updating |
| | | $needsUpdate = get_option(BASE.'icons_needs_update', []); |
| | | if (!is_array($needsUpdate)) { |
| | | $needsUpdate = []; |
| | | } |
| | | $needsUpdate[$this->source] = true; |
| | | update_option(BASE.'icons_needs_update', $needsUpdate); |
| | | } |
| | | |
| | | /** |
| | | * Check if icon exists in other sources |
| | | */ |
| | | protected function checkDuplicateAcrossInstances(string $name, string $style): void |
| | | { |
| | | $foundIn = []; |
| | | |
| | | foreach (self::$instances as $source => $instance) { |
| | | if (isset($instance->icons[$style]) && in_array($name, $instance->icons[$style])) { |
| | | $foundIn[] = $source; |
| | | } |
| | | } |
| | | |
| | | if (count($foundIn) > 1) { |
| | | error_log(sprintf( |
| | | '[IconsManager] Warning: Icon "%s" (%s) is registered in multiple sources: %s. Consider consolidating to avoid duplicate CSS output.', |
| | | $name, |
| | | $style, |
| | | implode(', ', $foundIn) |
| | | )); |
| | | } |
| | | } |
| | | |
| | | protected function addMap(): void |
| | | { |
| | | $map = get_option(BASE.'iconMap'); |
| | | if (!$map) { |
| | | $map = []; |
| | | if (Features::forSite()->has('referrals')){ |
| | | $map = [ |
| | | 'seo' => 'robot' |
| | | ]; |
| | | if (Site::has('referrals')) { |
| | | $map['referrals'] = 'hand-heart'; |
| | | } |
| | | if (Features::forSite()->has('dashboard')){ |
| | | if (Site::has('dashboard')) { |
| | | $map['dash'] = 'door'; |
| | | } |
| | | if (Features::forSite()->has('magicLink')){ |
| | | if (Site::has('magicLink')) { |
| | | $map['magicLink'] = 'magic-wand'; |
| | | } |
| | | if (Features::hasAnyIntegration()) { |
| | | if (Site::hasAnyIntegration()) { |
| | | $map['integrations'] = 'plugs-connected'; |
| | | } |
| | | update_option(BASE.'iconMap', $map); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Register WordPress hooks |
| | | * Register global hooks (only once) |
| | | */ |
| | | protected function registerHooks(): void |
| | | protected function registerGlobalHooks(): void |
| | | { |
| | | add_action('init', [$this, 'checkCSS']); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | add_action('wp_loaded', [self::class, 'checkCSS']); |
| | | add_action('shutdown', [self::class, 'checkCSS']); |
| | | } |
| | | |
| | | /** |
| | | * Register instance-specific hooks (every instance) |
| | | */ |
| | | protected function registerInstanceHooks(): void |
| | | { |
| | | // Register this source's stylesheet |
| | | add_action('init', [$this, 'registerStyle'], 11); |
| | | |
| | | // Auto-enqueue base icons on front-end |
| | | if ($this->source === 'icons') { |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | } |
| | | |
| | | // Auto-enqueue all in admin |
| | | add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | } |
| | | |
| | | public function checkCSS():void |
| | | public function enqueueIconStyles():void |
| | | { |
| | | // update_option(BASE.'icons_needs_update', true); |
| | | if (get_option(BASE.'icons_needs_update', false)) { |
| | | error_log('Regenerating CSS'); |
| | | // if (file_exists(JVB_CHILD_URL . "assets/css/{$this->source}.css")){ |
| | | wp_enqueue_style('jvb-icons-'.$this->source); |
| | | // } |
| | | |
| | | } |
| | | |
| | | public static function checkCSS(): void |
| | | { |
| | | $needsUpdate = get_option(BASE.'icons_needs_update', []); |
| | | if (!empty($needsUpdate)) { |
| | | error_log('Regenerating CSS for sources: ' . implode(', ', array_keys($needsUpdate))); |
| | | delete_option(BASE.'icons_needs_update'); |
| | | $this->regenerateCSS(); |
| | | self::regenerateAllCSS($needsUpdate); |
| | | } |
| | | } |
| | | |
| | | protected function regenerateCSS(): void |
| | | public static function regenerateAllCSS(array $sourcesToUpdate = []): void |
| | | { |
| | | error_log('[IconsManager]:regenerateCSS'); |
| | | $css = $this->generateIconCSS(); |
| | | $css_path = JVB_DIR.'/assets/css/icons.css'; |
| | | $css_dir = JVB_CHILD_DIR.'/assets/css/'; |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | CacheManager::updateTimestamp('icons'); |
| | | } else { |
| | | error_log('[IconsManager]Could not write css.'); |
| | | if (!file_exists($css_dir)) { |
| | | wp_mkdir_p($css_dir); |
| | | } |
| | | |
| | | // Load all icons from database option |
| | | $allIcons = get_option(BASE.'usedIcons', []); |
| | | |
| | | // If no specific sources provided, regenerate all |
| | | if (empty($sourcesToUpdate)) { |
| | | $sourcesToUpdate = array_fill_keys(array_keys($allIcons), true); |
| | | } |
| | | |
| | | // Generate CSS for each source that needs it |
| | | foreach ($sourcesToUpdate as $source => $needsUpdate) { |
| | | if (!$needsUpdate || !isset($allIcons[$source])) { |
| | | continue; |
| | | } |
| | | |
| | | // Get or create instance for this source |
| | | $instance = self::for($source); |
| | | |
| | | // Temporarily set icons from database |
| | | $originalIcons = $instance->icons; |
| | | $instance->icons = $allIcons[$source]; |
| | | |
| | | $css = $instance->generateIconCSS(); |
| | | $css_path = $css_dir . $source . '.css'; |
| | | |
| | | $instance->archiveCurrentVersion($css); |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | Cache::touch('icons_' . $source); |
| | | error_log("[IconsManager] Updated {$source}.css"); |
| | | } else { |
| | | error_log("[IconsManager] Could not write {$source}.css"); |
| | | } |
| | | |
| | | // Restore original icons |
| | | $instance->icons = $originalIcons; |
| | | } |
| | | } |
| | | |
| | | protected function regenerateCSS(array $sourcesToUpdate = []): void |
| | | { |
| | | error_log('[IconsManager]:regenerateCSS'); |
| | | $css_dir = JVB_CHILD_DIR.'/assets/css/'; |
| | | |
| | | if (!file_exists($css_dir)) { |
| | | wp_mkdir_p($css_dir); |
| | | } |
| | | |
| | | // If no specific sources provided, regenerate all |
| | | if (empty($sourcesToUpdate)) { |
| | | $sourcesToUpdate = array_fill_keys(array_keys(self::$instances), true); |
| | | } |
| | | |
| | | // Generate CSS only for sources that need it |
| | | foreach (self::$instances as $source => $instance) { |
| | | if (!isset($sourcesToUpdate[$source])) { |
| | | continue; // Skip this source |
| | | } |
| | | |
| | | $css = $instance->generateIconCSS(); |
| | | $css_path = $css_dir . $source . '.css'; |
| | | |
| | | // Archive current version before overwriting |
| | | $instance->archiveCurrentVersion($css); |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | Cache::touch('icons_' . $source); |
| | | error_log("[IconsManager] Updated {$source}.css"); |
| | | } else { |
| | | error_log("[IconsManager] Could not write {$source}.css"); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * - 'size' => 24 (for custom sizing via inline style) |
| | | * @return string HTML icon element |
| | | */ |
| | | public function getIcon(string $name, array $options = []): string |
| | | public function get(string $name, array $options = []): string |
| | | { |
| | | if (!array_key_exists('style', $options)) { |
| | | $options['style'] = $this->style; |
| | | if (empty($name)) { |
| | | //No icon requested |
| | | return ''; |
| | | } |
| | | $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name; |
| | | $style = $options['style'] ?? $this->style; |
| | | $name = $this->map[$name] ?? $name; |
| | | |
| | | // Validate icon exists |
| | | if (!$this->iconExists($name, $options['style'])) { |
| | | if (!$this->iconExists($name, $style)) { |
| | | error_log('[IconsManager] Icon not found: ' . $name); |
| | | return ''; |
| | | } |
| | | |
| | | $style = $options['style'] ?? $this->style; |
| | | // Track usage - only if not already tracked |
| | | if (!isset($this->icons[$style])) { |
| | | $this->icons[$style] = []; |
| | | } |
| | | |
| | | // Track icon usage |
| | | $this->trackIconUsage($name, $style); |
| | | if (!in_array($name, $this->icons[$style])) { |
| | | // Check if it's already in main source (for non-main sources) |
| | | if ($this->iconExistsInMainSource($name, $style)) { |
| | | // Don't add to this source, but still render the icon |
| | | // The CSS from icons.css will handle it |
| | | } else { |
| | | // Add to this source |
| | | $this->icons[$style][] = $name; |
| | | $this->checkDuplicateAcrossInstances($name, $style); |
| | | $this->saveIcons(); |
| | | } |
| | | } |
| | | |
| | | // Build classes |
| | | $classes = ['icon', 'icon-' . $name]; |
| | | if (!empty($options['class'])) { |
| | | // Build icon HTML (same as before) |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : ''; |
| | | $classes = ['icon', 'icon-' . $name.$styleClass]; |
| | | |
| | | if (isset($options['class'])) { |
| | | $classes[] = $options['class']; |
| | | } |
| | | |
| | | $attrs = ['class' => implode(' ', $classes)]; |
| | | |
| | | $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 (isset($options['label'])) { |
| | | $attrs['aria-label'] = esc_attr($options['label']); |
| | | $attrs['role'] = 'img'; |
| | | } elseif (isset($options['decorative']) && $options['decorative']) { |
| | | $attrs['aria-hidden'] = 'true'; |
| | | } |
| | | |
| | | 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'); |
| | | if (isset($options['size'])) { |
| | | $attrs['style'] = sprintf('--icon-size: %dpx;', absint($options['size'])); |
| | | } |
| | | |
| | | $attr_string = ''; |
| | | foreach ($attrs as $key => $value) { |
| | | $attr_string .= sprintf(' %s="%s"', $key, $value); |
| | | } |
| | | |
| | | return sprintf('<i%s></i>', $attr_string); |
| | | } |
| | | |
| | | /** |
| | |
| | | /** |
| | | * Get raw SVG content for CSS mask-image |
| | | */ |
| | | protected function getRawSvg(string $name, ?string $style = null): ?string |
| | | public function getRawSvg(string $name, ?string $style = null): ?string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | |
| | | // Clean up SVG for CSS usage |
| | | $svg = preg_replace("/([\n\t]+)/", ' ', $svg); |
| | | $svg = preg_replace('/>\s*</', '><', $svg); |
| | | $svg = trim($svg); |
| | | |
| | | return $svg; |
| | | return trim($svg); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Enqueue icon styles via REST endpoint |
| | | */ |
| | | public function enqueueIconStyles(): void |
| | | public function registerStyle(): void |
| | | { |
| | | $timestamp = CacheManager::getTimestamp('icons'); |
| | | $timestamp = Cache::lastModified('icons_' . $this->source); |
| | | $handle = 'jvb-icons-' . $this->source; |
| | | |
| | | wp_enqueue_style( |
| | | 'jvb-icons', |
| | | JVB_URL.'assets/css/icons.css', |
| | | wp_register_style( |
| | | $handle, |
| | | JVB_CHILD_URL . "assets/css/{$this->source}.css", |
| | | [], |
| | | $timestamp |
| | | ); |
| | |
| | | protected function generateIconCSS(): string |
| | | { |
| | | $css = ''; |
| | | foreach ($this->usedIcons as $style => $icons) { |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : ''; |
| | | foreach ($icons as $icon) { |
| | | |
| | | foreach ($this->icons as $style => $names) { |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : ''; |
| | | foreach ($names as $icon) { |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | if ($icon === 'caret-down') { |
| | | $css .= 'details summary::after,'; |
| | | } elseif ($icon === 'faders-horizontal') { |
| | | $css .= 'details.all-filters summary::after,'; |
| | | } elseif ($icon === 'link') { |
| | | $css .= 'input[type=url],'; |
| | | } elseif ($icon === apply_filters('jvbSeparatorLogo', 'logo')) { |
| | | $css .= 'hr.logo::before,'; |
| | | } |
| | | $css .= ".icon-{$icon}{$styleClass}{"; |
| | | $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');"; |
| | | $css .= "}"; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $this->minifyCss($css); |
| | | } |
| | | |
| | |
| | | |
| | | return trim($css); |
| | | } |
| | | public function getCSSIcon(string $icon, ?string $style=null):string |
| | | |
| | | public function getCSSIcon(string $icon, ?string $style = null): string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | |
| | | $icon = $this->map[$icon] ?? $icon; |
| | | |
| | | // Validate icon exists |
| | | if (!$this->iconExists($icon, $style)) { |
| | | error_log('[IconsManager] Icon not found: ' . $icon); |
| | | return ''; |
| | | } |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | return "data:image/svg+xml;base64,{$svg}"; |
| | | } |
| | | return ''; |
| | | } |
| | | public function getEncodedSVG(string $icon, ?string $style = null):string |
| | | |
| | | 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 ''; |
| | | }); |
| | | |
| | | function () use ($icon, $style) { |
| | | $svg = $this->getRawSvg($icon, $style); |
| | | if ($svg) { |
| | | return base64_encode($svg); |
| | | } |
| | | return ''; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function clearIconCache(): void |
| | | { |
| | | delete_option(BASE . 'icon_usage_list'); // Clear DB option |
| | | $this->cache->delete('icon_styles_css'); |
| | | CacheManager::updateTimestamp('icons'); |
| | | delete_option(BASE . 'icon_usage_list'); // Legacy |
| | | delete_option(BASE.'usedIcons'); |
| | | delete_option(BASE.'iconMap'); |
| | | |
| | | // Clear cache for all sources |
| | | foreach (self::$instances as $source => $instance) { |
| | | $instance->cache->forget('icon_styles_css'); |
| | | } |
| | | } |
| | | |
| | | protected function archiveCurrentVersion(string $css): void |
| | | { |
| | | $history = $this->getVersionHistory(); |
| | | |
| | | $icon_count = 0; |
| | | foreach ($this->icons as $style => $names) { |
| | | $icon_count += count($names); |
| | | } |
| | | |
| | | $newEntry = [ |
| | | 'css' => $css, |
| | | 'iconList' => $this->icons, |
| | | 'timestamp' => time(), |
| | | 'icon_count' => $icon_count, |
| | | 'size' => strlen($css), |
| | | 'size_formatted' => size_format(strlen($css), 2) |
| | | ]; |
| | | |
| | | array_unshift($history, $newEntry); |
| | | |
| | | if (count($history) > self::MAX_VERSIONS) { |
| | | $history = array_slice($history, 0, self::MAX_VERSIONS); |
| | | } |
| | | |
| | | update_option(BASE.'icon_css_history_' . $this->source, $history); |
| | | } |
| | | |
| | | public function getVersionHistory(): array |
| | | { |
| | | return get_option(BASE.'icon_css_history_' . $this->source, []); |
| | | } |
| | | |
| | | public function restoreVersion(int $timestamp): bool |
| | | { |
| | | $history = $this->getVersionHistory(); |
| | | |
| | | foreach ($history as $entry) { |
| | | if ($entry['timestamp'] === $timestamp) { |
| | | $css_path = JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css'; |
| | | |
| | | // Archive current before restoring |
| | | $current_css = file_get_contents($css_path); |
| | | if ($current_css !== false) { |
| | | $this->archiveCurrentVersion($current_css); |
| | | } |
| | | |
| | | // Restore the version |
| | | if (file_put_contents($css_path, $entry['css']) !== false) { |
| | | $this->icons = $entry['iconList']; |
| | | $this->saveIcons(); |
| | | Cache::touch('icons_' . $this->source); |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | error_log("[IconsManager] Version {$timestamp} not found in history for source {$this->source}"); |
| | | return false; |
| | | } |
| | | |
| | | public function forceRefresh(): void |
| | | { |
| | | $this->clearIconCache(); |
| | | $needsUpdate = get_option(BASE.'icons_needs_update', []); |
| | | if (!is_array($needsUpdate)) { |
| | | $needsUpdate = []; |
| | | } |
| | | $needsUpdate[$this->source] = true; |
| | | update_option(BASE.'icons_needs_update', $needsUpdate); |
| | | Cache::touch('icons_' . $this->source); |
| | | } |
| | | |
| | | public function mergeVersions(array $timestamps): bool |
| | | { |
| | | if (empty($timestamps)) { |
| | | return false; |
| | | } |
| | | |
| | | $history = get_option(BASE.'icon_css_history_' . $this->source, []); |
| | | $merged_icons = []; |
| | | |
| | | // Collect icons from selected versions |
| | | foreach ($history as $entry) { |
| | | if (in_array($entry['timestamp'], $timestamps)) { |
| | | foreach ($entry['iconList'] as $style => $icons) { |
| | | if (!isset($merged_icons[$style])) { |
| | | $merged_icons[$style] = []; |
| | | } |
| | | // Merge and keep unique |
| | | $merged_icons[$style] = array_unique( |
| | | array_merge($merged_icons[$style], $icons) |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (empty($merged_icons)) { |
| | | error_log('[IconsManager] No icons found in selected versions'); |
| | | return false; |
| | | } |
| | | |
| | | // Archive current version |
| | | $current_css = file_get_contents(JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css'); |
| | | if ($current_css !== false) { |
| | | $this->archiveCurrentVersion($current_css); |
| | | } |
| | | |
| | | // Update used icons and regenerate |
| | | $this->icons = $merged_icons; |
| | | $this->saveIcons(); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Check if icon already exists in the main 'icons' source |
| | | */ |
| | | protected function iconExistsInMainSource(string $name, string $style): bool |
| | | { |
| | | // If this IS the main source, no need to check |
| | | if ($this->source === 'icons') { |
| | | return false; |
| | | } |
| | | |
| | | // Check if main icons source exists |
| | | if (!isset(self::$instances['icons'])) { |
| | | return false; |
| | | } |
| | | |
| | | $mainIcons = self::$instances['icons']->icons; |
| | | return isset($mainIcons[$style]) && in_array($name, $mainIcons[$style]); |
| | | } |
| | | } |