File "breakpoints.php"

Full path: /home/dora/public_html/wp-content/themes/bricks/includes/breakpoints.php
File size: 15.18 KB
MIME-type: --
Charset: utf-8

<?php
namespace Bricks;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

/**
 * Breakpoints
 *
 * Default behavior: From largest to smallest breakpoints via max-width rules.
 *
 * Mobile-first possible via custom breakpoints:
 * Set a small breakpoint as 'base' to use min-width rules.
 *
 * Custom breakpoints @since 1.5.1
 */
class Breakpoints {
	public static $breakpoints     = [];
	public static $base_key        = 'desktop';
	public static $base_width      = 0;
	public static $is_mobile_first = false;

	public function __construct() {
		// To get localized strings for builder
		add_action( 'init', [ $this, 'init_breakpoints' ] );

		add_action( 'admin_notices', [ $this, 'admin_notice_regenerate_bricks_css_files' ] );
		add_action( 'wp_ajax_bricks_regenerate_bricks_css_files', [ $this, 'regenerate_bricks_css_files' ] );
		add_action( 'wp_ajax_bricks_update_breakpoints', [ $this, 'update_breakpoints' ] );
	}

	/**
	 * Calculate the breakpoints on init to get the proper breakpoints translations
	 *
	 * @since 1.5.1
	 */
	public static function init_breakpoints() {
		self::$breakpoints = self::get_breakpoints();
	}

	/**
	 * Automatically regenerate Bricks CSS files after theme update
	 *
	 * @since 1.5.1
	 */
	public function admin_notice_regenerate_bricks_css_files() {
		// Return: Custom breakpoints not active
		if ( ! Database::get_setting( 'customBreakpoints', false ) ) {
			return;
		}

		// STEP: Return if Bricks CSS files have been generated for this version (meaning options entry matches installed version of Bricks)
		$breakpoint_css_files_last_generated_in_version = get_option( BRICKS_BREAKPOINTS_LAST_GENERATED );

		if ( version_compare( BRICKS_VERSION, $breakpoint_css_files_last_generated_in_version, '==' ) ) {
			return;
		}

		// STEP: Regenerate Bricks CSS files for custom breakpoints (if db entry found)
		$custom_breakpoints = get_option( BRICKS_DB_BREAKPOINTS, false );

		if ( $custom_breakpoints ) {
			self::regenerate_bricks_css_files( true );
		}
	}

	/**
	 * Regenerate Bricks CSS files (via Bricks > Settings > General)
	 *
	 * E.g. frontend.min.css, element & woo CSS files, etc.
	 *
	 * Manual trigger: "Regenerate CSS files" button
	 * Auto trigger: After theme update (compare version number in db against current theme version)
	 *
	 * @since 1.5.1
	 */
	public static function regenerate_bricks_css_files( $return = false ) {
		$all_breakpoints     = self::$breakpoints;
		$default_breakpoints = self::get_default_breakpoints();

		foreach ( $all_breakpoints as $breakpoint ) {
			foreach ( $default_breakpoints as $default_breakpoint ) {
				if ( $breakpoint['key'] === $default_breakpoint['key'] && $default_breakpoint['key'] !== 'desktop' ) {
					$css_files_updated = self::update_media_rule_width_in_css_files( $default_breakpoint['width'], $breakpoint['width'], $default_breakpoint['width'] );
				}
			}
		}

		$updated = update_option( BRICKS_BREAKPOINTS_LAST_GENERATED, BRICKS_VERSION );

		// Manually triggered via admin_notice_regenerate_bricks_css_files
		if ( $return ) {
			return;
		}

		if ( bricks_is_ajax_call() ) {
			wp_send_json_success(
				[
					'default_breakpoints' => $default_breakpoints,
					'all_breakpoints'     => $all_breakpoints,
				]
			);
		}
	}

	/**
	 * Create breakpoint
	 *
	 * @since 1.5.1
	 */
	public function update_breakpoints() {
		Ajax::verify_nonce();

		// Only users with full access can update breakpoints (@since 1.5.4)
		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		$do            = ! empty( $_POST['do'] ) ? $_POST['do'] : false;
		$key           = ! empty( $_POST['key'] ) ? $_POST['key'] : false;
		$label         = ! empty( $_POST['label'] ) ? $_POST['label'] : false;
		$width         = ! empty( $_POST['width'] ) ? intval( $_POST['width'] ) : false;
		$width_builder = ! empty( $_POST['widthBuilder'] ) ? intval( $_POST['widthBuilder'] ) : false;
		$icon          = ! empty( $_POST['icon'] ) ? $_POST['icon'] : false;
		$base          = ! empty( $_POST['base'] ) ? $_POST['base'] : false;

		// Format key: All lowercase letters + underscores
		$key = strtolower( $key );
		$key = str_replace( '-', '_', $key );
		$key = str_replace( ' ', '_', $key );

		if ( $do !== 'configure' ) {
			if ( ! $key ) {
				wp_send_json_error( [ 'error' => esc_html__( 'Error', 'bricks' ) . ' (key)' ] );
			}

			if ( ! $label ) {
				wp_send_json_error( [ 'error' => esc_html__( 'Error', 'bricks' ) . ' (label)' ] );
			}
		}

		$breakpoints = self::get_breakpoints();
		$index       = array_search( $key, array_column( $breakpoints, 'key' ) );
		$breakpoint  = ! empty( $breakpoints[ $index ] ) ? $breakpoints[ $index ] : [];

		switch ( $do ) {
			// STEP: Create breakpoint
			case 'create':
				// Return: 'key' already exists
				if ( is_int( $index ) ) {
					wp_send_json_error( [ 'error' => esc_html__( 'Breakpoint already exists', 'bricks' ) . ' (' . $breakpoints[ $index ]['label'] . ')' ] );
				}

				// Return: 'label' already exists
				$index = array_search( $label, array_column( $breakpoints, 'label' ) );

				if ( is_int( $index ) ) {
					wp_send_json_error( [ 'error' => esc_html__( 'Breakpoint already exists', 'bricks' ) . ' (' . $breakpoints[ $index ]['label'] . ')' ] );
				}

				// Return: 'width' already exists
				$index = array_search( $width, array_column( $breakpoints, 'width' ) );

				if ( is_int( $index ) ) {
					wp_send_json_error( [ 'error' => esc_html__( 'Breakpoint already exists', 'bricks' ) . ' (' . $breakpoints[ $index ]['label'] . ')' ] );
				}

				// Return: 'widthBuilder' already exists
				$index = array_search( $width_builder, array_column( $breakpoints, 'widthBuilder' ) );

				if ( is_int( $index ) ) {
					wp_send_json_error( [ 'error' => esc_html__( 'Breakpoint already exists', 'bricks' ) . ' (' . $breakpoints[ $index ]['label'] . ')' ] );
				}

				$new_breakpoint = [
					'key'    => $key,
					'label'  => $label,
					'width'  => $width,
					'icon'   => $icon,
					'custom' => true, // To show delete icon
				];

				// Optional: Default width builder (when switching to breakpoint)
				if ( $width_builder ) {
					$new_breakpoint['widthBuilder'] = $width_builder;
				}

				$breakpoints[] = $new_breakpoint;
				break;

			// STEP: Edit breakpoint
			case 'edit':
				if ( $label ) {
					$breakpoints[ $index ]['label'] = $label;
				}

				if ( $width ) {
					// STEP: Width of breakpoint changed
					$old_width = ! empty( $breakpoint['width'] ) ? intval( $breakpoint['width'] ) : 0;

					if ( $old_width && $old_width != $width ) {
						$default_width = ! isset( $breakpoint['custom'] ) && $old_width ? $old_width : 0;

						$css_files_updated = self::update_media_rule_width_in_css_files( $old_width, $width, $default_width );
					}

					$breakpoints[ $index ]['width'] = $width;
				}

				if ( $width_builder ) {
					$breakpoints[ $index ]['widthBuilder'] = $width_builder;
				} else {
					unset( $breakpoints[ $index ]['widthBuilder'] );
				}

				if ( $icon ) {
					$breakpoints[ $index ]['icon'] = $icon;
				}

				// Is default breakpoint: Mark as 'edited' (to show reset icon in breakpoint manager)
				if ( ! isset( $breakpoint['custom'] ) ) {
					$breakpoints[ $index ]['edited'] = true;
				}
				break;

			// STEP: Pause custom breakpoint (if it's not 'base' OR 'desktop' breakpoint as desktop holds all non-CSS settings)
			case 'pause':
				if ( $breakpoint && isset( $breakpoint['custom'] ) && ! isset( $breakpoint['base'] ) && $breakpoint['key'] !== 'desktop' ) {
					if ( isset( $breakpoint['paused'] ) ) {
						unset( $breakpoint['paused'] );
					} else {
						$breakpoint['paused'] = true;
					}

					$breakpoints[ $index ] = $breakpoint;
				}
				break;

			// STEP: Reset (default) breakpoint
			case 'reset':
				$default_breakpoints      = self::get_default_breakpoints();
				$default_breakpoint_index = array_search( $key, array_column( $default_breakpoints, 'key' ) );
				$default_breakpoint       = $default_breakpoints[ $default_breakpoint_index ];

				$css_files_updated = self::update_media_rule_width_in_css_files( $width, $default_breakpoint['width'] );

				// Resetted base breakpoint
				if ( isset( $breakpoint['base'] ) ) {
					foreach ( $breakpoints as $i => $bp ) {
						if ( $bp['key'] === 'desktop' ) {
							$breakpoints[ $i ]['base'] = true;
						}
					}
				}

				$base = false;

				$breakpoints[ $index ] = $default_breakpoint;
				break;

			// STEP: Delete (custom) breakpoint
			case 'delete':
				unset( $breakpoints[ $index ] );

				$breakpoints = array_values( $breakpoints );
				break;

			// STEP: Configure breakpoints
			case 'configure':
				$default_breakpoints = self::get_default_breakpoints();

				// Reset width for all default breakpoint @media rules (skip 'desktop' as its the default base breakpoint and has no CSS file rules)
				foreach ( $breakpoints as $breakpoint ) {
					foreach ( $default_breakpoints as $default_breakpoint ) {
						if ( $breakpoint['key'] === $default_breakpoint['key'] && $default_breakpoint['key'] !== 'desktop' ) {
							$css_files_updated = self::update_media_rule_width_in_css_files( $breakpoint['width'], $default_breakpoint['width'] );
						}
					}
				}

				// Delete breakpoints from database: Delete custom & reset default breakpoints
				$updated = delete_option( BRICKS_DB_BREAKPOINTS );

				$breakpoints = $default_breakpoints;
				break;
		}

		// STEP: Set breakpoint as 'base'
		if ( $base == 'true' ) {
			// Loop over breakpoints to unset old & set new 'base' breakpoint
			foreach ( $breakpoints as $i => $bp ) {
				if ( $bp['key'] === $key ) {
					$breakpoints[ $i ]['base'] = true;
				} else {
					unset( $breakpoints[ $i ]['base'] );
				}
			}
		}

		// Sort breakpoints (descending by 'width')
		$widths = array_column( $breakpoints, 'width' );
		array_multisort( $widths, SORT_DESC, $breakpoints );

		// Update breakpoints in database
		if ( $do !== 'configure' ) {
			$updated = update_option( BRICKS_DB_BREAKPOINTS, $breakpoints );
		}

		// Get fresh breakpoint data to update $is_mobile_first to return breakpoints in correct order
		$breakpoints = self::get_breakpoints();

		$widths = array_column( $breakpoints, 'width' );
		array_multisort( $widths, self::$is_mobile_first ? SORT_ASC : SORT_DESC, $breakpoints );

		/**
		 * STEP: Breakpoints update: Now regenerate external files!
		 *
		 * As certain element CSS files contain @media default breakpoint rules.
		 * E.g.: layout element 'flex-wrap', .pricing-tables, etc.
		 */
		if ( Database::get_setting( 'cssLoading' ) === 'file' ) {
			$css_files_list = Assets_Files::get_css_files_list( true );

			foreach ( $css_files_list as $css_file_index => $css_file ) {
				$file_name = Assets_Files::regenerate_css_file( $css_file, $css_file_index, true );
			}
		}

		wp_send_json_success(
			[
				'key'             => $key,
				'label'           => $label,
				'width'           => $width,
				'widthBuilder'    => $width_builder,
				'icon'            => $icon,
				'base'            => $base,
				'updated'         => $updated,
				'breakpoints'     => $breakpoints,
				'is_mobile_first' => self::$is_mobile_first,
				'post'            => $_POST,
			]
		);
	}

	/**
	 * Update @media rules for breakpoint in CSS files
	 *
	 * When changing default breakpoint width OR default breakpoint reset.
	 */
	public static function update_media_rule_width_in_css_files( $current_width = 0, $new_width = 0, $default_width = 0 ) {
		/**
		 * STEP: Update @media rules in all Bricks CSS files
		 *
		 * frontend CSS, elements, integrations files
		 */
		if ( $current_width && $new_width ) {
			$css_file_paths = [
				BRICKS_PATH_ASSETS . 'css/frontend.min.css', // CSS loading method: Inline styles
				BRICKS_PATH_ASSETS . 'css/frontend-light.min.css', // CSS loading method: External files
			];

			// Add element CSS files to list
			$elements_css_file_paths = glob( BRICKS_PATH_ASSETS . 'css/elements/*.min.css' );
			$css_file_paths          = array_merge( $css_file_paths, $elements_css_file_paths );

			// Add integrations CSS files to list (WooCommerce, etc.)
			$integrations_css_file_paths = glob( BRICKS_PATH_ASSETS . 'css/integrations/*.min.css' );
			$css_file_paths              = array_merge( $css_file_paths, $integrations_css_file_paths );

			foreach ( $css_file_paths as $css_file_path ) {
				$css_file_content = file_get_contents( $css_file_path );

				$css_file_content = str_replace(
					"{$current_width}px)",
					"{$new_width}px)",
					$css_file_content
				);

				// Also try updating from default width in case CSS file doesn't have breakpoint database width (e.g. after updating Bricks, etc.)
				if ( $default_width && $default_width != $new_width ) {
					$css_file_content = str_replace(
						"{$default_width}px)",
						"{$new_width}px)",
						$css_file_content
					);
				}

				$css_file_updated = file_put_contents( $css_file_path, $css_file_content );
			}
		}
	}

	/**
	 * Default breakpoints
	 *
	 * - desktop (default base breakpoint)
	 * - tablet_portrait
	 * - mobile_landscape
	 * - mobile_portrait
	 *
	 * @return Array
	 */
	public static function get_default_breakpoints() {
		return [
			[
				'base'  => true, // 'base' marks breakpoint as base breakpoint
				'key'   => 'desktop',
				'label' => esc_html__( 'Desktop', 'bricks' ),
				'width' => 1279,
				'icon'  => 'laptop',
			],
			[
				'key'   => 'tablet_portrait',
				'label' => esc_html__( 'Tablet portrait', 'bricks' ),
				'width' => 991,
				'icon'  => 'tablet-portrait',
			],
			[
				'key'   => 'mobile_landscape',
				'label' => esc_html__( 'Mobile landscape', 'bricks' ),
				'width' => 767,
				'icon'  => 'phone-landscape',
			],
			[
				'key'   => 'mobile_portrait',
				'label' => esc_html__( 'Mobile portrait', 'bricks' ),
				'width' => 478,
				'icon'  => 'phone-portrait',
			],
		];
	}

	/**
	 * Get all breakpoints (default & custom)
	 */
	public static function get_breakpoints() {
		$default_breakpoints = self::get_default_breakpoints();

		// Get breakpoints from database (default & custom)
		$breakpoints = get_option( BRICKS_DB_BREAKPOINTS, false );

		// Use default breakpoints (no breakpoints found in database OR custom breakpoints disabled)
		if ( empty( $breakpoints ) || ! is_array( $breakpoints ) || ! Database::get_setting( 'customBreakpoints', false ) ) {
			$breakpoints = $default_breakpoints;
		}

		// STEP: Get base width
		foreach ( $breakpoints as $index => $breakpoint ) {
			if ( isset( $breakpoint['base'] ) ) {
				self::$base_key   = $breakpoint['key'];
				self::$base_width = intval( $breakpoint['width'] );

				// Is mobile first: Smallest breakpoint is base breakpoint
				self::$is_mobile_first = $index === count( $breakpoints ) - 1;
			}

			// Fallback to desktop as base breakpoint
			elseif ( self::$base_width === 0 && $breakpoint['key'] === 'desktop' ) {
				self::$base_key   = $breakpoint['key'];
				self::$base_width = intval( $breakpoint['width'] );
			}
		}

		// Sort breakpoints by width
		$widths = array_column( $breakpoints, 'width' );
		array_multisort( $widths, self::$is_mobile_first ? SORT_ASC : SORT_DESC, $breakpoints );

		return $breakpoints;
	}

	/**
	 * Get breakpoint by key
	 */
	public static function get_breakpoint_by( $key = 'key', $value = '' ) {
		$index = array_search( $value, array_column( self::$breakpoints, $key ) );

		if ( is_int( $index ) ) {
			return self::$breakpoints[ $index ];
		}
	}
}