File: /home/dreamlan/public_html/wp-content/plugins/admin-menu-editor/customizables/Controls/Control.php
<?php
namespace YahnisElsts\AdminMenuEditor\Customizable\Controls;
use YahnisElsts\AdminMenuEditor\Customizable\Rendering\Context;
use YahnisElsts\AdminMenuEditor\Customizable\Rendering\Renderer;
use YahnisElsts\AdminMenuEditor\Customizable\Settings;
use YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting;
use YahnisElsts\AdminMenuEditor\Customizable\Settings\Setting;
abstract class Control extends UiElement implements ControlContainer {
use Toggleable {
isEnabled as private traitIsEnabled;
}
/**
* @var string Type of the control
*/
protected $type = 'abstract';
/**
* @var AbstractSetting[]
*/
protected $settings = [];
/**
* @var null|Binding
*/
protected $mainBinding = null;
/**
* @var string|null Name of the Knockout component that renders this control.
*/
protected $koComponentName = null;
/**
* @var string|null
*/
protected $label = null;
/**
* @var bool Whether this control has a specific, well-defined form element
* that is the main input and needs an associated label.
*/
protected $hasPrimaryInput = false;
/**
* @var array List of CSS classes to apply to the primary input element.
*/
protected $inputClasses = [];
/**
* @var array List of HTML attributes to add to the input element.
*/
protected $inputAttributes = [];
protected static $totalInstances = 0;
protected $instanceNumber = 0;
public function __construct($settings = [], $params = [], $children = []) {
$this->instanceNumber = ++static::$totalInstances;
parent::__construct($params, $children);
$this->settings = $settings;
if ( !empty($this->settings) ) {
if ( isset($params['mainSetting']) ) {
$this->mainBinding = $this->settings[$params['mainSetting']];
} else {
$this->mainBinding = reset($this->settings);
}
}
//Label
if ( isset($params['label']) ) {
$this->label = $params['label'];
}
//Description
if ( isset($params['description']) ) {
$this->description = $params['description'];
}
//Enabled/disabled
$this->parseEnabledParam($params);
//ID. It can be assigned automatically if the control has a plain main setting (not a reference).
if ( $this->id === '' ) {
if ( $this->mainBinding instanceof AbstractSetting ) {
$this->id = preg_replace('/[^a-z0-9\-_]+/i', '_', $this->mainBinding->getId());
//The ID should not start with a number because it *could*
//be used as an HTML element ID.
if ( preg_match('/^[0-9]/', $this->id) ) {
$this->id = 'c' . $this->id;
}
}
}
//Input classes
if ( isset($params['inputClasses']) ) {
$this->inputClasses = (array)$params['inputClasses'];
}
//Input attributes
if ( isset($params['inputAttributes']) ) {
$this->inputAttributes = (array)$params['inputAttributes'];
}
}
abstract public function renderContent(Renderer $renderer, Context $context);
public function getInputClasses(?Context $context = null): array {
return $this->inputClasses;
}
public function getHtmlIdBase(?Context $context = null) {
//Note that this is not necessarily the same as the ID attribute
//of the HTML element generated by this control.
if ( $this->id === '' ) {
$generatedId = '';
if ( $this->mainBinding && $context ) {
$path = $context->resolveValuePath($this->mainBinding);
if ( !empty($path) ) {
$generatedId = implode('.', $path);
}
} else if ( $this->mainBinding instanceof AbstractSetting ) {
$generatedId = $this->mainBinding->getId();
}
if ( !empty($generatedId) ) {
$generatedId = preg_replace('/[^a-z0-9\-_]+/i', '_', $generatedId);
//The ID should not start with a number because it *could*
//be used as an HTML element ID.
if ( preg_match('/^[0-9]/', $generatedId) ) {
$generatedId = 'c' . $generatedId;
}
return $generatedId;
}
return 'ame_' . $this->type . '_' . $this->instanceNumber;
}
return parent::getHtmlIdBase($context);
}
public function getLabel(?Context $context = null): string {
if ( isset($this->label) ) {
return $this->label;
}
if ( !empty($this->mainBinding) ) {
return $this->mainBinding->resolveLabel($context);
}
return '';
}
public function getLabelTargetId(?Context $context = null) {
return $this->getPrimaryInputId($context);
}
/**
* Does this control have an input or other form-related element that
* could be meaningfully associated with the control's label?
*
* @return bool
*/
public function supportsLabelAssociation() {
return $this->hasPrimaryInput;
}
/**
* Whether this control renders its own label as part of its content.
*
* For example, a checkbox control will usually wrap a label element around
* its input element, so it would return true here.
*
* @return boolean
*/
public function includesOwnLabel(): bool {
return false;
}
public function parentLabelEnabled() {
return $this->supportsLabelAssociation();
}
/**
* @return AbstractSetting[]
*/
public function getSettings() {
return $this->settings;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @param Context|null $context
* @return bool
*/
public function isEnabled(?Context $context = null) {
$enabled = $this->traitIsEnabled($context);
if ( !empty($this->mainBinding) ) {
$enabled = $enabled && $this->mainBinding->isEditableByUser($context);
}
return $enabled;
}
/**
* @param Context|null $context
* @return string
*/
public function getPrimaryInputId(?Context $context = null) {
if ( !$this->hasPrimaryInput ) {
return '';
}
return '_ame-cntrl-' . $this->getHtmlIdBase($context);
}
/**
* Generate an HTML tag based on the input settings of this control.
*
* This is mainly intended for creating input tags, but it also works for textarea tags.
*
* @param Context $context
* @param array<string,mixed> $attributes
* @param string $tagName
* @param string|null $content
* @return string
*/
protected function buildInputElement(
?Context $context = null,
$attributes = [],
$tagName = 'input',
$content = null
) {
//Merge input classes and any classes specified in the $attributes array.
$classes = $this->getInputClasses($context);
if ( isset($attributes['class']) ) {
$classes = array_merge($classes, (array)$attributes['class']);
unset($attributes['class']);
}
$attributes = array_merge(
[
'id' => $this->hasPrimaryInput ? $this->getPrimaryInputId($context) : null,
'class' => $classes,
'name' => $this->getFieldName($context),
'disabled' => !$this->isEnabled($context),
//Add the setting ID for the admin customizer module.
'data-ac-setting-id' => $this->getAutoAcSettingId(),
],
$attributes,
$this->inputAttributes
);
return $this->buildTag($tagName, $attributes, $content);
}
/**
* Generate an HTML tag that will serve as the outer container for this control.
*
* This is intended to be used for fieldset or div tags that wrap the entire control, and it
* will include classes and styles from the control's properties.
*
* @param array $prependClasses
* @param array $attributes
* @param string $tagName
* @return string
*/
protected function buildContainerElement(
array $prependClasses = [],
array $attributes = [],
string $tagName = 'div'
): string {
$mergedAttributes = array_merge(
[
'class' => array_merge($prependClasses, $this->classes),
'style' => $this->styles,
],
$attributes
);
return $this->buildTag($tagName, $mergedAttributes);
}
/**
* As buildContainerElement(), but specifically for fieldset tags. Adds the enabled/disabled state
* if applicable.
*
* @param Context $context
* @param array $prependClasses
* @param array $attributes
* @return string
*/
protected function buildFieldsetContainer(Context $context, array $prependClasses = [], array $attributes = []): string {
$attributes['disabled'] = !$this->isEnabled($context);
$attributes['data-bind'] = $this->makeKoDataBind($this->getKoEnableBinding());
return $this->buildContainerElement($prependClasses, $attributes, 'fieldset');
}
protected function getMainSettingValue($default = null, ?Context $context = null) {
if ( $this->mainBinding instanceof Setting ) {
return $this->mainBinding->getValue($default);
} else if ( $this->mainBinding instanceof Settings\WithSchema\SingularSetting ) {
return $this->mainBinding->getValue($default);
} else if ( $context && $this->mainBinding ) {
return $context->resolveValue($this->mainBinding, $default);
}
return $default;
}
/**
* Generate the "name" attribute value for a form element associated with
* this control.
*
* @param Context|null $context
* @param string|null $compositeChildKey Only for composite settings: the key of the child setting.
* @param Binding|null $setting The setting shown in the field. Defaults to the main setting.
* @return string
*/
protected function getFieldName(?Context $context = null, $compositeChildKey = null, ?Binding $setting = null) {
if ( $setting === null ) {
$setting = $this->mainBinding;
}
if ( !($setting instanceof Binding) ) {
return $this->getHtmlIdBase($context);
}
if ( $context ) {
$path = $context->resolveValuePath($setting);
if ( empty($path) ) {
return $this->getHtmlIdBase($context);
}
if ( !empty($compositeChildKey) ) {
$path[] = $compositeChildKey;
}
} else if ( $setting instanceof AbstractSetting ) {
if ( ($compositeChildKey === null) || ($compositeChildKey === '') ) {
$id = $setting->getId();
} else {
$id = $setting->getId() . '.' . $compositeChildKey;
if ( $setting instanceof Settings\CompositeSetting ) {
$child = $setting->getChild($compositeChildKey);
if ( $child ) {
$id = $child->getId();
}
}
}
//Convert dot notation to array notation.
$path = explode('.', $id);
} else {
return $this->getHtmlIdBase($context);
}
$root = array_shift($path);
if ( empty($path) ) {
return $root;
}
return $root . '[' . implode('][', $path) . ']';
}
public function getAutoGroupTitle() {
if ( $this->mainBinding instanceof AbstractSetting ) {
$customTitle = $this->mainBinding->getCustomGroupTitle();
if ( !empty($customTitle) ) {
return $customTitle;
}
}
return $this->getLabel();
}
/**
* Get a version of this control that is compatible with the Admin Customizer module.
*
* May return this object if it is already compatible or if there is
* no better alternative.
*
* @return Control
*/
public function getAdminCustomizerControl() {
return $this;
}
protected function getAutoAcSettingId() {
if ( ($this->mainBinding instanceof AbstractSetting) && $this->hasPrimaryInput ) {
return $this->mainBinding->getId();
}
return null;
}
protected function getKoObservableExpression($defaultValue, ?AbstractSetting $setting = null) {
if ( $setting === null ) {
$setting = $this->mainBinding;
}
if ( !($setting instanceof AbstractSetting) ) {
return wp_json_encode($defaultValue);
}
$settingId = $setting->getId();
return sprintf(
'$root.getSettingObservable(%s, %s)',
wp_json_encode($settingId),
wp_json_encode($defaultValue)
);
}
protected function makeKoDataBind($bindingValues) {
if ( empty($bindingValues) ) {
return '';
}
$bindings = [];
foreach ($bindingValues as $binding => $value) {
$bindings[] = sprintf('%s: %s', $binding, $value);
}
return implode(', ', $bindings);
}
protected function getJsUiElementType() {
return 'control';
}
public function serializeForJs(Context $context): array {
$result = parent::serializeForJs($context);
$label = $this->getLabel();
if ( !empty($label) ) {
$result['label'] = $label;
}
if ( $this->koComponentName === null ) {
return $result;
}
$result['component'] = $this->koComponentName;
//Map setting names to IDs.
$settingIds = [];
$settings = $this->settings;
//The main setting is always available as "value". This means that a setting
//could be duplicated in the "settings" array, but that should be fine.
if ( $this->mainBinding instanceof Binding ) {
$settings['value'] = $this->mainBinding;
}
foreach ($settings as $name => $setting) {
//Skip non-string keys. Controls that care about accessing multiple
//distinct settings should have custom keys.
if ( !is_string($name) ) {
continue;
}
//Recurse into structs and add all child settings separately as "name.child".
//However, don't split up composite settings.
if (
($setting instanceof Settings\AbstractStructSetting)
&& !($setting instanceof Settings\CompositeSetting)
) {
foreach (
AbstractSetting::recursivelyIterateSettings($setting, false, $name)
as $key => $childSetting
) {
$settingIds[$key] = self::serializeBindingForJs($childSetting, $context);
}
} else if ( $setting instanceof Binding ) {
$settingIds[$name] = self::serializeBindingForJs($setting, $context);
}
}
$result['settings'] = $settingIds;
//Enabled state or condition.
$result['enabled'] = $this->serializeConditionForJs();
//Label association settings for components that display this control as a child.
$includesOwnLabel = $this->includesOwnLabel();
if ( $includesOwnLabel ) {
$result['includesOwnLabel'] = true;
}
$labelTargetId = $this->getLabelTargetId();
if ( $this->supportsLabelAssociation() && !empty($labelTargetId) ) {
$result['labelTargetId'] = $labelTargetId;
}
return $result;
}
protected function getKoComponentParams(): array {
$params = parent::getKoComponentParams();
//Primary input ID.
if ( $this->hasPrimaryInput ) {
$params['primaryInputId'] = $this->getPrimaryInputId();
}
//Input classes.
$inputClasses = $this->getInputClasses();
if ( !empty($inputClasses) ) {
$params['inputClasses'] = $inputClasses;
}
//Input attributes.
if ( !empty($this->inputAttributes) ) {
$params['inputAttributes'] = $this->inputAttributes;
}
return $params;
}
public function getChildren(): array {
return $this->children;
}
public function getAllDescendants() {
foreach ($this->children as $child) {
yield $child;
if ( $child instanceof ControlContainer ) {
yield from $child->getAllDescendants();
}
}
}
public function getAllReferencedSettings(Context $context) {
foreach ($this->getSettings() as $setting) {
if ( $setting instanceof AbstractSetting ) {
yield $setting->getId() => $setting;
} else if ( $setting instanceof Binding ) {
$option = $context->resolveBinding($setting);
if ( $option->isDefined() ) {
$resolution = $option->get();
$leafSetting = $resolution->getSetting();
if ( $leafSetting instanceof AbstractSetting ) {
yield $leafSetting->getId() => $leafSetting;
}
}
}
}
foreach ($this->getChildren() as $child) {
yield from $child->getAllReferencedSettings($context);
}
}
}