PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.6.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.6.0
2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 1.11.0 All 47 releases
thinkrank / includes / editor / elementor / class-toc-widget.php

class-toc-widget.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.6.0, at includes/editor/elementor/class-toc-widget.php

195 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Elementor Table of Contents Widget
5 *
6 * The Elementor counterpart of the thinkrank/toc Gutenberg block. Elementor
7 * content has no block tree to derive headings from server-side, so the list
8 * is built client-side: a small dependency-free script scans the page's
9 * headings, assigns ids where missing, and renders the linked list — the
10 * same approach Elementor Pro's own TOC widget uses.
11 *
12 * @package ThinkRank
13 * @subpackage Editor\Elementor
14 * @since 1.18.0
15 */
16
17 declare(strict_types=1);
18
19 namespace ThinkRank\Editor\Elementor;
20
21 // Prevent direct access
22 if (!defined('ABSPATH')) {
23 exit;
24 }
25
26 /**
27 * TOC Widget.
28 *
29 * @since 1.18.0
30 */
31 class TOC_Widget extends \Elementor\Widget_Base {
32
33 public function get_name(): string {
34 return 'thinkrank-toc';
35 }
36
37 public function get_title(): string {
38 return __('Table of Contents (ThinkRank)', 'thinkrank');
39 }
40
41 public function get_icon(): string {
42 return 'eicon-table-of-contents';
43 }
44
45 public function get_categories(): array {
46 return ['thinkrank'];
47 }
48
49 public function get_keywords(): array {
50 return ['table of contents', 'toc', 'index', 'anchor', 'thinkrank'];
51 }
52
53 public function get_style_depends(): array {
54 return ['thinkrank-toc-block'];
55 }
56
57 protected function register_controls(): void {
58 $this->start_controls_section('section_content', [
59 'label' => __('Table of Contents', 'thinkrank'),
60 ]);
61
62 $this->add_control('heading', [
63 'label' => __('Title', 'thinkrank'),
64 'type' => \Elementor\Controls_Manager::TEXT,
65 'default' => __('Table of Contents', 'thinkrank'),
66 'label_block' => true,
67 ]);
68
69 $this->add_control('heading_tag', [
70 'label' => __('Heading tag', 'thinkrank'),
71 'type' => \Elementor\Controls_Manager::SELECT,
72 'default' => 'h2',
73 'options' => ['h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => __('Paragraph', 'thinkrank')],
74 ]);
75
76 $this->add_control('max_level', [
77 'label' => __('Include headings up to', 'thinkrank'),
78 'type' => \Elementor\Controls_Manager::SELECT,
79 'default' => '3',
80 'options' => ['2' => 'H2', '3' => 'H3', '4' => 'H4'],
81 ]);
82
83 $this->add_control('list_style', [
84 'label' => __('List style', 'thinkrank'),
85 'type' => \Elementor\Controls_Manager::SELECT,
86 'default' => 'disc',
87 'options' => [
88 'disc' => __('Bulleted', 'thinkrank'),
89 'decimal' => __('Numbered', 'thinkrank'),
90 'none' => __('Plain', 'thinkrank'),
91 ],
92 ]);
93
94 $this->end_controls_section();
95 }
96
97 protected function render(): void {
98 $settings = $this->get_settings_for_display();
99
100 $heading_tag = in_array($settings['heading_tag'] ?? 'h2', ['h2', 'h3', 'h4', 'p'], true)
101 ? $settings['heading_tag']
102 : 'h2';
103 $max_level = in_array($settings['max_level'] ?? '3', ['2', '3', '4'], true) ? (int) $settings['max_level'] : 3;
104 $list_style = in_array($settings['list_style'] ?? 'disc', ['disc', 'decimal', 'none'], true)
105 ? $settings['list_style']
106 : 'disc';
107 $widget_id = 'thinkrank-toc-' . esc_attr($this->get_id());
108
109 echo '<div class="thinkrank-toc" id="' . $widget_id . '">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
110
111 if (!empty($settings['heading'])) {
112 printf(
113 '<%1$s class="thinkrank-toc__heading">%2$s</%1$s>',
114 esc_html($heading_tag), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
115 esc_html($settings['heading'])
116 );
117 }
118
119 printf(
120 '<nav aria-label="%s"><ul class="thinkrank-toc__list thinkrank-toc__list--%s"></ul></nav>',
121 esc_attr__('Table of contents', 'thinkrank'),
122 esc_attr($list_style)
123 );
124
125 echo '</div>';
126
127 $this->render_builder_script($widget_id, $max_level);
128 }
129
130 /**
131 * Inline, dependency-free script that builds the list from the page's
132 * headings (h2..maxLevel), assigning ids to headings that lack one.
133 *
134 * @param string $widget_id DOM id of this widget instance.
135 * @param int $max_level Deepest heading level to include.
136 * @return void
137 */
138 private function render_builder_script(string $widget_id, int $max_level): void {
139 $selector = implode(',', array_map(
140 static fn($level) => 'h' . $level,
141 range(2, max(2, $max_level))
142 ));
143
144 ?>
145 <script>
146 ( function() {
147 var init = function() {
148 var widget = document.getElementById( <?php echo wp_json_encode($widget_id); ?> );
149 if ( ! widget ) {
150 return;
151 }
152 var list = widget.querySelector( '.thinkrank-toc__list' );
153 var scope = widget.closest( '[data-elementor-type]' ) || document.body;
154 var used = {};
155 var count = 0;
156 scope.querySelectorAll( <?php echo wp_json_encode($selector); ?> ).forEach( function( el ) {
157 if ( widget.contains( el ) || ! el.textContent.trim() ) {
158 return;
159 }
160 if ( ! el.id ) {
161 var base = el.textContent.trim().toLowerCase()
162 .normalize( 'NFKD' ).replace( /[̀-ͯ]/g, '' )
163 .replace( /[^a-z0-9\s-]/g, '' ).trim()
164 .replace( /[\s-]+/g, '-' ) || 'section';
165 var id = base, n = 2;
166 while ( used[ id ] || document.getElementById( id ) ) {
167 id = base + '-' + ( n++ );
168 }
169 el.id = id;
170 }
171 used[ el.id ] = true;
172 var li = document.createElement( 'li' );
173 li.className = 'thinkrank-toc__item thinkrank-toc__item--level-' + el.tagName.charAt( 1 );
174 var a = document.createElement( 'a' );
175 a.href = '#' + el.id;
176 a.textContent = el.textContent.trim();
177 li.appendChild( a );
178 list.appendChild( li );
179 count++;
180 } );
181 if ( ! count ) {
182 widget.style.display = 'none';
183 }
184 };
185 if ( 'loading' === document.readyState ) {
186 document.addEventListener( 'DOMContentLoaded', init );
187 } else {
188 init();
189 }
190 } )();
191 </script>
192 <?php
193 }
194 }
195