'header', 'above_footer' => 'footer', ]; /** * Tags that a wrapper filter can choose. * * @var array */ const WRAPPER_TAGS = [ 'header', 'footer', 'div', 'section', 'aside', 'nav' ]; /** * Rendered Elementor content for this request, by template ID. * * @var array */ private static $content = []; /** * Template IDs that render right now. Stops a template that embeds itself. * * @var array */ private static $rendering = []; /** * Section types that printed on this request. * * @var array */ private static $printed = []; /** * Adds the hooks. */ public function __construct() { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ], 20 ); add_filter( 'body_class', [ $this, 'body_class' ] ); } /** * Renders the matching templates while wp_head() still waits. * * Elementor enqueues the document CSS, the widget CSS, and the fonts during * the render. The output functions print the cached markup later. * * @return void */ public function enqueue() { $ids = []; foreach ( array_keys( Post_Type::get_types() ) as $type ) { $id = Resolver::get_template_id( $type ); if ( $id ) { $ids[] = $id; } } if ( empty( $ids ) ) { return; } wp_enqueue_style( 'headfoot-frontend', HEADFOOT_URL . 'assets/css/frontend.css', [], HEADFOOT_VERSION ); if ( ! class_exists( '\Elementor\Plugin' ) ) { return; } // Elementor loads its frontend styles only on pages built with Elementor. \Elementor\Plugin::instance()->frontend->enqueue_styles(); foreach ( array_unique( $ids ) as $id ) { self::get_content( $id ); if ( class_exists( '\Elementor\Core\Files\CSS\Post' ) ) { $css = new \Elementor\Core\Files\CSS\Post( $id ); $css->enqueue(); } } } /** * Returns the Elementor content of any template. * * @param int $id Template ID. * @return string */ public static function get_content( $id ) { $id = (int) $id; if ( ! $id ) { return ''; } if ( isset( self::$content[ $id ] ) ) { return self::$content[ $id ]; } if ( ! class_exists( '\Elementor\Plugin' ) || isset( self::$rendering[ $id ] ) ) { return ''; } self::$rendering[ $id ] = true; self::$content[ $id ] = (string) \Elementor\Plugin::instance()->frontend->get_builder_content_for_display( $id ); unset( self::$rendering[ $id ] ); return self::$content[ $id ]; } /** * Returns the markup of one template type with its wrapper. * * @param string $type Template type. * @return string Empty string when no template matches. */ public static function get_html( $type ) { $type = Post_Type::sanitize_type( $type ); $id = Resolver::get_template_id( $type ); if ( ! $id ) { return ''; } switch ( $type ) { case 'header': $wrapper = [ 'tag' => 'header', 'attributes' => [ 'id' => 'masthead', 'class' => 'headfoot-header', 'itemscope' => 'itemscope', 'itemtype' => 'https://schema.org/WPHeader', ], ]; break; case 'footer': $wrapper = [ 'tag' => 'footer', 'attributes' => [ 'id' => 'colophon', 'class' => 'headfoot-footer', 'itemscope' => 'itemscope', 'itemtype' => 'https://schema.org/WPFooter', ], ]; break; default: $wrapper = [ 'tag' => 'div', 'attributes' => [ 'class' => 'headfoot-section headfoot-' . str_replace( '_', '-', $type ), ], ]; } $default_tag = $wrapper['tag']; /** * Filters the wrapper element of a template type. * * The hook names are headfoot_header_wrapper, headfoot_below_header_wrapper, * headfoot_above_footer_wrapper, and headfoot_footer_wrapper. The plugin * escapes the tag and every attribute after the filter. * * @param array $wrapper [ 'tag' => string, 'attributes' => [ name => value ] ]. * @param int $id Template ID. */ $wrapper = apply_filters( "headfoot_{$type}_wrapper", $wrapper, $id ); $wrapper = is_array( $wrapper ) ? $wrapper : []; $tag = isset( $wrapper['tag'] ) && in_array( $wrapper['tag'], self::WRAPPER_TAGS, true ) ? $wrapper['tag'] : $default_tag; $attributes = isset( $wrapper['attributes'] ) && is_array( $wrapper['attributes'] ) ? $wrapper['attributes'] : []; if ( self::is_editor_preview() ) { $attributes['data-headfoot-preview'] = '1'; } $html = '<' . $tag; foreach ( $attributes as $name => $value ) { $name = preg_replace( '/[^a-z0-9_:-]/', '', strtolower( (string) $name ) ); // An event handler attribute holds script, so the wrapper never prints one. if ( '' === $name || 0 === strpos( $name, 'on' ) || ! is_scalar( $value ) ) { continue; } $html .= ' ' . $name . '="' . esc_attr( (string) $value ) . '"'; } // Elementor builds the content and escapes the output of every widget. return $html . '>' . self::get_content( $id ) . ''; } /** * Returns the markup of an area: the header with the section below it, or * the footer with the section above it. * * @param string $area header or footer. * @return string */ public static function get_area_html( $area ) { if ( 'footer' === $area ) { return self::get_html( 'above_footer' ) . self::get_html( 'footer' ); } return self::get_html( 'header' ) . self::get_html( 'below_header' ); } /** * Prints one template type. * * A section type prints once per request. Later calls print nothing. * * @param string $type Template type. * @return void */ public static function render( $type ) { $type = Post_Type::sanitize_type( $type ); if ( isset( self::$printed[ $type ] ) ) { return; } $id = Resolver::get_template_id( $type ); if ( ! $id ) { return; } if ( isset( self::SECTIONS[ $type ] ) ) { self::$printed[ $type ] = true; } /** * Fires before the markup of a template type. * * The hook names are headfoot_before_header, headfoot_before_below_header, * headfoot_before_above_footer, and headfoot_before_footer. * * @param int $id Template ID. */ do_action( "headfoot_before_{$type}", $id ); echo self::get_html( $type ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_html() escapes the wrapper, and Elementor escapes the widget output. /** * Fires after the markup of a template type. * * The hook names are headfoot_after_header, headfoot_after_below_header, * headfoot_after_above_footer, and headfoot_after_footer. * * @param int $id Template ID. */ do_action( "headfoot_after_{$type}", $id ); } /** * Prints an area: the header with the section below it, or the footer with * the section above it. * * @param string $area header or footer. * @return void */ public static function render_area( $area ) { if ( 'footer' === $area ) { self::render( 'above_footer' ); self::render( 'footer' ); return; } self::render( 'header' ); self::render( 'below_header' ); } /** * Clears the request state. Tests use this. * * @return void */ public static function reset() { self::$content = []; self::$printed = []; } /** * Adds a body class for every template type that renders. * * @param array $classes Body classes. * @return array */ public function body_class( $classes ) { foreach ( array_keys( Post_Type::get_types() ) as $type ) { if ( Resolver::get_template_id( $type ) ) { $classes[] = 'headfoot-has-' . str_replace( '_', '-', $type ); } } return $classes; } /** * Tells whether the request is the preview iframe of the Elementor editor. * * @return bool */ private static function is_editor_preview() { if ( ! class_exists( '\Elementor\Plugin' ) ) { return false; } $preview = \Elementor\Plugin::instance()->preview; return is_object( $preview ) && method_exists( $preview, 'is_preview_mode' ) && $preview->is_preview_mode(); } }