aristath/kirki

View on GitHub
packages/kirki-framework/field-typography/src/Field/Typography.php

Summary

Maintainability
F
5 days
Test Coverage
<?php
/**
 * Override field methods
 *
 * @package   kirki-framework/control-typography
 * @copyright Copyright (c) 2019, Ari Stathopoulos (@aristath)
 * @license   https://opensource.org/licenses/MIT
 * @since     1.0
 */

namespace Kirki\Field;

use Kirki\Field;
use Kirki\GoogleFonts;
use Kirki\Module\Webfonts\Fonts;

/**
 * Field overrides.
 *
 * @since 1.0
 */
class Typography extends Field {

    /**
     * The field type.
     *
     * @access public
     * @since 1.0
     * @var string
     */
    public $type = 'kirki-typography';

    /**
     * Has the glogal gfonts var been added already?
     *
     * @static
     * @access private
     * @since 1.0
     * @var bool
     */
    private static $gfonts_var_added = false;

    /**
     * An array of typography controls.
     *
     * This is used by the typography script for any custom logic
     * that has to be applied to typography controls.
     *
     * @static
     * @access private
     * @since 1.0
     * @var array
     */
    private static $typography_controls = [];

    /**
     * An array of standard font variants.
     *
     * @access private
     * @since 1.0.1
     *
     * @var array
     */
    private static $std_variants;

    /**
     * An array of complete font variants.
     *
     * @access private
     * @since 1.0.1
     *
     * @var array
     */
    private static $complete_variants;

    /**
     * Extra logic for the field.
     *
     * Adds all sub-fields.
     *
     * @access public
     * @param array $args The arguments of the field.
     */
    public function init( $args = [] ) {

        self::$typography_controls[] = $args['settings'];

        self::$std_variants = [
            [
                'value' => 'regular',
                'label' => __( 'Regular', 'kirki' ),
            ],
            [
                'value' => 'italic',
                'label' => __( 'Italic', 'kirki' ),
            ],
            [
                'value' => '700',
                'label' => __( '700', 'kirki' ),
            ],
            [
                'value' => '700italic',
                'label' => __( '700 Italic', 'kirki' ),
            ],
        ];

        self::$complete_variants = [
            [
                'value' => 'regular',
                'label' => __( 'Regular', 'kirki' ),
            ],
            [
                'value' => 'italic',
                'label' => __( 'Italic', 'kirki' ),
            ],
            [
                'value' => '100',
                'label' => __( '100', 'kirki' ),
            ],
            [
                'value' => '100italic',
                'label' => __( '100 Italic', 'kirki' ),
            ],
            [
                'value' => '200',
                'label' => __( '200', 'kirki' ),
            ],
            [
                'value' => '200italic',
                'label' => __( '200 Italic', 'kirki' ),
            ],
            [
                'value' => '300',
                'label' => __( '300', 'kirki' ),
            ],
            [
                'value' => '300italic',
                'label' => __( '300 Italic', 'kirki' ),
            ],
            [
                'value' => '500',
                'label' => __( '500', 'kirki' ),
            ],
            [
                'value' => '500italic',
                'label' => __( '500 Italic', 'kirki' ),
            ],
            [
                'value' => '600',
                'label' => __( '600', 'kirki' ),
            ],
            [
                'value' => '600italic',
                'label' => __( '600 Italic', 'kirki' ),
            ],
            [
                'value' => '700',
                'label' => __( '700', 'kirki' ),
            ],
            [
                'value' => '700italic',
                'label' => __( '700 Italic', 'kirki' ),
            ],
            [
                'value' => '800',
                'label' => __( '800', 'kirki' ),
            ],
            [
                'value' => '800italic',
                'label' => __( '800 Italic', 'kirki' ),
            ],
            [
                'value' => '900',
                'label' => __( '900', 'kirki' ),
            ],
            [
                'value' => '900italic',
                'label' => __( '900 Italic', 'kirki' ),
            ],
        ];

        $this->add_sub_fields( $args );

        add_action( 'customize_controls_enqueue_scripts', [ $this, 'enqueue_control_scripts' ] );
        add_action( 'customize_preview_init', [ $this, 'enqueue_preview_scripts' ] );
        add_filter( 'kirki_output_control_classnames', [ $this, 'output_control_classnames' ] );
    }

    /**
     * Add sub-fields.
     *
     * @access private
     * @since 1.0
     * @param array $args The field arguments.
     * @return void
     */
    private function add_sub_fields( $args ) {

        $args['kirki_config'] = isset( $args['kirki_config'] ) ? $args['kirki_config'] : 'global';

        $defaults = isset( $args['default'] ) ? $args['default'] : [];

        /**
         * Add a hidden field, the label & description.
         */
        new \Kirki\Field\Generic(
            wp_parse_args(
                [
                    'sanitize_callback' => isset( $args['sanitize_callback'] ) ? $args['sanitize_callback'] : [ __CLASS__, 'sanitize' ],
                    'wrapper_opts'      => [
                        'gap' => 'small',
                    ],
                    'input_attrs'       => '',
                    'choices'           => [
                        'type'        => 'hidden',
                        'parent_type' => 'kirki-typography',
                    ],
                ],
                $args
            )
        );

        $args['parent_setting'] = $args['settings'];
        $args['output']         = [];
        $args['wrapper_attrs']  = [
            'data-kirki-parent-control-type' => 'kirki-typography',
        ];

        if ( isset( $args['transport'] ) && 'auto' === $args['transport'] ) {
            $args['transport'] = 'postMessage';
        }

        /**
         * Add font-family selection controls.
         * These include font-family and variant.
         * They are grouped here because all they are required.
         * in order to get the right googlefont variant.
         */
        if ( isset( $args['default']['font-family'] ) ) {

            $args['wrapper_attrs']['kirki-typography-subcontrol-type'] = 'font-family';

            /**
             * Add font-family control.
             */
            new \Kirki\Field\ReactSelect(
                wp_parse_args(
                    [
                        'label'       => esc_html__( 'Font Family', 'kirki' ),
                        'description' => '',
                        'settings'    => $args['settings'] . '[font-family]',
                        'default'     => isset( $args['default']['font-family'] ) ? $args['default']['font-family'] : '',
                        'input_attrs' => $this->filter_preferred_choice_setting( 'input_attrs', 'font-family', $args ),
                        'choices'     => [], // The choices will be populated later inside `get_font_family_choices` function in this file.
                        'css_vars'    => [],
                        'output'      => [],
                    ],
                    $args
                )
            );

            /**
             * Add font variant.
             */
            $font_variant = isset( $args['default']['variant'] ) ? $args['default']['variant'] : 'regular';

            if ( isset( $args['default']['font-weight'] ) ) {
                $font_variant = 400 === $args['default']['font-weight'] || '400' === $args['default']['font-weight'] ? 'regular' : $args['default']['font-weight'];
            }

            $args['wrapper_attrs']['kirki-typography-subcontrol-type'] = 'font-variant';

            new \Kirki\Field\ReactSelect(
                wp_parse_args(
                    [
                        'label'       => esc_html__( 'Font Variant', 'kirki' ),
                        'description' => '',
                        'settings'    => $args['settings'] . '[variant]',
                        'default'     => $font_variant,
                        'input_attrs' => $this->filter_preferred_choice_setting( 'input_attrs', 'variant', $args ),
                        'choices'     => self::$std_variants,
                        'css_vars'    => [],
                        'output'      => [],
                    ],
                    $args
                )
            );

        }

        $font_size_field_specified = isset( $defaults['font-size'] );
        $color_field_specified     = isset( $defaults['color'] );

        if ( $font_size_field_specified || $color_field_specified ) {
            $group = [
                'font-size' => [
                    'type'         => 'dimension',
                    'label'        => esc_html__( 'Font Size', 'kirki' ),
                    'is_specified' => $font_size_field_specified,
                ],
                'color'     => [
                    'type'         => 'react-colorful',
                    'label'        => esc_html__( 'Font Color', 'kirki' ),
                    'is_specified' => $color_field_specified,
                    'choices'      => [
                        'alpha'       => true,
                        'label_style' => 'top',
                    ],
                ],
            ];

            $this->generate_controls_group( $group, $args );
        }

        $text_align_field_specified     = isset( $defaults['text-align'] );
        $text_transform_field_specified = isset( $defaults['text-transform'] );

        if ( $text_align_field_specified || $text_transform_field_specified ) {
            $group = [
                'text-align'     => [
                    'type'         => 'react-select',
                    'label'        => esc_html__( 'Text Align', 'kirki' ),
                    'is_specified' => $text_align_field_specified,
                    'choices'      => [
                        'initial' => esc_html__( 'Initial', 'kirki' ),
                        'left'    => esc_html__( 'Left', 'kirki' ),
                        'center'  => esc_html__( 'Center', 'kirki' ),
                        'right'   => esc_html__( 'Right', 'kirki' ),
                        'justify' => esc_html__( 'Justify', 'kirki' ),
                    ],
                ],
                'text-transform' => [
                    'type'         => 'react-select',
                    'label'        => esc_html__( 'Text Transform', 'kirki' ),
                    'is_specified' => $text_transform_field_specified,
                    'choices'      => [
                        'none'       => esc_html__( 'None', 'kirki' ),
                        'capitalize' => esc_html__( 'Capitalize', 'kirki' ),
                        'uppercase'  => esc_html__( 'Uppercase', 'kirki' ),
                        'lowercase'  => esc_html__( 'Lowercase', 'kirki' ),
                    ],
                ],
            ];

            $this->generate_controls_group( $group, $args );
        }

        $text_decoration_field_specified = isset( $defaults['text-decoration'] );

        if ( $text_decoration_field_specified ) {
            $group = [
                'text-decoration' => [
                    'type'         => 'react-select',
                    'label'        => esc_html__( 'Text Decoration', 'kirki' ),
                    'is_specified' => $text_decoration_field_specified,
                    'choices'      => [
                        'none'         => esc_html__( 'None', 'kirki' ),
                        'underline'    => esc_html__( 'Underline', 'kirki' ),
                        'line-through' => esc_html__( 'Line Through', 'kirki' ),
                        'overline'     => esc_html__( 'Overline', 'kirki' ),
                        'solid'        => esc_html__( 'Solid', 'kirki' ),
                        'wavy'         => esc_html__( 'Wavy', 'kirki' ),
                    ],
                ],
            ];

            $this->generate_controls_group( $group, $args );
        }

        $line_height_field_specified    = isset( $defaults['line-height'] );
        $letter_spacing_field_specified = isset( $defaults['letter-spacing'] );

        if ( $line_height_field_specified || $letter_spacing_field_specified ) {
            $group = [
                'line-height'    => [
                    'type'         => 'dimension',
                    'label'        => esc_html__( 'Line Height', 'kirki' ),
                    'is_specified' => $line_height_field_specified,
                    'choices'      => [
                        'label_position' => 'bottom',
                    ],
                ],
                'letter-spacing' => [
                    'type'         => 'dimension',
                    'label'        => esc_html__( 'Letter Spacing', 'kirki' ),
                    'is_specified' => $letter_spacing_field_specified,
                    'choices'      => [
                        'label_position' => 'bottom',
                    ],
                ],
            ];

            $this->generate_controls_group( $group, $args );
        }

        $margin_top_field_specified    = isset( $defaults['margin-top'] );
        $margin_bottom_field_specified = isset( $defaults['margin-bottom'] );

        if ( $margin_top_field_specified || $margin_bottom_field_specified ) {
            $group = [
                'margin-top'    => [
                    'type'         => 'dimension',
                    'label'        => esc_html__( 'Margin Top', 'kirki' ),
                    'is_specified' => $margin_top_field_specified,
                    'choices'      => [
                        'label_position' => 'bottom',
                    ],
                ],
                'margin-bottom' => [
                    'type'         => 'dimension',
                    'label'        => esc_html__( 'Margin Bottom', 'kirki' ),
                    'is_specified' => $margin_bottom_field_specified,
                    'choices'      => [
                        'label_position' => 'bottom',
                    ],
                ],
            ];

            $this->generate_controls_group( $group, $args );
        }

        new \Kirki\Field\Custom(
            [
                'settings'        => 'kirki_group_end_' . $args['settings'],
                'label'           => '',
                'description'     => '',
                'default'         => '<div class="kirki-typography-end"><hr></div>',
                'section'         => $args['section'],
                'active_callback' => isset( $args['active_callback'] ) ? $args['active_callback'] : '',
                'css_vars'        => [],
                'output'          => [],
            ]
        );

    }

    /**
     * Generate controls group.
     *
     * @param array $group The group data.
     * @param array $args The field args.
     */
    public function generate_controls_group( $group, $args ) {

        $total_specified = 0;
        $field_width     = 100;

        foreach ( $group as $css_prop => $control ) {
            if ( $control['is_specified'] ) {
                $total_specified++;
            }
        }

        if ( $total_specified > 1 ) {
            $field_width = floor( 100 / $total_specified );
        }

        $group_count = 0;

        foreach ( $group as $css_prop => $control ) {
            if ( $control['is_specified'] ) {
                $group_count++;

                $group_classname  = 'kirki-group-item';
                $group_classname .= 1 === $group_count ? ' kirki-group-start' : ( $group_count === $total_specified ? ' kirki-group-end' : '' );

                $control_class = str_ireplace( '-', ' ', $control['type'] );
                $control_class = ucwords( $control_class );
                $control_class = str_replace( ' ', '', $control_class );
                $control_class = '\\Kirki\\Field\\' . $control_class;

                new $control_class(
                    wp_parse_args(
                        [
                            'label'         => isset( $control['label'] ) ? $control['label'] : '',
                            'description'   => isset( $control['description'] ) ? $control['description'] : '',
                            'settings'      => $args['settings'] . '[' . $css_prop . ']',
                            'default'       => $args['default'][ $css_prop ],
                            'wrapper_attrs' => wp_parse_args(
                                [
                                    'data-kirki-typography-css-prop' => $css_prop,
                                    'kirki-typography-subcontrol-type' => $css_prop,
                                    'class' => '{default_class} ' . $group_classname . ' kirki-w' . $field_width,
                                ],
                                $args['wrapper_attrs']
                            ),
                            'input_attrs'   => $this->filter_preferred_choice_setting( 'input_attrs', $css_prop, $args ),
                            'choices'       => ( isset( $control['choices'] ) ? $control['choices'] : [] ),
                            'css_vars'      => [],
                            'output'        => [],
                        ],
                        $args
                    )
                );

            }
        }

    }

    /**
     * Sanitizes typography controls
     *
     * @static
     * @since 1.0
     * @param array $value The value.
     * @return array
     */
    public static function sanitize( $value ) {

        if ( ! is_array( $value ) ) {
            return [];
        }

        foreach ( $value as $key => $val ) {
            switch ( $key ) {
                case 'font-family':
                    $value['font-family'] = sanitize_text_field( $val );
                    break;

                case 'variant':
                    // Use 'regular' instead of 400 for font-variant.
                    $value['variant'] = ( 400 === $val || '400' === $val ) ? 'regular' : $val;

                    // Get font-weight from variant.
                    $value['font-weight'] = filter_var( $value['variant'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION );
                    $value['font-weight'] = ( 'regular' === $value['variant'] || 'italic' === $value['variant'] ) ? '400' : (string) absint( $value['font-weight'] );

                    // Get font-style from variant.
                    if ( ! isset( $value['font-style'] ) ) {
                        $value['font-style'] = ( false === strpos( $value['variant'], 'italic' ) ) ? 'normal' : 'italic';
                    }

                    break;

                case 'text-align':
                    if ( ! in_array( $val, [ '', 'inherit', 'left', 'center', 'right', 'justify' ], true ) ) {
                        $value['text-align'] = '';
                    }

                    break;

                case 'text-transform':
                    if ( ! in_array( $val, [ '', 'none', 'capitalize', 'uppercase', 'lowercase', 'initial', 'inherit' ], true ) ) {
                        $value['text-transform'] = '';
                    }

                    break;

                case 'text-decoration':
                    if ( ! in_array( $val, [ '', 'none', 'underline', 'overline', 'line-through', 'solid', 'wavy', 'initial', 'inherit' ], true ) ) {
                        $value['text-transform'] = '';
                    }

                    break;

                case 'color':
                    $value['color'] = '' === $value['color'] ? '' : \Kirki\Field\ReactColorful::sanitize( $value['color'] );
                    break;

                default:
                    $value[ $key ] = sanitize_text_field( $value[ $key ] );
            }
        }

        return $value;

    }

    /**
     * Enqueue scripts & styles.
     *
     * @access public
     * @since 1.0
     * @return void
     */
    public function enqueue_control_scripts() {

        wp_enqueue_style( 'kirki-control-typography', \Kirki\URL::get_from_path( dirname( dirname( __DIR__ ) ) . '/dist/control.css' ), [], '1.0' );

        wp_enqueue_script( 'kirki-control-typography', \Kirki\URL::get_from_path( dirname( dirname( __DIR__ ) ) . '/dist/control.js' ), [], '1.0', true );

        wp_localize_script( 'kirki-control-typography', 'kirkiTypographyControls', self::$typography_controls );

        $args      = $this->args;
        $std_fonts = [];
        $variants  = [
            'standard' => self::$std_variants,
            'complete' => self::$complete_variants,
        ];

        if ( ! isset( $args['choices']['fonts'] ) || ! isset( $args['choices']['fonts']['standard'] ) ) {
            $standard_fonts = Fonts::get_standard_fonts();

            foreach ( $standard_fonts as $font ) {
                if ( isset( $font['variants'] ) ) {
                    $variants[ $font['stack'] ] = $font['variants'];
                }
            }
        } elseif ( is_array( $args['choices']['fonts']['standard'] ) ) {
            foreach ( $args['choices']['fonts']['standard'] as $key => $val ) {
                $key = ( \is_int( $key ) ) ? $val : $key;

                if ( isset( $val['variants'] ) ) {
                    $variants[ $key ] = $val['variants'];
                }
            }
        }

        wp_localize_script(
            'kirki-control-typography',
            'kirkiFontVariants',
            $variants
        );

        if ( ! self::$gfonts_var_added ) {
            $google = new GoogleFonts();
            wp_localize_script( 'kirki-control-typography', 'kirkiGoogleFonts', $google->get_array() );
            self::$gfonts_var_added = true;
        }

    }

    /**
     * Enqueue scripts for customize_preview_init.
     *
     * @access public
     * @since 1.0
     * @return void
     */
    public function enqueue_preview_scripts() {

        wp_enqueue_script( 'kirki-preview-typography', \Kirki\URL::get_from_path( dirname( dirname( __DIR__ ) ) . '/dist/preview.js' ), [ 'wp-hooks' ], '1.0', true );

    }

    /**
     * Prefer control specific value over field value
     *
     * @access public
     * @since 4.0
     * @param $setting
     * @param $choice
     * @param $args
     *
     * @return string
     */
    public function filter_preferred_choice_setting( $setting, $choice, $args ) {

        // Fail early.
        if ( ! isset( $args[ $setting ] ) ) {
            return '';
        }

        // If a specific field for the choice is set
        if ( isset( $args[ $setting ][ $choice ] ) ) {
            return $args[ $setting ][ $choice ];
        }

        // Unset input_attrs of all other choices.
        foreach ( $args['choices'] as $id => $set ) {
            if ( $id !== $choice && isset( $args[ $setting ][ $id ] ) ) {
                unset( $args[ $setting ][ $id ] );
            } elseif ( ! isset( $args[ $setting ][ $id ] ) ) {
                $args[ $setting ] = '';
            }
        }

        return $args[ $setting ];

    }

    /**
     * Populate the font family choices.
     *
     * It's separated from the `add_sub_field` function to prevent a bug
     * when hooking a function into `kirki_fonts_standard_fonts` hook after registering the field.
     *
     * When a function is hooked to `kirki_fonts_standard_fonts` before registering the field, it will work.
     * But if it's hooked after field registration, then the function won't be available.
     *
     * @access private
     * @since 1.0.1
     *
     * @return array
     */
    private function get_font_family_choices() {

        $args = $this->args;

        // Figure out how to sort the fonts.
        $sorting   = 'alpha';
        $max_fonts = 9999;

        if ( isset( $args['choices'] ) && isset( $args['choices']['fonts'] ) && isset( $args['choices']['fonts']['google'] ) ) {
            if ( isset( $args['choices']['fonts']['google'][0] ) && in_array( $args['choices']['fonts']['google'][0], [ 'alpha', 'popularity', 'trending' ], true ) ) {
                $sorting = $args['choices']['fonts']['google'][0];
            }
            if ( isset( $args['choices']['fonts']['google'][1] ) && is_int( $args['choices']['fonts']['google'][1] ) ) {
                $max_fonts = (int) $args['choices']['fonts']['google'][1];
            }
        }

        $google  = new GoogleFonts();
        $g_fonts = $google->get_google_fonts_by_args(
            [
                'sort'  => $sorting,
                'count' => $max_fonts,
            ]
        );

        $std_fonts = [];

        if ( ! isset( $args['choices']['fonts'] ) || ! isset( $args['choices']['fonts']['standard'] ) ) {
            $standard_fonts = Fonts::get_standard_fonts();

            foreach ( $standard_fonts as $font ) {
                $std_fonts[ $font['stack'] ] = $font['label'];
            }
        } elseif ( is_array( $args['choices']['fonts']['standard'] ) ) {
            foreach ( $args['choices']['fonts']['standard'] as $key => $val ) {
                $key               = ( \is_int( $key ) ) ? $val : $key;
                $std_fonts[ $key ] = $val;
            }
        }

        $choices = [];

        $choices['default'] = [
            esc_html__( 'CSS Defaults', 'kirki' ),
            [
                '' => esc_html__( 'Default Browser Font-Family', 'kirki' ),
            ],
        ];

        if ( isset( $args['choices'] ) && isset( $args['choices']['fonts'] ) && isset( $args['choices']['fonts']['families'] ) ) {
            foreach ( $args['choices']['fonts']['families'] as $font_family_key => $font_family_value ) {
                if ( ! isset( $choices[ $font_family_key ] ) ) {
                    $choices[ $font_family_key ] = [];
                }

                $family_opts = [];

                foreach ( $font_family_value['children'] as $font_family ) {
                    $family_opts[ $font_family['id'] ] = $font_family['text'];
                }

                $choices[ $font_family_key ] = [
                    $font_family_value['text'],
                    $family_opts,
                ];
            }
        }

        $choices['standard'] = [
            esc_html__( 'Standard Fonts', 'kirki' ),
            $std_fonts,
        ];

        $choices['google'] = [
            esc_html__( 'Google Fonts', 'kirki' ),
            array_combine( array_values( $g_fonts ), array_values( $g_fonts ) ),
        ];

        if ( empty( $choices['standard'][1] ) ) {
            $choices = array_combine( array_values( $g_fonts ), array_values( $g_fonts ) );
        } elseif ( empty( $choices['google'][1] ) ) {
            $choices = $std_fonts;
        }

        return $choices;

    }

    /**
     * Filter arguments before creating the control.
     *
     * @access public
     * @since 0.1
     * @param array                $args         The field arguments.
     * @param WP_Customize_Manager $wp_customize The customizer instance.
     * @return array
     */
    public function filter_control_args( $args, $wp_customize ) {

        if ( $args['settings'] === $this->args['settings'] . '[font-family]' ) {
            $args            = parent::filter_control_args( $args, $wp_customize );
            $args['choices'] = $this->get_font_family_choices();
        }

        return $args;

    }

    /**
     * Adds a custom output class for typography fields.
     *
     * @access public
     * @since 1.0
     * @param array $classnames The array of classnames.
     * @return array
     */
    public function output_control_classnames( $classnames ) {

        $classnames['kirki-typography'] = '\Kirki\Field\CSS\Typography';
        return $classnames;

    }

    /**
     * Override parent method. No need to register any setting.
     *
     * @access public
     * @since 0.1
     * @param WP_Customize_Manager $wp_customize The customizer instance.
     * @return void
     */
    public function add_setting( $wp_customize ) {}

    /**
     * Override the parent method. No need for a control.
     *
     * @access public
     * @since 0.1
     * @param WP_Customize_Manager $wp_customize The customizer instance.
     * @return void
     */
    public function add_control( $wp_customize ) {}

}