start_controls_section('section_content', [ 'label' => __('Guide', 'thinkrank'), ]); $this->add_control('heading', [ 'label' => __('Title', 'thinkrank'), 'type' => \Elementor\Controls_Manager::TEXT, 'placeholder' => __('How to …', 'thinkrank'), 'label_block' => true, ]); $this->add_control('heading_tag', [ 'label' => __('Heading tag', 'thinkrank'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'h2', 'options' => ['h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => __('Paragraph', 'thinkrank')], ]); $this->add_control('description', [ 'label' => __('Description', 'thinkrank'), 'type' => \Elementor\Controls_Manager::TEXTAREA, ]); $repeater = new \Elementor\Repeater(); $repeater->add_control('title', [ 'label' => __('Step title', 'thinkrank'), 'type' => \Elementor\Controls_Manager::TEXT, 'label_block' => true, ]); $repeater->add_control('text', [ 'label' => __('Step description', 'thinkrank'), 'type' => \Elementor\Controls_Manager::WYSIWYG, ]); $repeater->add_control('image', [ 'label' => __('Step image', 'thinkrank'), 'type' => \Elementor\Controls_Manager::MEDIA, ]); $this->add_control('steps', [ 'label' => __('Steps', 'thinkrank'), 'type' => \Elementor\Controls_Manager::REPEATER, 'fields' => $repeater->get_controls(), 'default' => [['title' => '', 'text' => '']], 'title_field' => '{{{ title }}}', ]); $this->add_control('show_numbers', [ 'label' => __('Numbered steps', 'thinkrank'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'default' => 'yes', ]); $this->end_controls_section(); $this->start_controls_section('section_duration', [ 'label' => __('Total time', 'thinkrank'), ]); foreach (['days' => __('Days', 'thinkrank'), 'hours' => __('Hours', 'thinkrank'), 'minutes' => __('Minutes', 'thinkrank')] as $key => $label) { $this->add_control("total_{$key}", [ 'label' => $label, 'type' => \Elementor\Controls_Manager::NUMBER, 'min' => 0, 'default' => 0, ]); } $this->end_controls_section(); $this->start_controls_section('section_schema', [ 'label' => __('Schema', 'thinkrank'), ]); $this->add_control('output_schema', [ 'label' => __('Output HowTo schema (JSON-LD)', 'thinkrank'), 'description' => __('Adds HowTo structured data for rich results.', 'thinkrank'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'default' => 'yes', ]); $this->end_controls_section(); } protected function render(): void { $settings = $this->get_settings_for_display(); $steps = is_array($settings['steps'] ?? null) ? $settings['steps'] : []; $items = array_values(array_filter($steps, static function ($step) { return !empty($step['title']) || !empty($step['text']) || !empty($step['image']['url']); })); if (empty($items)) { return; } $heading_tag = in_array($settings['heading_tag'] ?? 'h2', ['h2', 'h3', 'h4', 'p'], true) ? $settings['heading_tag'] : 'h2'; $numbered = 'yes' === ($settings['show_numbers'] ?? 'yes'); $list_tag = $numbered ? 'ol' : 'ul'; echo '
'; if (!empty($settings['heading'])) { printf( '<%1$s class="thinkrank-howto__heading">%2$s', esc_html($heading_tag), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html($settings['heading']) ); } if (!empty($settings['description'])) { echo '

' . esc_html($settings['description']) . '

'; } $duration = $this->format_duration($settings); if ('' !== $duration) { echo '

' . esc_html__('Total time:', 'thinkrank') . ' ' . esc_html($duration) . '

'; } echo '<' . $list_tag . ' class="thinkrank-howto__steps">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped foreach ($items as $step) { echo '
  • '; if (!empty($step['title'])) { echo '
    ' . esc_html((string) $step['title']) . '
    '; } if (!empty($step['image']['url'])) { printf( '%s', esc_url((string) $step['image']['url']), esc_attr((string) get_post_meta((int) ($step['image']['id'] ?? 0), '_wp_attachment_image_alt', true)) ); } if (!empty($step['text'])) { echo '
    ' . wp_kses_post((string) $step['text']) . '
    '; } echo '
  • '; } echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
    '; $this->maybe_render_schema($settings, $items); } /** * Human-readable total time, '' when unset. * * @param array $settings Widget settings. * @return string */ private function format_duration(array $settings): string { $days = max(0, (int) ($settings['total_days'] ?? 0)); $hours = max(0, (int) ($settings['total_hours'] ?? 0)); $minutes = max(0, (int) ($settings['total_minutes'] ?? 0)); $parts = []; if ($days > 0) { /* translators: %d: number of days. */ $parts[] = sprintf(_n('%d day', '%d days', $days, 'thinkrank'), $days); } if ($hours > 0) { /* translators: %d: number of hours. */ $parts[] = sprintf(_n('%d hour', '%d hours', $hours, 'thinkrank'), $hours); } if ($minutes > 0) { /* translators: %d: number of minutes. */ $parts[] = sprintf(_n('%d minute', '%d minutes', $minutes, 'thinkrank'), $minutes); } return implode(', ', $parts); } /** * Emit HowTo JSON-LD on the front end (never in the editor preview). * * @param array $settings Widget settings. * @param array $items Non-empty steps. * @return void */ private function maybe_render_schema(array $settings, array $items): void { if ('yes' !== ($settings['output_schema'] ?? 'yes')) { return; } if (\Elementor\Plugin::$instance->editor->is_edit_mode()) { return; } $step_entities = []; foreach ($items as $step) { $title = trim(wp_strip_all_tags((string) ($step['title'] ?? ''))); $text = trim(wp_strip_all_tags((string) ($step['text'] ?? ''))); if ($title === '' && $text === '') { continue; } $entity = ['@type' => 'HowToStep']; if ($title !== '' && $text !== '') { $entity['name'] = $title; $entity['text'] = $text; } else { $entity['text'] = $text !== '' ? $text : $title; } if (!empty($step['image']['url'])) { $entity['image'] = [ '@type' => 'ImageObject', 'url' => esc_url_raw((string) $step['image']['url']), ]; } $step_entities[] = $entity; } if (empty($step_entities)) { return; } $heading = trim(wp_strip_all_tags((string) ($settings['heading'] ?? ''))); $schema = [ '@context' => 'https://schema.org', '@type' => 'HowTo', 'name' => $heading !== '' ? $heading : get_the_title(), 'step' => $step_entities, ]; $description = trim(wp_strip_all_tags((string) ($settings['description'] ?? ''))); if ($description !== '') { $schema['description'] = $description; } $days = max(0, (int) ($settings['total_days'] ?? 0)); $hours = max(0, (int) ($settings['total_hours'] ?? 0)); $minutes = max(0, (int) ($settings['total_minutes'] ?? 0)); if ($days + $hours + $minutes > 0) { $schema['totalTime'] = sprintf('P%dDT%dH%dM', $days, $hours, $minutes); } $json = wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); if (false !== $json) { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } }