PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.2
FrontBlocks for Gutenberg/GeneratePress v1.5.2
1.5.2 1.5.1 1.4.0 1.5.0 trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Frontend / ReadingTime.php
frontblocks / includes / Frontend Last commit date
Animations.php 3 months ago BackButton.php 9 months ago BeforeAfter.php 3 months ago BlockPatterns.php 6 months ago Carousel.php 2 months ago ColumnsSameHeight.php 2 months ago ContainerEdgeAlignment.php 1 week ago CookieNotice.php 5 days ago Counter.php 3 months ago DownloadButton.php 2 months ago Events.php 2 months ago FaqSchema.php 2 months ago FluidTypography.php 2 months ago Gallery.php 1 week ago GravityFormsInline.php 3 months ago Headline.php 2 months ago InsertPost.php 3 months ago Maintenance.php 1 week ago ProductCategories.php 2 months ago ReadingProgress.php 9 months ago ReadingTime.php 1 week ago ScrollTop.php 1 week ago ShapeAnimations.php 1 week ago StackedImages.php 6 months ago StickyColumn.php 2 months ago SvgUpload.php 2 months ago Testimonials.php 10 months ago TextAnimation.php 3 months ago UserText.php 2 months ago
ReadingTime.php
322 lines
1 <?php
2 /**
3 * Reading Time module for FrontBlocks.
4 *
5 * @package FrontBlocks
6 * @author Alex Castellón <castellon@close.technology>
7 * @copyright 2025 Closemarketing
8 * @version 1.0
9 */
10
11 namespace FrontBlocks\Frontend;
12
13 use WP_Block_Type_Registry;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ReadingTime class.
19 *
20 * @since 1.0.0
21 */
22 class ReadingTime {
23
24 /**
25 * Constructor.
26 */
27 public function __construct() {
28 add_action( 'init', array( $this, 'register_reading_time_block' ), 20 );
29 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_script' ) );
30 add_action( 'enqueue_block_assets', array( $this, 'enqueue_style' ) );
31 add_shortcode( 'frontblocks_reading_time', array( $this, 'reading_time_shortcode' ) );
32 }
33
34 /**
35 * Calculate reading time for a post or page.
36 *
37 * @param int|null $post_id Post ID. If null, uses current post.
38 * @return int Reading time in minutes.
39 */
40 public function calculate_reading_time( $post_id = null ) {
41 if ( null === $post_id ) {
42 $post_id = get_the_ID();
43 }
44
45 if ( ! $post_id ) {
46 return 0;
47 }
48
49 // Check if reading time is already stored in post meta.
50 $reading_time = get_post_meta( $post_id, 'reading_time', true );
51
52 if ( ! $reading_time ) {
53 // Calculate reading time.
54 $post_content = get_post_field( 'post_content', $post_id );
55 $word_count = str_word_count( wp_strip_all_tags( $post_content ) );
56
57 // Average reading speed: 225 words per minute.
58 $reading_time = ceil( $word_count / 225 );
59
60 // Minimum 1 minute.
61 $reading_time = max( 1, $reading_time );
62 }
63
64 return (int) $reading_time;
65 }
66
67 /**
68 * Enqueue the block's style on both the frontend and the block editor
69 * (including inside its iframed canvas).
70 *
71 * Hooked on enqueue_block_assets rather than enqueue_block_editor_assets:
72 * the editor canvas is rendered in an iframe, and a style enqueued via the
73 * editor-only hook is appended to the wp-admin document instead of that
74 * iframe, which WordPress now flags as an incorrect registration.
75 *
76 * @return void
77 */
78 public function enqueue_style() {
79 wp_register_style(
80 'frontblocks-reading-time-style',
81 FRBL_PLUGIN_URL . 'assets/reading-time/frontblocks-reading-time.css',
82 array(),
83 FRBL_VERSION
84 );
85
86 if ( is_admin() || has_block( 'frontblocks/reading-time' ) ) {
87 wp_enqueue_style( 'frontblocks-reading-time-style' );
88 }
89 }
90
91 /**
92 * Enqueue the block editor's own script (and its React dependencies).
93 *
94 * @return void
95 */
96 public function enqueue_editor_script() {
97 // Register React if not already registered.
98 if ( ! wp_script_is( 'react', 'registered' ) ) {
99 wp_register_script(
100 'react',
101 'https://unpkg.com/react@18/umd/react.production.min.js',
102 array(),
103 '18.0.0',
104 true
105 );
106 }
107
108 if ( ! wp_script_is( 'react-dom', 'registered' ) ) {
109 wp_register_script(
110 'react-dom',
111 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
112 array( 'react' ),
113 '18.0.0',
114 true
115 );
116 }
117
118 wp_enqueue_script(
119 'frontblocks-reading-time-option',
120 FRBL_PLUGIN_URL . 'assets/reading-time/frontblocks-reading-time.js',
121 array( 'react', 'react-dom', 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-block-editor', 'wp-compose', 'wp-i18n' ),
122 FRBL_VERSION,
123 true
124 );
125
126 wp_localize_script(
127 'frontblocks-reading-time-option',
128 'frblReadingTime',
129 array(
130 'nonce' => wp_create_nonce( 'wp_rest' ),
131 )
132 );
133 }
134
135 /**
136 * Register the Reading Time block.
137 *
138 * @return void
139 */
140 public function register_reading_time_block() {
141 $args = array(
142 'editor_script' => 'frontblocks-reading-time-option',
143 'render_callback' => array( $this, 'render_reading_time_block' ),
144 'attributes' => array(
145 'postId' => array(
146 'type' => 'number',
147 'default' => 0,
148 ),
149 'showIcon' => array(
150 'type' => 'boolean',
151 'default' => true,
152 ),
153 'prefix' => array(
154 'type' => 'string',
155 'default' => '',
156 ),
157 'suffix' => array(
158 'type' => 'string',
159 'default' => 'min',
160 ),
161 'className' => array(
162 'type' => 'string',
163 'default' => '',
164 ),
165 'textColor' => array(
166 'type' => 'string',
167 'default' => 'inherit',
168 ),
169 'backgroundColor' => array(
170 'type' => 'string',
171 'default' => 'transparent',
172 ),
173 'fontSize' => array(
174 'type' => 'number',
175 'default' => 16,
176 ),
177 'iconColor' => array(
178 'type' => 'string',
179 'default' => 'currentColor',
180 ),
181 'alignment' => array(
182 'type' => 'string',
183 'default' => 'left',
184 ),
185 'padding' => array(
186 'type' => 'number',
187 'default' => 10,
188 ),
189 'borderRadius' => array(
190 'type' => 'number',
191 'default' => 5,
192 ),
193 ),
194 );
195
196 if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/reading-time' ) ) {
197 register_block_type(
198 'frontblocks/reading-time',
199 $args
200 );
201 }
202 }
203
204 /**
205 * Render the Reading Time block on frontend.
206 *
207 * @param array $attributes Block attributes.
208 * @return string HTML output.
209 */
210 public function render_reading_time_block( $attributes ) {
211 $post_id = absint( $attributes['postId'] ?? 0 );
212
213 // If no post ID specified, use current post.
214 if ( 0 === $post_id ) {
215 $post_id = get_the_ID();
216 }
217
218 if ( ! $post_id ) {
219 return '';
220 }
221
222 $reading_time = $this->calculate_reading_time( $post_id );
223
224 $show_icon = $attributes['showIcon'] ?? true;
225 $prefix = sanitize_text_field( $attributes['prefix'] ?? '' );
226 $suffix = sanitize_text_field( $attributes['suffix'] ?? 'min' );
227 $text_color = sanitize_text_field( $attributes['textColor'] ?? 'inherit' );
228 $bg_color = sanitize_text_field( $attributes['backgroundColor'] ?? 'transparent' );
229 $font_size = absint( $attributes['fontSize'] ?? 16 );
230 $icon_color = sanitize_text_field( $attributes['iconColor'] ?? 'currentColor' );
231 $alignment = sanitize_text_field( $attributes['alignment'] ?? 'left' );
232 $padding = absint( $attributes['padding'] ?? 10 );
233 $border_radius = absint( $attributes['borderRadius'] ?? 5 );
234
235 $wrapper_class = 'frbl-reading-time-wrapper align-' . esc_attr( $alignment );
236 if ( ! empty( $attributes['className'] ) ) {
237 $wrapper_class .= ' ' . esc_attr( $attributes['className'] );
238 }
239
240 $style_vars = sprintf(
241 '--frbl-text-color: %s; --frbl-bg-color: %s; --frbl-font-size: %dpx; --frbl-icon-color: %s; --frbl-padding: %dpx; --frbl-border-radius: %dpx;',
242 $text_color,
243 $bg_color,
244 $font_size,
245 $icon_color,
246 $padding,
247 $border_radius
248 );
249
250 $icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
251
252 ob_start();
253 ?>
254 <div class="<?php echo esc_attr( $wrapper_class ); ?>">
255 <div class="frbl-reading-time" style="<?php echo esc_attr( $style_vars ); ?>">
256 <?php if ( $show_icon ) : ?>
257 <span class="frbl-reading-time-icon"><?php echo $icon_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
258 <?php endif; ?>
259 <span class="frbl-reading-time-text">
260 <?php
261 if ( $prefix ) {
262 echo esc_html( $prefix ) . ' ';
263 }
264 echo esc_html( $reading_time );
265 if ( $suffix ) {
266 echo ' ' . esc_html( $suffix );
267 }
268 ?>
269 </span>
270 </div>
271 </div>
272 <?php
273 return ob_get_clean();
274 }
275
276 /**
277 * Shortcode for reading time.
278 * Usage: [frontblocks_reading_time] or [frontblocks_reading_time post_id="123" prefix="This blog takes" suffix="minutes to read"]
279 *
280 * @param array $atts Shortcode attributes.
281 * @return string HTML output.
282 */
283 public function reading_time_shortcode( $atts ) {
284 $atts = shortcode_atts(
285 array(
286 'post_id' => 0,
287 'show_icon' => 'yes',
288 'prefix' => '',
289 'suffix' => 'min',
290 'text_color' => 'inherit',
291 'bg_color' => 'transparent',
292 'font_size' => 16,
293 'icon_color' => 'currentColor',
294 'alignment' => 'left',
295 'padding' => 10,
296 'border_radius' => 5,
297 ),
298 $atts,
299 'frontblocks_reading_time'
300 );
301
302 $attributes = array(
303 'postId' => absint( $atts['post_id'] ),
304 'showIcon' => 'yes' === strtolower( $atts['show_icon'] ),
305 'prefix' => $atts['prefix'],
306 'suffix' => $atts['suffix'],
307 'textColor' => $atts['text_color'],
308 'backgroundColor' => $atts['bg_color'],
309 'fontSize' => absint( $atts['font_size'] ),
310 'iconColor' => $atts['icon_color'],
311 'alignment' => $atts['alignment'],
312 'padding' => absint( $atts['padding'] ),
313 'borderRadius' => absint( $atts['border_radius'] ),
314 );
315
316 return $this->render_reading_time_block( $attributes );
317 }
318 }
319
320 new ReadingTime();
321
322