PluginProbe
Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code / 5.2.0
Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code v5.2.0
5.5.3 3.1.0 3.2.0 3.2.1 3.3.0 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.6.0 3.7.0 3.8.0 3.8.1 3.8.2 3.8.3 4.0.0 4.0.2 4.0.3 4.0.4 4.0.5 4.1.0 4.1.1 All 78 releases
copy-the-code / includes / class-shortcode.php

class-shortcode.php in Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code 5.2.0, at includes/class-shortcode.php

1,016 lines 29.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Unified Shortcode Handler
4 *
5 * Provides `[copy]` shortcode with design presets (button, icon, inline, cover).
6 * Also provides backward-compatible `[copy_inline]` shortcode.
7 *
8 * Reuses the same styles and JS from Global Injector for consistency.
9 *
10 * @package CTC
11 * @since 5.0.0
12 */
13
14 namespace CTC;
15
16 use CTC\Global_Injector\Styles\Button;
17 use CTC\Global_Injector\Styles\Icon;
18 use CTC\Global_Injector\Styles\Cover;
19
20 /**
21 * Shortcode Class
22 *
23 * Single source of truth for all copy shortcodes.
24 */
25 class Shortcode {
26
27 /**
28 * Instance
29 *
30 * @var Shortcode|null
31 */
32 private static $instance = null;
33
34 /**
35 * Track if styles have been enqueued.
36 *
37 * @var bool
38 */
39 private $styles_enqueued = false;
40
41 /**
42 * Track which presets were used on the page (for conditional CSS).
43 *
44 * @var array<string, true>
45 */
46 private $used_presets = [];
47
48 /**
49 * Get instance.
50 *
51 * @return Shortcode
52 */
53 public static function get() {
54 if ( null === self::$instance ) {
55 self::$instance = new self();
56 }
57 return self::$instance;
58 }
59
60 /**
61 * Constructor.
62 */
63 private function __construct() {
64 // Register shortcodes.
65 add_shortcode( 'copy', [ $this, 'render_copy_shortcode' ] );
66 add_shortcode( 'copy_inline', [ $this, 'render_copy_inline_shortcode' ] );
67 add_shortcode( 'ctc', [ $this, 'render_copy_shortcode' ] );
68
69 // Enqueue scripts/styles when shortcode is used.
70 add_action( 'wp_footer', [ $this, 'maybe_enqueue_assets' ], 5 );
71 }
72
73 /**
74 * Get default attributes for [copy] shortcode.
75 *
76 * @return array Default attributes.
77 */
78 public static function get_default_atts() {
79 return [
80 // Content.
81 'text' => '', // Content to copy (or use content between tags).
82 'display' => '', // What to display (default: same as text).
83 'target' => '', // CSS selector to copy from (advanced).
84 'copy-as' => 'text', // 'text' or 'html' (legacy).
85 'copy_as' => '', // Copy as: text, html, text_and_html, image, json, svg.
86
87 // Preset/Style.
88 'preset' => 'inline', // 'button', 'icon', 'inline', 'cover'.
89
90 // Text.
91 'button-text' => '', // Button text (overrides preset default).
92 'success-text' => '', // Success message.
93 'tooltip' => '', // Tooltip text.
94
95 // Icon.
96 'icon' => '', // Icon key: clipboard, copy, link, etc.
97 'icon-position' => '', // 'left' or 'right'.
98 'show-icon' => '', // 'yes' or 'no'.
99
100 // Colors (overrides preset).
101 'color' => '', // Text color.
102 'bg' => '', // Background color.
103 'hover-bg' => '', // Hover background color.
104 'icon-color' => '', // Icon color (for icon preset).
105
106 // Layout.
107 'class' => '', // Additional CSS class.
108 'id' => '', // Custom ID.
109
110 // Redirect: URL to open after copy (e.g. store page).
111 'redirect' => '', // Preferred: URL to open after copying.
112 'link' => '', // Backward compatibility: same as redirect.
113
114 // Legacy attributes (backward compatibility).
115 'copied-text' => '', // Maps to success-text.
116 'style' => '', // Maps to preset.
117 'tag' => '', // Legacy: HTML tag.
118 'title' => '', // Legacy: Tooltip.
119 'content' => '', // Legacy: Content to copy.
120 'hidden' => '', // Legacy: Hide display text.
121 ];
122 }
123
124 /**
125 * Render [copy] shortcode.
126 *
127 * @param array $atts Shortcode attributes.
128 * @param string $content Shortcode content.
129 * @return string HTML output.
130 */
131 public function render_copy_shortcode( $atts = [], $content = '' ) {
132 $atts = shortcode_atts( self::get_default_atts(), $atts, 'copy' );
133
134 // Normalize legacy attributes.
135 $atts = $this->normalize_legacy_atts( $atts, $content );
136
137 /**
138 * Filter shortcode attributes before rendering.
139 *
140 * @since 5.0.0
141 *
142 * @param array $atts Normalized shortcode attributes.
143 * @param string $content Shortcode content.
144 */
145 $atts = apply_filters( 'ctc/shortcode/atts', $atts, $content );
146
147 // Mark that we need to enqueue assets.
148 $this->styles_enqueued = true;
149
150 // Track preset for conditional CSS (only load styles for presets used).
151 $preset = $atts['preset'] ?? 'inline';
152 $this->used_presets[ $preset ] = true;
153
154 // Render based on preset.
155 switch ( $preset ) {
156 case 'button':
157 return $this->render_button_preset( $atts );
158
159 case 'icon':
160 return $this->render_icon_preset( $atts );
161
162 case 'cover':
163 return $this->render_cover_preset( $atts );
164
165 case 'inline':
166 default:
167 return $this->render_inline_preset( $atts );
168 }
169 }
170
171 /**
172 * Render [copy_inline] shortcode (backward compatible).
173 *
174 * Maps to [copy preset="inline"].
175 *
176 * @param array $atts Shortcode attributes.
177 * @param string $content Shortcode content.
178 * @return string HTML output.
179 */
180 public function render_copy_inline_shortcode( $atts = [], $content = '' ) {
181 // Map copy_inline attributes to copy attributes.
182 $mapped_atts = [
183 'preset' => 'inline',
184 'text' => isset( $atts['text'] ) ? $atts['text'] : $content,
185 'display' => isset( $atts['display'] ) ? $atts['display'] : '',
186 'success-text' => isset( $atts['tooltip'] ) ? $atts['tooltip'] : __( 'Copied', 'ctc' ),
187 'class' => isset( $atts['style'] ) ? 'ctc-inline-style-' . $atts['style'] : '',
188 'hidden' => isset( $atts['hidden'] ) ? $atts['hidden'] : '',
189 ];
190
191 return $this->render_copy_shortcode( $mapped_atts, $content );
192 }
193
194 /**
195 * Normalize legacy attributes to new format.
196 *
197 * @param array $atts Shortcode attributes.
198 * @param string $content Shortcode content.
199 * @return array Normalized attributes.
200 */
201 private function normalize_legacy_atts( $atts, $content ) {
202 // Decode HTML entities in content so [ ] work (e.g. &#91; &#93;).
203 if ( ! empty( $atts['content'] ) ) {
204 $atts['content'] = html_entity_decode( $atts['content'], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
205 }
206
207 // Legacy support: 'content' attribute contains what to copy.
208 // 'text' attribute is the display text in legacy usage.
209 // Example: [copy text="Location" content="/wp [...]"] - displays "Location", copies "/wp [...]"
210 if ( ! empty( $atts['content'] ) ) {
211 // Legacy mode: content = what to copy, text = what to display.
212 $copy_text = $atts['content'];
213 $display_text = ! empty( $atts['text'] ) ? $atts['text'] : $atts['content'];
214
215 $atts['text'] = $copy_text;
216 $atts['display'] = $display_text;
217 } elseif ( empty( $atts['text'] ) && ! empty( $content ) ) {
218 // Modern mode: text = both copy and display (unless display is set).
219 $atts['text'] = $content;
220 }
221
222 // Decode HTML entities in text (supports &#91; for [ and &#93; for ] etc.).
223 // This allows users to use HTML entities for special characters in shortcode attributes.
224 if ( ! empty( $atts['text'] ) ) {
225 $atts['text'] = html_entity_decode( $atts['text'], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
226 // Fix WordPress wptexturize() converting -- to en-dash inside CSS var().
227 $atts['text'] = $this->fix_var_texturize( $atts['text'] );
228 }
229
230 // Display text: prefer inner content when present, else default to copy text.
231 $content_trimmed = isset( $content ) ? trim( (string) $content ) : '';
232 if ( ! empty( $content_trimmed ) && empty( $atts['display'] ) ) {
233 $atts['display'] = html_entity_decode( $content_trimmed, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
234 } elseif ( empty( $atts['display'] ) ) {
235 $atts['display'] = $atts['text'];
236 } else {
237 $atts['display'] = html_entity_decode( $atts['display'], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
238 }
239
240 // Success text.
241 if ( empty( $atts['success-text'] ) && ! empty( $atts['copied-text'] ) ) {
242 $atts['success-text'] = $atts['copied-text'];
243 }
244 if ( empty( $atts['success-text'] ) ) {
245 $atts['success-text'] = __( 'Copied!', 'ctc' );
246 }
247
248 // Tooltip.
249 if ( empty( $atts['tooltip'] ) && ! empty( $atts['title'] ) ) {
250 $atts['tooltip'] = $atts['title'];
251 }
252 if ( empty( $atts['tooltip'] ) ) {
253 $atts['tooltip'] = __( 'Copy to clipboard', 'ctc' );
254 }
255
256 // Preset from legacy style.
257 if ( empty( $atts['preset'] ) && ! empty( $atts['style'] ) ) {
258 $style_map = [
259 'icon' => 'icon',
260 'button' => 'button',
261 'cover' => 'cover',
262 ];
263 if ( isset( $style_map[ $atts['style'] ] ) ) {
264 $atts['preset'] = $style_map[ $atts['style'] ];
265 }
266 }
267
268 // Button text.
269 if ( empty( $atts['button-text'] ) ) {
270 $atts['button-text'] = __( 'Copy', 'ctc' );
271 }
272
273 // Redirect URL: prefer redirect attribute; fall back to link for backward compatibility.
274 if ( ! empty( $atts['redirect'] ) ) {
275 $atts['link'] = $atts['redirect'];
276 }
277
278 return $atts;
279 }
280
281 /**
282 * Fix WordPress wptexturize() inside CSS var() functions.
283 *
284 * WordPress converts -- to en-dash (–) and --- to em-dash (—).
285 * This breaks CSS custom properties like var(--wp--preset--color--bg).
286 *
287 * This method only fixes content inside var() to avoid affecting
288 * legitimate typography like "2020–2025" or "He said—yes".
289 *
290 * @since 5.0.0
291 *
292 * @param string $text Text to process.
293 * @return string Processed text with var() contents fixed.
294 */
295 private function fix_var_texturize( $text ) {
296 if ( empty( $text ) || strpos( $text, 'var(' ) === false ) {
297 return $text;
298 }
299
300 // Replace en-dash/em-dash with hyphens only inside var().
301 return preg_replace_callback(
302 '/var\s*\(([^)]+)\)/',
303 function ( $matches ) {
304 $inside = str_replace(
305 [ '', '' ], // En-dash, Em-dash (Unicode).
306 [ '--', '---' ], // Double, Triple hyphen.
307 $matches[1]
308 );
309 return 'var(' . $inside . ')';
310 },
311 $text
312 );
313 }
314
315 /**
316 * Render inline preset.
317 *
318 * Displays text with a copy icon. Best for coupon codes, promo codes, etc.
319 *
320 * Supports:
321 * - tag="a" : Uses native anchor tag with theme styling (no custom CSS overrides).
322 * - show-icon="no" : Hides the copy icon.
323 *
324 * @param array $atts Shortcode attributes.
325 * @return string HTML output.
326 */
327 private function render_inline_preset( $atts ) {
328 $id = ! empty( $atts['id'] ) ? $atts['id'] : 'ctc-inline-' . wp_generate_password( 8, false, false );
329 $hidden_class = 'yes' === $atts['hidden'] ? 'ctc-inline-hidden' : '';
330 $custom_class = ! empty( $atts['class'] ) ? ' ' . esc_attr( $atts['class'] ) : '';
331
332 // Determine if using native anchor tag (inherits theme styling).
333 $use_native_tag = 'a' === strtolower( $atts['tag'] );
334
335 // Determine if icon should be shown (default: yes, unless show-icon="no").
336 $show_icon = ! in_array( strtolower( $atts['show-icon'] ), [ 'no', 'false', '0' ], true );
337
338 // Build CSS class based on tag type.
339 if ( $use_native_tag ) {
340 // Native mode: Use theme's anchor styling, minimal CTC styling.
341 $this->used_presets['native'] = true;
342 $css_class = 'ctc-shortcode ctc-shortcode--native' . $custom_class;
343 } else {
344 // Modern mode: Full CTC styling.
345 $css_class = 'ctc-shortcode ctc-shortcode--inline' . $custom_class;
346 }
347
348 // Build inline styles (only for non-native mode).
349 $inline_style = '';
350 if ( ! $use_native_tag && ! empty( $atts['color'] ) ) {
351 $inline_style .= '--ctc-inline-color: ' . esc_attr( $atts['color'] ) . ';';
352 }
353
354 // Determine HTML tag.
355 $tag = $use_native_tag ? 'a' : 'span';
356
357 // Copy as: prefer copy_as.
358 $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : '';
359
360 // Build icon HTML.
361 $icon_html = '';
362 if ( $show_icon ) {
363 $icon_key = ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard';
364 $icon_html = '<span class="ctc-shortcode__icon" aria-hidden="true">' .
365 wp_kses( $this->get_icon_svg( $icon_key ), $this->get_allowed_svg_tags() ) .
366 '</span>';
367 }
368
369 ob_start();
370 ?>
371 <<?php echo esc_attr( $tag ); ?>
372 id="<?php echo esc_attr( $id ); ?>"
373 class="<?php echo esc_attr( $css_class ); ?>"
374 data-ctc-copy="<?php echo esc_attr( $atts['text'] ); ?>"
375 data-ctc-success="<?php echo esc_attr( $atts['success-text'] ); ?>"
376 data-ctc-format="<?php echo esc_attr( $atts['copy-as'] ); ?>"
377 <?php if ( ! empty( $copy_as_value ) ) : ?>
378 data-ctc-copy-as="<?php echo esc_attr( $copy_as_value ); ?>"
379 <?php endif; ?>
380 <?php if ( ! empty( $atts['target'] ) ) : ?>
381 data-ctc-target="<?php echo esc_attr( $atts['target'] ); ?>"
382 <?php endif; ?>
383 <?php if ( $inline_style ) : ?>
384 style="<?php echo esc_attr( $inline_style ); ?>"
385 <?php endif; ?>
386 <?php if ( $use_native_tag ) : ?>
387 href="javascript:void(0);"
388 title="<?php echo esc_attr( $atts['tooltip'] ); ?>"
389 <?php else : ?>
390 role="button"
391 tabindex="0"
392 aria-label="<?php echo esc_attr( $atts['tooltip'] ); ?>"
393 <?php endif; ?>
394 >
395 <span class="ctc-shortcode__text <?php echo esc_attr( $hidden_class ); ?>"><?php echo esc_html( $atts['display'] ); ?></span>
396 <?php echo $icon_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
397 <span class="ctc-shortcode__success" aria-live="polite"></span>
398 </<?php echo esc_attr( $tag ); ?>>
399 <?php
400 return ob_get_clean();
401 }
402
403 /**
404 * Render button preset.
405 *
406 * Uses Button style class for consistent styling with Global Injector.
407 *
408 * @param array $atts Shortcode attributes.
409 * @return string HTML output.
410 */
411 private function render_button_preset( $atts ) {
412 $id = ! empty( $atts['id'] ) ? $atts['id'] : Button::generate_button_id();
413 $custom_class = ! empty( $atts['class'] ) ? ' ' . esc_attr( $atts['class'] ) : '';
414
415 // Build config for Button style.
416 $config = [
417 'text' => [
418 'button_text' => $atts['button-text'],
419 'success_text' => $atts['success-text'],
420 ],
421 'icon' => [
422 'enabled' => 'no' !== $atts['show-icon'],
423 'position' => ! empty( $atts['icon-position'] ) ? $atts['icon-position'] : 'left',
424 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard',
425 ],
426 'style' => [],
427 ];
428
429 // Add custom colors if provided.
430 if ( ! empty( $atts['color'] ) ) {
431 $config['style']['text_color'] = $atts['color'];
432 }
433 if ( ! empty( $atts['bg'] ) ) {
434 $config['style']['background_color'] = $atts['bg'];
435 }
436 if ( ! empty( $atts['hover-bg'] ) ) {
437 $config['style']['hover_background_color'] = $atts['hover-bg'];
438 }
439
440 // Merge with defaults.
441 $config = Button::merge_config( $config );
442
443 // Build inline styles.
444 $inline_styles = Button::build_inline_styles( $config['style'] );
445
446 // Get icon SVG.
447 $icon_svg = '';
448 if ( $config['icon']['enabled'] ) {
449 $icon_svg = '<span class="ctc-shortcode__icon">' . Button::get_icon_svg( $config['icon']['icon_key'] ) . '</span>';
450 }
451
452 // Build content based on icon position.
453 $button_content = '';
454 if ( 'left' === $config['icon']['position'] && $icon_svg ) {
455 $button_content .= $icon_svg;
456 }
457 $button_content .= '<span class="ctc-shortcode__text">' . esc_html( $config['text']['button_text'] ) . '</span>';
458 if ( 'right' === $config['icon']['position'] && $icon_svg ) {
459 $button_content .= $icon_svg;
460 }
461
462 // Copy as: prefer copy_as.
463 $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : '';
464
465 ob_start();
466 ?>
467 <button
468 type="button"
469 id="<?php echo esc_attr( $id ); ?>"
470 class="ctc-shortcode ctc-shortcode--button<?php echo esc_attr( $custom_class ); ?>"
471 style="<?php echo esc_attr( $inline_styles ); ?>"
472 data-ctc-copy="<?php echo esc_attr( $atts['text'] ); ?>"
473 data-ctc-success="<?php echo esc_attr( $config['text']['success_text'] ); ?>"
474 data-ctc-original="<?php echo esc_attr( $config['text']['button_text'] ); ?>"
475 data-ctc-format="<?php echo esc_attr( $atts['copy-as'] ); ?>"
476 <?php if ( ! empty( $copy_as_value ) ) : ?>
477 data-ctc-copy-as="<?php echo esc_attr( $copy_as_value ); ?>"
478 <?php endif; ?>
479 <?php if ( ! empty( $atts['target'] ) ) : ?>
480 data-ctc-target="<?php echo esc_attr( $atts['target'] ); ?>"
481 <?php endif; ?>
482 <?php if ( ! empty( $atts['link'] ) ) : ?>
483 data-ctc-link="<?php echo esc_attr( $atts['link'] ); ?>"
484 <?php endif; ?>
485 aria-label="<?php echo esc_attr( $atts['tooltip'] ); ?>"
486 >
487 <?php echo $button_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
488 </button>
489 <?php
490 return ob_get_clean();
491 }
492
493 /**
494 * Render icon preset.
495 *
496 * Uses Icon style class for consistent styling with Global Injector.
497 *
498 * @param array $atts Shortcode attributes.
499 * @return string HTML output.
500 */
501 private function render_icon_preset( $atts ) {
502 $id = ! empty( $atts['id'] ) ? $atts['id'] : Icon::generate_icon_id();
503 $custom_class = ! empty( $atts['class'] ) ? ' ' . esc_attr( $atts['class'] ) : '';
504
505 // Build config for Icon style.
506 $config = [
507 'text' => [
508 'tooltip_text' => $atts['tooltip'],
509 'success_text' => $atts['success-text'],
510 ],
511 'icon' => [
512 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard',
513 ],
514 'style' => [],
515 ];
516
517 // Add custom colors if provided.
518 if ( ! empty( $atts['icon-color'] ) ) {
519 $config['style']['icon_color'] = $atts['icon-color'];
520 }
521 if ( ! empty( $atts['bg'] ) ) {
522 $config['style']['bg_color'] = $atts['bg'];
523 }
524
525 // Merge with defaults.
526 $config = Icon::merge_config( $config );
527
528 // Build inline styles.
529 $inline_styles = Icon::build_inline_styles( $config['style'] );
530
531 // Get icon SVG.
532 $icon_svg = '<span class="ctc-shortcode__icon">' . Icon::get_icon_svg( $config['icon']['icon_key'] ) . '</span>';
533
534 // Copy as: prefer copy_as.
535 $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : '';
536
537 ob_start();
538 ?>
539 <button
540 type="button"
541 id="<?php echo esc_attr( $id ); ?>"
542 class="ctc-shortcode ctc-shortcode--icon<?php echo esc_attr( $custom_class ); ?>"
543 style="<?php echo esc_attr( $inline_styles ); ?>"
544 data-ctc-copy="<?php echo esc_attr( $atts['text'] ); ?>"
545 data-ctc-success="<?php echo esc_attr( $config['text']['success_text'] ); ?>"
546 data-ctc-tooltip="<?php echo esc_attr( $config['text']['tooltip_text'] ); ?>"
547 data-ctc-format="<?php echo esc_attr( $atts['copy-as'] ); ?>"
548 <?php if ( ! empty( $copy_as_value ) ) : ?>
549 data-ctc-copy-as="<?php echo esc_attr( $copy_as_value ); ?>"
550 <?php endif; ?>
551 <?php if ( ! empty( $atts['target'] ) ) : ?>
552 data-ctc-target="<?php echo esc_attr( $atts['target'] ); ?>"
553 <?php endif; ?>
554 aria-label="<?php echo esc_attr( $config['text']['tooltip_text'] ); ?>"
555 >
556 <?php echo $icon_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
557 </button>
558 <?php
559 return ob_get_clean();
560 }
561
562 /**
563 * Render cover preset.
564 *
565 * Wraps content with hover overlay and copy button.
566 *
567 * @param array $atts Shortcode attributes.
568 * @return string HTML output.
569 */
570 private function render_cover_preset( $atts ) {
571 $id = ! empty( $atts['id'] ) ? $atts['id'] : Cover::generate_cover_id();
572 $custom_class = ! empty( $atts['class'] ) ? ' ' . esc_attr( $atts['class'] ) : '';
573
574 // Build config for Cover style.
575 $config = [
576 'text' => [
577 'button_text' => $atts['button-text'],
578 'success_text' => $atts['success-text'],
579 ],
580 'icon' => [
581 'enabled' => 'no' !== $atts['show-icon'],
582 'position' => ! empty( $atts['icon-position'] ) ? $atts['icon-position'] : 'left',
583 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard',
584 ],
585 'button' => [],
586 ];
587
588 // Add custom colors if provided.
589 if ( ! empty( $atts['color'] ) ) {
590 $config['button']['text_color'] = $atts['color'];
591 }
592 if ( ! empty( $atts['bg'] ) ) {
593 $config['button']['bg_color'] = $atts['bg'];
594 }
595 if ( ! empty( $atts['hover-bg'] ) ) {
596 $config['button']['hover_bg_color'] = $atts['hover-bg'];
597 }
598
599 // Merge with defaults.
600 $config = Cover::merge_config( $config );
601
602 // Get icon SVG.
603 $icon_svg = '';
604 if ( $config['icon']['enabled'] ) {
605 $icon_svg = '<span class="ctc-cover-icon">' . Cover::get_icon_svg( $config['icon']['icon_key'] ) . '</span>';
606 }
607
608 // Build button content.
609 $button_content = '';
610 if ( 'left' === $config['icon']['position'] && $icon_svg ) {
611 $button_content .= $icon_svg;
612 }
613 $button_content .= '<span class="ctc-cover-text">' . esc_html( $config['text']['button_text'] ) . '</span>';
614 if ( 'right' === $config['icon']['position'] && $icon_svg ) {
615 $button_content .= $icon_svg;
616 }
617
618 ob_start();
619 ?>
620 <div
621 id="<?php echo esc_attr( $id ); ?>"
622 class="ctc-shortcode ctc-shortcode--cover<?php echo esc_attr( $custom_class ); ?>"
623 data-ctc-format="<?php echo esc_attr( $atts['copy-as'] ); ?>"
624 >
625 <div class="ctc-shortcode__content">
626 <?php echo wp_kses_post( $atts['display'] ); ?>
627 </div>
628 <div
629 class="ctc-cover-overlay"
630 data-ctc-copy="<?php echo esc_attr( $atts['text'] ); ?>"
631 data-ctc-success="<?php echo esc_attr( $config['text']['success_text'] ); ?>"
632 <?php if ( ! empty( $atts['target'] ) ) : ?>
633 data-ctc-target="<?php echo esc_attr( $atts['target'] ); ?>"
634 <?php endif; ?>
635 role="button"
636 tabindex="0"
637 aria-label="<?php echo esc_attr( $atts['tooltip'] ); ?>"
638 >
639 <button type="button" class="ctc-cover-button" aria-label="<?php echo esc_attr( $atts['tooltip'] ); ?>">
640 <?php echo $button_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
641 </button>
642 </div>
643 </div>
644 <?php
645 return ob_get_clean();
646 }
647
648 /**
649 * Get icon SVG by key.
650 *
651 * @param string $icon_key Icon key.
652 * @return string SVG markup.
653 */
654 private function get_icon_svg( $icon_key ) {
655 $icons = [
656 'clipboard' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>',
657 'copy' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>',
658 'link' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>',
659 'check' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
660 ];
661
662 return isset( $icons[ $icon_key ] ) ? $icons[ $icon_key ] : $icons['clipboard'];
663 }
664
665 /**
666 * Get success icon SVG.
667 *
668 * @return string SVG markup.
669 */
670 private function get_success_icon_svg() {
671 return '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
672 }
673
674 /**
675 * Get allowed SVG tags for wp_kses.
676 *
677 * @return array Allowed HTML tags and attributes.
678 */
679 private function get_allowed_svg_tags() {
680 return [
681 'svg' => [
682 'xmlns' => true,
683 'fill' => true,
684 'viewbox' => true,
685 'stroke' => true,
686 'width' => true,
687 'height' => true,
688 'class' => true,
689 'aria-hidden' => true,
690 ],
691 'path' => [
692 'd' => true,
693 'stroke-linecap' => true,
694 'stroke-linejoin' => true,
695 'stroke-width' => true,
696 'fill' => true,
697 ],
698 ];
699 }
700
701 /**
702 * Maybe enqueue assets if shortcode was used.
703 *
704 * @return void
705 */
706 public function maybe_enqueue_assets() {
707 if ( ! $this->styles_enqueued ) {
708 return;
709 }
710
711 wp_enqueue_script(
712 'ctc-lib-core',
713 CTC_URI . 'assets/frontend/js/lib/ctc.js',
714 [],
715 CTC_VER,
716 true
717 );
718
719 wp_enqueue_script(
720 'ctc-shortcode',
721 CTC_URI . 'assets/frontend/js/shortcode.js',
722 [ 'ctc-lib-core' ],
723 CTC_VER,
724 true
725 );
726
727 // Enqueue inline CSS (only for presets used on the page, minified).
728 wp_register_style( 'ctc-shortcode', false, [], CTC_VER );
729 wp_enqueue_style( 'ctc-shortcode' );
730 wp_add_inline_style( 'ctc-shortcode', $this->get_inline_css() );
731 }
732
733 /**
734 * Minify CSS: strip comments and collapse whitespace.
735 *
736 * @param string $css Raw CSS.
737 * @return string Minified CSS.
738 */
739 private function minify_css( $css ) {
740 $css = (string) preg_replace( '/\/\*[\s\S]*?\*\//', '', $css );
741 $css = (string) preg_replace( '/\s+/', ' ', $css );
742 return trim( $css );
743 }
744
745 /**
746 * Get inline CSS for shortcode styles.
747 * Builds from base + only the preset chunks that were used, then minifies.
748 *
749 * @return string CSS styles.
750 */
751 private function get_inline_css() {
752 $parts = [ $this->get_css_base() ];
753 $presets = array_keys( $this->used_presets );
754 $chunk_map = [
755 'inline' => 'get_css_inline',
756 'native' => 'get_css_native',
757 'button' => 'get_css_button',
758 'icon' => 'get_css_icon',
759 'cover' => 'get_css_cover',
760 ];
761 foreach ( $presets as $preset ) {
762 if ( isset( $chunk_map[ $preset ] ) ) {
763 $parts[] = $this->{$chunk_map[ $preset ]}();
764 }
765 }
766 return $this->minify_css( implode( "\n", $parts ) );
767 }
768
769 /**
770 * Base + shared CSS (vars, icon, success, table, keyframes). Always included when shortcode is used.
771 *
772 * @return string
773 */
774 private function get_css_base() {
775 return '
776 .ctc-shortcode {
777 --ctc-inline-color: currentColor;
778 --ctc-inline-hover-color: currentColor;
779 }
780 .ctc-shortcode__icon {
781 display: inline-flex;
782 align-items: center;
783 justify-content: center;
784 }
785 .ctc-shortcode__icon svg {
786 width: 14px;
787 height: 14px;
788 }
789 table .ctc-shortcode__icon,
790 table .ctc-shortcode__icon svg,
791 table .ctc-cover-icon,
792 table .ctc-cover-icon svg {
793 width: 14px !important;
794 height: 14px !important;
795 min-width: 14px;
796 min-height: 14px;
797 max-width: 14px !important;
798 max-height: 14px !important;
799 flex-shrink: 0;
800 }
801 table .ctc-cover-icon svg {
802 width: 12px !important;
803 height: 12px !important;
804 max-width: 12px !important;
805 max-height: 12px !important;
806 }
807 .ctc-shortcode__success {
808 position: absolute;
809 pointer-events: none;
810 }
811 .ctc-shortcode--copied {
812 animation: ctc-shortcode-pulse 0.3s ease;
813 }
814 @keyframes ctc-shortcode-pulse {
815 0%, 100% { transform: scale(1); }
816 50% { transform: scale(1.05); }
817 }
818 ';
819 }
820
821 /**
822 * Inline preset CSS.
823 *
824 * @return string
825 */
826 private function get_css_inline() {
827 return '
828 .ctc-shortcode--inline {
829 display: inline-flex;
830 align-items: center;
831 gap: 4px;
832 cursor: pointer;
833 color: var(--ctc-inline-color);
834 transition: opacity 0.15s ease;
835 }
836 .ctc-shortcode--inline:hover {
837 opacity: 0.8;
838 }
839 .ctc-shortcode--inline:focus {
840 outline: 2px solid currentColor;
841 outline-offset: 2px;
842 }
843 .ctc-shortcode--inline .ctc-inline-hidden {
844 display: none;
845 }
846 ';
847 }
848
849 /**
850 * Native preset CSS.
851 *
852 * @return string
853 */
854 private function get_css_native() {
855 return '
856 .ctc-shortcode--native {
857 display: inline-flex;
858 align-items: center;
859 gap: 4px;
860 cursor: pointer;
861 }
862 .ctc-shortcode--native .ctc-shortcode__icon {
863 opacity: 0.7;
864 transition: opacity 0.15s ease;
865 }
866 .ctc-shortcode--native:hover .ctc-shortcode__icon {
867 opacity: 1;
868 }
869 ';
870 }
871
872 /**
873 * Button preset CSS.
874 *
875 * @return string
876 */
877 private function get_css_button() {
878 return '
879 .ctc-shortcode--button {
880 display: inline-flex;
881 align-items: center;
882 gap: 6px;
883 border: none;
884 cursor: pointer;
885 font-family: inherit;
886 line-height: 1.4;
887 transition: all 0.15s ease;
888 color: var(--ctc-text-color, #ffffff);
889 background: var(--ctc-bg, #4f46e5);
890 font-size: var(--ctc-font-size, 13px);
891 font-weight: var(--ctc-font-weight, 600);
892 padding: var(--ctc-padding-y, 8px) var(--ctc-padding-x, 16px);
893 border-radius: var(--ctc-border-radius, 6px);
894 }
895 .ctc-shortcode--button:hover {
896 background: var(--ctc-hover-bg, #4338ca);
897 }
898 .ctc-shortcode--button:focus {
899 outline: 2px solid var(--ctc-bg, #4f46e5);
900 outline-offset: 2px;
901 }
902 .ctc-shortcode--button:active {
903 transform: scale(0.97);
904 }
905 .ctc-shortcode--button .ctc-shortcode__icon svg {
906 width: 14px;
907 height: 14px;
908 }
909 ';
910 }
911
912 /**
913 * Icon preset CSS.
914 *
915 * @return string
916 */
917 private function get_css_icon() {
918 return '
919 .ctc-shortcode--icon {
920 display: inline-flex;
921 align-items: center;
922 justify-content: center;
923 border-style: solid;
924 cursor: pointer;
925 transition: all 0.2s ease;
926 color: var(--ctc-icon-color, #6b7280);
927 background: var(--ctc-icon-bg, transparent);
928 border-color: var(--ctc-icon-border, #d1d5db);
929 border-width: var(--ctc-icon-border-width, 1px);
930 padding: var(--ctc-icon-padding, 6px);
931 border-radius: var(--ctc-icon-radius, 6px);
932 }
933 .ctc-shortcode--icon:hover {
934 color: var(--ctc-icon-hover-color, #374151);
935 border-color: var(--ctc-icon-hover-border, #9ca3af);
936 background: var(--ctc-icon-hover-bg, #f3f4f6);
937 }
938 .ctc-shortcode--icon:focus {
939 outline: 2px solid var(--ctc-icon-border, #d1d5db);
940 outline-offset: 2px;
941 }
942 .ctc-shortcode--icon .ctc-shortcode__icon svg {
943 width: var(--ctc-icon-size, 16px);
944 height: var(--ctc-icon-size, 16px);
945 }
946 ';
947 }
948
949 /**
950 * Cover preset CSS.
951 *
952 * @return string
953 */
954 private function get_css_cover() {
955 return '
956 .ctc-shortcode--cover {
957 position: relative;
958 display: block;
959 }
960 .ctc-shortcode--cover .ctc-shortcode__content {
961 display: block;
962 }
963 .ctc-shortcode--cover .ctc-cover-overlay {
964 position: absolute;
965 inset: 0;
966 z-index: 10;
967 border-radius: inherit;
968 transition: all 0.3s ease;
969 cursor: pointer;
970 opacity: 0;
971 background: rgba(15, 23, 42, 0.2);
972 display: flex;
973 align-items: center;
974 justify-content: center;
975 }
976 .ctc-shortcode--cover:hover .ctc-cover-overlay {
977 opacity: 1;
978 background: rgba(79, 70, 229, 0.3);
979 backdrop-filter: blur(2px);
980 -webkit-backdrop-filter: blur(2px);
981 }
982 .ctc-shortcode--cover .ctc-cover-button {
983 transform: scale(0.9);
984 opacity: 0;
985 transition: all 0.2s ease;
986 color: #0f172a;
987 background: rgba(255, 255, 255, 0.95);
988 font-size: 10px;
989 font-weight: 700;
990 padding: 6px 12px;
991 border-radius: 9999px;
992 display: inline-flex;
993 align-items: center;
994 gap: 6px;
995 border: 1px solid rgba(255, 255, 255, 0.5);
996 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
997 white-space: nowrap;
998 cursor: pointer;
999 }
1000 .ctc-shortcode--cover:hover .ctc-cover-button {
1001 opacity: 1;
1002 transform: scale(1);
1003 }
1004 .ctc-shortcode--cover .ctc-cover-button:hover {
1005 background: #f1f5f9;
1006 transform: scale(1.05);
1007 }
1008 .ctc-shortcode--cover .ctc-cover-icon svg {
1009 width: 12px;
1010 height: 12px;
1011 }
1012 ';
1013 }
1014 }
1015
1016