<?php
namespace YahnisElsts\AdminMenuEditor\Customizable\Controls;
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 {
use Toggleable {
isEnabled as private traitIsEnabled;
}
/**
* @var string Type of the control
*/
protected $type = 'abstract';
/**
* @var \YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting[]
*/
protected $settings = [];
/**
* @var null|\YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting
*/
protected $mainSetting = null;
/**
* @var string|null Name of the Knockout component that renders this control.
*/
protected $koComponentName = null;
/**
* @var string
*/
protected $label = '';
/**
* @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 = []) {
$this->instanceNumber = ++static::$totalInstances;
parent::__construct($params);
$this->settings = $settings;
if ( !empty($this->settings) ) {
if ( isset($params['mainSetting']) ) {
$this->mainSetting = $this->settings[$params['mainSetting']];
} else {
$this->mainSetting = reset($this->settings);
}
}
//Label
if ( isset($params['label']) ) {
$this->label = $params['label'];
} else if ( !empty($this->mainSetting) ) {
$this->label = $this->mainSetting->getLabel();
}
//Description
if ( isset($params['description']) ) {
$this->description = $params['description'];
} else if ( !empty($this->mainSetting) ) {
$this->description = $this->mainSetting->getDescription();
}
//Enabled/disabled
$this->parseEnabledParam($params);
//ID. Note that this is not necessarily the same as the ID attribute
//of the HTML element generated by this control.
if ( $this->id === '' ) {
if ( isset($this->mainSetting) ) {
$this->id = preg_replace('/[^a-z0-9\-_]+/i', '_', $this->mainSetting->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;
}
} else {
$this->id = 'ame_' . $this->type . '_' . $this->instanceNumber;
}
}
//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);
/**
* @return string
*/
public function getLabel() {
return $this->label;
}
public function getLabelTargetId() {
return $this->getPrimaryInputId();
}
/**
* 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() {
return false;
}
public function parentLabelEnabled() {
return $this->supportsLabelAssociation();
}
/**
* @return \YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting[]
*/
public function getSettings() {
return $this->settings;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @return bool
*/
public function isEnabled() {
$enabled = $this->traitIsEnabled();
if ( !empty($this->mainSetting) ) {
$enabled = $enabled && $this->mainSetting->isEditableByUser();
}
return $enabled;
}
/**
* @return string
*/
public function getPrimaryInputId() {
if ( !$this->hasPrimaryInput ) {
return '';
}
return '_ame-cntrl-' . $this->id;
}
/**
* 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 array<string,mixed> $attributes
* @param string $tagName
* @param string|null $content
* @return string
*/
protected function buildInputElement(
$attributes = [],
$tagName = 'input',
$content = null
) {
//Merge input classes and any classes specified in the $attributes array.
$classes = $this->inputClasses;
if ( isset($attributes['class']) ) {
$classes = array_merge($classes, (array)$attributes['class']);
unset($attributes['class']);
}
$attributes = array_merge(
[
'id' => $this->hasPrimaryInput ? $this->getPrimaryInputId() : null,
'class' => $classes,
'name' => $this->getFieldName(),
'disabled' => !$this->isEnabled(),
//Add the setting ID for the admin customizer module.
'data-ac-setting-id' => $this->getAutoAcSettingId(),
],
$attributes,
$this->inputAttributes
);
return $this->buildTag($tagName, $attributes, $content);
}
protected function getMainSettingValue($default = null) {
if ( $this->mainSetting instanceof Setting ) {
return $this->mainSetting->getValue($default);
} else if ( $this->mainSetting instanceof Settings\WithSchema\SingularSetting ) {
return $this->mainSetting->getValue($default);
}
return $default;
}
/**
* Generate the "name" attribute value for a form element associated with
* this control.
*
* @param string|null $compositeChildKey Only for composite settings: the key of the child setting.
* @param Settings\AbstractSetting|null $setting The setting shown in the field. Defaults to the main setting.
* @return string
*/
protected function getFieldName($compositeChildKey = null, $setting = null) {
if ( $setting === null ) {
$setting = $this->mainSetting;
}
if ( !($setting instanceof Settings\AbstractSetting) ) {
return $this->id;
}
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.
$parts = explode('.', $id);
$root = array_shift($parts);
if ( empty($parts) ) {
return $root;
}
return $root . '[' . implode('][', $parts) . ']';
}
public function getAutoGroupTitle() {
if ( $this->mainSetting instanceof Settings\AbstractSetting ) {
$customTitle = $this->mainSetting->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->mainSetting instanceof AbstractSetting) && $this->hasPrimaryInput ) {
return $this->mainSetting->getId();
}
return null;
}
protected function getKoObservableExpression($defaultValue, AbstractSetting $setting = null) {
if ( $setting === null ) {
$setting = $this->mainSetting;
}
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() {
$result = parent::serializeForJs();
$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->mainSetting instanceof AbstractSetting ) {
$settings['value'] = $this->mainSetting;
}
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] = $childSetting->getId();
}
} else if ( $setting instanceof AbstractSetting ) {
$settingIds[$name] = $setting->getId();
}
}
$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() {
$params = parent::getKoComponentParams();
//Primary input ID.
if ( $this->hasPrimaryInput ) {
$params['primaryInputId'] = $this->getPrimaryInputId();
}
//Input classes.
if ( !empty($this->inputClasses) ) {
$params['inputClasses'] = $this->inputClasses;
}
//Input attributes.
if ( !empty($this->inputAttributes) ) {
$params['inputAttributes'] = $this->inputAttributes;
}
return $params;
}
} |