__('HowTo (ThinkRank)', 'thinkrank'), 'description' => __('A numbered list of steps that emits HowTo schema.', 'thinkrank'), 'category' => __('ThinkRank', 'thinkrank'), 'slug' => self::SLUG, 'dir' => THINKRANK_PLUGIN_DIR . 'includes/editor/beaver/howto/', 'url' => THINKRANK_PLUGIN_URL . 'includes/editor/beaver/howto/', 'partial_refresh' => true, ]); } /** * Render the steps. * * @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); $items = self::usable_steps($settings); if (empty($items)) { if (self::in_builder()) { echo '
' . esc_html__('Add a step to get started.', 'thinkrank') . '
'; } return; } wp_enqueue_style('thinkrank-howto-block'); $heading_tag = self::heading_tag($settings); $list_tag = empty($settings['show_numbers']) ? 'ul' : 'ol'; $output = '
'; $heading = trim((string) ($settings['heading'] ?? '')); if ('' !== $heading) { $output .= sprintf( '<%1$s class="thinkrank-howto__heading">%2$s', esc_html($heading_tag), esc_html($heading) ); } $description = trim((string) ($settings['description'] ?? '')); if ('' !== $description) { $output .= '

' . esc_html($description) . '

'; } $duration = self::format_duration($settings); if ('' !== $duration) { $output .= '

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

'; } $output .= '<' . $list_tag . ' class="thinkrank-howto__steps">'; foreach ($items as $step) { $output .= '
  • '; $title = trim((string) ($step['title'] ?? '')); if ('' !== $title) { $output .= '
    ' . esc_html($title) . '
    '; } $image = self::step_image($step); if (null !== $image) { $output .= sprintf( '%s', esc_url($image['url']), esc_attr($image['alt']) ); } $text = trim((string) ($step['text'] ?? '')); if ('' !== $text) { $output .= '
    ' . wp_kses_post($text) . '
    '; } $output .= '
  • '; } $output .= '
    '; echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->maybe_render_schema($settings, $items); } /** * The repeater rows worth rendering. * * @since 2.5.0 * @param array $settings Module settings. * @return array */ public static function usable_steps(array $settings): array { $steps = is_array($settings['steps'] ?? null) ? $settings['steps'] : []; return array_values(array_filter($steps, static function ($step) { return is_array($step) && (!empty($step['title']) || !empty($step['text']) || !empty($step['image'])); })); } /** * A step's image, resolved to a url and alt text. * * Beaver Builder's photo field stores an attachment id, so the url is * looked up rather than read off the setting. * * @since 2.5.0 * @param array $step One step row. * @return array{url:string,alt:string}|null */ private static function step_image(array $step): ?array { $id = (int) ($step['image'] ?? 0); if ($id <= 0) { return null; } $url = wp_get_attachment_image_url($id, 'large'); if (!$url) { return null; } return [ 'url' => (string) $url, 'alt' => (string) get_post_meta($id, '_wp_attachment_image_alt', true), ]; } /** * The duration fields as [days, hours, minutes]. * * @since 2.5.0 * @param array $settings Module settings. * @return array{0:int,1:int,2:int} */ private static function duration_parts(array $settings): array { return [ max(0, (int) ($settings['total_days'] ?? 0)), max(0, (int) ($settings['total_hours'] ?? 0)), max(0, (int) ($settings['total_minutes'] ?? 0)), ]; } /** * The duration as human-readable text, or '' when nothing is set. * * @since 2.5.0 * @param array $settings Module settings. * @return string */ private static function format_duration(array $settings): string { [$days, $hours, $minutes] = self::duration_parts($settings); $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); } /** * Whether Beaver Builder's editor is rendering this. * * @since 2.5.0 * @return bool */ private static function in_builder(): bool { return class_exists('\\FLBuilderModel') && (bool) \FLBuilderModel::is_builder_active(); } /** * Emit HowTo JSON-LD on the front end. * * @param array $settings Module settings. * @param array $items Usable steps. * @return void */ private function maybe_render_schema(array $settings, array $items): void { // The Schema master switch and the matrix's per-content-type switch. // This widget echoes its own '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * A heading tag from the allowed set. * * @param array $settings Module settings. * @return string */ private static function heading_tag(array $settings): string { $tag = (string) ($settings['heading_tag'] ?? 'h2'); return in_array($tag, ['h2', 'h3', 'h4', 'p'], true) ? $tag : 'h2'; } } FLBuilder::register_settings_form('thinkrank_howto_step_form', [ 'title' => __('Step', 'thinkrank'), 'tabs' => [ 'general' => [ 'title' => __('General', 'thinkrank'), 'sections' => [ 'general' => [ 'title' => '', 'fields' => [ 'title' => [ 'type' => 'text', 'label' => __('Step title', 'thinkrank'), 'connections' => ['string'], ], 'text' => [ 'type' => 'editor', 'label' => __('Step description', 'thinkrank'), 'media_buttons' => false, 'connections' => ['string'], ], 'image' => [ 'type' => 'photo', 'label' => __('Step image', 'thinkrank'), 'show_remove' => true, ], ], ], ], ], ], ]); FLBuilder::register_module('ThinkRank_Beaver_Howto_Module', [ 'general' => [ 'title' => __('Steps', 'thinkrank'), 'sections' => [ 'content' => [ 'title' => '', 'fields' => [ 'heading' => [ 'type' => 'text', 'label' => __('Title', 'thinkrank'), 'placeholder' => __('How to …', 'thinkrank'), 'connections' => ['string'], ], 'heading_tag' => [ 'type' => 'select', 'label' => __('Heading tag', 'thinkrank'), 'default' => 'h2', 'options' => [ 'h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => __('Paragraph', 'thinkrank'), ], ], 'description' => [ 'type' => 'textarea', 'label' => __('Description', 'thinkrank'), 'rows' => 4, 'connections' => ['string'], ], 'steps' => [ 'type' => 'form', 'label' => __('Step', 'thinkrank'), 'form' => 'thinkrank_howto_step_form', 'preview_text' => 'title', 'multiple' => true, ], 'show_numbers' => [ 'type' => 'select', 'label' => __('Numbered steps', 'thinkrank'), 'default' => '1', 'options' => [ '1' => __('Yes', 'thinkrank'), '0' => __('No', 'thinkrank'), ], ], ], ], ], ], 'duration' => [ 'title' => __('Duration', 'thinkrank'), 'sections' => [ 'duration' => [ 'title' => '', 'fields' => [ 'total_days' => ['type' => 'unit', 'label' => __('Days', 'thinkrank'), 'slider' => false], 'total_hours' => ['type' => 'unit', 'label' => __('Hours', 'thinkrank'), 'slider' => false], 'total_minutes' => ['type' => 'unit', 'label' => __('Minutes', 'thinkrank'), 'slider' => false], ], ], ], ], 'schema' => [ 'title' => __('Schema', 'thinkrank'), 'sections' => [ 'schema' => [ 'title' => '', 'fields' => [ 'output_schema' => [ 'type' => 'select', 'label' => __('Output HowTo schema (JSON-LD)', 'thinkrank'), 'default' => '1', 'options' => [ '1' => __('Yes', 'thinkrank'), '0' => __('No', 'thinkrank'), ], 'help' => __('Adds HowTo structured data for rich results.', 'thinkrank'), ], ], ], ], ], ]);