PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / trunk
FrontBlocks for Gutenberg/GeneratePress vtrunk
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 1 month ago BackButton.php 7 months ago BeforeAfter.php 1 month ago BlockPatterns.php 4 months ago Carousel.php 1 week ago ColumnsSameHeight.php 1 week ago ContainerEdgeAlignment.php 4 weeks ago Counter.php 1 month ago DownloadButton.php 1 week ago Events.php 4 weeks ago FaqSchema.php 1 week ago FluidTypography.php 4 weeks ago Gallery.php 8 months ago GravityFormsInline.php 1 month ago Headline.php 4 weeks ago InsertPost.php 1 month ago ProductCategories.php 4 weeks ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 4 weeks ago StackedImages.php 4 months ago StickyColumn.php 4 weeks ago SvgUpload.php 4 weeks ago Testimonials.php 8 months ago TextAnimation.php 1 month ago UserText.php 4 weeks ago
ReadingTime.php
324 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_block_editor_assets' ) );
30 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
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 frontend styles.
69 *
70 * @return void
71 */
72 public function enqueue_frontend_styles() {
73 wp_register_style(
74 'frontblocks-reading-time-style',
75 FRBL_PLUGIN_URL . 'assets/reading-time/frontblocks-reading-time.css',
76 array(),
77 FRBL_VERSION
78 );
79
80 if ( is_admin() || has_block( 'frontblocks/reading-time' ) ) {
81 wp_enqueue_style( 'frontblocks-reading-time-style' );
82 }
83 }
84
85 /**
86 * Enqueue block editor assets.
87 *
88 * @return void
89 */
90 public function enqueue_block_editor_assets() {
91 // Enqueue styles for editor.
92 wp_enqueue_style(
93 'frontblocks-reading-time-style',
94 FRBL_PLUGIN_URL . 'assets/reading-time/frontblocks-reading-time.css',
95 array(),
96 FRBL_VERSION
97 );
98
99 // Register React if not already registered.
100 if ( ! wp_script_is( 'react', 'registered' ) ) {
101 wp_register_script(
102 'react',
103 'https://unpkg.com/react@18/umd/react.production.min.js',
104 array(),
105 '18.0.0',
106 true
107 );
108 }
109
110 if ( ! wp_script_is( 'react-dom', 'registered' ) ) {
111 wp_register_script(
112 'react-dom',
113 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
114 array( 'react' ),
115 '18.0.0',
116 true
117 );
118 }
119
120 wp_enqueue_script(
121 'frontblocks-reading-time-option',
122 FRBL_PLUGIN_URL . 'assets/reading-time/frontblocks-reading-time.js',
123 array( 'react', 'react-dom', 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-block-editor', 'wp-compose', 'wp-i18n' ),
124 FRBL_VERSION,
125 true
126 );
127
128 wp_localize_script(
129 'frontblocks-reading-time-option',
130 'frblReadingTime',
131 array(
132 'nonce' => wp_create_nonce( 'wp_rest' ),
133 )
134 );
135 }
136
137 /**
138 * Register the Reading Time block.
139 *
140 * @return void
141 */
142 public function register_reading_time_block() {
143 $args = array(
144 'editor_script' => 'frontblocks-reading-time-option',
145 'render_callback' => array( $this, 'render_reading_time_block' ),
146 'attributes' => array(
147 'postId' => array(
148 'type' => 'number',
149 'default' => 0,
150 ),
151 'showIcon' => array(
152 'type' => 'boolean',
153 'default' => true,
154 ),
155 'prefix' => array(
156 'type' => 'string',
157 'default' => '',
158 ),
159 'suffix' => array(
160 'type' => 'string',
161 'default' => 'min',
162 ),
163 'className' => array(
164 'type' => 'string',
165 'default' => '',
166 ),
167 'textColor' => array(
168 'type' => 'string',
169 'default' => 'inherit',
170 ),
171 'backgroundColor' => array(
172 'type' => 'string',
173 'default' => 'transparent',
174 ),
175 'fontSize' => array(
176 'type' => 'number',
177 'default' => 16,
178 ),
179 'iconColor' => array(
180 'type' => 'string',
181 'default' => 'currentColor',
182 ),
183 'alignment' => array(
184 'type' => 'string',
185 'default' => 'left',
186 ),
187 'padding' => array(
188 'type' => 'number',
189 'default' => 10,
190 ),
191 'borderRadius' => array(
192 'type' => 'number',
193 'default' => 5,
194 ),
195 ),
196 );
197
198 if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/reading-time' ) ) {
199 register_block_type(
200 'frontblocks/reading-time',
201 $args
202 );
203 }
204 }
205
206 /**
207 * Render the Reading Time block on frontend.
208 *
209 * @param array $attributes Block attributes.
210 * @return string HTML output.
211 */
212 public function render_reading_time_block( $attributes ) {
213 $post_id = absint( $attributes['postId'] ?? 0 );
214
215 // If no post ID specified, use current post.
216 if ( 0 === $post_id ) {
217 $post_id = get_the_ID();
218 }
219
220 if ( ! $post_id ) {
221 return '';
222 }
223
224 $reading_time = $this->calculate_reading_time( $post_id );
225
226 $show_icon = $attributes['showIcon'] ?? true;
227 $prefix = sanitize_text_field( $attributes['prefix'] ?? '' );
228 $suffix = sanitize_text_field( $attributes['suffix'] ?? 'min' );
229 $text_color = sanitize_text_field( $attributes['textColor'] ?? 'inherit' );
230 $bg_color = sanitize_text_field( $attributes['backgroundColor'] ?? 'transparent' );
231 $font_size = absint( $attributes['fontSize'] ?? 16 );
232 $icon_color = sanitize_text_field( $attributes['iconColor'] ?? 'currentColor' );
233 $alignment = sanitize_text_field( $attributes['alignment'] ?? 'left' );
234 $padding = absint( $attributes['padding'] ?? 10 );
235 $border_radius = absint( $attributes['borderRadius'] ?? 5 );
236
237 $wrapper_class = 'frbl-reading-time-wrapper align-' . esc_attr( $alignment );
238 if ( ! empty( $attributes['className'] ) ) {
239 $wrapper_class .= ' ' . esc_attr( $attributes['className'] );
240 }
241
242 $style_vars = sprintf(
243 '--frbl-text-color: %s; --frbl-bg-color: %s; --frbl-font-size: %dpx; --frbl-icon-color: %s; --frbl-padding: %dpx; --frbl-border-radius: %dpx;',
244 $text_color,
245 $bg_color,
246 $font_size,
247 $icon_color,
248 $padding,
249 $border_radius
250 );
251
252 $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>';
253
254 ob_start();
255 ?>
256 <div class="<?php echo esc_attr( $wrapper_class ); ?>">
257 <div class="frbl-reading-time" style="<?php echo esc_attr( $style_vars ); ?>">
258 <?php if ( $show_icon ) : ?>
259 <span class="frbl-reading-time-icon"><?php echo $icon_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
260 <?php endif; ?>
261 <span class="frbl-reading-time-text">
262 <?php
263 if ( $prefix ) {
264 echo esc_html( $prefix ) . ' ';
265 }
266 echo esc_html( $reading_time );
267 if ( $suffix ) {
268 echo ' ' . esc_html( $suffix );
269 }
270 ?>
271 </span>
272 </div>
273 </div>
274 <?php
275 return ob_get_clean();
276 }
277
278 /**
279 * Shortcode for reading time.
280 * Usage: [frontblocks_reading_time] or [frontblocks_reading_time post_id="123" prefix="This blog takes" suffix="minutes to read"]
281 *
282 * @param array $atts Shortcode attributes.
283 * @return string HTML output.
284 */
285 public function reading_time_shortcode( $atts ) {
286 $atts = shortcode_atts(
287 array(
288 'post_id' => 0,
289 'show_icon' => 'yes',
290 'prefix' => '',
291 'suffix' => 'min',
292 'text_color' => 'inherit',
293 'bg_color' => 'transparent',
294 'font_size' => 16,
295 'icon_color' => 'currentColor',
296 'alignment' => 'left',
297 'padding' => 10,
298 'border_radius' => 5,
299 ),
300 $atts,
301 'frontblocks_reading_time'
302 );
303
304 $attributes = array(
305 'postId' => absint( $atts['post_id'] ),
306 'showIcon' => 'yes' === strtolower( $atts['show_icon'] ),
307 'prefix' => $atts['prefix'],
308 'suffix' => $atts['suffix'],
309 'textColor' => $atts['text_color'],
310 'backgroundColor' => $atts['bg_color'],
311 'fontSize' => absint( $atts['font_size'] ),
312 'iconColor' => $atts['icon_color'],
313 'alignment' => $atts['alignment'],
314 'padding' => absint( $atts['padding'] ),
315 'borderRadius' => absint( $atts['border_radius'] ),
316 );
317
318 return $this->render_reading_time_block( $attributes );
319 }
320 }
321
322 new ReadingTime();
323
324