Source: php/delivery/class-lazy-load.php

<?php
/**
 * Lazy Load.
 *
 * @package Cloudinary
 */

namespace Cloudinary\Delivery;

use Cloudinary\Connect\Api;
use Cloudinary\Delivery_Feature;
use Cloudinary\Plugin;
use Cloudinary\String_Replace;
use Cloudinary\UI\Component\HTML;
use Cloudinary\Utils;

/**
 * Class Responsive_Breakpoints
 *
 * @package Cloudinary
 */
class Lazy_Load extends Delivery_Feature {

	/**
	 * Holds the settings slug.
	 *
	 * @var string
	 */
	protected $settings_slug = 'media_display';

	/**
	 * Holds the enabler slug.
	 *
	 * @var string
	 */
	protected $enable_slug = 'use_lazy_load';

	/**
	 * Lazy_Load constructor.
	 *
	 * @param \Cloudinary\Plugin $plugin The main instance of the plugin.
	 */
	public function __construct( Plugin $plugin ) {
		parent::__construct( $plugin );
		add_filter( 'cloudinary_image_tag-disabled', array( $this, 'js_noscript' ), 10, 2 );
	}

	/**
	 * Setup hooks.
	 */
	public function setup_hooks() {
		add_action( 'wp', array( $this, 'init_lazy_script_maybe' ) );
		add_filter( 'cloudinary_lazy_load_bypass', array( $this, 'bypass_lazy_load' ), 10, 2 );
	}

	/**
	 * Check if the page request is a dynamic lazy load request.
	 */
	public function init_lazy_script_maybe() {

		$flag = Utils::get_sanitized_text( 'cloudinary_lazy_load_loader' );
		if ( $flag ) {
			$expires = HOUR_IN_SECONDS;
			header( 'Cache-Control: max-age=' . $expires );
			cache_javascript_headers();
			echo $this->get_inline_script(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			exit;
		}
	}

	/**
	 * Maybe bypass the lazy load.
	 *
	 * @param bool  $bypass      Whether to bypass lazy load.
	 * @param array $tag_element The TAG element.
	 *
	 * @return bool
	 */
	public function bypass_lazy_load( $bypass, $tag_element ) {

		// Bypass if eager loading.
		if ( ! empty( $tag_element['loading'] ) && 'eager' === $tag_element['loading'] ) {
			$bypass = true;
		}

		/**
		 * Filter the classes that bypass lazy loading.
		 *
		 * @hook   cloudinary_lazy_load_bypass_classes
		 * @since  3.0.9
		 *
		 * @param $classes {array} Classes that bypass the Lazy Load.
		 *
		 * @return {bool}
		 *
		 * @example
		 * <?php
		 *
		 * // Extend bypass lazy load classes to include `skip-lazy`.
		 * add_filter(
		 *    'cloudinary_lazy_load_bypass_classes',
		 *    function( $classes ) {
		 *         $classes[] = 'skip-lazy';
		 *         return $classes;
		 *    }
		 * );
		 */
		$bypass_classes = apply_filters( 'cloudinary_lazy_load_bypass_classes', array( 'cld-bypass-lazy' ) );

		if ( ! $bypass && ! empty( array_intersect( $bypass_classes, $tag_element['atts']['class'] ) ) ) {
			$bypass = true;
		}

		return $bypass;
	}

	/**
	 * Get the inline script for lazy load.
	 *
	 * @retrun string
	 */
	public function get_inline_script() {
		$config = $this->get_config();

		return 'var CLDLB = ' . wp_json_encode( $config ) . ';' . file_get_contents( $this->plugin->dir_path . 'js/inline-loader.js' );
	}

	/**
	 * Get the config for lazy load.
	 *
	 * @return array
	 */
	public function get_config() {
		$config = $this->config; // Get top most config.
		if ( 'off' !== $config['lazy_placeholder'] ) {
			$config['placeholder'] = API::generate_transformation_string( $this->get_placeholder_transformations( $config['lazy_placeholder'] ) );
		}
		$config['base_url'] = $this->media->base_url;

		return $config;
	}

	/**
	 * Wrap image tags in noscript to allow no-javascript browsers to get images.
	 *
	 * @param string $tag         The original html tag.
	 * @param array  $tag_element The original tag_element.
	 *
	 * @return string
	 */
	public function js_noscript( $tag, $tag_element ) {

		$options          = $tag_element['atts'];
		$options['class'] = implode( ' ', $options['class'] );

		unset(
			$options['srcset'],
			$options['sizes'],
			$options['loading'],
			$options['src'],
			$options['class']
		);
		$atts = array(
			'data-image' => wp_json_encode( $options ),
		);

		return HTML::build_tag( 'noscript', $atts ) . $tag . HTML::build_tag( 'noscript', null, 'close' );
	}

	/**
	 * Get the placeholder generation transformations.
	 *
	 * @param string $placeholder The placeholder to get.
	 *
	 * @return array
	 */
	public function get_placeholder_transformations( $placeholder ) {

		$transformations = array(
			'predominant' => array(
				array(
					'$currWidth'  => 'w',
					'$currHeight' => 'h',
				),
				array(
					'width'        => 'iw_div_2',
					'aspect_ratio' => 1,
					'crop'         => 'pad',
					'background'   => 'auto',
				),
				array(
					'crop'    => 'crop',
					'width'   => 10,
					'height'  => 10,
					'gravity' => 'north_east',
				),
				array(
					'width'  => '$currWidth',
					'height' => '$currHeight',
					'crop'   => 'fill',
				),
				array(
					'fetch_format' => 'auto',
					'quality'      => 'auto',
				),
			),
			'vectorize'   => array(
				array(
					'effect'       => 'vectorize:3:0.1',
					'fetch_format' => 'svg',
				),
			),
			'blur'        => array(
				array(
					'effect'       => 'blur:2000',
					'quality'      => 1,
					'fetch_format' => 'auto',
				),
			),
			'pixelate'    => array(
				array(
					'effect'       => 'pixelate',
					'quality'      => 1,
					'fetch_format' => 'auto',
				),
			),
		);

		return $transformations[ $placeholder ];
	}

	/**
	 * Add features to a tag element set.
	 *
	 * @param array $tag_element The tag element set.
	 *
	 * @return array
	 */
	public function add_features( $tag_element ) {
		if ( Utils::is_amp() ) {
			return $tag_element;
		}
		static $has_loader = false;
		// Bypass file formats that shouldn't be lazy loaded.
		if (
			in_array(
				$tag_element['format'],
				/**
				 * Filter out file formats for Lazy Load.
				 *
				 * @hook  cloudinary_lazy_load_bypass_formats
				 * @since 3.0.0
				 *
				 * @param $formats {array) The list of formats to exclude.
				 *
				 * @return {array}
				 */
				apply_filters( 'cloudinary_lazy_load_bypass_formats', array( 'svg' ) ),
				true
			)
		) {
			return $tag_element;
		}

		/**
		 * Short circuit the lazy load.
		 *
		 * @hook  cloudinary_lazy_load_bypass
		 *
		 * @since 3.0.9
		 *
		 * @param $short_circuit {bool}  The short circuit value.
		 * @param $tag_element   {array} The tag element.
		 *
		 * @return {bool}
		 *
		 * @example
		 * <?php
		 *
		 * // Bypass lazy load for images with ID `feature-image`.
		 * add_filter(
		 *    'cloudinary_lazy_load_bypass',
		 *    function( $bypass, $tag_element ) {
		 *        if ( 'feature-image! === $tag_element['id'] ) {
		 *            $bypass = true;
		 *        }
		 *        return $bypass;
		 *    }
		 * );
		 */
		if ( apply_filters( 'cloudinary_lazy_load_bypass', false, $tag_element ) ) {
			return $tag_element;
		}

		$sizes = array(
			$tag_element['width'],
			$tag_element['height'],
		);

		// Capture the original size.
		$tag_element['atts']['data-size'] = array_filter( $sizes );

		$colors    = $this->config['lazy_custom_color'];
		$animation = array(
			$colors,
		);
		if ( 'on' === $this->config['lazy_animate'] ) {
			preg_match_all( '/(\d\.*)+/', $colors, $matched );
			$fade        = $matched[0];
			$fade[3]     = 0.1;
			$fade        = 'rgba(' . implode( ',', $fade ) . ')';
			$animation[] = $fade;
			$animation[] = $colors;
		}
		$colors = implode( ';', $animation );

		// Add svg placeholder.
		$tag_element['atts']['src'] = Utils::svg_encoded( $tag_element['atts']['width'], $tag_element['atts']['height'], $colors );
		if ( isset( $tag_element['atts']['srcset'] ) ) {
			$tag_element['atts']['data-srcset'] = $tag_element['atts']['srcset'];
			$tag_element['atts']['data-sizes']  = $tag_element['atts']['sizes'];
			unset( $tag_element['atts']['srcset'], $tag_element['atts']['sizes'] );
		}
		if ( ! Utils::is_admin() ) {
			$tag_element['atts']['data-delivery'] = $this->media->get_media_delivery( $tag_element['id'] );
			if ( empty( $tag_element['atts']['onload'] ) ) {
				$tag_element['atts']['onload'] = '';
			}
			$loader = 'null;';
			if ( ! $has_loader && Utils::is_frontend_ajax() ) {
				$has_loader = true;
				$url        = add_query_arg( 'cloudinary_lazy_load_loader', true, trailingslashit( Utils::home_url() ) );
				$loader     = 'document.body.appendChild(document.createElement(\'script\')).src=\'' . $url . '\';this.onload=null;';
			}
			// Since we're appending to the onload, check it isn't already in, as it may run twice i.e full page caching.
			if ( false === strpos( $tag_element['atts']['onload'], 'CLDBind' ) ) {
				$tag_element['atts']['data-cloudinary'] = 'lazy';
				$tag_element['atts']['onload']         .= ';window.CLDBind?CLDBind(this):' . $loader;
			}
		}

		return $tag_element;
	}

	/**
	 * Register front end hooks.
	 */
	public function register_assets() {
		wp_register_script( 'cld-lazy-load', $this->plugin->dir_url . 'js/lazy-load.js', null, $this->plugin->version, false );
	}

	/**
	 * Apply front end filters on the enqueue_assets hook.
	 */
	public function enqueue_assets() {
		if ( Utils::is_frontend_ajax() ) {
			return;
		}

		ob_start();
		Utils::print_inline_tag( $this->get_inline_script() );
		$script        = ob_get_clean();
		$script_holder = '<meta name="cld-loader">';
		$allow         = array(
			'meta' => array(
				'name' => true,
			),
		);
		echo wp_kses( $script_holder, $allow );
		String_Replace::replace( $script_holder, $script );
	}

	/**
	 * Enqueue assets if not AMP.
	 */
	public function maybe_enqueue_assets() {
		if ( ! Utils::is_amp() ) {
			parent::maybe_enqueue_assets();
		}
	}

	/**
	 * Check if component is active.
	 *
	 * @return bool
	 */
	public function is_active() {

		return ( ! Utils::is_admin() && ! Utils::is_rest_api() ) || Utils::is_frontend_ajax();
	}

	/**
	 * Add the settings.
	 *
	 * @param array $pages The pages to add to.
	 *
	 * @return array
	 */
	public function register_settings( $pages ) {

		$pages['lazy_loading'] = array(
			'page_title'          => __( 'Lazy loading', 'cloudinary' ),
			'menu_title'          => __( 'Lazy loading', 'cloudinary' ),
			'priority'            => 5,
			'requires_connection' => true,
			'sidebar'             => true,
			'settings'            => array(
				array(
					'type'        => 'panel',
					'title'       => __( 'Lazy Loading', 'cloudinary' ),
					'priority'    => 9,
					'option_name' => 'media_display',
					array(
						'type' => 'tabs',
						'tabs' => array(
							'image_setting' => array(
								'text' => __( 'Settings', 'cloudinary' ),
								'id'   => 'settings',
							),
							'image_preview' => array(
								'text' => __( 'Preview', 'cloudinary' ),
								'id'   => 'preview',
							),
						),
					),
					array(
						'type' => 'row',
						array(
							'type'   => 'column',
							'tab_id' => 'settings',
							array(
								'type'               => 'on_off',
								'description'        => __( 'Enable lazy loading', 'cloudinary' ),
								'tooltip_text'       => __( 'Lazy loading delays the initialization of your web assets to improve page load times.', 'cloudinary' ),
								'optimisation_title' => __( 'Lazy loading', 'cloudinary' ),
								'slug'               => 'use_lazy_load',
								'default'            => 'on',
							),
							array(
								'type'      => 'group',
								'condition' => array(
									'use_lazy_load' => true,
								),
								array(
									'type'         => 'text',
									'title'        => __( 'Lazy loading threshold', 'cloudinary' ),
									'tooltip_text' => __( 'How far down the page to start lazy loading assets.', 'cloudinary' ),
									'slug'         => 'lazy_threshold',
									'attributes'   => array(
										'style'            => array(
											'width:100px;display:block;',
										),
										'data-auto-suffix' => '*px;em;rem;vh',
									),
									'default'      => '100px',
								),
								array(
									'type'    => 'tag',
									'element' => 'hr',
								),
								array(
									'type'        => 'color',
									'title'       => __( 'Pre-loader color', 'cloudinary' ),
									'description' => __(
										'On page load, the pre-loader is used to fill the space while the image is downloaded, preventing content shift.',
										'cloudinary'
									),
									'slug'        => 'lazy_custom_color',
									'default'     => 'rgba(153,153,153,0.5)',
								),
								array(
									'type'        => 'on_off',
									'description' => __( 'Pre-loader animation', 'cloudinary' ),
									'slug'        => 'lazy_animate',
									'default'     => 'on',
								),
								array(
									'type'    => 'tag',
									'element' => 'hr',
								),
								array(
									'type'        => 'radio',
									'title'       => __( 'Placeholder generation type', 'cloudinary' ),
									'description' => __(
										"Placeholders are low-res representations of the image, that's loaded below the fold. They are then replaced with the actual image, just before it comes into view.",
										'cloudinary'
									),
									'slug'        => 'lazy_placeholder',
									'default'     => 'blur',
									'condition'   => array(
										'use_lazy_load' => true,
									),
									'options'     => array(
										'blur'        => __( 'Blur', 'cloudinary' ),
										'pixelate'    => __( 'Pixelate', 'cloudinary' ),
										'vectorize'   => __( 'Vectorize', 'cloudinary' ),
										'predominant' => __( 'Dominant Color', 'cloudinary' ),
										'off'         => __( 'Off', 'cloudinary' ),
									),
								),
								array(
									'type'    => 'tag',
									'element' => 'hr',
								),
								array(
									'type'         => 'select',
									'slug'         => 'dpr',
									'priority'     => 8,
									'title'        => __( 'DPR settings', 'cloudinary' ),
									'tooltip_text' => __( 'The device pixel ratio to use for your generated images.', 'cloudinary' ),
									'default'      => '2X',
									'options'      => array(
										'off' => __( 'Off', 'cloudinary' ),
										'2X'  => __( 'Auto (2x)', 'cloudinary' ),
										'max' => __( 'Max DPR', 'cloudinary' ),
									),
								),
							),
						),
						array(
							'type'      => 'column',
							'tab_id'    => 'preview',
							'class'     => array(
								'cld-ui-preview',
							),
							'condition' => array(
								'use_lazy_load' => true,
							),
							array(
								'type'    => 'lazyload_preview',
								'title'   => __( 'Preview', 'cloudinary' ),
								'slug'    => 'lazyload_preview',
								'default' => CLOUDINARY_ENDPOINTS_PREVIEW_IMAGE . 'w_600/sample.jpg',
							),
						),
					),
					array(
						'type'  => 'info_box',
						'icon'  => $this->plugin->dir_url . 'css/images/academy-icon.svg',
						'title' => __( 'Need help?', 'cloudinary' ),
						'text'  => sprintf(
							// Translators: The HTML for opening and closing link tags.
							__(
								'Watch free lessons on how to use the Lazy Load Settings in the %1$sCloudinary Academy%2$s.',
								'cloudinary'
							),
							'<a href="https://training.cloudinary.com/learn/course/introduction-to-cloudinary-for-wordpress-administrators-70-minute-course-1h85/lessons/lazily-loading-and-delivering-responsive-images-1003?page=1" target="_blank" rel="noopener noreferrer">',
							'</a>'
						),
					),
				),
			),
		);

		return $pages;
	}
}