| 1 |
<?php |
| 2 |
/** |
| 3 |
* Table of Contents block for Smart Post Show. |
| 4 |
* |
| 5 |
* @package Smart_Post_Show_Pro |
| 6 |
* @subpackage Smart_Post_Show_Pro/blocks/includes |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace SmartPostShow\Blocks; |
| 10 |
|
| 11 |
use SmartPostShow\Blocks\Helper; |
| 12 |
|
| 13 |
use SmartPostShow\Blocks\Block_Base; |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
/** |
| 19 |
* Table of Contents block class. |
| 20 |
*/ |
| 21 |
class Table_Of_Content extends Block_Base { |
| 22 |
|
| 23 |
/** |
| 24 |
* Set block properties. |
| 25 |
*/ |
| 26 |
protected function set_block_properties() { |
| 27 |
$this->block_name = 'table-of-content'; |
| 28 |
$this->scripts = array( 'sp_smart_post_smart_toc_script' ); |
| 29 |
$this->styles = array( |
| 30 |
'sp_smart_post_blocks_css', |
| 31 |
'sp_smart_post_blocks_style', |
| 32 |
'sp_smart_post_blocks_social_icons_style', |
| 33 |
); |
| 34 |
|
| 35 |
$this->keywords = array( 'TOC', 'contents', 'post contents', 'table of contents', 'headings' ); |
| 36 |
} |
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
/** |
| 41 |
* Render the block content. |
| 42 |
* |
| 43 |
* @param array $attributes Block attributes. |
| 44 |
* @param string $content Block content. |
| 45 |
* @param array $blocks Block instances. |
| 46 |
* @return string |
| 47 |
*/ |
| 48 |
public function render_block( $attributes, $content = '', $blocks = array() ) { |
| 49 |
if ( Helper::is_editor_page() ) { |
| 50 |
return $content; |
| 51 |
} |
| 52 |
if ( ! is_singular() ) { |
| 53 |
return ''; |
| 54 |
} |
| 55 |
|
| 56 |
// Extract attributes. |
| 57 |
$align_class = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : ''; |
| 58 |
$unique_id = $attributes['uniqueId'] ?? ''; |
| 59 |
$supported_heading_tag = $attributes['supportedHeadingTag'] ?? array(); |
| 60 |
$block_name = isset( $attributes['blockName'] ) ? $attributes['blockName'] : ''; |
| 61 |
$title = isset( $attributes['tocHeading'] ) ? $attributes['tocHeading'] : 'Table of Contents'; |
| 62 |
$offset_top = isset( $attributes['offsetTop'] ) ? $attributes['offsetTop'] : 50; |
| 63 |
$list_style = isset( $attributes['listStyle'] ) ? $attributes['listStyle'] : 'bullet'; |
| 64 |
$content_alignment = isset( $attributes['tocAlignment'] ) ? $attributes['tocAlignment'] : 'left'; |
| 65 |
$copy_link = isset( $attributes['copyLink'] ) ? $attributes['copyLink'] : true; |
| 66 |
$smooth_scroll = $attributes['smoothScroll'] ?? true; |
| 67 |
$toc_collapsed = isset( $attributes['tocCollapsed'] ) ? $attributes['tocCollapsed'] : false; |
| 68 |
$collapsed_initially = isset( $attributes['collapsedInitially'] ) ? $attributes['collapsedInitially'] : false; |
| 69 |
$child_item_collapsible = isset( $attributes['childItemCollapsible'] ) ? $attributes['childItemCollapsible'] : false; |
| 70 |
$list_hierarchy = isset( $attributes['listHierarchy'] ) ? $attributes['listHierarchy'] : true; |
| 71 |
$display_control = isset( $attributes['displayControl'] ) ? $attributes['displayControl'] : 'static'; |
| 72 |
$floating_position = isset( $attributes['floatingPosition'] ) ? $attributes['floatingPosition'] : 'top-left'; |
| 73 |
|
| 74 |
// New attributes for collapsible icons. |
| 75 |
$collapsible_icon_source = isset( $attributes['CollapsibleIconSource'] ) ? $attributes['CollapsibleIconSource'] : 'four'; |
| 76 |
$collapsible_button_type = isset( $attributes['CollapsibleButtonType'] ) ? $attributes['CollapsibleButtonType'] : 'icon'; |
| 77 |
$collapse_text = isset( $attributes['collapseText'] ) ? $attributes['collapseText'] : 'Collapse'; |
| 78 |
$expand_text = isset( $attributes['expandText'] ) ? $attributes['expandText'] : 'Expand'; |
| 79 |
$collapsible_color = isset( $attributes['collapsibleColor'] ) ? $attributes['collapsibleColor'] : ''; |
| 80 |
$collapsible_icon_position = isset( $attributes['collapsibleIconPosition'] ) ? $attributes['collapsibleIconPosition'] : 'right'; |
| 81 |
$child_collapsible_icon_source = isset( $attributes['childCollapsibleIconSource'] ) ? $attributes['childCollapsibleIconSource'] : 'four'; |
| 82 |
$heading_hash_url = isset( $attributes['headingHashUrl'] ) ? $attributes['headingHashUrl'] : false; |
| 83 |
$preset = isset( $attributes['preset'] ) ? $attributes['preset'] : 'presetOne'; |
| 84 |
$headings = $this->sps_get_headings_with_id( $supported_heading_tag ); |
| 85 |
|
| 86 |
// Flatten headings if hierarchy is not enabled. |
| 87 |
if ( ! $list_hierarchy ) { |
| 88 |
$headings = $this->flatten_toc( $headings ); |
| 89 |
} |
| 90 |
|
| 91 |
if ( empty( $headings ) ) { |
| 92 |
if ( current_user_can( 'edit_posts' ) ) { |
| 93 |
// Show message to admin/editor. |
| 94 |
return '<div class="sps-toc-empty">No headings found in this post for table of content.</div>'; |
| 95 |
} else { |
| 96 |
return ''; |
| 97 |
} |
| 98 |
} |
| 99 |
$floating_position_class = ''; |
| 100 |
if ( 'floating' === $display_control ) { |
| 101 |
$floating_position_class = 'sp-toc-floating-' . $floating_position; |
| 102 |
} |
| 103 |
|
| 104 |
// Define icon SVGs. |
| 105 |
$icon_svgs = array( |
| 106 |
'one' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9L12 15L18 9" stroke="' . $collapsible_color['color'] . '" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>', |
| 107 |
'two' => '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 14 8" fill="none"><path d="M1 1L7 7L13 1" stroke="' . $collapsible_color['color'] . '" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>', |
| 108 |
'three' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.325 16.325L4.225 9.225C3.925 8.925 3.925 8.325 4.225 8.025L5.02499 7.225C5.32499 6.925 5.925 6.925 6.225 7.225L11.925 12.825L17.625 7.225C17.925 6.925 18.525 6.925 18.825 7.225L19.625 8.025C19.925 8.325 19.925 8.925 19.625 9.225L12.525 16.325C12.225 16.725 11.625 16.725 11.325 16.325Z" fill="' . $collapsible_color['color'] . '" /></svg>', |
| 109 |
'four_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.75 4C12.1642 4 12.5 4.33579 12.5 4.75V11H18.75C19.1642 11 19.5 11.3358 19.5 11.75C19.5 12.1642 19.1642 12.5 18.75 12.5H12.5V18.75C12.5 19.1642 12.1642 19.5 11.75 19.5C11.3358 19.5 11 19.1642 11 18.75V12.5H4.75C4.33579 12.5 4 12.1642 4 11.75C4 11.3358 4.33579 11 4.75 11H11V4.75C11 4.33579 11.3358 4 11.75 4Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 110 |
'four_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.75 11C19.1642 11 19.5 11.3358 19.5 11.75C19.5 12.1642 19.1642 12.5 18.75 12.5H4.75C4.33579 12.5 4 12.1642 4 11.75C4 11.3358 4.33579 11 4.75 11H18.75Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 111 |
'five_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 4C12.5523 4 13 4.44772 13 5V10.5H18.5C19.0523 10.5 19.5 10.9477 19.5 11.5V12C19.5 12.5523 19.0523 13 18.5 13H13V18.5C13 19.0523 12.5523 19.5 12 19.5H11.5C10.9477 19.5 10.5 19.0523 10.5 18.5V13H5C4.44772 13 4 12.5523 4 12V11.5C4 10.9477 4.44772 10.5 5 10.5H10.5V5C10.5 4.44772 10.9477 4 11.5 4H12Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 112 |
'five_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.5625 10.675C19.1148 10.675 19.5625 11.1228 19.5625 11.675V12.175C19.5625 12.7273 19.1148 13.175 18.5625 13.175H5.0625C4.51022 13.175 4.0625 12.7273 4.0625 12.175V11.675C4.0625 11.1228 4.51022 10.675 5.0625 10.675H18.5625Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 113 |
'six' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M16.925 11.1L17.5249 11.7C17.8249 12 17.8249 12.4 17.5249 12.6L12.325 17.8C12.025 18.1 11.625 18.1 11.425 17.8L6.225 12.6C5.925 12.3 5.925 11.9 6.225 11.7L6.82498 11.1C7.12498 10.8 7.525 10.9 7.725 11.1L10.825 14.3V6.6C10.825 6.2 11.125 6 11.425 6H12.325C12.725 6 12.925 6.3 12.925 6.6V14.3L16.0249 11.1C16.2249 10.9 16.625 10.9 16.925 11.1Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 114 |
'seven_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M18.2002 10C18.2002 5.47126 14.5287 1.7998 10 1.7998C5.47126 1.7998 1.7998 5.47126 1.7998 10C1.7998 14.5287 5.47126 18.2002 10 18.2002V20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20V18.2002C14.5287 18.2002 18.2002 14.5287 18.2002 10Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.2994 8.94904H11.051V5.70064C11.051 5.31847 10.7325 5 10.3503 5H9.64968C9.26752 5 8.94904 5.31847 8.94904 5.70064V8.94904H5.70064C5.31847 8.94904 5 9.26752 5 9.64968V10.3503C5 10.7325 5.31847 11.051 5.70064 11.051H8.94904V14.2994C8.94904 14.6815 9.26752 15 9.64968 15H10.3503C10.7325 15 11.051 14.6815 11.051 14.2994V11.051H14.2994C14.6815 11.051 15 10.7325 15 10.3503V9.64968C15 9.26752 14.6815 8.94904 14.2994 8.94904Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 115 |
'seven_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M18.2002 10C18.2002 5.47126 14.5287 1.7998 10 1.7998C5.47126 1.7998 1.7998 5.47126 1.7998 10C1.7998 14.5287 5.47126 18.2002 10 18.2002V20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20V18.2002C14.5287 18.2002 18.2002 14.5287 18.2002 10Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.3082 9H5.69182C5.31446 9 5 9.30303 5 9.66667V10.3333C5 10.697 5.31446 11 5.69182 11H14.3082C14.6855 11 15 10.697 15 10.3333V9.66667C15 9.30303 14.6855 9 14.3082 9Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 116 |
'eight' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 20C7.32891 20 4.8177 18.9598 2.92891 17.0711C1.0402 15.1823 0 12.6711 0 10C0 7.32891 1.0402 4.8177 2.92891 2.92891C4.8177 1.0402 7.32891 0 10 0C12.6711 0 15.1823 1.0402 17.0711 2.92891C18.9598 4.81766 20 7.32891 20 10C20 12.6711 18.9598 15.1823 17.0711 17.0711C15.1823 18.9598 12.6711 20 10 20ZM10 1.5625C7.74625 1.5625 5.62742 2.44016 4.03379 4.03379C2.44016 5.62742 1.5625 7.74629 1.5625 10C1.5625 12.2537 2.44016 14.3726 4.03379 15.9662C5.62742 17.5598 7.74629 18.4375 10 18.4375C12.2537 18.4375 14.3726 17.5598 15.9662 15.9662C17.5598 14.3726 18.4375 12.2537 18.4375 10C18.4375 7.74629 17.5598 5.62742 15.9662 4.03379C14.3726 2.44016 12.2537 1.5625 10 1.5625Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.9308 6.79996C14.7309 6.79996 14.5309 6.87625 14.3784 7.02879L10.001 11.4061L5.62371 7.02879C5.31859 6.72367 4.82394 6.72367 4.51886 7.02879C4.21378 7.33391 4.21375 7.82855 4.51886 8.13363L9.44863 13.0634C9.75375 13.3685 10.2484 13.3685 10.5535 13.0634L15.4832 8.13363C15.7884 7.82852 15.7884 7.33387 15.4832 7.02879C15.3307 6.87625 15.1308 6.79996 14.9308 6.79996Z" fill="' . $collapsible_color['color'] . '"/></svg>', |
| 117 |
); |
| 118 |
|
| 119 |
// Get the appropriate icon based on source and expanded state. |
| 120 |
$get_collapsible_icon = function ( $is_expanded ) use ( $collapsible_icon_source, $icon_svgs ) { |
| 121 |
switch ( $collapsible_icon_source ) { |
| 122 |
case 'one': |
| 123 |
return $icon_svgs['one']; |
| 124 |
case 'two': |
| 125 |
return $icon_svgs['two']; |
| 126 |
case 'three': |
| 127 |
return $icon_svgs['three']; |
| 128 |
case 'four': |
| 129 |
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed']; |
| 130 |
case 'five': |
| 131 |
return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed']; |
| 132 |
case 'six': |
| 133 |
return $icon_svgs['six']; |
| 134 |
case 'seven': |
| 135 |
return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed']; |
| 136 |
case 'eight': |
| 137 |
return $icon_svgs['eight']; |
| 138 |
default: |
| 139 |
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed']; |
| 140 |
} |
| 141 |
}; |
| 142 |
|
| 143 |
// Get the appropriate child icon based on source and expanded state. |
| 144 |
$get_child_collapsible_icon = function ( $is_expanded ) use ( $child_collapsible_icon_source, $icon_svgs ) { |
| 145 |
switch ( $child_collapsible_icon_source ) { |
| 146 |
case 'one': |
| 147 |
return $icon_svgs['one']; |
| 148 |
case 'two': |
| 149 |
return $icon_svgs['two']; |
| 150 |
case 'three': |
| 151 |
return $icon_svgs['three']; |
| 152 |
case 'four': |
| 153 |
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed']; |
| 154 |
case 'five': |
| 155 |
return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed']; |
| 156 |
case 'six': |
| 157 |
return $icon_svgs['six']; |
| 158 |
case 'seven': |
| 159 |
return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed']; |
| 160 |
case 'eight': |
| 161 |
return $icon_svgs['eight']; |
| 162 |
default: |
| 163 |
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed']; |
| 164 |
} |
| 165 |
}; |
| 166 |
|
| 167 |
ob_start(); |
| 168 |
?> |
| 169 |
<div id="<?php echo esc_attr( $unique_id ); ?>" |
| 170 |
class="sp-table-of-content-toc sp-smart-post-<?php echo esc_attr( $block_name ); ?> <?php echo esc_attr( $align_class ); ?> sp-toc-position-<?php echo esc_attr( $display_control ); ?> <?php echo esc_attr( $floating_position_class ); ?>" |
| 171 |
data-smooth-scroll="<?php echo esc_attr( $smooth_scroll ? 'true' : 'false' ); ?>" |
| 172 |
data-offset-top="<?php echo esc_attr( $offset_top ); ?>" |
| 173 |
data-button-type="<?php echo esc_attr( $collapsible_button_type ); ?>" |
| 174 |
data-collapse-text="<?php echo esc_attr( $collapse_text ); ?>" |
| 175 |
data-expand-text="<?php echo esc_attr( $expand_text ); ?>" |
| 176 |
data-icon-source="<?php echo esc_attr( $collapsible_icon_source ); ?>" |
| 177 |
data-child-icon-source="<?php echo esc_attr( $child_collapsible_icon_source ); ?>" |
| 178 |
data-preset="<?php echo esc_attr( $preset ); ?>" |
| 179 |
data-hash-url="<?php echo esc_attr( $heading_hash_url ); ?>"> |
| 180 |
<div class="sps-toc-wrapper" |
| 181 |
data-icons='<?php echo esc_attr( wp_json_encode( $icon_svgs ) ); ?>'> |
| 182 |
|
| 183 |
|
| 184 |
<div class="sps-toc-header-row"> |
| 185 |
<div class="sps-toc-header"> |
| 186 |
<strong class="sps-toc-title"><?php echo esc_html( $title ); ?></strong> |
| 187 |
|
| 188 |
<?php if ( 'icon' === $collapsible_button_type && 'besideTitle' === $collapsible_icon_position && $toc_collapsed ) : ?> |
| 189 |
<button |
| 190 |
class="sps-toc-main-toggle <?php echo ! $collapsed_initially ? 'expanded' : ''; ?>" |
| 191 |
aria-label="<?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?>" |
| 192 |
aria-expanded="<?php echo ! $collapsed_initially ? 'true' : 'false'; ?>"> |
| 193 |
|
| 194 |
<?php |
| 195 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 196 |
echo $get_collapsible_icon( ! $collapsed_initially ); |
| 197 |
?> |
| 198 |
|
| 199 |
</button> |
| 200 |
<?php endif; ?> |
| 201 |
|
| 202 |
</div> |
| 203 |
<?php if ( $toc_collapsed ) : ?> |
| 204 |
<div class="sp-main-collapse-toggle"> |
| 205 |
|
| 206 |
<?php if ( 'right' === $collapsible_icon_position || 'icon' !== $collapsible_button_type ) : ?> |
| 207 |
<button |
| 208 |
class="sps-toc-main-toggle <?php echo ! $collapsed_initially ? 'expanded' : ''; ?>" |
| 209 |
aria-label="<?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?>" |
| 210 |
aria-expanded="<?php echo ! $collapsed_initially ? 'true' : 'false'; ?>"> |
| 211 |
|
| 212 |
<?php if ( 'icon' === $collapsible_button_type ) : ?> |
| 213 |
<?php |
| 214 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 215 |
echo $get_collapsible_icon( ! $collapsed_initially ); |
| 216 |
?> |
| 217 |
|
| 218 |
<?php elseif ( 'text' === $collapsible_button_type ) : ?> |
| 219 |
<?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?> |
| 220 |
<?php endif; ?> |
| 221 |
|
| 222 |
</button> |
| 223 |
<?php endif; ?> |
| 224 |
|
| 225 |
</div> |
| 226 |
|
| 227 |
<?php endif; ?> |
| 228 |
</div> |
| 229 |
|
| 230 |
<!-- Always render the container but hide it with CSS when initially collapsed --> |
| 231 |
<div class="sps-toc-container <?php echo esc_attr( $preset ); ?><?php echo ( $toc_collapsed && $collapsed_initially ) ? ' sps-toc-hidden' : ''; ?>"> |
| 232 |
<?php if ( in_array( $preset, array( 'presetThree', 'presetFour', 'presetFive' ) ) ) : ?> |
| 233 |
<div class="sps-toc-indicator"></div> |
| 234 |
<?php endif; ?> |
| 235 |
<ul class="sps-toc-list sps-toc-style-<?php echo esc_attr( $list_style ); ?>"> |
| 236 |
<?php $this->render_toc_items( $headings, $list_style, 0, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?> |
| 237 |
</ul> |
| 238 |
</div> |
| 239 |
</div> |
| 240 |
</div> |
| 241 |
<?php |
| 242 |
return ob_get_clean(); |
| 243 |
} |
| 244 |
|
| 245 |
/** |
| 246 |
* Flatten TOC structure if hierarchy is disabled. |
| 247 |
* |
| 248 |
* @param array $items TOC items. |
| 249 |
* @return array Flattened TOC items. |
| 250 |
*/ |
| 251 |
private function flatten_toc( $items ) { |
| 252 |
$result = array(); |
| 253 |
|
| 254 |
foreach ( $items as $item ) { |
| 255 |
// Add current item. |
| 256 |
$result[] = array( |
| 257 |
'id' => $item['id'], |
| 258 |
'text' => $item['text'], |
| 259 |
'level' => $item['level'], |
| 260 |
); |
| 261 |
|
| 262 |
// Recursively add children. |
| 263 |
if ( ! empty( $item['children'] ) ) { |
| 264 |
$children = $this->flatten_toc( $item['children'] ); |
| 265 |
$result = array_merge( $result, $children ); |
| 266 |
} |
| 267 |
} |
| 268 |
|
| 269 |
return $result; |
| 270 |
} |
| 271 |
|
| 272 |
/** |
| 273 |
* Recursive function to render TOC items. |
| 274 |
* |
| 275 |
* @param array $items Array of TOC items to render. |
| 276 |
* @param string $list_style The list style to apply. |
| 277 |
* @param int $depth Current depth level. |
| 278 |
* @param string $content_alignment for content alignment. |
| 279 |
* @param string $copy_link to copy link. |
| 280 |
* @param bool $child_item_collapsible Whether child items can be collapsed. |
| 281 |
* @param callable $get_child_collapsible_icon Function to get child icon. |
| 282 |
* @param string $heading_hash_url toggle. |
| 283 |
* @param string $preset The current preset. |
| 284 |
*/ |
| 285 |
private function render_toc_items( $items, $list_style = '', $depth = 0, $content_alignment = 'left', $copy_link = true, $child_item_collapsible = false, $get_child_collapsible_icon = null, $heading_hash_url = true, $preset = 'presetOne' ) { |
| 286 |
foreach ( $items as $item ) { |
| 287 |
if ( empty( $item['id'] ) || empty( $item['text'] ) ) { |
| 288 |
continue; |
| 289 |
} |
| 290 |
|
| 291 |
$has_children = ! empty( $item['children'] ) && is_array( $item['children'] ); |
| 292 |
$is_selected = false; // This will be handled in JavaScript. |
| 293 |
?> |
| 294 |
<li class="sps-toc-item <?php echo $has_children ? 'has-children' : ''; ?> sps-toc-level-<?php echo esc_attr( $item['level'] ); ?>" |
| 295 |
data-depth="<?php echo esc_attr( $depth ); ?>" |
| 296 |
data-item-id="<?php echo esc_attr( $item['id'] ); ?>"> |
| 297 |
<div class="sps-toc-link-wrapper sps-toc-<?php echo esc_attr( $content_alignment ); ?> "> |
| 298 |
<a href="#<?php echo esc_attr( $item['id'] ); ?>" class="sps-toc-link"> |
| 299 |
<?php if ( 'none' !== $list_style && 'bullet' !== $list_style ) : ?> |
| 300 |
<span class="sps-toc-number"></span> |
| 301 |
<?php endif; ?> |
| 302 |
<span class="sps-toc-text"><?php echo esc_html( $item['text'] ); ?></span> |
| 303 |
</a> |
| 304 |
|
| 305 |
<div class="sps-toc-actions"> |
| 306 |
<?php if ( $copy_link ) : ?> |
| 307 |
<button class="sps-toc-copy" |
| 308 |
data-clipboard-text="<?php echo esc_url( get_permalink() . '#' . $item['id'] ); ?>" |
| 309 |
title="Copy link" |
| 310 |
aria-label="Copy link to this section"> |
| 311 |
# |
| 312 |
</button> |
| 313 |
<?php endif; ?> |
| 314 |
|
| 315 |
<?php if ( $has_children && $child_item_collapsible && is_callable( $get_child_collapsible_icon ) ) : ?> |
| 316 |
<button |
| 317 |
class="sps-toc-toggle expanded" |
| 318 |
aria-label="<?php echo esc_attr__( 'Collapse section', 'post-carousel' ); ?>" |
| 319 |
aria-expanded="true"> |
| 320 |
<?php |
| 321 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 322 |
echo $get_child_collapsible_icon( true ); |
| 323 |
?> |
| 324 |
</button> |
| 325 |
|
| 326 |
<?php endif; ?> |
| 327 |
</div> |
| 328 |
</div> |
| 329 |
|
| 330 |
<?php if ( $has_children ) : ?> |
| 331 |
<ul class="sps-toc-sublist"> |
| 332 |
<?php $this->render_toc_items( $item['children'], $list_style, $depth + 1, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?> |
| 333 |
</ul> |
| 334 |
<?php endif; ?> |
| 335 |
</li> |
| 336 |
<?php |
| 337 |
} |
| 338 |
} |
| 339 |
|
| 340 |
|
| 341 |
/** |
| 342 |
* Sps_get_headings_with_id function |
| 343 |
* |
| 344 |
* @param array $supported_heading_tag All heading tags. |
| 345 |
* @return array |
| 346 |
*/ |
| 347 |
private function sps_get_headings_with_id( $supported_heading_tag ) { |
| 348 |
$content = get_the_content(); |
| 349 |
$headings = array(); |
| 350 |
|
| 351 |
// Extract allowed heading numbers from $supported_heading_tag. |
| 352 |
$allowed_levels = array_map( |
| 353 |
function ( $item ) { |
| 354 |
return intval( substr( $item['value'], 1 ) ); // 'h2' → 2 |
| 355 |
}, |
| 356 |
$supported_heading_tag |
| 357 |
); |
| 358 |
|
| 359 |
$pattern = '/<h([1-6])([^>]*)>(.*?)<\/h\1>/is'; |
| 360 |
|
| 361 |
if ( preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER ) ) { |
| 362 |
$stack = array(); |
| 363 |
|
| 364 |
foreach ( $matches as $m ) { |
| 365 |
$level = intval( $m[1] ); |
| 366 |
|
| 367 |
// Skip headings not in allowed_levels. |
| 368 |
if ( ! in_array( $level, $allowed_levels ) ) { |
| 369 |
continue; |
| 370 |
} |
| 371 |
|
| 372 |
$attrs = $m[2]; |
| 373 |
$inner = $m[3]; |
| 374 |
$html = $m[0]; |
| 375 |
|
| 376 |
if ( preg_match( '/id="([^"]+)"/', $attrs, $id_match ) ) { |
| 377 |
$id = $id_match[1]; |
| 378 |
} else { |
| 379 |
$id = $this->generate_heading_id( wp_strip_all_tags( $inner ) ); |
| 380 |
$html = preg_replace( |
| 381 |
'/<h([1-6])([^>]*)>/i', |
| 382 |
'<h$1 id="' . $id . '"$2>', |
| 383 |
$html, |
| 384 |
1 |
| 385 |
); |
| 386 |
} |
| 387 |
|
| 388 |
$item = array( |
| 389 |
'level' => $level, |
| 390 |
'text' => wp_strip_all_tags( $inner ), |
| 391 |
'id' => $id, |
| 392 |
'html' => $html, |
| 393 |
'children' => array(), |
| 394 |
); |
| 395 |
|
| 396 |
// Build nested structure. |
| 397 |
while ( ! empty( $stack ) && $level <= end( $stack )['level'] ) { |
| 398 |
array_pop( $stack ); |
| 399 |
} |
| 400 |
|
| 401 |
if ( empty( $stack ) ) { |
| 402 |
$headings[] = $item; |
| 403 |
$stack[] = &$headings[ count( $headings ) - 1 ]; |
| 404 |
} else { |
| 405 |
$parent = &$stack[ count( $stack ) - 1 ]; |
| 406 |
$parent['children'][] = $item; |
| 407 |
$stack[] = &$parent['children'][ count( $parent['children'] ) - 1 ]; |
| 408 |
} |
| 409 |
|
| 410 |
unset( $item, $parent ); |
| 411 |
} |
| 412 |
} |
| 413 |
|
| 414 |
return $headings; |
| 415 |
} |
| 416 |
|
| 417 |
/** |
| 418 |
* Generate heading id function |
| 419 |
* |
| 420 |
* @param string $text Heading text. |
| 421 |
* @return string |
| 422 |
*/ |
| 423 |
private function generate_heading_id( $text ) { |
| 424 |
$id = mb_strtolower( $text, 'UTF-8' ); |
| 425 |
$id = preg_replace( '/[\s_.,!?;:()\[\]{}"\'`~@#$%^&*+=<>\/\\\\|]+/u', '-', $id ); |
| 426 |
$id = preg_replace( '/[^\p{L}\p{N}-]/u', '', $id ); |
| 427 |
$id = preg_replace( '/-+/', '-', $id ); |
| 428 |
$id = trim( $id, '-' ); |
| 429 |
|
| 430 |
if ( empty( $id ) ) { |
| 431 |
$id = 'heading-' . uniqid(); |
| 432 |
} |
| 433 |
return $id; |
| 434 |
} |
| 435 |
} |
| 436 |
|