GIF94;
| Path : /var/www/wordpress/wp-content/plugins/oxymade/includes/ |
| Current File : /var/www/wordpress/wp-content/plugins/oxymade/includes/typography.php |
<?php
namespace OxyMade\Typography;
/**
* Typography configuration and management
*/
class TypographyManager {
/**
* Default typography configuration
*/
const DEFAULT_CONFIG = [
'typescale_enabled' => false,
'fluid_enabled' => true,
'base_font_size_mobile' => 16,
'base_font_size_desktop' => 18,
'typescale_mobile' => 1.25,
'typescale_desktop' => 1.333,
'custom_sizes' => []
];
/**
* Initialize typography manager
*/
public static function init() {
if (function_exists('add_action')) {
add_action('wp_ajax_oxymade_save_typography_config', [self::class, 'ajax_save_typography_config']);
add_action('wp_ajax_oxymade_get_typography_config', [self::class, 'ajax_get_typography_config']);
}
}
/**
* Get typography configuration
*/
public static function get_config() {
$config = function_exists('get_option') ? get_option('oxymade_typography_config', []) : [];
// Merge with defaults, but prioritize saved values
$merged_config = array_merge(self::DEFAULT_CONFIG, $config);
// Ensure custom_sizes is always an array
if (!isset($merged_config['custom_sizes']) || !is_array($merged_config['custom_sizes'])) {
$merged_config['custom_sizes'] = [];
}
// Populate fluid_sizes from heading presets if empty
if (empty($merged_config['fluid_sizes']) && function_exists('get_option')) {
$presets = get_option('oxymade_heading_presets', []);
if (!empty($presets)) {
$fluid_sizes = [];
foreach ($presets as $key => $preset) {
if (isset($preset['mobile']) && isset($preset['desktop'])) {
$fluid_sizes[$key] = [
'mobile' => (int) $preset['mobile'],
'desktop' => (int) $preset['desktop'],
];
}
}
if (!empty($fluid_sizes)) {
$merged_config['fluid_sizes'] = $fluid_sizes;
}
}
}
return $merged_config;
}
/**
* Save typography configuration
*/
public static function save_config($config) {
$sanitized_config = self::sanitize_config($config);
if (function_exists('update_option')) {
update_option('oxymade_typography_config', $sanitized_config);
return true; // Always return true since we processed the config
}
return true;
}
/**
* Sanitize configuration data
*/
public static function sanitize_config($config) {
$sanitized = [];
// Handle boolean values that might come as strings from AJAX
$sanitized['typescale_enabled'] = filter_var($config['typescale_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
$sanitized['fluid_enabled'] = filter_var($config['fluid_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
$sanitized['base_font_size_mobile'] = max(12, min(24, intval($config['base_font_size_mobile'] ?? 16)));
$sanitized['base_font_size_desktop'] = max(14, min(32, intval($config['base_font_size_desktop'] ?? 18)));
$sanitized['typescale_mobile'] = max(1.0, min(2.0, floatval($config['typescale_mobile'] ?? 1.25)));
$sanitized['typescale_desktop'] = max(1.0, min(2.0, floatval($config['typescale_desktop'] ?? 1.333)));
// Sanitize custom sizes - handle multiple mode data
$sanitized['custom_sizes'] = [];
$sanitized['fluid_sizes'] = [];
$sanitized['custom_units_sizes'] = [];
// Sanitize active custom_sizes
if (!empty($config['custom_sizes']) && is_array($config['custom_sizes'])) {
$sanitized['custom_sizes'] = self::sanitize_custom_sizes_array($config['custom_sizes']);
}
// Sanitize fluid_sizes
if (!empty($config['fluid_sizes']) && is_array($config['fluid_sizes'])) {
$sanitized['fluid_sizes'] = self::sanitize_custom_sizes_array($config['fluid_sizes']);
}
// Sanitize custom_units_sizes
if (!empty($config['custom_units_sizes']) && is_array($config['custom_units_sizes'])) {
$sanitized['custom_units_sizes'] = self::sanitize_custom_sizes_array($config['custom_units_sizes']);
}
return $sanitized;
}
/**
* Sanitize custom sizes array
*/
private static function sanitize_custom_sizes_array($sizes) {
$sanitized = [];
foreach ($sizes as $key => $size) {
$sanitized_key = function_exists('sanitize_key') ? sanitize_key($key) : preg_replace('/[^a-z0-9_\-]/', '', strtolower($key));
if (is_array($size)) {
if (isset($size['mobile']) && isset($size['desktop'])) {
// Fluid custom sizes (mobile + desktop)
$mobile_val = intval($size['mobile']);
$desktop_val = intval($size['desktop']);
// If values are 0 or empty, use defaults
if ($mobile_val <= 0 || $desktop_val <= 0) {
$defaults = self::get_default_custom_sizes();
if (isset($defaults[$key])) {
$mobile_val = $defaults[$key]['mobile'];
$desktop_val = $defaults[$key]['desktop'];
}
}
$sanitized[$sanitized_key] = [
'mobile' => max(8, min(120, $mobile_val)),
'desktop' => max(8, min(200, $desktop_val))
];
} elseif (isset($size['value'])) {
// Fixed custom sizes (single value)
$value = sanitize_text_field($size['value']);
// If value is empty, use default
if (empty($value)) {
$defaults = self::get_default_custom_sizes();
if (isset($defaults[$key])) {
$value = $defaults[$key]['value'];
}
}
$sanitized[$sanitized_key] = [
'value' => $value
];
}
}
}
return $sanitized;
}
/**
* Extract heading preset sizes from design set global settings JSON
*
* @param array $typography_settings The settings.typography portion of the design set JSON
* @return array Heading presets keyed by name (hero, h1-h6) with mobile/desktop/font_weight/line_height/letter_spacing
*/
public static function extract_heading_presets_from_globals($typography_settings) {
$presets = [];
$typography_presets = $typography_settings['global_typography']['typography_presets'] ?? [];
if (empty($typography_presets)) {
return $presets;
}
// Map preset labels to heading names (multiple label variations per heading)
$label_map = [
'hero-heading' => 'hero', 'hero' => 'hero',
'h1-heading' => 'h1', 'h1' => 'h1',
'h2-heading' => 'h2', 'h2' => 'h2',
'h3-heading' => 'h3', 'h3' => 'h3',
'h4-heading' => 'h4', 'h4' => 'h4',
'h5-heading' => 'h5', 'h5' => 'h5',
'h6-heading' => 'h6', 'h6' => 'h6',
];
foreach ($typography_presets as $preset_entry) {
$label = $preset_entry['preset']['label'] ?? '';
// Normalize: lowercase, trim, replace spaces/underscores with hyphens
$normalized = strtolower(trim(str_replace([' ', '_'], '-', $label)));
if (!isset($label_map[$normalized])) {
continue;
}
$heading_name = $label_map[$normalized];
$typo = $preset_entry['custom']['customTypography'] ?? [];
$desktop = $typo['fontSize']['breakpoint_base']['number'] ?? null;
$mobile = $typo['fontSize']['breakpoint_phone_portrait']['number'] ?? null;
if ($desktop === null) {
continue;
}
// If no mobile size, derive from desktop (roughly 60% for large, closer for small)
if ($mobile === null) {
$mobile = max(14, round($desktop * 0.6));
}
// Clean float precision in percentage strings (e.g. "110.00000000000001%" → "110%")
$line_height = $typo['advanced']['lineHeight']['breakpoint_base']['style'] ?? null;
$letter_spacing = $typo['advanced']['letterSpacing']['breakpoint_base']['style'] ?? null;
if ($line_height !== null) {
$line_height = preg_replace_callback('/[\d.]+/', fn($m) => rtrim(rtrim(number_format((float)$m[0], 2, '.', ''), '0'), '.'), $line_height);
}
if ($letter_spacing !== null) {
$letter_spacing = preg_replace_callback('/[\d.]+/', fn($m) => rtrim(rtrim(number_format((float)$m[0], 2, '.', ''), '0'), '.'), $letter_spacing);
}
$presets[$heading_name] = [
'mobile' => (int) $mobile,
'desktop' => (int) $desktop,
'font_weight' => $typo['fontWeight']['breakpoint_base'] ?? null,
'line_height' => $line_height,
'letter_spacing' => $letter_spacing,
];
}
return $presets;
}
/**
* Generate typography variables based on configuration
*/
public static function generate_typography_variables() {
$config = self::get_config();
// Get fluid_text_sizing from oxymade_settings (the toggle in Step 3)
// This toggle ONLY affects text sizes (text-xs, text-sm, etc.), NOT headings
$oxymade_settings = function_exists('get_option') ? get_option('oxymade_settings', []) : [];
$fluid_text_sizing = isset($oxymade_settings['fluid_text_sizing']) ? $oxymade_settings['fluid_text_sizing'] : true;
$variables = [];
// Always use config-based heading generation
if ($config['typescale_enabled']) {
$heading_vars = self::generate_headings_with_typescale($config);
} else {
$heading_vars = self::generate_headings_custom($config);
}
// Generate TEXT SIZES (controlled by fluid text toggle ONLY, independent of heading settings)
$text_vars = self::generate_text_sizes($config, $fluid_text_sizing);
// Combine both
$variables = array_merge($heading_vars, $text_vars);
return $variables;
}
/**
* Generate TEXT SIZES (controlled by fluid text toggle ONLY)
*/
private static function generate_text_sizes($config, $fluid_text_sizing) {
$variables = [];
$custom_sizes = $config['custom_sizes'] ?? [];
$default_sizes = self::get_default_custom_sizes();
$text_names = [
'text-xs',
'text-sm',
'text-base',
'text-lg',
'text-xl',
'text-2xl',
'text-3xl',
'text-4xl',
'text-5xl',
'text-6xl',
'text-7xl'
];
foreach ($text_names as $name) {
// Use custom sizes if available, otherwise use defaults from get_default_custom_sizes()
if (isset($custom_sizes[$name]) && isset($custom_sizes[$name]['mobile']) && isset($custom_sizes[$name]['desktop'])) {
$mobile = $custom_sizes[$name]['mobile'];
$desktop = $custom_sizes[$name]['desktop'];
} else {
$mobile = $default_sizes[$name]['mobile'] ?? 16;
$desktop = $default_sizes[$name]['desktop'] ?? 18;
}
// Use fluid_text_sizing toggle (independent of heading settings)
if ($fluid_text_sizing) {
$value = self::generate_fluid_value($mobile, $desktop);
} else {
$value = $desktop . 'px';
}
$variables[] = [
'id' => 'om-' . $name,
'type' => 'unit',
'label' => self::format_typography_label($name),
'cssVariableName' => $name,
'collection' => 'OxyMade Typography',
'value' => [
'number' => $value,
'unit' => 'custom',
'style' => $value
]
];
}
return $variables;
}
/**
* Generate HEADINGS using typescale
* Always generates clamp() values using calculated mobile/desktop sizes
*/
private static function generate_headings_with_typescale($config) {
$variables = [];
$base_mobile = $config['base_font_size_mobile'];
$base_desktop = $config['base_font_size_desktop'];
$scale_mobile = $config['typescale_mobile'];
$scale_desktop = $config['typescale_desktop'];
$heading_levels = [
'hero' => 6,
'h1' => 5,
'h2' => 4,
'h3' => 3,
'h4' => 2,
'h5' => 1,
'h6' => 0,
];
foreach ($heading_levels as $name => $level) {
$mobile_size = $base_mobile * pow($scale_mobile, $level);
$desktop_size = $base_desktop * pow($scale_desktop, $level);
$value = self::generate_fluid_value($mobile_size, $desktop_size);
$variables[] = [
'id' => 'om-' . $name,
'type' => 'unit',
'label' => self::format_typography_label($name),
'cssVariableName' => $name,
'collection' => 'OxyMade Typography',
'value' => [
'number' => $value,
'unit' => 'custom',
'style' => $value
]
];
}
return $variables;
}
/**
* Generate HEADINGS using custom/fluid/fixed settings (when typescale is OFF)
*/
private static function generate_headings_custom($config) {
$variables = [];
$custom_sizes = $config['custom_sizes'] ?? [];
$default_sizes = self::get_default_custom_sizes();
$heading_levels = ['hero', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
foreach ($heading_levels as $name) {
// Custom Units mode: user provides a raw CSS value (e.g., "2.5rem", "clamp(...)")
if (isset($custom_sizes[$name]['value']) && !empty($custom_sizes[$name]['value'])) {
$value = $custom_sizes[$name]['value'];
} else {
// Fluid Custom mode: mobile/desktop pair → clamp() or fixed px
if (isset($custom_sizes[$name]['mobile']) && isset($custom_sizes[$name]['desktop'])) {
$mobile = $custom_sizes[$name]['mobile'];
$desktop = $custom_sizes[$name]['desktop'];
} else {
$mobile = $default_sizes[$name]['mobile'] ?? 16;
$desktop = $default_sizes[$name]['desktop'] ?? 18;
}
if ($config['fluid_enabled']) {
$value = self::generate_fluid_value($mobile, $desktop);
} else {
$value = $desktop . 'px';
}
}
$variables[] = [
'id' => 'om-' . $name,
'type' => 'unit',
'label' => self::format_typography_label($name),
'cssVariableName' => $name,
'collection' => 'OxyMade Typography',
'value' => [
'number' => $value,
'unit' => 'custom',
'style' => $value
]
];
}
return $variables;
}
/**
* Generate fluid clamp() value
* Note: Using 1rem = 10px for easier calculations
*/
private static function generate_fluid_value($mobile, $desktop) {
// Convert to rem for better scaling (1rem = 10px)
$mobile_rem = $mobile / 10;
$desktop_rem = $desktop / 10;
// Calculate viewport width scaling
$vw_scale = ($desktop_rem - $mobile_rem) / 61.25; // 61.25vw = 100vw - 38.75vw (mobile to desktop range)
return sprintf('clamp(%.3frem, %.3frem + %.3fvw, %.3frem)',
$mobile_rem,
$mobile_rem,
$vw_scale,
$desktop_rem
);
}
/**
* Get default custom sizes
*/
public static function get_default_custom_sizes() {
return [
'hero' => ['mobile' => 40, 'desktop' => 72, 'value' => '72px'],
'h1' => ['mobile' => 32, 'desktop' => 48, 'value' => '48px'],
'h2' => ['mobile' => 24, 'desktop' => 32, 'value' => '32px'],
'h3' => ['mobile' => 20, 'desktop' => 28, 'value' => '28px'],
'h4' => ['mobile' => 18, 'desktop' => 24, 'value' => '24px'],
'h5' => ['mobile' => 16, 'desktop' => 20, 'value' => '20px'],
'h6' => ['mobile' => 14, 'desktop' => 18, 'value' => '18px'],
'text-xs' => ['mobile' => 12, 'desktop' => 12, 'value' => '12px'],
'text-sm' => ['mobile' => 14, 'desktop' => 14, 'value' => '14px'],
'text-base' => ['mobile' => 16, 'desktop' => 16, 'value' => '16px'],
'text-lg' => ['mobile' => 17, 'desktop' => 18, 'value' => '18px'],
'text-xl' => ['mobile' => 18, 'desktop' => 20, 'value' => '20px'],
'text-2xl' => ['mobile' => 20, 'desktop' => 24, 'value' => '24px'],
'text-3xl' => ['mobile' => 24, 'desktop' => 30, 'value' => '30px'],
'text-4xl' => ['mobile' => 30, 'desktop' => 36, 'value' => '36px'],
'text-5xl' => ['mobile' => 38, 'desktop' => 48, 'value' => '48px'],
'text-6xl' => ['mobile' => 48, 'desktop' => 60, 'value' => '60px'],
'text-7xl' => ['mobile' => 56, 'desktop' => 72, 'value' => '72px']
];
}
/**
* Format typography label
*/
private static function format_typography_label($name) {
$label = str_replace('-', ' ', $name);
$label = ucwords($label);
return $label;
}
/**
* AJAX handler for saving typography configuration
*/
public static function ajax_save_typography_config() {
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'oxymade-admin-nonce')) {
wp_send_json_error('Invalid security token');
exit;
}
if (!current_user_can('manage_options')) {
wp_send_json_error('You do not have permission to perform this action');
exit;
}
$config = $_POST['config'] ?? [];
$result = self::save_config($config);
if ($result) {
// Auto-apply: re-sync typography variables with Oxygen
if (class_exists('\\OxyMade\\Variables\\VariableManager')) {
\OxyMade\Variables\VariableManager::sync_typography_with_oxygen('add_new');
}
wp_send_json_success(['message' => 'Typography configuration saved and applied!']);
} else {
wp_send_json_error('Failed to save typography configuration');
}
exit;
}
/**
* AJAX handler for getting typography configuration
*/
public static function ajax_get_typography_config() {
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'oxymade-admin-nonce')) {
wp_send_json_error('Invalid security token');
exit;
}
if (!current_user_can('manage_options')) {
wp_send_json_error('You do not have permission to perform this action');
exit;
}
$config = self::get_config();
wp_send_json_success(['config' => $config]);
exit;
}
}
// Initialize the typography manager
TypographyManager::init();