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 +269 -4 3.2.8trunk View file →
@@ -52,24 +52,37 @@
52 52 );
53 53
54 54 $asset_file = include NOTIFICATIONX_PATH . 'blocks/controls/dist/index.asset.php';
55 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).
56 59 wp_register_script(
57 60 'notificationx-block-controls',
58 61 plugins_url( $index_js, __FILE__ ),
59 - $asset_file['dependencies'],
62 + array_merge( $asset_file['dependencies'], [ 'lodash' ] ),
60 63 $asset_file['version'],
61 64 false
62 65 );
63 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 +
64 77 $asset_file = include NOTIFICATIONX_PATH . 'blocks/notificationx/index.asset.php';
65 - $asset_file['dependencies'][] = 'notificationx-pro-blocks-edit-post';
66 78 $index_js = 'notificationx/index.js';
67 79 wp_register_script(
68 80 'notificationx-block-editor',
69 81 plugins_url( $index_js, __FILE__ ),
70 82 array_merge($asset_file['dependencies'], ['notificationx-block-controls']),
71 - $asset_file['version']
83 + $asset_file['version'],
84 + false // Editor script: must load in the head, which is the existing default.
72 85 );
73 86
74 87 $editor_css = 'notificationx/editor.css';
75 88 wp_register_style(
@@ -134,8 +147,58 @@
134 147 ),
135 148 ),
136 149 ]
137 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 + ] );
138 201 }
139 202
140 203 function notificationx_render_callback( $block_attributes, $content ) {
141 204 if( ! is_admin() ){
@@ -142,8 +205,9 @@
142 205 wp_enqueue_style('notificationx-block');
143 206 wp_enqueue_script('notificationx-block-frontend');
144 207 }
145 208 if ( is_admin() || $this->isRestUrl() ) {
209 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
146 210 do_action( 'nx_ignore_analytics' );
147 211 }
148 212 $nx_id = ! empty( $block_attributes['nx_id'] ) ? esc_attr($block_attributes['nx_id']) : '';
149 213 $product_id = ! empty( $block_attributes['product_id'] ) ? $block_attributes['product_id'] : '';
@@ -154,8 +218,9 @@
154 218 return $html;
155 219 }
156 220
157 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.
158 223 do_action( 'nx_ignore_analytics' );
159 224 $nx_id = ! empty( $block_attributes['nx_id'] ) ? esc_attr($block_attributes['nx_id']) : '';
160 225 $product_id = ! empty( $block_attributes['product_id'] ) ? $block_attributes['product_id'] : '';
161 226 $post_type = ! empty( $block_attributes['post_type'] ) ? $block_attributes['post_type'] : '';
@@ -163,9 +228,9 @@
163 228 if( 'wp_template' == $post_type ) {
164 229 add_filter('nx_is_preview',function(){
165 230 return true;
166 231 });
167 - $product_id = rand();
232 + $product_id = wp_rand();
168 233 }
169 234 $shortcode = do_shortcode( "[notificationx_inline post_type='{$post_type}' product_id='{$product_id}' id='{$nx_id}' show_link=false]" );
170 235 if ( $shortcode ) {
171 236 $html .= $shortcode;
@@ -174,8 +239,208 @@
174 239 }
175 240 $html .= '</div>';
176 241
177 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>';
178 443 }
179 444
180 445 function isRestUrl() {
181 446 if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {