apply_to_catalog( $content, $design ); } if ( 'single-academy_courses' === $slug ) { return $this->apply_to_course_page( $content, $design ); } return $content; } /** * Replace the card in a course grid, and set its columns. * * @param string $content Template content. * @param array $design Design. * @return string */ private function apply_to_catalog( $content, array $design ) { $blocks = parse_blocks( $content ); $columns = (int) $design['catalog']['columns']['desktop']; $card = $this->card_blocks( $design ); $this->walk( $blocks, function ( &$block ) use ( $card, $columns ) { if ( 'core/post-template' !== $block['blockName'] ) { return; } $block['attrs']['layout'] = [ 'type' => 'grid', 'columnCount' => $columns, ]; $this->replace_inner( $block, $card ); } ); return $this->serialize( $blocks ); } /** * Rebuild a course page from the design: section order, what is switched * off, and which side the enroll box is on. * * @param string $content Template content. * @param array $design Design. * @return string */ private function apply_to_course_page( $content, array $design ) { $blocks = parse_blocks( $content ); $sections = $this->section_blocks( $design ); $this->walk( $blocks, function ( &$block ) use ( $sections, $design ) { if ( 'core/columns' !== $block['blockName'] || count( $block['innerBlocks'] ) < 2 ) { return; } $main = $block['innerBlocks'][0]; $sidebar = $block['innerBlocks'][1]; $this->replace_inner( $main, $sections ); if ( 'none' === $design['single']['sidebar'] ) { $main['attrs']['width'] = '100%'; $this->replace_inner( $block, [ $main ] ); return; } $columns = 'left' === $design['single']['sidebar'] ? [ $sidebar, $main ] : [ $main, $sidebar ]; $this->replace_inner( $block, $columns ); } ); return $this->serialize( $blocks ); } /** * The blocks of a course card, in the design's order. * * @param array $design Design. * @return array Parsed blocks. */ public function card_blocks( array $design ) { $pattern = Patterns::post( 'card' ); if ( $pattern ) { return Patterns::block( $pattern ); } return parse_blocks( $this->card_markup( $design ) ); } /** * The markup of a course card, in the design's order. * * @param array $design Design. * @return string */ public function card_markup( array $design ) { $radius = (int) $design['style']['radius']; $markup = [ // These sizes are still baked in here (the value at the moment this // pattern is (re)generated) — Gutenberg's style engine mangles a raw // var(--x) reference used as a border/typography JSON attribute // value into invalid content (confirmed: it corrupts "--" into the // literal text "u002d", which then fails the block's own save() // round-trip and shows "Block contains unexpected or invalid // content" for every nested block). Actually following Shape // changes after the fact is handled instead by the plain CSS // override in tokens() below, which never touches block attributes. 'image' => sprintf( '', esc_attr( $design['catalog']['imageRatio'] ), max( 0, $radius - 1 ) ), 'badge' => '', 'categories' => '', // No font size here on purpose: the Shape title size is a CSS // default in tokens() instead, so moving that slider reaches cards // already on a page and a size set on this block still wins. 'title' => '', 'excerpt' => '', 'instructors' => '', 'meta' => '', 'rating' => '', 'price' => '', 'button' => '', 'wishlist' => '', ]; $visible = []; foreach ( $design['catalog']['elements'] as $element ) { if ( ! empty( $element['visible'] ) && isset( $markup[ $element['key'] ] ) ) { $visible[] = $element['key']; } } $image = ''; if ( $visible && 'image' === $visible[0] ) { $image = $markup['image']; array_shift( $visible ); } // Price, rating and the button share one row at the foot of the card // when they are the last parts; anywhere else they follow the order. $row = [ 'rating', 'price', 'button', 'wishlist' ]; $footer = ''; while ( $visible && in_array( end( $visible ), $row, true ) ) { $footer = $markup[ array_pop( $visible ) ] . $footer; } $body = ''; foreach ( $visible as $key ) { if ( 'image' !== $key ) { $body .= $markup[ $key ]; } } if ( '' !== $footer ) { $footer = '' . '
' . $footer . '
' . ''; } $card = sprintf( '' . '
' . '%2$s' . '' . '
%3$s%4$s
' . '' . '
' . '', max( 0, $radius ), $image, $body, $footer ); return $card; } /** * The blocks of a course page's main column, in the design's order. * * @param array $design Design. * @return array Parsed blocks. */ public function section_blocks( array $design ) { $pattern = Patterns::post( 'page' ); if ( $pattern ) { return Patterns::block( $pattern ); } return parse_blocks( $this->section_markup( $design ) ); } /** * The markup of a course page's main column, in the design's order. * * @param array $design Design. * @return string */ public function section_markup( array $design ) { $markup = [ 'media' => '', 'categories' => '', 'title' => '', 'meta' => '' . '
' . '' . '' . '' . '
', 'description' => '', 'benefits' => '', 'curriculum' => '', 'attachments' => '', 'instructorProfile' => '', 'ratingSummary' => '', 'reviewForm' => '', 'reviews' => '', ]; $content = ''; foreach ( $design['single']['sections'] as $section ) { if ( ! empty( $section['visible'] ) && isset( $markup[ $section['key'] ] ) ) { $content .= $markup[ $section['key'] ]; } } return $content; } /** * Colours and sizes from the design, as CSS. * * @param array $design Design. * @return string */ public function tokens( array $design ) { $style = $design['style']; $css = ''; // Colours come from Academy's palette (Customize → Colours & Style), which // prints for every page; the design only adds shape. $css .= '--academy-card-radius:' . (int) $style['radius'] . 'px;'; $css .= '--academy-button-radius:' . (int) $style['buttonRadius'] . 'px;'; $columns = $design['catalog']['columns']; $grid = sprintf( '@media(max-width:1024px){.academy-course-archive .wp-block-post-template{grid-template-columns:repeat(%1$d,minmax(0,1fr))!important}}' . '@media(max-width:600px){.academy-course-archive .wp-block-post-template{grid-template-columns:repeat(%2$d,minmax(0,1fr))!important}}', (int) $columns['tablet'], (int) $columns['mobile'] ); // The card pattern bakes its radius in as plain px at the moment it is // (re)generated (see card_markup()) rather than as a var(--x) block // attribute, because Gutenberg's style engine mangles that into invalid // block content. A later Shape change reaches already-generated cards // through this override instead. // // The title size is not baked in, so it needs no !important. It is a // plain two-class rule: that beats the theme's own heading / Post Title // styles (element or :root :where() rules), so the Shape slider sets the // card title size, while a size set on that Post Title block in the // editor still wins — a preset class carries !important and a custom // value is an inline style. (Wrapped in :where() it had no specificity // and the theme's h3 size won, so the slider did nothing.) $shape = sprintf( '.academy-course-card{border-radius:%1$dpx!important}' . '.academy-course-card .wp-block-post-featured-image img{border-radius:%2$dpx %2$dpx 0 0!important}' . '.academy-course-card .wp-block-post-title{font-size:%3$dpx}', max( 0, (int) $style['radius'] ), max( 0, (int) $style['radius'] - 1 ), max( 0, (int) $style['titleSize'] ) ); return ':root{' . $css . '}' . $grid . $shape . $this->classic_shape( $style ); } /** * Card corners, button corners and title size for the classic course cards * (`.academy-course`, drawn by PHP templates). Block cards carry these in * their own markup, so without this the Shape settings do nothing on a site * that still uses the classic templates. * * Only once the design has been saved (or is being previewed): the design's * defaults are not the classic card's own look, and a site that never * touched Customize should not change on update. * * @param array $style Design style. * @return string */ private function classic_shape( array $style ) { if ( '__none__' === get_option( Settings::OPTION, '__none__' ) ) { return ''; } // `:root body` outweighs the card's own nested rules without !important. return sprintf( ':root body .academy-course{border-radius:%1$dpx;overflow:hidden}' . ':root body .academy-course .academy-course__title{font-size:%2$dpx}' . ':root body .academy-course .academy-btn{border-radius:%3$dpx}', (int) $style['radius'], (int) $style['titleSize'], (int) $style['buttonRadius'] ); } /** * Print the design's CSS on the front end. * * @return void */ public function print_tokens() { // Settings::sanitize() constrains every token to a value primitive // (hex colors, numeric sizes, enum keywords). wp_strip_all_tags adds a // belt-and-braces guarantee that no stray `` or `