Plugin Directory

Changeset 3479303


Ignore:
Timestamp:
03/10/2026 05:19:34 PM (3 weeks ago)
Author:
whiteshadow
Message:

Add a RadioCardGroup control.

Also refactored choice controls in general a bit by moving per-option children to the base class and a few other minor things. And added some utility methods for building the outer container element of various controls - there was some code duplication going on since multiple controls had similarly structured containers.

Location:
admin-menu-editor/trunk/customizables
Files:
2 added
9 edited

Legend:

Unmodified
Added
Removed
  • admin-menu-editor/trunk/customizables/Builders/ElementBuilderFactory.php

    r3474817 r3479303  
    117117    public function radioGroup($idOrSetting = null) {
    118118        return new RadioGroupBuilder($this->findSettings($idOrSetting));
     119    }
     120
     121    /**
     122     * @param Setting|null|string $idOrSetting
     123     * @return RadioGroupBuilder<Controls\RadioCardGroup>
     124     */
     125    public function radioCardGroup($idOrSetting = null): RadioGroupBuilder {
     126        return new RadioGroupBuilder($this->findSettings($idOrSetting), [], Controls\RadioCardGroup::class);
    119127    }
    120128
  • admin-menu-editor/trunk/customizables/Builders/RadioGroupBuilder.php

    r2869947 r3479303  
    66
    77class RadioGroupBuilder extends ControlBuilder {
    8     public function __construct($settings = [], $params = []) {
    9         parent::__construct(RadioGroup::class, $settings, $params);
     8    public function __construct($settings = [], $params = [], string $className = RadioGroup::class) {
     9        parent::__construct($className, $settings, $params);
    1010    }
    1111
    12     public function choiceChild($value, $childControl) {
     12    public function choiceChild($value, $childControl): self {
    1313        if ( !is_string($value) ) {
    1414            //Because we use the value as an array key, it must be a string
  • admin-menu-editor/trunk/customizables/Controls/ChoiceControl.php

    r3400655 r3479303  
    33namespace YahnisElsts\AdminMenuEditor\Customizable\Controls;
    44
     5use YahnisElsts\AdminMenuEditor\Customizable\Rendering\Context;
    56use YahnisElsts\AdminMenuEditor\Customizable\Schemas\Enum;
    67use YahnisElsts\AdminMenuEditor\Customizable\Settings\EnumSetting;
     
    2021     */
    2122    protected $options = [];
     23
     24    /**
     25     * @var array Maps option values to controls in $this->children. Each option can have up to one child control.
     26     */
     27    protected array $optionChildIndex = [];
    2228
    2329    public function __construct($settings = [], $params = [], $children = []) {
     
    5763            }
    5864        }
     65
     66        if ( isset($params['choiceChildren']) ) {
     67            foreach ($params['choiceChildren'] as $value => $childControl) {
     68                $this->children[] = $childControl;
     69                $index = count($this->children) - 1;
     70                $this->optionChildIndex[$value] = $index;
     71            }
     72        }
     73    }
     74
     75    protected function addOptionChild($optionValue, UiElement $childControl) {
     76        $this->children[] = $childControl;
     77        $index = count($this->children) - 1;
     78        $this->optionChildIndex[$optionValue] = $index;
     79    }
     80
     81    protected function getOptionChild($optionValue): ?UiElement {
     82        if ( !isset($this->optionChildIndex[$optionValue]) ) {
     83            return null;
     84        }
     85        $index = $this->optionChildIndex[$optionValue];
     86        return $this->children[$index] ?? null;
     87    }
     88
     89    protected function hasOptionChild($optionValue): bool {
     90        if ( isset($this->optionChildIndex[$optionValue]) ) {
     91            $index = $this->optionChildIndex[$optionValue];
     92            return isset($this->children[$index]);
     93        }
     94        return false;
     95    }
     96
     97    protected function generateRadioInputFor(
     98        ChoiceControlOption $option,
     99        string $fieldName,
     100        bool $isChecked,
     101        Context $context
     102    ): string {
     103        return $this->buildTag(
     104            'input',
     105            array_merge(array(
     106                'type'      => 'radio',
     107                'name'      => $fieldName,
     108                'value'     => $this->mainBinding->encodeForForm($option->value),
     109                'class'     => $this->getInputClasses($context),
     110                'checked'   => $isChecked,
     111                'disabled'  => !$option->enabled,
     112                'data-bind' => $this->makeKoDataBind([
     113                    'checked'                   => $this->getKoObservableExpression($option->value),
     114                    'checkedValue'              => wp_json_encode($option->value),
     115                    'ameObservableChangeEvents' => 'true',
     116                ]),
     117            ), $this->inputAttributes)
     118        );
    59119    }
    60120
     
    67127            $this->options
    68128        );
     129
     130        //Option values can be things that aren't valid JS identifiers, so we'll serialize
     131        //the option-to-child relationship as an array of value + child index pairs.
     132        $pairs = [];
     133        foreach ($this->optionChildIndex as $value => $childIndex) {
     134            $pairs[] = [$value, $childIndex];
     135        }
     136        $params['valueChildIndexes'] = $pairs;
     137
    69138        return $params;
    70139    }
  • admin-menu-editor/trunk/customizables/Controls/Control.php

    r3406324 r3479303  
    258258
    259259        return $this->buildTag($tagName, $attributes, $content);
     260    }
     261
     262    /**
     263     * Generate an HTML tag that will serve as the outer container for this control.
     264     *
     265     * This is intended to be used for fieldset or div tags that wrap the entire control, and it
     266     * will include classes and styles from the control's properties.
     267     *
     268     * @param array $prependClasses
     269     * @param array $attributes
     270     * @param string $tagName
     271     * @return string
     272     */
     273    protected function buildContainerElement(
     274        array   $prependClasses = [],
     275        array   $attributes = [],
     276        string  $tagName = 'div'
     277    ): string {
     278        $mergedAttributes = array_merge(
     279            [
     280                'class' => array_merge($prependClasses, $this->classes),
     281                'style' => $this->styles,
     282            ],
     283            $attributes
     284        );
     285
     286        return $this->buildTag($tagName, $mergedAttributes);
     287    }
     288
     289    /**
     290     * As buildContainerElement(), but specifically for fieldset tags. Adds the enabled/disabled state
     291     * if applicable.
     292     *
     293     * @param Context $context
     294     * @param array $prependClasses
     295     * @param array $attributes
     296     * @return string
     297     */
     298    protected function buildFieldsetContainer(Context $context, array $prependClasses = [], array $attributes = []): string {
     299        $attributes['disabled'] = !$this->isEnabled($context);
     300        $attributes['data-bind'] = $this->makeKoDataBind($this->getKoEnableBinding());
     301        return $this->buildContainerElement($prependClasses, $attributes, 'fieldset');
    260302    }
    261303
  • admin-menu-editor/trunk/customizables/Controls/RadioButtonBar.php

    r3397404 r3479303  
    1212    protected $declinesExternalLineBreaks = true;
    1313
    14     protected $controlClass = 'ame-radio-button-bar-control';
     14    protected string $controlClass = 'ame-radio-button-bar-control';
    1515
    1616    public function renderContent(Renderer $renderer, Context $context) {
     
    1919
    2020        //phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
    21         echo $this->buildTag(
    22             'fieldset',
    23             [
    24                 'class'     => array_merge([$this->controlClass], $this->classes),
    25                 'style'     => $this->styles,
    26                 'disabled'  => !$this->isEnabled($context),
    27                 'data-bind' => $this->makeKoDataBind($this->getKoEnableBinding()),
    28             ]
    29         );
     21        echo $this->buildFieldsetContainer($context, [$this->controlClass]);
    3022        foreach ($this->options as $option) {
    3123            $isChecked = ($currentValue === $option->value);
     
    3628            ));
    3729
    38             echo $this->buildTag(
    39                 'input',
    40                 array_merge(array(
    41                     'type'      => 'radio',
    42                     'name'      => $fieldName,
    43                     'value'     => $this->mainBinding->encodeForForm($option->value),
    44                     'class'     => $this->getInputClasses($context),
    45                     'checked'   => $isChecked,
    46                     'disabled'  => !$option->enabled,
    47                     'data-bind' => $this->makeKoDataBind([
    48                         'checked'                   => $this->getKoObservableExpression($option->value),
    49                         'checkedValue'              => wp_json_encode($option->value),
    50                         'ameObservableChangeEvents' => 'true',
    51                     ]),
    52                 ), $this->inputAttributes)
    53             );
     30            echo $this->generateRadioInputFor($option, $fieldName, $isChecked, $context);
    5431
    5532            $buttonContent = esc_html($option->label);
  • admin-menu-editor/trunk/customizables/Controls/RadioGroup.php

    r3471264 r3479303  
    2424    protected $descriptionsAsTooltips = false;
    2525
    26     /**
    27      * @var Control[]
    28      */
    29     protected $choiceChildren = [];
    30 
    3126    public function __construct($settings = [], $params = [], $children = []) {
    3227        parent::__construct($settings, $params, $children);
    3328
    34         if ( isset($params['choiceChildren']) ) {
    35             $this->choiceChildren = $params['choiceChildren'];
    36         }
    37 
    38         $this->wrapStyle = isset($params['wrap']) ? $params['wrap'] : self::WRAP_PARAGRAPH;
     29        $this->wrapStyle = $params['wrap'] ?? self::WRAP_PARAGRAPH;
    3930        switch ($this->wrapStyle) {
    4031            case self::WRAP_LINE_BREAK:
     
    6253
    6354        $classes = $this->classes;
    64         $hasNestedControls = !empty($this->choiceChildren);
     55        $hasNestedControls = !empty($this->optionChildIndex);
    6556        if ( $hasNestedControls ) {
    6657            $classes[] = 'ame-rg-has-nested-controls';
     
    9182            echo $beforeOption;
    9283            $labelClasses = ['ame-rg-option-label'];
    93             if ( is_string($option->value) && isset($this->choiceChildren[$option->value]) ) {
     84            if ( is_string($option->value) && $this->hasOptionChild($option->value) ) {
    9485                $labelClasses[] = 'ame-rg-has-choice-child';
    9586            }
     
    129120            echo $afterOption;
    130121
    131             if ( is_string($option->value) && isset($this->choiceChildren[$option->value]) ) {
    132                 $childControl = $this->choiceChildren[$option->value];
    133                 echo HtmlHelper::tag('span', ['class' => 'ame-rg-nested-control']);
    134                 $renderer->renderControl($childControl, $context);
    135                 echo '</span>';
     122            if ( is_string($option->value) ) {
     123                $child = $this->getOptionChild($option->value);
     124                if ( $child instanceof Control ) {
     125                    echo HtmlHelper::tag('span', ['class' => 'ame-rg-nested-control']);
     126                    $renderer->renderControl($child, $context);
     127                    echo '</span>';
     128                }
    136129            }
    137130        }
     
    146139     * @return string
    147140     */
    148     protected function getRadioInputId($option) {
     141    protected function getRadioInputId(ChoiceControlOption $option): string {
    149142        return $this->getRadioInputPrefix() . sanitize_key(strval($option->value));
    150143    }
    151144
    152     protected function getRadioInputPrefix() {
     145    protected function getRadioInputPrefix(): string {
    153146        return self::INPUT_ID_PREFIX . $this->instanceNumber . '-';
    154     }
    155 
    156     public function serializeForJs(Context $context): array {
    157         $result = parent::serializeForJs($context);
    158         if ( !isset($result['children']) ) {
    159             $result['children'] = [];
    160             foreach ($this->choiceChildren as $child) {
    161                 $result['children'][] = $child->serializeForJs($context);
    162             }
    163         }
    164         return $result;
    165147    }
    166148
     
    168150        $params = parent::getKoComponentParams();
    169151
    170         $hasNestedControls = !empty($this->choiceChildren);
     152        $hasNestedControls = !empty($this->optionChildIndex);
    171153        $params['wrapStyle'] = $hasNestedControls ? self::WRAP_NONE : $this->wrapStyle;
    172154        $params['radioInputPrefix'] = $this->getRadioInputPrefix();
    173155
    174         if ( $hasNestedControls ) {
    175             //Values can be things that aren't valid JS identifiers, so we'll serialize
    176             //the value-to-child relationship as an array of value + child index pairs.
    177             $valueChildIndexes = [];
    178             $i = 0;
    179             foreach ($this->choiceChildren as $value => $child) {
    180                 $valueChildIndexes[] = [$value, $i];
    181                 $i++;
    182             }
    183             $params['valueChildIndexes'] = $valueChildIndexes;
    184         }
    185 
    186156        return $params;
    187157    }
    188 
    189     public function getChildren(): array {
    190         return array_unique(array_merge(array_values($this->choiceChildren), $this->children), SORT_REGULAR);
    191     }
    192 
    193     public function getAllDescendants() {
    194         foreach ($this->getChildren() as $child) {
    195             yield $child;
    196             if ( $child instanceof ControlContainer ) {
    197                 yield from $child->getAllDescendants();
    198             }
    199         }
    200     }
    201158}
  • admin-menu-editor/trunk/customizables/assets/_combined-base-controls.scss

    r3405244 r3479303  
    88@import "radio-button-bar";
    99@import "radio-group";
     10@import "radio-card-group";
  • admin-menu-editor/trunk/customizables/assets/controls.css

    r3405244 r3479303  
    270270}
    271271
     272/*
     273Standard WordPress admin colors.
     274
     275Announcement post:
     276https://make.wordpress.org/core/2021/02/23/standardization-of-wp-admin-colors-in-wordpress-5-7/
     277
     278Source:
     279https://codepen.io/ryelle/pen/WNGVEjw
     280
     281A "wp" prefix has been added to avoid name conflicts with other code in this plugin.
     282 */
     283.ame-radio-card-group-control {
     284  display: grid;
     285  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
     286  grid-gap: 0.5rem;
     287}
     288
     289.ame-radio-card, .form-table td fieldset label.ame-radio-card {
     290  display: flex;
     291  flex-direction: row;
     292  align-items: flex-start;
     293  gap: 0.75rem;
     294  padding: 0.75rem;
     295  margin: 0;
     296  border: 1px solid #dcdcde;
     297  border-radius: 4px;
     298  background: #fff;
     299  cursor: pointer;
     300  transition: none;
     301}
     302.ame-radio-card:hover, .form-table td fieldset label.ame-radio-card:hover {
     303  border-color: #c3c4c7;
     304}
     305.ame-radio-card:has(> .ame-radio-card-input-wrapper input[type=radio]:checked), .form-table td fieldset label.ame-radio-card:has(> .ame-radio-card-input-wrapper input[type=radio]:checked) {
     306  border-color: #3582c4;
     307  outline: 1px solid #3582c4;
     308  outline-offset: 0;
     309}
     310
     311.ame-radio-card-group-control label.ame-radio-card {
     312  margin: 0 !important;
     313}
     314
     315.ame-radio-card-input-wrapper {
     316  flex: 0 0 auto;
     317}
     318
     319.ame-radio-card-body {
     320  display: flex;
     321  flex-direction: column;
     322  justify-content: center;
     323}
     324
     325.ame-radio-card-label {
     326  color: #646970;
     327}
     328
    272329/*# sourceMappingURL=controls.css.map */
  • admin-menu-editor/trunk/customizables/assets/controls.css.map

    r3405244 r3479303  
    1 {"version":3,"sourceRoot":"","sources":["../../css/_collections.scss","../../css/_ui-constants.scss","_image-selector.scss","_code-editor.scss","../../css/_forms.scss","_number-input.scss","_popup-slider.scss","_radio-button-bar.scss","_radio-group.scss"],"names":[],"mappings":"AAGA;EAEC,aCM4B;;ADF5B;EACC;;;AAQF;EACC,aCGwB;EDAxB;EACA;;;AAGD;EACC;IACC,aCHyB;IDOzB;;;AE/BD;EACC;EACA;EACA;EACA;EAEA;EACA;;AAEA;EACC;;AAIF;EACC;EACA;EACA;;AAGD;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;;AAKF;EACC;EACA;;AAGA;EACC;EACA;EACA;EACA;;AAKD;EACC;;AAID;EACC;;AAGD;EACC;;AAKD;EACC;;;ACjED;EACC;;AAED;EACC;EACA;EACA;;;ACOA;EACC,cALY;;AAQZ;EACC;;;ACZJ;EACC,OAFiB;;;AAOlB;EACC,OARiB;;;ACLlB;EACC;;;AAGD;EAoBC;EAEA;EACA;EACA;EACA;EACA;;AAzBA;EACC;;AA2BD;EACC;EACA,QA1BY;EA2BZ;EACA;EAEA;;AAID;EACC;EACA;EACA,WArCY;EAyCZ;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA,eAvCgB;;AA2ClB;EACC;EACA,OAvDY;EAwDZ,QAxDY;EA0DZ;EACA;EACA;EAEA;EAEA;EACA;;AAOD;EACC;EAEA;EACA,QALS;EAOT,eAPS;EAQT;EACA;EAEA;EACA;EACA;EAEA;;AAEA;EACC;EACA;EACA,OApBQ;EAqBR,QArBQ;EAsBR;EAEA;EAEA;EACA;;AAIF;EACC;;AAGD;EACC;EAEA;;;ACtHF;EACC;EACA;;AAEA;EHFA;EACA;EACA;EACA;EACA;;AGEA;EACC;;AAID;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAID;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;;AAGC;EACC;;AAMF;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;;ACjEH;EAKC;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;;AAQF;EACC;;AAGA;EACC;;AAOD;EACC;;;AAMD;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAOD;EACC;;AACA;EACC","file":"controls.css"}
     1{"version":3,"sourceRoot":"","sources":["../../css/_collections.scss","../../css/_ui-constants.scss","_image-selector.scss","_code-editor.scss","../../css/_forms.scss","_number-input.scss","_popup-slider.scss","_radio-button-bar.scss","_radio-group.scss","../../css/_wp-admin-colors.scss","_radio-card-group.scss"],"names":[],"mappings":"AAGA;EAEC,aCM4B;;ADF5B;EACC;;;AAQF;EACC,aCGwB;EDAxB;EACA;;;AAGD;EACC;IACC,aCHyB;IDOzB;;;AE/BD;EACC;EACA;EACA;EACA;EAEA;EACA;;AAEA;EACC;;AAIF;EACC;EACA;EACA;;AAGD;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;;AAKF;EACC;EACA;;AAGA;EACC;EACA;EACA;EACA;;AAKD;EACC;;AAID;EACC;;AAGD;EACC;;AAKD;EACC;;;ACjED;EACC;;AAED;EACC;EACA;EACA;;;ACOA;EACC,cALY;;AAQZ;EACC;;;ACZJ;EACC,OAFiB;;;AAOlB;EACC,OARiB;;;ACLlB;EACC;;;AAGD;EAoBC;EAEA;EACA;EACA;EACA;EACA;;AAzBA;EACC;;AA2BD;EACC;EACA,QA1BY;EA2BZ;EACA;EAEA;;AAID;EACC;EACA;EACA,WArCY;EAyCZ;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA,eAvCgB;;AA2ClB;EACC;EACA,OAvDY;EAwDZ,QAxDY;EA0DZ;EACA;EACA;EAEA;EAEA;EACA;;AAOD;EACC;EAEA;EACA,QALS;EAOT,eAPS;EAQT;EACA;EAEA;EACA;EACA;EAEA;;AAEA;EACC;EACA;EACA,OApBQ;EAqBR,QArBQ;EAsBR;EAEA;EAEA;EACA;;AAIF;EACC;;AAGD;EACC;EAEA;;;ACtHF;EACC;EACA;;AAEA;EHFA;EACA;EACA;EACA;EACA;;AGEA;EACC;;AAID;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAID;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;;AAGC;EACC;;AAMF;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;;ACjEH;EAKC;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;;AAQF;EACC;;AAGA;EACC;;AAOD;EACC;;;AAMD;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAOD;EACC;;AACA;EACC;;;AClEJ;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;ACGA;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EAEA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;EACA;;;AAIF;EAEC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAOD;EACC,OTjDqB","file":"controls.css"}
Note: See TracChangeset for help on using the changeset viewer.