is_type( 'simple' ) ) { $set = self::product_set( $product ); return null === $set ? null : array( 'variations' => array(), 'default' => $set, 'options' => $options, ); } if ( ! $product instanceof WC_Product_Variable ) { return null; } $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = wc_get_product( $child_id ); if ( ! $variation instanceof WC_Product_Variation || 'publish' !== $variation->get_status() ) { continue; } $set = self::product_set( $variation ); if ( null === $set ) { continue; } $variations[] = array( 'attributes' => $variation->get_variation_attributes() ) + $set; } if ( empty( $variations ) ) { return null; } $picked = array(); foreach ( $product->get_default_attributes() as $name => $value ) { $picked[ 'attribute_' . sanitize_title( $name ) ] = (string) $value; } $data = array( 'variations' => $variations, 'default' => self::match( $variations, $picked ), 'options' => $options, ); if ( ! empty( $options['dots'] ) ) { $data['dots'] = self::dot_colors( $variations ); } return $data; } /** * The dot color for each option, keyed like WooCommerce's option chip * IDs (`attribute_color-orange`). An option gets a dot only when every * variation with it shares one color, like a color attribute; a size * that comes in several colors gets none. * * @param array $variations Variations with `attributes` and their color. * @return array Hex colors by chip ID. */ public static function dot_colors( $variations ) { $seen = array(); foreach ( $variations as $variation ) { foreach ( $variation['attributes'] as $name => $value ) { if ( '' === (string) $value ) { continue; } $id = $name . '-' . sanitize_title( $value ); $seen[ $id ][ $variation['color'] ] = true; } } $dots = array(); foreach ( $seen as $id => $colors ) { if ( 1 === count( $colors ) ) { $dots[ $id ] = (string) key( $colors ); } } return $dots; } /** * The colors for one accent: the accent, the text on it, the accent as * text on the page, and the tint. * * @param string $accent Hex color, or empty. * @return array{color: string, on: string, ink: string, tint: string}|null Null for no color. */ public static function color_set( $accent ) { $accent = sanitize_hex_color( (string) $accent ); if ( ! $accent ) { return null; } return array( 'color' => $accent, 'on' => Contrast::on( $accent ), 'ink' => Tone::ink( $accent ), 'tint' => Tone::tint( $accent ), ); } /** * The color set for a variation or simple product, after the * `purplish_color_set` filter. * * @param \WC_Product $product Variation or simple product. * @return array{color: string, on: string, ink: string, tint: string}|null Null for no color. */ public static function product_set( $product ) { /** * Filters the color set for a variation or simple product, so a store * can force its own colors. Return null for no color. * * @since 1.1.0 * * @param array|null $set `color`, `on` (text on the color), `ink` (the color as text on the page) and `tint`, or null for no color. * @param \WC_Product $product Variation or simple product. */ $set = apply_filters( 'purplish_color_set', self::color_set( Resolver::accent( $product ) ), $product ); if ( ! is_array( $set ) || ! isset( $set['color'] ) ) { return null; } // A filtered set may leave out or break a part; work that part out from the color. $worked_out = self::color_set( $set['color'] ); if ( null === $worked_out ) { return null; } foreach ( array( 'on', 'ink', 'tint' ) as $part ) { $value = isset( $set[ $part ] ) ? sanitize_hex_color( (string) $set[ $part ] ) : ''; if ( $value ) { $worked_out[ $part ] = $value; } } return $worked_out; } /** * The color set the picked values point to. The same rule as the * script: keep the variations that fit every picked value (an empty * value in a variation means any), and use their color when they all * share one. * * @param array $variations Variations with `attributes` and their color set. * @param array $picked Picked values by `attribute_*` name; empty means not picked. * @return array{color: string, on: string, ink: string, tint: string}|null Null when nothing is picked or the colors differ. */ public static function match( $variations, $picked ) { $picked = array_filter( array_map( 'strval', $picked ), 'strlen' ); if ( empty( $picked ) ) { return null; } $colors = array(); foreach ( $variations as $variation ) { foreach ( $picked as $name => $value ) { $own = isset( $variation['attributes'][ $name ] ) ? (string) $variation['attributes'][ $name ] : ''; if ( '' !== $own && $own !== $value ) { continue 2; } } $colors[ $variation['color'] ] = array( 'color' => $variation['color'], 'on' => $variation['on'], 'ink' => $variation['ink'], 'tint' => $variation['tint'], ); } return 1 === count( $colors ) ? reset( $colors ) : null; } /** * The data for the page being shown, worked out once. * * @return array|null */ private function current() { if ( false === $this->data ) { $this->data = is_product() ? self::for_product( wc_get_product( get_queried_object_id() ), self::options() ) : null; } return $this->data; } /** * Loads the script and styles, prints the data for the script, and puts * the default colors on `body`. * * @return void */ public function enqueue() { $data = $this->current(); if ( null === $data ) { return; } $asset = include PURPLISH_DIR . 'build/frontend.asset.php'; // A simple product has nothing to follow, so it needs only the styles. if ( ! empty( $data['variations'] ) ) { wp_enqueue_script( self::HANDLE, PURPLISH_URL . 'build/frontend.js', $asset['dependencies'], $asset['version'], array( 'strategy' => 'defer' ) ); wp_add_inline_script( self::HANDLE, 'window.purplishData = ' . wp_json_encode( $data ) . ';', 'before' ); } wp_enqueue_style( self::HANDLE, PURPLISH_URL . 'build/style-frontend.css', array(), $asset['version'] ); wp_style_add_data( self::HANDLE, 'rtl', 'replace' ); if ( null !== $data['default'] ) { wp_add_inline_style( self::HANDLE, sprintf( 'body{--purplish-accent:%1$s;--purplish-on:%2$s;--purplish-ink:%3$s;--purplish-tint:%4$s}', sanitize_hex_color( $data['default']['color'] ), sanitize_hex_color( $data['default']['on'] ), sanitize_hex_color( $data['default']['ink'] ), sanitize_hex_color( $data['default']['tint'] ) ) ); } } /** * Adds the part and transition classes, and `purplish-active` when the * page starts with a color. * * @param string[] $classes Body classes. * @return string[] */ public function body_class( $classes ) { $data = $this->current(); if ( null === $data ) { return $classes; } $classes[] = 'purplish'; if ( null !== $data['default'] ) { $classes[] = 'purplish-active'; } foreach ( $data['options']['parts'] as $part ) { $classes[] = 'purplish-part-' . sanitize_html_class( $part ); } $classes[] = 'purplish-transition-' . sanitize_html_class( $data['options']['transition'] ); foreach ( Options::SWITCHES as $switch ) { if ( ! empty( $data['options'][ $switch ] ) ) { $classes[] = 'purplish-' . $switch; } } return $classes; } }