__('Table of Contents (ThinkRank)', 'thinkrank'), 'description' => __('An automatic index of the page headings, with navigation schema.', 'thinkrank'), 'category' => __('ThinkRank', 'thinkrank'), 'slug' => self::SLUG, 'dir' => THINKRANK_PLUGIN_DIR . 'includes/editor/beaver/toc/', 'url' => THINKRANK_PLUGIN_URL . 'includes/editor/beaver/toc/', 'partial_refresh' => true, ]); } /** * Render the shell, then the script that fills it. * * @since 2.5.0 * @param object|array $settings Module settings. * @return void */ public function render_content($settings): void { $settings = ThinkRank_Beaver_FAQ_Module::to_array($settings); wp_enqueue_style('thinkrank-toc-block'); $max_level = self::max_level($settings); $list_style = self::list_style($settings); // Beaver Builder already owns the module's own id/classes on its // wrapper, so the script addresses this instance through a data // attribute of ours instead of competing for the id. `$this->node` is // the layout node id, unique per module instance on the page. $uid = sanitize_html_class((string) $this->node); $output = '
'; $heading = trim((string) ($settings['heading'] ?? '')); if ('' !== $heading) { $output .= sprintf( '<%1$s class="thinkrank-toc__heading">%2$s', esc_html(self::heading_tag($settings)), esc_html($heading) ); } $output .= sprintf( '', esc_attr__('Table of contents', 'thinkrank'), esc_attr($list_style) ); $output .= '
'; echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped // The TOC schema is built client-side, so it bypasses both Schema_Graph // and the block filter and kept publishing an ItemList with Schema // switched off (#688). $schema = !empty($settings['output_schema']) && (!class_exists('ThinkRank\\Frontend\\Schema_Graph') || \ThinkRank\Frontend\Schema_Graph::output_allowed()); $this->render_builder_script($uid, $max_level, $schema); } /** * The dependency-free script that fills the list in. * * It scans Beaver Builder's content wrapper for h2..maxLevel, gives each * heading an id if it has none, and appends one linked item per heading. * With schema on it then publishes the same anchors as * SiteNavigationElement, so the list and the structured data cannot drift. * * The module hides itself when the page has no headings — an empty "Table * of Contents" box is worse than none. * * @param string $uid This module instance's layout node id. * @param int $max_level Deepest heading level to include. * @param bool $schema Whether to publish SiteNavigationElement. * @return void */ private function render_builder_script(string $uid, int $max_level, bool $schema): void { $selector = implode(',', array_map( static fn($level) => 'h' . $level, range(2, max(2, $max_level)) )); ?> [ 'title' => __('Contents', 'thinkrank'), 'sections' => [ 'content' => [ 'title' => '', 'fields' => [ 'heading' => [ 'type' => 'text', 'label' => __('Title', 'thinkrank'), 'default' => __('Table of Contents', 'thinkrank'), 'connections' => ['string'], ], 'heading_tag' => [ 'type' => 'select', 'label' => __('Heading tag', 'thinkrank'), 'default' => 'h2', 'options' => [ 'h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => __('Paragraph', 'thinkrank'), ], ], 'max_level' => [ 'type' => 'select', 'label' => __('Include headings up to', 'thinkrank'), 'default' => '3', 'options' => ['2' => 'H2', '3' => 'H3', '4' => 'H4'], ], 'list_style' => [ 'type' => 'select', 'label' => __('List style', 'thinkrank'), 'default' => 'disc', 'options' => [ 'disc' => __('Bulleted', 'thinkrank'), 'decimal' => __('Numbered', 'thinkrank'), 'none' => __('Plain', 'thinkrank'), ], ], ], ], ], ], 'schema' => [ 'title' => __('Schema', 'thinkrank'), 'sections' => [ 'schema' => [ 'title' => '', 'fields' => [ 'output_schema' => [ 'type' => 'select', 'label' => __('Output navigation schema (JSON-LD)', 'thinkrank'), 'default' => '1', 'options' => [ '1' => __('Yes', 'thinkrank'), '0' => __('No', 'thinkrank'), ], 'help' => __('Adds SiteNavigationElement structured data for the listed sections.', 'thinkrank'), ], ], ], ], ], ]);