| 1 |
<?php |
| 2 |
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- view template receives variables via extract(); prefixing is impractical. |
| 3 |
|
| 4 |
/** |
| 5 |
* The single-doc meta row, as an Elementor widget. |
| 6 |
* |
| 7 |
* Emits the same `.betterdocs-doc-meta` wrapper as |
| 8 |
* views/templates/parts/doc-meta.php, so the leading and trailing groups land on |
| 9 |
* opposite edges and all three controls sit on one centre line without this widget |
| 10 |
* shipping any layout CSS. |
| 11 |
* |
| 12 |
* @var array $attributes |
| 13 |
* @var bool $enable_reading_time |
| 14 |
* @var bool $enable_listen |
| 15 |
* @var array $listen_args Per-instance overrides for Listen::render(). |
| 16 |
* @var bool $enable_ai_actions |
| 17 |
* @var array $ai_actions_args Per-instance overrides for AIActions::render(). |
| 18 |
* @var bool $is_editing Elementor's canvas or preview iframe. |
| 19 |
*/ |
| 20 |
|
| 21 |
use WPDeveloper\BetterDocs\Core\AIActions; |
| 22 |
use WPDeveloper\BetterDocs\Core\Listen; |
| 23 |
use WPDeveloper\BetterDocs\Shortcodes\AIActions as AIActionsShortcode; |
| 24 |
use WPDeveloper\BetterDocs\Shortcodes\Listen as ListenShortcode; |
| 25 |
|
| 26 |
if ( ! defined( 'ABSPATH' ) ) { |
| 27 |
exit; |
| 28 |
} |
| 29 |
|
| 30 |
$reading_time = ''; |
| 31 |
if ( ! empty( $enable_reading_time ) ) { |
| 32 |
// Defaulted rather than dereferenced: Elementor fills in every registered |
| 33 |
// control, but a widget saved before one existed can still arrive short of a |
| 34 |
// key if the control was renamed or a third party filtered it away. |
| 35 |
$att = function ( $key ) use ( $attributes ) { |
| 36 |
return isset( $attributes[ $key ] ) ? (string) $attributes[ $key ] : ''; |
| 37 |
}; |
| 38 |
|
| 39 |
// esc_attr, not raw: these land inside double-quoted shortcode attributes, |
| 40 |
// where an unescaped quote would break out of the attribute. |
| 41 |
$reading_time = (string) do_shortcode( |
| 42 |
'[betterdocs_reading_time' |
| 43 |
. ' singular_reading_text="' . esc_attr( $att( 'singular_ert_reading_text' ) ) . '"' |
| 44 |
. ' reading_text="' . esc_attr( $att( 'ert_reading_text' ) ) . '"' |
| 45 |
. ' reading_title="' . esc_attr( $att( 'ert_reading_title' ) ) . '"]' |
| 46 |
); |
| 47 |
} |
| 48 |
|
| 49 |
$listen = ''; |
| 50 |
if ( ! empty( $enable_listen ) && isset( betterdocs()->listen ) ) { |
| 51 |
ob_start(); |
| 52 |
|
| 53 |
if ( ! empty( $is_editing ) && ! betterdocs()->listen->has_listen( get_the_ID(), $listen_args ) ) { |
| 54 |
// A template edited against a preview that is not a readable doc resolves to |
| 55 |
// nothing — and a widget that renders nothing cannot be seen, selected or |
| 56 |
// moved. It reads as broken rather than as empty, so the canvas gets static |
| 57 |
// chrome instead. Same reasoning as the AI Actions placeholder below. |
| 58 |
$listen_label = ! empty( $attributes['listen_button_label'] ) |
| 59 |
? $attributes['listen_button_label'] |
| 60 |
: betterdocs()->settings->get( 'listen_button_label' ); |
| 61 |
?> |
| 62 |
<?php |
| 63 |
// Spans, not buttons, and that is the whole safety mechanism: the delegated |
| 64 |
// handlers in public/betterdocs.js bind to `button.betterdocs-listen-…`, so |
| 65 |
// nothing here can reach BetterDocsListen — which is important, because this |
| 66 |
// placeholder has no player element for it to measure and open. Same |
| 67 |
// arrangement as the AI Actions placeholder below. |
| 68 |
?> |
| 69 |
<div class="betterdocs-listen betterdocs-listen-placeholder"> |
| 70 |
<span class="betterdocs-listen-trigger"> |
| 71 |
<span class="betterdocs-listen-icon" aria-hidden="true"> |
| 72 |
<?php echo wp_kses( Listen::icon( 'headphones' ), Listen::svg_kses() ); ?> |
| 73 |
</span> |
| 74 |
<span class="betterdocs-listen-label"><?php echo esc_html( $listen_label ); ?></span> |
| 75 |
</span> |
| 76 |
</div> |
| 77 |
<?php |
| 78 |
} else { |
| 79 |
// Handed off by token rather than as attributes: `show_speed` is tri-state |
| 80 |
// and a shortcode attribute string cannot express "inherit the setting". |
| 81 |
// See ListenShortcode::push_context(). |
| 82 |
$listen_context = ListenShortcode::push_context( $listen_args ); |
| 83 |
|
| 84 |
echo do_shortcode( '[betterdocs_listen context="' . esc_attr( $listen_context ) . '"]' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source. |
| 85 |
} |
| 86 |
|
| 87 |
$listen = trim( (string) ob_get_clean() ); |
| 88 |
} |
| 89 |
|
| 90 |
$ai_actions = ''; |
| 91 |
if ( ! empty( $enable_ai_actions ) && isset( betterdocs()->ai_actions ) ) { |
| 92 |
ob_start(); |
| 93 |
|
| 94 |
if ( ! empty( $is_editing ) && ! betterdocs()->ai_actions->has_actions( get_the_ID(), $ai_actions_args ) ) { |
| 95 |
// The action list is resolved per doc, so a template edited against a |
| 96 |
// preview that is not a readable doc resolves to nothing — and a widget |
| 97 |
// that renders nothing cannot be seen, selected or moved. It reads as |
| 98 |
// broken rather than as empty, so the canvas gets static chrome instead. |
| 99 |
$svg_kses = AIActions::svg_kses(); |
| 100 |
$button_label = ! empty( $attributes['ai_actions_button_label'] ) |
| 101 |
? $attributes['ai_actions_button_label'] |
| 102 |
: betterdocs()->settings->get( 'ai_actions_button_label' ); |
| 103 |
?> |
| 104 |
<div class="betterdocs-ai-actions betterdocs-ai-actions-placeholder"> |
| 105 |
<span class="betterdocs-ai-actions-primary"> |
| 106 |
<span class="betterdocs-ai-actions-icon" aria-hidden="true"> |
| 107 |
<?php echo wp_kses( AIActions::icon( 'copy' ), $svg_kses ); ?> |
| 108 |
</span> |
| 109 |
<span class="betterdocs-ai-actions-label"><?php echo esc_html( $button_label ); ?></span> |
| 110 |
</span> |
| 111 |
<span class="betterdocs-ai-actions-toggle"> |
| 112 |
<span class="betterdocs-ai-actions-caret" aria-hidden="true"> |
| 113 |
<?php echo wp_kses( AIActions::icon( 'caret' ), $svg_kses ); ?> |
| 114 |
</span> |
| 115 |
</span> |
| 116 |
</div> |
| 117 |
<?php |
| 118 |
} else { |
| 119 |
// Handed off by token rather than as attributes: the args carry a |
| 120 |
// tri-state `enabled_actions` map and free text that a shortcode |
| 121 |
// attribute string cannot round-trip. See AIActionsShortcode::push_context(). |
| 122 |
$ai_actions_context = AIActionsShortcode::push_context( $ai_actions_args ); |
| 123 |
|
| 124 |
echo do_shortcode( '[betterdocs_ai_actions context="' . esc_attr( $ai_actions_context ) . '"]' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source. |
| 125 |
} |
| 126 |
|
| 127 |
$ai_actions = trim( (string) ob_get_clean() ); |
| 128 |
} |
| 129 |
|
| 130 |
// Everything switched off, or a doc where nothing resolves: emit no wrapper |
| 131 |
// rather than an empty flex row consuming vertical space. |
| 132 |
if ( '' === trim( $reading_time ) && '' === $listen && '' === $ai_actions ) { |
| 133 |
return; |
| 134 |
} |
| 135 |
|
| 136 |
// One leading box holding both pills, rather than one each: the row's `> *` rule |
| 137 |
// zeroes child block margins, and the 8px gap between the pills is the leading |
| 138 |
// box's own, so two boxes would space them by the row's wider 16px. |
| 139 |
$leading = trim( $reading_time ) . $listen; |
| 140 |
?> |
| 141 |
<div class="betterdocs-doc-meta"> |
| 142 |
<?php if ( '' !== $leading ) : ?> |
| 143 |
<div class="betterdocs-doc-meta-start"> |
| 144 |
<?php echo $leading; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source. ?> |
| 145 |
</div> |
| 146 |
<?php endif; ?> |
| 147 |
|
| 148 |
<?php if ( '' !== $ai_actions ) : ?> |
| 149 |
<div class="betterdocs-doc-meta-end"> |
| 150 |
<?php echo $ai_actions; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source. ?> |
| 151 |
</div> |
| 152 |
<?php endif; ?> |
| 153 |
</div> |
| 154 |
|