(template)", Elementor active → Full Width (`elementor_header_footer`) with * container → [aoo-shop-part part=header] · [aoo-shop-filters layout=bar] · [aoo-shop-part part=grid] * (W3 widget names — `aoo-shop-part` control `part`, default DEFAULT_PART; `aoo-shop-filters` control `layout`). * post_content always carries the same three shortcodes so the page still works without Elementor. * * @package AooShop */ defined( 'ABSPATH' ) || exit; class AooShop_Templates { /** Elementor widget names (W3). */ const WIDGET_PART = 'aooshop-part'; const WIDGET_FILTERS = 'aooshop-filters'; /** Meta on sample pages this plugin created (value = type) — uninstall deletes those pages. */ const SAMPLE_META = '_aooshop_sample'; /** Part an `aoo-shop-part` widget renders when its `part` setting is absent (Elementor stores only changed values). */ const DEFAULT_PART = 'grid'; /** @var AooShop_Template_Page */ private $template; public function __construct( AooShop_Template_Page $template ) { $this->template = $template; } /** * Save assignments {type: page id}. Unusable ids (not a published page, or the central page) become 0. * * @param array $map Raw input. * @return array Saved map. */ public function save( array $map ) { $central = (int) get_option( AooShop_Template_Page::OPTION, 0 ); $out = array(); foreach ( AooShop_Template_Page::TEMPLATE_TYPES as $type ) { $id = isset( $map[ $type ] ) ? absint( $map[ $type ] ) : 0; if ( $id && ( $id === $central || ! AooShop_Template_Page::usable( $id ) ) ) { $id = 0; } $out[ $type ] = $id; if ( $id ) { $this->template->apply_meta( $id, false ); } } update_option( AooShop_Template_Page::TEMPLATES_OPTION, $out, true ); do_action( 'aooshop_templates_saved', $out ); return $out; } /** Assign one type (keeps the others). */ public function assign( $type, $id ) { $map = $this->template->assigned(); $map[ $type ] = (int) $id; return $this->save( $map ); } public static function elementor_active() { return did_action( 'elementor/loaded' ) && class_exists( '\\Elementor\\Plugin' ) && defined( 'ELEMENTOR_VERSION' ); } /** Shortcode body of a sample page (also the fallback content of the Elementor version). */ public static function sample_shortcodes() { return "[aooshop_content part=\"header\"]\n[aooshop_filters layout=\"bar\"]\n[aooshop_content part=\"grid\"]"; } private static function el_id() { return substr( md5( uniqid( (string) wp_rand(), true ) ), 0, 7 ); } private static function widget( $type, array $settings ) { return array( 'id' => self::el_id(), 'elType' => 'widget', 'widgetType' => $type, 'settings' => $settings, 'elements' => array(), 'isInner' => false, ); } /** `_elementor_data` of a sample page (container when the experiment is on, else section → column). */ public static function sample_elementor_data() { $widgets = array( self::widget( self::WIDGET_PART, array( 'part' => 'header' ) ), self::widget( self::WIDGET_FILTERS, array( 'layout' => 'bar' ) ), self::widget( self::WIDGET_PART, array( 'part' => 'grid' ) ), ); $container = true; if ( self::elementor_active() ) { $el = \Elementor\Plugin::$instance; if ( isset( $el->experiments ) && method_exists( $el->experiments, 'is_feature_active' ) ) { $container = (bool) $el->experiments->is_feature_active( 'container' ); } } if ( $container ) { return array( array( 'id' => self::el_id(), 'elType' => 'container', 'settings' => array( 'content_width' => 'boxed', 'flex_direction' => 'column', 'flex_wrap' => 'nowrap' ), // wrap = Chrome sizes column items at min-content width (tall gaps on phones). 'elements' => $widgets, 'isInner' => false, ), ); } return array( array( 'id' => self::el_id(), 'elType' => 'section', 'settings' => array(), 'elements' => array( array( 'id' => self::el_id(), 'elType' => 'column', 'settings' => array( '_column_size' => 100 ), 'elements' => $widgets, 'isInner' => false, ), ), 'isInner' => false, ), ); } /** * Create a sample template page for a type and assign it. * * @param string $type One of AooShop_Template_Page::TEMPLATE_TYPES. * @return int|WP_Error Page id. */ public function create_sample( $type ) { $labels = AooShop_Template_Page::type_labels(); if ( ! isset( $labels[ $type ] ) ) { return new WP_Error( 'aoo_bad_type', __( 'Invalid page type.', 'aoo-shop' ) ); } $elementor = self::elementor_active(); $id = wp_insert_post( array( 'post_type' => 'page', 'post_status' => 'publish', /* translators: %s: page kind, e.g. หน้าหมวด */ 'post_title' => sprintf( __( 'AOO · %s (template)', 'aoo-shop' ), $labels[ $type ] ), 'post_name' => 'aoo-template-' . $type, 'post_content' => self::sample_shortcodes(), 'comment_status' => 'closed', 'ping_status' => 'closed', ), true ); if ( is_wp_error( $id ) ) { return $id; } $id = (int) $id; update_post_meta( $id, self::SAMPLE_META, $type ); if ( $elementor ) { update_post_meta( $id, '_wp_page_template', 'elementor_header_footer' ); update_post_meta( $id, '_elementor_edit_mode', 'builder' ); update_post_meta( $id, '_elementor_template_type', 'wp-page' ); update_post_meta( $id, '_elementor_version', ELEMENTOR_VERSION ); update_post_meta( $id, '_elementor_data', wp_slash( wp_json_encode( self::sample_elementor_data() ) ) ); $el = \Elementor\Plugin::$instance; if ( isset( $el->files_manager ) && method_exists( $el->files_manager, 'clear_cache' ) ) { $el->files_manager->clear_cache(); } } $this->assign( $type, $id ); // → apply_meta (noindex, hidden title, Astra layout…). do_action( 'aooshop_template_sample_created', $id, $type, $elementor ); return $id; } // ── part scanning ────────────────────────────────────────────────── /** * Count shop parts on a page as it renders: `_elementor_data` when built with Elementor and Elementor is * active, else post_content. `[aooshop_content]` (no part) counts as `full`. A page with no token at all * gets the whole page appended by the renderer → reported as `auto`. * * @return array{full:int, header:int, filters:int, grid:int, footer:int, auto:bool, source:string} */ public function part_counts( $post_id ) { $c = array( 'full' => 0, 'header' => 0, 'filters' => 0, 'grid' => 0, 'footer' => 0, 'auto' => false, 'source' => 'content' ); $data = null; if ( self::elementor_active() && 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) { $raw = get_post_meta( $post_id, '_elementor_data', true ); $data = is_string( $raw ) ? json_decode( $raw, true ) : ( is_array( $raw ) ? $raw : null ); } if ( is_array( $data ) ) { $c['source'] = 'elementor'; $this->walk_elementor( $data, $c ); } else { $post = get_post( $post_id ); $this->scan_text( $post ? (string) $post->post_content : '', $c ); } $c['auto'] = 0 === $c['full'] + $c['header'] + $c['filters'] + $c['grid'] + $c['footer']; return $c; } private function walk_elementor( array $elements, array &$c ) { foreach ( $elements as $el ) { if ( ! is_array( $el ) ) { continue; } $settings = isset( $el['settings'] ) && is_array( $el['settings'] ) ? $el['settings'] : array(); if ( isset( $el['elType'] ) && 'widget' === $el['elType'] && isset( $el['widgetType'] ) && self::WIDGET_PART === $el['widgetType'] ) { $part = isset( $settings['part'] ) && is_string( $settings['part'] ) && '' !== $settings['part'] ? sanitize_key( $settings['part'] ) : self::DEFAULT_PART; if ( 'full' === $part || 'all' === $part ) { $c['full']++; } elseif ( isset( $c[ $part ] ) && is_int( $c[ $part ] ) ) { $c[ $part ]++; } } else { // Shortcode / text / HTML widgets: any string setting may hold [aooshop_content …]. array_walk_recursive( $settings, function ( $v ) use ( &$c ) { if ( is_string( $v ) && false !== strpos( $v, '[aooshop_content' ) ) { $this->scan_text( $v, $c ); } } ); } if ( ! empty( $el['elements'] ) && is_array( $el['elements'] ) ) { $this->walk_elementor( $el['elements'], $c ); } } } private function scan_text( $text, array &$c ) { if ( ! preg_match_all( '/\[aooshop_content(\s[^\]]*)?\]/', (string) $text, $m ) ) { return; } foreach ( $m[1] as $raw_atts ) { $atts = shortcode_parse_atts( trim( (string) $raw_atts ) ); $part = is_array( $atts ) && isset( $atts['part'] ) ? sanitize_key( (string) $atts['part'] ) : ''; if ( '' === $part ) { $c['full']++; } elseif ( isset( $c[ $part ] ) && is_int( $c[ $part ] ) ) { $c[ $part ]++; } } } /** * Thai warnings for a template page (empty = fine). * * @return string[] */ public function warnings( $post_id ) { $out = array(); if ( ! AooShop_Template_Page::usable( $post_id ) ) { return array( __( 'This page is not published — the central shop page is used until it is.', 'aoo-shop' ) ); } $c = $this->part_counts( $post_id ); if ( $c['auto'] ) { return array( __( 'This page has no shop sections yet — the full shop is added after the content automatically.', 'aoo-shop' ) ); } $headers = $c['full'] + $c['header']; $grids = $c['full'] + $c['grid']; if ( 0 === $headers ) { $out[] = __( 'No header section — the page will have no h1, which is bad for SEO.', 'aoo-shop' ); } elseif ( $headers >= 2 ) { /* translators: %d: number of header sections */ $out[] = sprintf( __( '%d header sections — the page will have duplicate h1 headings. Keep only one.', 'aoo-shop' ), $headers ); } if ( 0 === $grids ) { $out[] = __( 'No product grid — shoppers will not see any products.', 'aoo-shop' ); } elseif ( $grids >= 2 ) { /* translators: %d: number of product grids */ $out[] = sprintf( __( '%d product grids — products will be shown more than once.', 'aoo-shop' ), $grids ); } return $out; } }