return $css; }, ]; }, 'ame-menu-style-bundle' ); } /** * @param MenuColorSettings|null $currentSettings * @param array $customMenu * @param StyleGenerator|null $globalGenerator * @return array{string, array|null} A CSS string and either the modified custom menu or NULL if no changes were made. */ private function generateMenuColorStyle($currentSettings, $customMenu, $globalGenerator = null) { $cssBlocks = []; if ( $currentSettings ) { if ( !$globalGenerator ) { $globalGenerator = $this->getGlobalColorGenerator($currentSettings); } $globalCss = $globalGenerator->generateCss(); if ( !empty($globalCss) ) { $cssBlocks[] = $globalCss; } } $colorizedMenuCount = 0; if ( !empty($customMenu['tree']) ) { $usedIds = []; $generator = $this->getPartialStyleGenerator(false); foreach ($customMenu['tree'] as &$item) { if ( empty($item['colors']) ) { continue; } $colorizedMenuCount++; //Each item needs to have a unique ID so that we can target it in CSS. //Using a class would be cleaner, but the selectors wouldn't have enough //specificity to override WP defaults. $id = \ameMenuItem::get($item, 'hookname'); if ( empty($id) || isset($usedIds[$id]) ) { $id = (empty($id) ? 'ame-colorized-item' : $id) . '-'; $id .= $colorizedMenuCount . '-t' . time(); $item['hookname'] = $id; } $usedIds[$id] = true; $subType = \ameMenuItem::get($item, 'sub_type'); if ( $subType === 'heading' ) { $extraSelectors = ['.ame-menu-heading-item']; } else { $extraSelectors = []; } $this->setColorVariablesOn($generator, function ($variableName) use ($item) { return \ameMenuItem::get($item['colors'], $variableName, ''); }); $itemCss = $generator->generateCss(); if ( !empty($itemCss) ) { //Replace the placeholder item ID with the real ID. //Sanitization note: WordPress replaces special characters in the ID //with dashes before output. See /wp-admin/menu-header.php, line #110 //in WP 5.5-alpha. $sanitizedId = preg_replace('|[^a-zA-Z0-9_:.]|', '-', $id); //Headings need more specific selectors to override heading defaults. $replacement = implode('', $extraSelectors) . '#' . $sanitizedId; $itemCss = str_replace('#menu-id-placeholder', $replacement, $itemCss); $cssBlocks[] = sprintf( '/* %1$s (%2$s) */', str_replace('*/', ' ', \ameMenuItem::get($item, 'menu_title', 'Untitled menu')), str_replace('*/', ' ', \ameMenuItem::get($item, 'file', '(no URL)')) ); $cssBlocks[] = $itemCss; } } } $css = implode("\n", $cssBlocks); if ( $colorizedMenuCount > 0 ) { $modifiedMenu = $customMenu; } else { $modifiedMenu = null; } return [$css, $modifiedMenu]; } } class MenuColorSettings extends AbstractSettingsDictionary { const SETTING_ID_PREFIX = 'ws_menu_colors--'; const CONFIG_CHILD_KEY = 'color_presets'; public function __construct(StorageInterface $store) { parent::__construct($store, self::SETTING_ID_PREFIX); } protected function createDefaults() { return []; //No custom colors by default. } protected function createSettings() { $presetSlot = $this->store->buildSlot(self::CONFIG_CHILD_KEY); $colorPresets = new MapSetting( self::SETTING_ID_PREFIX . 'colorPresets', $presetSlot, [ 'keyValidators' => [ new StringValidator(0, 250, true, null, true), [StringValidator::class, 'sanitizeStripTags'], ], 'valueValidators' => [ function ($colorList, \WP_Error $errors) { $validColors = []; $hasErrors = false; foreach ($colorList as $key => $color) { if ( !in_array($key, MenuColorsModule::MENU_COLOR_VARIABLES, true) ) { $errors->add('invalid_color', 'Invalid color variable: ' . $key); $hasErrors = true; continue; } $validatedColor = ColorValidator::validateHex($color, $errors); if ( !is_wp_error($validatedColor) ) { $validColors[$key] = $validatedColor; } else { $hasErrors = true; } } return $hasErrors ? $errors : $validColors; }, ], ] ); //The active preset usually refers to the "[global]" preset in the color //preset collection. It's registered as a separate setting for convoluted //backwards compatibility reasons. $activePreset = new UserDefinedStruct( self::SETTING_ID_PREFIX . 'activePreset', $presetSlot->buildSlot('[global]') ); $activePreset->addTags(AbstractSetting::TAG_ADMIN_THEME); foreach (MenuColorsModule::MENU_COLOR_VARIABLES as $variable => $label) { $activePreset->createChild( $variable, ColorSetting::class, [ 'label' => $label, 'deleteWhenBlank' => true, 'supportsPostMessage' => true, ] ); } return [$colorPresets, $activePreset]; } } class ColorPresetDropdown extends Control { protected $hasPrimaryInput = true; private $isGlobalPresetVisible = true; public function __construct($settings = [], $params = []) { parent::__construct($settings, $params); if ( isset($params['globalPresetVisible']) ) { $this->isGlobalPresetVisible = (bool)$params['globalPresetVisible']; } } public function renderContent(Renderer $renderer) { $dropdownId = $this->getPrimaryInputId(); //Container for the dropdown and the "Delete" button. echo HtmlHelper::tag( 'div', [ 'class' => 'ame-mc-color-preset-control', 'id' => $dropdownId . '-container', 'data-bind' => $this->makeKoDataBind([ 'ameObservableChangeEvents' => '{ observable: ' . $this->getKoObservableExpression($this->mainSetting) . ', sendInitEvent: true }', ]), 'data-global-preset-visible' => $this->isGlobalPresetVisible ? '1' : '0', ] ); echo HtmlHelper::tag( 'label', [ 'for' => $dropdownId, 'class' => 'hidden', ], 'Presets' ); //The dropdown will mostly be populated by JavaScript. $optionTags = [ HtmlHelper::tag( 'option', [ 'value' => '', 'selected' => 'selected', 'disabled' => 'disabled', 'class' => 'ame-meta-option', ], 'Select a preset' ), ]; echo HtmlHelper::tag( 'select', [ 'id' => $dropdownId, 'class' => 'ame-mc-preset-dropdown', ], implode("\n", $optionTags) ); //Delete button/link. echo ' ', HtmlHelper::tag( 'a', [ 'href' => '#', 'class' => 'hidden ame-mc-delete-color-preset', ], 'Delete preset' ); echo ''; //Enqueue our script if not already done. wp_enqueue_script(MenuColorsModule::mainScriptHandle); } }