PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
← All changes | blocks/Blocks.php +452 -0 0.2.5.7trunk View file →
@@ -1,0 +1,452 @@
1 +<?php
2 +/**
3 + * Functions to register client-side assets (scripts and stylesheets) for the
4 + * Gutenberg block.
5 + *
6 + * @package notificationx
7 + */
8 +
9 +namespace NotificationX\Blocks;
10 +
11 +use NotificationX\Core\Helper;
12 +use NotificationX\GetInstance;
13 +
14 +/**
15 + *
16 + * @method static Blocks get_instance($args = null)
17 + */
18 +class Blocks {
19 + /**
20 + * Instance of NotificationX
21 + *
22 + * @var Blocks
23 + */
24 + use GetInstance;
25 +
26 +
27 + public function __construct() {
28 + StyleHandler::get_instance();
29 + add_action( 'init', [ $this, 'notificationx_block_init' ] );
30 + }
31 +
32 + /**
33 + * Registers all block assets so that they can be enqueued through Gutenberg in
34 + * the corresponding context.
35 + *
36 + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets/
37 + */
38 + function notificationx_block_init() {
39 + // Skip block registration if Gutenberg is not enabled/merged.
40 + if ( ! function_exists( 'register_block_type' ) ) {
41 + return;
42 + }
43 + $dir = dirname( __FILE__ );
44 +
45 + // Enqueue Controls CSS & JS
46 + $controls_css = 'controls/dist/index.css';
47 + wp_register_style(
48 + 'notificationx-block-controls-css',
49 + plugins_url( $controls_css, __FILE__ ),
50 + [],
51 + filemtime( "{$dir}/{$controls_css}" )
52 + );
53 +
54 + $asset_file = include NOTIFICATIONX_PATH . 'blocks/controls/dist/index.asset.php';
55 + $index_js = 'controls/dist/index.js';
56 + // The controls bundle relies on the global `lodash` but its asset file
57 + // doesn't declare it; add it explicitly so the bundle works on any
58 + // editor screen (not just where style-handler.js happens to load it).
59 + wp_register_script(
60 + 'notificationx-block-controls',
61 + plugins_url( $index_js, __FILE__ ),
62 + array_merge( $asset_file['dependencies'], [ 'lodash' ] ),
63 + $asset_file['version'],
64 + false
65 + );
66 +
67 + // The inline block's edit.js reads `nx_style_handler.editor_type` to pick
68 + // the right preview-device store. Attach that global to the lightweight,
69 + // always-loaded controls handle so it is available on every editor screen
70 + // — without dragging in the heavy style-handler.js / wp-edit-site bundle.
71 + // The site editor overrides editor_type to 'edit-site' in StyleHandler.
72 + wp_localize_script( 'notificationx-block-controls', 'nx_style_handler', [
73 + 'sth_nonce' => wp_create_nonce( 'nx_style_handler_nonce' ),
74 + 'editor_type' => 'edit-post',
75 + ] );
76 +
77 + $asset_file = include NOTIFICATIONX_PATH . 'blocks/notificationx/index.asset.php';
78 + $index_js = 'notificationx/index.js';
79 + wp_register_script(
80 + 'notificationx-block-editor',
81 + plugins_url( $index_js, __FILE__ ),
82 + array_merge($asset_file['dependencies'], ['notificationx-block-controls']),
83 + $asset_file['version'],
84 + false // Editor script: must load in the head, which is the existing default.
85 + );
86 +
87 + $editor_css = 'notificationx/editor.css';
88 + wp_register_style(
89 + 'notificationx-block-editor',
90 + plugins_url( $editor_css, __FILE__ ),
91 + array( 'notificationx-block-controls-css' ),
92 + filemtime( "{$dir}/{$editor_css}" )
93 + );
94 +
95 + $style_css = 'notificationx/style.css';
96 + wp_register_style(
97 + 'notificationx-block',
98 + plugins_url( $style_css, __FILE__ ),
99 + [],
100 + filemtime( "{$dir}/{$style_css}" )
101 + );
102 + wp_register_script(
103 + 'notificationx-block-frontend',
104 + plugins_url( 'notificationx/frontend.js', __FILE__ ),
105 + [],
106 + filemtime( "{$dir}/notificationx/frontend.js" ),
107 + true
108 + );
109 + wp_localize_script('notificationx-block-frontend', 'notificationxBlockRest', [
110 + 'root' => rest_url(),
111 + ]);
112 + register_block_type( 'notificationx-pro/notificationx',
113 + [
114 + 'editor_script' => 'notificationx-block-editor',
115 + 'editor_style' => 'notificationx-block-editor',
116 + // 'style' => 'notificationx-block',
117 + // 'script' => 'notificationx-block-frontend',
118 + 'render_callback' => [ $this, 'notificationx_render_callback' ],
119 + 'attributes' => array(
120 + 'nx_id' => array(
121 + 'type' => 'string',
122 + ),
123 + 'blockId' => array(
124 + 'type' => 'string',
125 + ),
126 + 'product_id' => array(
127 + 'type' => 'string',
128 + ),
129 + ),
130 + ]
131 + );
132 + register_block_type( 'notificationx-pro/notificationx-render',
133 + [
134 + 'render_callback' => [ $this, 'gutenberg_examples_dynamic_render_callback' ],
135 + 'attributes' => array(
136 + 'nx_id' => array(
137 + 'type' => 'string',
138 + ),
139 + 'blockId' => array(
140 + 'type' => 'string',
141 + ),
142 + 'product_id' => array(
143 + 'type' => 'string',
144 + ),
145 + 'post_type' => array(
146 + 'type' => 'string',
147 + ),
148 + ),
149 + ]
150 + );
151 +
152 + // ── NotificationX Countdown Timer block ──────────────────────────
153 + $countdown_asset_path = NOTIFICATIONX_PATH . 'blocks/countdown/index.asset.php';
154 + $countdown_asset = file_exists( $countdown_asset_path )
155 + ? include $countdown_asset_path
156 + : [ 'dependencies' => [], 'version' => NOTIFICATIONX_VERSION ];
157 +
158 + wp_register_script(
159 + 'notificationx-countdown-editor',
160 + plugins_url( 'countdown/index.js', __FILE__ ),
161 + array_merge( $countdown_asset['dependencies'], [ 'notificationx-block-controls' ] ),
162 + $countdown_asset['version'],
163 + false // Editor script: must load in the head, which is the existing default.
164 + );
165 + wp_set_script_translations( 'notificationx-countdown-editor', 'notificationx' );
166 +
167 + $countdown_editor_css = 'countdown/editor.css';
168 + wp_register_style(
169 + 'notificationx-countdown-editor',
170 + plugins_url( $countdown_editor_css, __FILE__ ),
171 + [ 'notificationx-block-controls-css' ],
172 + filemtime( "{$dir}/{$countdown_editor_css}" )
173 + );
174 +
175 + // Frontend base layout CSS — distinct handle from Elementor's `nx-countdown`
176 + // (which is only registered on Elementor hooks), same source file.
177 + wp_register_style(
178 + 'notificationx-countdown-style',
179 + NOTIFICATIONX_PUBLIC_URL . 'css/nx-countdown.css',
180 + [],
181 + NOTIFICATIONX_VERSION
182 + );
183 + wp_register_script(
184 + 'notificationx-countdown-frontend',
185 + plugins_url( 'countdown/frontend.js', __FILE__ ),
186 + [],
187 + filemtime( "{$dir}/countdown/frontend.js" ),
188 + true
189 + );
190 +
191 + register_block_type( 'notificationx/countdown', [
192 + 'editor_script' => 'notificationx-countdown-editor',
193 + 'editor_style' => 'notificationx-countdown-editor',
194 + // `style` is injected into both the editor canvas iframe and the
195 + // frontend (only when the block is present), so the base layout CSS
196 + // applies in the editor preview too.
197 + 'style' => 'notificationx-countdown-style',
198 + 'render_callback' => [ $this, 'countdown_render_callback' ],
199 + 'attributes' => $this->countdown_block_attributes(),
200 + ] );
201 + }
202 +
203 + function notificationx_render_callback( $block_attributes, $content ) {
204 + if( ! is_admin() ){
205 + wp_enqueue_style('notificationx-block');
206 + wp_enqueue_script('notificationx-block-frontend');
207 + }
208 + if ( is_admin() || $this->isRestUrl() ) {
209 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
210 + do_action( 'nx_ignore_analytics' );
211 + }
212 + $nx_id = ! empty( $block_attributes['nx_id'] ) ? esc_attr($block_attributes['nx_id']) : '';
213 + $product_id = ! empty( $block_attributes['product_id'] ) ? $block_attributes['product_id'] : '';
214 + $block_id = ! empty( $block_attributes['blockId'] ) ? esc_attr($block_attributes['blockId']) : '';
215 + $html = '<div class="' . $block_id . ' notificationx-block-wrapper" data-nx_id="' . $nx_id . '">';
216 + $html .= do_shortcode( "[notificationx_inline product_id='{$product_id}' id='{$nx_id}']" );
217 + $html .= '</div>';
218 + return $html;
219 + }
220 +
221 + function gutenberg_examples_dynamic_render_callback( $block_attributes, $content ) {
222 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
223 + do_action( 'nx_ignore_analytics' );
224 + $nx_id = ! empty( $block_attributes['nx_id'] ) ? esc_attr($block_attributes['nx_id']) : '';
225 + $product_id = ! empty( $block_attributes['product_id'] ) ? $block_attributes['product_id'] : '';
226 + $post_type = ! empty( $block_attributes['post_type'] ) ? $block_attributes['post_type'] : '';
227 + $html = '<div class="' . $block_attributes['blockId'] . ' notificationx-block-wrapper">';
228 + if( 'wp_template' == $post_type ) {
229 + add_filter('nx_is_preview',function(){
230 + return true;
231 + });
232 + $product_id = wp_rand();
233 + }
234 + $shortcode = do_shortcode( "[notificationx_inline post_type='{$post_type}' product_id='{$product_id}' id='{$nx_id}' show_link=false]" );
235 + if ( $shortcode ) {
236 + $html .= $shortcode;
237 + } else {
238 + $html .= '<p class="nx-shortcode-notice">' . __( 'There is no data in this notification.', 'notificationx' ) . '</p>';
239 + }
240 + $html .= '</div>';
241 +
242 + return wp_kses($html, Helper::nx_allowed_html());
243 + }
244 +
245 + /**
246 + * Server-side attribute schema for the countdown block.
247 + *
248 + * Defaults MUST match the JS attribute defaults (blocks/countdown/components/attributes.js)
249 + * — Gutenberg omits attributes left at their default from the saved markup,
250 + * so the frontend relies on these defaults for any unchanged value.
251 + *
252 + * @return array
253 + */
254 + private function countdown_block_attributes() {
255 + return [
256 + 'blockId' => [ 'type' => 'string' ],
257 + 'blockStyles' => [ 'type' => 'object' ],
258 + 'countdownType' => [ 'type' => 'string', 'default' => 'due_date' ],
259 + 'dueTime' => [ 'type' => 'string', 'default' => '' ],
260 + 'evergreenHours' => [ 'type' => 'number', 'default' => 11 ],
261 + 'evergreenMinutes' => [ 'type' => 'number', 'default' => 59 ],
262 + 'evergreenRecurring' => [ 'type' => 'boolean', 'default' => false ],
263 + 'recurringRestartAfter' => [ 'type' => 'number', 'default' => 0 ],
264 + 'recurringStopTime' => [ 'type' => 'string', 'default' => '' ],
265 + 'layout' => [ 'type' => 'string', 'default' => 'grid' ],
266 + 'labelView' => [ 'type' => 'string', 'default' => 'nx-countdown-label-block' ],
267 + 'showDays' => [ 'type' => 'boolean', 'default' => true ],
268 + 'daysLabel' => [ 'type' => 'string', 'default' => 'Days' ],
269 + 'showHours' => [ 'type' => 'boolean', 'default' => true ],
270 + 'hoursLabel' => [ 'type' => 'string', 'default' => 'Hours' ],
271 + 'showMinutes' => [ 'type' => 'boolean', 'default' => true ],
272 + 'minutesLabel' => [ 'type' => 'string', 'default' => 'Minutes' ],
273 + 'showSeconds' => [ 'type' => 'boolean', 'default' => true ],
274 + 'secondsLabel' => [ 'type' => 'string', 'default' => 'Seconds' ],
275 + 'showSeparator' => [ 'type' => 'boolean', 'default' => false ],
276 + 'separatorStyle' => [ 'type' => 'string', 'default' => 'dotted' ],
277 + 'expireType' => [ 'type' => 'string', 'default' => 'none' ],
278 + 'expiryTitle' => [ 'type' => 'string', 'default' => 'The countdown is finished!' ],
279 + 'expiryText' => [ 'type' => 'string', 'default' => 'Thank you for being part of this event.' ],
280 + 'redirectUrl' => [ 'type' => 'string', 'default' => '#' ],
281 + ];
282 + }
283 +
284 + /**
285 + * Renders the countdown block on the frontend.
286 + *
287 + * Mirrors \NotificationX\Extensions\Elementor\CountdownWidget::render() markup
288 + * (so the shared frontend logic stays identical) and prints the per-instance
289 + * scoped CSS computed in the editor.
290 + *
291 + * @param array $attributes Block attributes.
292 + * @return string
293 + */
294 + function countdown_render_callback( $attributes, $content = '' ) {
295 + if ( ! is_admin() ) {
296 + wp_enqueue_style( 'notificationx-countdown-style' );
297 + wp_enqueue_script( 'notificationx-countdown-frontend' );
298 + }
299 +
300 + $a = wp_parse_args( (array) $attributes, [
301 + 'blockId' => '',
302 + 'blockStyles' => [],
303 + 'countdownType' => 'due_date',
304 + 'dueTime' => '',
305 + 'evergreenHours' => 11,
306 + 'evergreenMinutes' => 59,
307 + 'evergreenRecurring' => false,
308 + 'recurringRestartAfter' => 0,
309 + 'recurringStopTime' => '',
310 + 'layout' => 'grid',
311 + 'labelView' => 'nx-countdown-label-block',
312 + 'showDays' => true,
313 + 'daysLabel' => 'Days',
314 + 'showHours' => true,
315 + 'hoursLabel' => 'Hours',
316 + 'showMinutes' => true,
317 + 'minutesLabel' => 'Minutes',
318 + 'showSeconds' => true,
319 + 'secondsLabel' => 'Seconds',
320 + 'showSeparator' => false,
321 + 'separatorStyle' => 'dotted',
322 + 'expireType' => 'none',
323 + 'expiryTitle' => '',
324 + 'expiryText' => '',
325 + 'redirectUrl' => '#',
326 + ] );
327 +
328 + $block_id = ! empty( $a['blockId'] ) ? sanitize_html_class( $a['blockId'] ) : 'nx-countdown-' . wp_rand( 1000, 9999 );
329 +
330 + // GMT offset string (matches the JS `new Date(dateStr)` parsing).
331 + $gmt_offset = get_option( 'gmt_offset' );
332 + $offset_str = ( $gmt_offset < 0 ? '' : '+' ) . str_replace(
333 + [ '.25', '.5', '.75' ],
334 + [ ':15', ':30', ':45' ],
335 + (string) $gmt_offset
336 + );
337 +
338 + $due_date_str = '';
339 + if ( 'due_date' === $a['countdownType'] && ! empty( $a['dueTime'] ) ) {
340 + $due_date_str = gmdate( 'M d Y G:i:s', strtotime( $a['dueTime'] ) ) . ' ' . $offset_str;
341 + }
342 +
343 + $evergreen_seconds = 0;
344 + if ( 'evergreen' === $a['countdownType'] ) {
345 + $evergreen_seconds = absint( $a['evergreenHours'] ) * HOUR_IN_SECONDS;
346 + $evergreen_seconds += absint( $a['evergreenMinutes'] ) * MINUTE_IN_SECONDS;
347 + }
348 +
349 + $separator_class = '';
350 + if ( ! empty( $a['showSeparator'] ) ) {
351 + $separator_class = 'nx-countdown-show-separator nx-countdown-separator-' . sanitize_html_class( $a['separatorStyle'] );
352 + }
353 +
354 + // ── Wrapper data attributes ──
355 + $wrapper_attrs = ' data-countdown-id="' . esc_attr( $block_id ) . '"';
356 + $wrapper_attrs .= ' data-countdown-type="' . esc_attr( $a['countdownType'] ) . '"';
357 + $wrapper_attrs .= ' data-expire-type="' . esc_attr( $a['expireType'] ) . '"';
358 +
359 + if ( 'text' === $a['expireType'] ) {
360 + $wrapper_attrs .= ' data-expiry-title="' . esc_attr( wp_kses_post( $a['expiryTitle'] ) ) . '"';
361 + $wrapper_attrs .= ' data-expiry-text="' . esc_attr( wp_kses_post( $a['expiryText'] ) ) . '"';
362 + } elseif ( 'url' === $a['expireType'] ) {
363 + $wrapper_attrs .= ' data-redirect-url="' . esc_url( $a['redirectUrl'] ) . '"';
364 + }
365 +
366 + if ( 'evergreen' === $a['countdownType'] ) {
367 + $wrapper_attrs .= ' data-evergreen-time="' . absint( $evergreen_seconds ) . '"';
368 + if ( ! empty( $a['evergreenRecurring'] ) ) {
369 + $stop_time = ! empty( $a['recurringStopTime'] )
370 + ? gmdate( 'M d Y G:i:s', strtotime( $a['recurringStopTime'] ) ) . ' ' . $offset_str
371 + : '';
372 + $wrapper_attrs .= ' data-evergreen-recurring="' . absint( $a['recurringRestartAfter'] ) . '"';
373 + $wrapper_attrs .= ' data-evergreen-recurring-stop="' . esc_attr( $stop_time ) . '"';
374 + }
375 + }
376 +
377 + $layout_class = 'nx-countdown-layout-' . sanitize_html_class( ! empty( $a['layout'] ) ? $a['layout'] : 'grid' );
378 + $label_view = sanitize_html_class( ! empty( $a['labelView'] ) ? $a['labelView'] : 'nx-countdown-label-block' );
379 + $list_classes = trim( 'nx-countdown-container ' . $layout_class . ' ' . $label_view . ' ' . $separator_class );
380 +
381 + // ── Time-unit items ──
382 + $units = [
383 + 'days' => [ 'show' => $a['showDays'], 'label' => $a['daysLabel'], 'attr' => 'data-days' ],
384 + 'hours' => [ 'show' => $a['showHours'], 'label' => $a['hoursLabel'], 'attr' => 'data-hours' ],
385 + 'minutes' => [ 'show' => $a['showMinutes'], 'label' => $a['minutesLabel'], 'attr' => 'data-minutes' ],
386 + 'seconds' => [ 'show' => $a['showSeconds'], 'label' => $a['secondsLabel'], 'attr' => 'data-seconds' ],
387 + ];
388 +
389 + $items_html = '';
390 + foreach ( $units as $unit => $data ) {
391 + if ( empty( $data['show'] ) ) {
392 + continue;
393 + }
394 + $label_html = '';
395 + if ( ! empty( $data['label'] ) ) {
396 + $label_html = '<span class="nx-countdown-label">' . esc_html( $data['label'] ) . '</span>';
397 + }
398 + $items_html .= '<li class="nx-countdown-item"><div class="nx-countdown-' . esc_attr( $unit ) . '">'
399 + . '<span ' . $data['attr'] . ' class="nx-countdown-digits">00</span>'
400 + . $label_html
401 + . '</div></li>';
402 + }
403 +
404 + $html = $this->countdown_inline_styles( $block_id, $a['blockStyles'] );
405 + $html .= '<div class="' . esc_attr( $block_id ) . ' nx-countdown-wrapper"' . $wrapper_attrs . '>';
406 + $html .= '<ul id="nx-countdown-' . esc_attr( $block_id ) . '" class="' . esc_attr( $list_classes ) . '" data-date="' . esc_attr( $due_date_str ) . '">';
407 + $html .= $items_html;
408 + $html .= '</ul></div>';
409 +
410 + return $html;
411 + }
412 +
413 + /**
414 + * Builds the per-instance scoped <style> tag from the editor-computed CSS.
415 + *
416 + * @param string $block_id Unique block class (scopes every selector).
417 + * @param array|object $block_styles { desktop, tab, mobile } raw CSS strings.
418 + * @return string
419 + */
420 + private function countdown_inline_styles( $block_id, $block_styles ) {
421 + $block_styles = (array) $block_styles;
422 + $desktop = isset( $block_styles['desktop'] ) ? (string) $block_styles['desktop'] : '';
423 + $tab = isset( $block_styles['tab'] ) ? (string) $block_styles['tab'] : '';
424 + $mobile = isset( $block_styles['mobile'] ) ? (string) $block_styles['mobile'] : '';
425 +
426 + $css = $desktop;
427 + if ( '' !== trim( $tab ) ) {
428 + $css .= '@media (max-width:1024px){' . $tab . '}';
429 + }
430 + if ( '' !== trim( $mobile ) ) {
431 + $css .= '@media (max-width:767px){' . $mobile . '}';
432 + }
433 +
434 + $css = trim( $css );
435 + if ( '' === $css ) {
436 + return '';
437 + }
438 +
439 + // Defensive: CSS never needs "<"; strip it to prevent a </style> breakout.
440 + $css = str_replace( '<', '', $css );
441 +
442 + return '<style>' . $css . '</style>';
443 + }
444 +
445 + function isRestUrl() {
446 + if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
447 + return false;
448 + }
449 + return true;
450 + }
451 +
452 +}