[ 'button_text' => 'Copy Code', 'success_text' => 'Copied!', ], 'icon' => [ 'enabled' => true, 'position' => 'left', // 'left' or 'right' 'icon_key' => 'clipboard', 'custom_url' => null, // Pro feature: custom icon URL ], 'style' => [ 'text_color' => '#ffffff', 'background_color' => '#4f46e5', 'hover_background_color' => '#4338ca', 'font_size' => '12px', 'font_weight' => '700', 'padding_x' => '12px', 'padding_y' => '6px', 'border_radius' => '6px', ], ]; } /** * Get available icons for button style. * * Each icon has a key and SVG markup. * The key is stored in config, SVG is used for rendering. * * @return array Array of icons with key => SVG markup. */ public static function get_icons() { return [ 'clipboard' => [ 'label' => 'Clipboard', 'svg' => '', ], 'copy' => [ 'label' => 'Copy', 'svg' => '', ], 'paperclip' => [ 'label' => 'Attachment', 'svg' => '', ], 'link' => [ 'label' => 'Link', 'svg' => '', ], 'document' => [ 'label' => 'Document', 'svg' => '', ], 'check' => [ 'label' => 'Checkmark', 'svg' => '', ], ]; } /** * Get success icon (shown after copy). * * @return string SVG markup for success icon. */ public static function get_success_icon() { return ''; } /** * Generate a unique button ID. * * @return string Unique button ID. */ public static function generate_button_id() { return 'ctc-copy-btn-' . wp_generate_password( 8, false, false ); } /** * Merge user config with defaults. * * @param array $user_config User-provided configuration. * @return array Merged configuration. */ public static function merge_config( $user_config = [] ) { $defaults = self::get_default_config(); // Deep merge $merged = $defaults; if ( isset( $user_config['text'] ) && is_array( $user_config['text'] ) ) { $merged['text'] = array_merge( $defaults['text'], $user_config['text'] ); } if ( isset( $user_config['icon'] ) && is_array( $user_config['icon'] ) ) { $merged['icon'] = array_merge( $defaults['icon'], $user_config['icon'] ); } if ( isset( $user_config['style'] ) && is_array( $user_config['style'] ) ) { $merged['style'] = array_merge( $defaults['style'], $user_config['style'] ); } return $merged; } /** * Get the icon SVG by key. * * @param string $icon_key Icon key. * @return string SVG markup or empty string if not found. */ public static function get_icon_svg( $icon_key ) { $icons = self::get_icons(); if ( isset( $icons[ $icon_key ]['svg'] ) ) { return $icons[ $icon_key ]['svg']; } // Fallback to clipboard icon return $icons['clipboard']['svg']; } /** * Darken a hex color by a percentage. * * @param string $hex Hex color (e.g., '#4f46e5'). * @param int $percent Percentage to darken (0-100). * @return string Darkened hex color. */ public static function darken_color( $hex, $percent = 10 ) { // Remove # if present. $hex = ltrim( $hex, '#' ); // Parse hex to RGB. $r = hexdec( substr( $hex, 0, 2 ) ); $g = hexdec( substr( $hex, 2, 2 ) ); $b = hexdec( substr( $hex, 4, 2 ) ); // Darken each channel. $factor = 1 - ( $percent / 100 ); $r = (int) round( $r * $factor ); $g = (int) round( $g * $factor ); $b = (int) round( $b * $factor ); // Ensure values are in valid range. $r = max( 0, min( 255, $r ) ); $g = max( 0, min( 255, $g ) ); $b = max( 0, min( 255, $b ) ); // Convert back to hex. return '#' . sprintf( '%02x%02x%02x', $r, $g, $b ); } /** * Build inline CSS variables from style config. * Auto-calculates hover background if not provided (darkens by 12%). * * @param array $style Style configuration. * @return string Inline style attribute value. */ public static function build_inline_styles( $style ) { // Auto-calculate hover background if not provided. $bg_color = isset( $style['background_color'] ) ? $style['background_color'] : '#4f46e5'; $hover_bg = isset( $style['hover_background_color'] ) && ! empty( $style['hover_background_color'] ) ? $style['hover_background_color'] : self::darken_color( $bg_color, 12 ); $css_vars = [ '--ctc-text-color' => $style['text_color'], '--ctc-bg' => $bg_color, '--ctc-hover-bg' => $hover_bg, '--ctc-font-size' => $style['font_size'], '--ctc-font-weight' => $style['font_weight'], '--ctc-padding-x' => $style['padding_x'], '--ctc-padding-y' => $style['padding_y'], '--ctc-border-radius' => $style['border_radius'], ]; // Map CSS var names to property keys for unit detection. $var_to_prop = [ '--ctc-font-size' => 'font_size', '--ctc-padding-x' => 'padding_x', '--ctc-padding-y' => 'padding_y', '--ctc-border-radius' => 'border_radius', ]; $inline = []; foreach ( $css_vars as $var => $value ) { // Add 'px' unit for numeric properties. if ( isset( $var_to_prop[ $var ] ) && is_numeric( $value ) ) { $value = $value . 'px'; } $inline[] = esc_attr( $var ) . ': ' . esc_attr( $value ); } return implode( '; ', $inline ); } /** * Render the button HTML. * * This is the main render function used on the frontend. * The same logic should be replicated in JS for admin preview. * * @param array $config Button configuration (merged with defaults). * @param string $button_id Optional. Unique button ID. Auto-generated if not provided. * @return string Button HTML markup. */ public static function render( $config = [], $button_id = null ) { // Merge with defaults $config = self::merge_config( $config ); // Generate button ID if not provided if ( empty( $button_id ) ) { $button_id = self::generate_button_id(); } // Get icon SVG $icon_svg = ''; if ( $config['icon']['enabled'] ) { if ( ! empty( $config['icon']['custom_url'] ) ) { // Pro feature: custom icon $icon_svg = ''; } else { $icon_svg = '' . self::get_icon_svg( $config['icon']['icon_key'] ) . ''; } } // Build inline styles $inline_styles = self::build_inline_styles( $config['style'] ); // Build button content based on icon position $button_content = ''; if ( $config['icon']['position'] === 'left' && $icon_svg ) { $button_content .= $icon_svg; } $button_content .= '' . esc_html( $config['text']['button_text'] ) . ''; if ( $config['icon']['position'] === 'right' && $icon_svg ) { $button_content .= $icon_svg; } // Build the button HTML $html = sprintf( '', esc_attr( $button_id ), esc_attr( $inline_styles ), esc_attr( $config['text']['success_text'] ), esc_attr( $config['text']['button_text'] ), esc_attr( $config['text']['button_text'] ), $button_content ); return $html; } /** * Get base CSS for button style. * * This CSS uses CSS variables that are set inline on each button. * These are the "hidden" styles that users don't customize via UI. * * @return string CSS styles. */ public static function get_base_css() { return ' .ctc-copy-button { /* User-customizable via CSS variables */ color: var(--ctc-text-color, #ffffff); background-color: var(--ctc-bg, #4f46e5); font-size: var(--ctc-font-size, 12px); font-weight: var(--ctc-font-weight, 700); padding: var(--ctc-padding-y, 6px) var(--ctc-padding-x, 12px); border-radius: var(--ctc-border-radius, 6px); /* Hidden CSS - always applied, not customizable via UI */ display: inline-flex; align-items: center; gap: 4px; border: none; cursor: pointer; transition: background-color 0.2s ease; position: absolute; top: 8px; right: 8px; z-index: 10; font-family: inherit; line-height: 1; } .ctc-copy-button:hover { background-color: var(--ctc-hover-bg, #4338ca); } .ctc-copy-button:focus { outline: 2px solid var(--ctc-bg, #4f46e5); outline-offset: 2px; } .ctc-copy-button .ctc-btn-icon { display: inline-flex; align-items: center; justify-content: center; } .ctc-copy-button .ctc-btn-icon svg { width: 14px; height: 14px; } .ctc-copy-button .ctc-btn-icon img { width: 14px; height: 14px; object-fit: contain; } .ctc-copy-button .ctc-btn-text { white-space: nowrap; } /* Success state */ .ctc-copy-button.ctc-copied { background-color: #059669; } .ctc-copy-button.ctc-copied:hover { background-color: #047857; } '; } /** * Get stylesheet CSS for Global Injector (frontend). * * Used when outputting inline CSS for Global Injector rules. Uses selectors * .ctc-copy-btn--button and the same CSS variables as build_inline_styles(). * This is the Global Injector counterpart to get_base_css() (which targets .ctc-copy-button for shortcode/block). * * @return string Unminified CSS. */ public static function get_global_injector_css() { return ' .ctc-copy-btn--button { padding: var(--ctc-padding-y, 8px) var(--ctc-padding-x, 16px); background: var(--ctc-bg, #4f46e5); color: var(--ctc-text-color, #fff); border-radius: var(--ctc-border-radius, 6px); font-size: var(--ctc-font-size, 13px); } .ctc-copy-btn--button:hover { background: var(--ctc-hover-bg, #4338ca); filter: brightness(1.05); } .ctc-copy-btn__icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .ctc-copy-btn__icon svg { width: 14px; height: 14px; } .ctc-copy-btn__text { white-space: nowrap; } '; } /** * Get data to localize for JavaScript. * * This includes everything JS needs to render the button preview. * * @return array Data for wp_localize_script. */ public static function get_localize_data() { return [ 'defaultConfig' => self::get_default_config(), 'icons' => self::get_icons(), 'successIcon' => self::get_success_icon(), 'baseCSS' => self::get_base_css(), ]; } }