PluginProbe
WP EXtra – One Click Optimize / trunk
WP EXtra – One Click Optimize vtrunk
8.7.1 8.6.8 8.7.0 trunk 5.9 8.0 8.5.0 8.5.4 8.5.5 8.6.0 8.6.1 8.6.2 8.6.3 8.6.5
wp-extra / src / Modules / Common / TOC / Renderer.php

Renderer.php in WP EXtra – One Click Optimize trunk, at src/Modules/Common/TOC/Renderer.php

269 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TOC Renderer
4 *
5 * Modern, clean HTML and SVG generator for TOC layouts, counters, toggle icons, and sticky drawer.
6 *
7 * @package WPEXtra\Modules\Common\TOC
8 */
9
10 namespace WPEXtra\Modules\Common\TOC;
11
12 use WPEXtra\Helper;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class Renderer {
19
20 /**
21 * Render standard Table of Contents box.
22 *
23 * @param array $headings Nested tree of headings.
24 * @param array $options Optional runtime option overrides.
25 * @return string
26 */
27 public static function render( array $headings, array $options = array() ) {
28 if ( empty( $headings ) ) {
29 return '';
30 }
31
32 $heading_text = isset( $options['heading_text'] ) ? $options['heading_text'] : Helper::get_option( 'heading_text', esc_html__( 'Table of Contents', 'wp-extra' ) );
33 $counter_style = isset( $options['counter'] ) ? $options['counter'] : Helper::get_option( 'counter', 'none' );
34 $collapse_mode = isset( $options['subheading_collapse_mode'] ) ? $options['subheading_collapse_mode'] : Helper::get_option( 'subheading_collapse_mode', 'open_first' );
35 $is_init_hide = (bool) Helper::get_option( 'visibility_hide_by_default', false );
36 $show_toggle = (bool) Helper::get_option( 'visibility', true );
37
38 $container_classes = array(
39 'wptoc-container',
40 'wptoc-layout-default',
41 'wptoc-counter-' . sanitize_html_class( $counter_style ),
42 'wptoc-mode-' . sanitize_html_class( $collapse_mode ),
43 );
44
45 if ( $is_init_hide ) {
46 $container_classes[] = 'wptoc-collapsed';
47 $container_classes[] = 'toc_close';
48 }
49
50 $title_icon = self::get_title_prefix_icon();
51 $toggle_icon = $show_toggle ? self::get_toggle_icon() : '';
52
53 ob_start();
54 ?>
55 <div id="wptoc-container" class="<?php echo esc_attr( implode( ' ', $container_classes ) ); ?>" role="navigation" aria-label="<?php echo esc_attr( $heading_text ); ?>">
56 <div class="wptoc-title-container">
57 <p class="wptoc-title">
58 <?php echo $title_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
59 <span class="wptoc-title-text"><?php echo esc_html( $heading_text ); ?></span>
60 </p>
61 <?php if ( $show_toggle ) : ?>
62 <button type="button" class="wptoc-title-toggle wptoc-toggle" aria-label="<?php echo esc_attr__( 'Toggle table of contents', 'wp-extra' ); ?>" aria-expanded="<?php echo $is_init_hide ? 'false' : 'true'; ?>">
63 <?php echo $toggle_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
64 </button>
65 <?php endif; ?>
66 </div>
67 <nav<?php echo $is_init_hide ? ' style="display: none;"' : ''; ?>>
68 <?php echo self::render_list( $headings, 1, $collapse_mode ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
69 </nav>
70 </div>
71 <?php
72 return ob_get_clean();
73 }
74
75 /**
76 * Recursive nested list renderer.
77 *
78 * @param array $items
79 * @param int $depth
80 * @param string $collapse_mode
81 * @return string
82 */
83 public static function render_list( array $items, $depth = 1, $collapse_mode = 'open_first' ) {
84 if ( empty( $items ) ) {
85 return '';
86 }
87
88 $html = '<ul class="wptoc-list wptoc-depth-' . intval( $depth ) . '">';
89
90 foreach ( $items as $index => $item ) {
91 $has_children = ! empty( $item['children'] );
92 $is_active = ( 0 === $index && 1 === $depth );
93 $li_classes = array(
94 'wptoc-item',
95 'wptoc-heading-level-' . intval( $item['level'] ),
96 );
97
98 if ( $is_active ) {
99 $li_classes[] = 'active';
100 }
101
102 if ( $has_children ) {
103 $li_classes[] = 'wptoc-has-children';
104 if ( 'open_all' === $collapse_mode || ( 'open_first' === $collapse_mode && 0 === $index && 1 === $depth ) ) {
105 $li_classes[] = 'wptoc-open';
106 } else {
107 $li_classes[] = 'wptoc-closed';
108 }
109 }
110
111 $html .= '<li class="' . esc_attr( implode( ' ', $li_classes ) ) . '">';
112 $html .= '<div class="wptoc-item-row' . ( $is_active ? ' active' : '' ) . '">';
113
114 $html .= '<a href="#' . esc_attr( $item['id'] ) . '" class="wptoc-link">'
115 . '<span class="wptoc-text">' . esc_html( $item['title'] ) . '</span>'
116 . '</a>';
117
118 if ( $has_children ) {
119 $html .= '<button type="button" class="wptoc-toggle-sub" aria-label="' . esc_attr__( 'Toggle subheadings', 'wp-extra' ) . '">'
120 . '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'
121 . '</button>';
122 }
123
124 $html .= '</div>';
125
126 if ( $has_children ) {
127 $html .= self::render_list( $item['children'], $depth + 1, $collapse_mode );
128 }
129
130 $html .= '</li>';
131 }
132
133 $html .= '</ul>';
134 return $html;
135 }
136
137 /**
138 * Render Sticky TOC Popup & FAB floating button in wp_footer.
139 *
140 * @param array $headings
141 * @return string
142 */
143 public static function render_sticky_footer( array $headings ) {
144 if ( empty( $headings ) || ! Helper::get_option( 'sticky-toggle', true ) ) {
145 return '';
146 }
147
148 $sticky_pos = Helper::get_option( 'sticky-toggle-position', 'bottom-left' );
149 $pos_class = 'wptoc-pos-' . sanitize_html_class( $sticky_pos );
150 $fab_text = trim( (string) Helper::get_option( 'sticky_button_text', '' ) );
151 $has_fab_text = ! empty( $fab_text );
152 $fab_class = $has_fab_text ? 'wptoc-fab-pill' : 'wptoc-fab-icon-only';
153 $heading_text = Helper::get_option( 'heading_text', esc_html__( 'Table of Contents', 'wp-extra' ) );
154 $counter_style = Helper::get_option( 'counter', 'none' );
155 $collapse_mode = Helper::get_option( 'subheading_collapse_mode', 'open_first' );
156
157 ob_start();
158 ?>
159 <div class="wptoc-sticky-wrapper <?php echo esc_attr( $pos_class ); ?>" role="navigation" aria-label="<?php echo esc_attr__( 'Table of Contents Sticky Navigation', 'wp-extra' ); ?>">
160 <div id="wptoc-sticky-popup" class="wptoc-sticky-popup hide wptoc-counter-<?php echo sanitize_html_class( $counter_style ); ?> <?php echo esc_attr( $pos_class ); ?>">
161 <div class="wptoc-title-container">
162 <p class="wptoc-title">
163 <?php echo self::get_title_prefix_icon(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
164 <span class="wptoc-title-text"><?php echo esc_html( $heading_text ); ?></span>
165 </p>
166 <button type="button" class="wptoc-sticky-close-btn" onclick="wpTOC_hideBar(event)" aria-label="<?php echo esc_attr__( 'Close table of contents', 'wp-extra' ); ?>">
167 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
168 </button>
169 </div>
170 <nav class="wptoc-sidebar">
171 <?php echo self::render_list( $headings, 1, $collapse_mode ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
172 </nav>
173 </div>
174 <a class="wptoc-open-icon wptoc-fab-button <?php echo esc_attr( $pos_class . ' ' . $fab_class ); ?>" href="#" role="button" onclick="wpTOC_showBar(event)" aria-label="<?php echo esc_attr__( 'Open table of contents', 'wp-extra' ); ?>">
175 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
176 <line x1="8" y1="6" x2="21" y2="6"></line>
177 <line x1="8" y1="12" x2="21" y2="12"></line>
178 <line x1="8" y1="18" x2="21" y2="18"></line>
179 <line x1="3" y1="6" x2="3.01" y2="6"></line>
180 <line x1="3" y1="12" x2="3.01" y2="12"></line>
181 <line x1="3" y1="18" x2="3.01" y2="18"></line>
182 </svg>
183 <?php if ( $has_fab_text ) : ?>
184 <span class="wptoc-fab-text"><?php echo esc_html( $fab_text ); ?></span>
185 <?php endif; ?>
186 </a>
187 </div>
188 <?php
189 return ob_get_clean();
190 }
191
192 /**
193 * Render Header Toggle Icon SVG.
194 *
195 * @return string
196 */
197 public static function get_toggle_icon() {
198 $icon_style = Helper::get_option( 'toggle_icon_style', 'chevron' );
199 $svg = '';
200
201 switch ( $icon_style ) {
202 case 'chevron':
203 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
204 break;
205 case 'caret':
206 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>';
207 break;
208 case 'plus_minus':
209 $svg = '<svg class="wptoc-svg-icon-minus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>'
210 . '<svg class="wptoc-svg-icon-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
211 break;
212 case 'grid':
213 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="6" cy="6" r="2.5"/><circle cx="18" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="18" r="2.5"/></svg>';
214 break;
215 case 'list':
216 default:
217 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>';
218 break;
219 }
220
221 return '<span class="wptoc-icon-toggle-span wptoc-icon-style-' . sanitize_html_class( $icon_style ) . '">' . $svg . '</span>';
222 }
223
224 /**
225 * Render Title Prefix Icon SVG.
226 *
227 * @return string
228 */
229 public static function get_title_prefix_icon() {
230 $style = Helper::get_option( 'title_icon_style', 'none' );
231 if ( empty( $style ) || 'none' === $style ) {
232 return '';
233 }
234
235 $svg = '';
236 switch ( $style ) {
237 case 'toc_tree':
238 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="currentColor" focusable="false"><path d="M5 5c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 1 1 0-2Zm3 0h11c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 1 1 0-2Zm-3 8c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h11c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm0-4c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 1 1 0-2Zm3 0h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm-3 8c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Z" fill-rule="evenodd"></path></svg>';
239 break;
240 case 'list':
241 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>';
242 break;
243 case 'bookmark':
244 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>';
245 break;
246 case 'book':
247 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>';
248 break;
249 case 'layers':
250 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>';
251 break;
252 case 'sparkle':
253 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>';
254 break;
255 case 'settings':
256 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
257 break;
258 case 'bullet':
259 $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="6"/></svg>';
260 break;
261 default:
262 $svg = '';
263 break;
264 }
265
266 return ! empty( $svg ) ? '<span class="wptoc-title-icon">' . $svg . '</span>' : '';
267 }
268 }
269