controls['heading'] = [ 'label' => esc_html__('Title', 'thinkrank'), 'type' => 'text', 'default' => esc_html__('Table of Contents', 'thinkrank'), ]; $this->controls['headingTag'] = [ 'label' => esc_html__('Heading tag', 'thinkrank'), 'type' => 'select', 'options' => [ 'h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => esc_html__('Paragraph', 'thinkrank'), ], 'default' => 'h2', 'inline' => true, ]; $this->controls['maxLevel'] = [ 'label' => esc_html__('Include headings up to', 'thinkrank'), 'type' => 'select', 'options' => ['2' => 'H2', '3' => 'H3', '4' => 'H4'], 'default' => '3', 'inline' => true, ]; $this->controls['listStyle'] = [ 'label' => esc_html__('List style', 'thinkrank'), 'type' => 'select', 'options' => [ 'disc' => esc_html__('Bulleted', 'thinkrank'), 'decimal' => esc_html__('Numbered', 'thinkrank'), 'none' => esc_html__('Plain', 'thinkrank'), ], 'default' => 'disc', 'inline' => true, ]; $this->controls['outputSchema'] = [ 'label' => esc_html__('Output navigation schema (JSON-LD)', 'thinkrank'), 'type' => 'checkbox', 'default' => true, 'description' => esc_html__('Adds SiteNavigationElement structured data for the listed sections.', 'thinkrank'), ]; } /** * Render. * * @return void */ public function render(): void { $settings = $this->settings; $max_level = self::max_level($settings); $list_style = self::list_style($settings); // Bricks already puts its own `brxe-` on the root, and // `set_attribute()` appends rather than replaces — setting an `id` here // produced `id="brxe-etoc thinkrank-toc-etoc"`, one invalid id // containing a space that `getElementById()` could never match. A data // attribute is ours alone and collides with nothing. $uid = sanitize_html_class((string) $this->id); $this->set_attribute('_root', 'class', 'thinkrank-toc'); $this->set_attribute('_root', 'data-thinkrank-toc', $uid); $output = '
render_attributes('_root') . '>'; $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($this->render_dynamic_data($heading)) ); } $output .= sprintf( '', esc_attr__('Table of contents', 'thinkrank'), esc_attr($list_style) ); $output .= '
'; echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->render_builder_script($uid, $max_level, !empty($settings['outputSchema'])); } /** * The dependency-free script that fills the list in. * * It scans the Bricks 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 apart. * * The element hides itself when the page has no headings — an empty * "Table of Contents" box is worse than none. * * @param string $uid This element instance's Bricks 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)) )); ?>