571 lines
15 KiB
PHP
571 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Web\Form\IplElement;
|
|
|
|
use Icinga\Exception\ProgrammingError;
|
|
use Icinga\Module\Director\IcingaConfig\ExtensibleSet as Set;
|
|
use Icinga\Module\Director\Web\Form\IconHelper;
|
|
use ipl\Html\BaseHtmlElement;
|
|
use ipl\Html\Html;
|
|
use gipfl\Translation\TranslationHelper;
|
|
|
|
class ExtensibleSetElement extends BaseHtmlElement
|
|
{
|
|
use TranslationHelper;
|
|
|
|
protected $tag = 'ul';
|
|
|
|
/** @var Set */
|
|
protected $set;
|
|
|
|
private $id;
|
|
|
|
private $name;
|
|
|
|
private $value;
|
|
|
|
private $description;
|
|
|
|
private $multiOptions;
|
|
|
|
private $validOptions;
|
|
|
|
private $chosenOptionCount = 0;
|
|
|
|
private $suggestionContext;
|
|
|
|
private $sorted = false;
|
|
|
|
private $disabled = false;
|
|
|
|
private $remainingAttribs;
|
|
|
|
private $hideOptions = [];
|
|
|
|
private $inherited;
|
|
|
|
private $inheritedFrom;
|
|
|
|
protected $defaultAttributes = [
|
|
'class' => 'extensible-set'
|
|
];
|
|
|
|
protected function __construct($name)
|
|
{
|
|
$this->name = $this->id = $name;
|
|
}
|
|
|
|
public function hideOptions($options)
|
|
{
|
|
$this->hideOptions = array_merge($this->hideOptions, $options);
|
|
return $this;
|
|
}
|
|
|
|
private function setMultiOptions($options)
|
|
{
|
|
$this->multiOptions = $options;
|
|
$this->validOptions = $this->flattenOptions($options);
|
|
}
|
|
|
|
protected function isValidOption($option)
|
|
{
|
|
if ($this->validOptions === null) {
|
|
if ($this->suggestionContext === null) {
|
|
return true;
|
|
} else {
|
|
// TODO: ask suggestionContext, if any
|
|
return true;
|
|
}
|
|
} else {
|
|
return in_array($option, $this->validOptions);
|
|
}
|
|
}
|
|
|
|
private function disable($disable = true)
|
|
{
|
|
$this->disabled = (bool) $disable;
|
|
}
|
|
|
|
private function isDisabled()
|
|
{
|
|
return $this->disabled;
|
|
}
|
|
|
|
private function isSorted()
|
|
{
|
|
return $this->sorted;
|
|
}
|
|
|
|
public function setValue($value)
|
|
{
|
|
if ($value instanceof Set) {
|
|
$value = $value->toPlainObject();
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
$value = array_filter($value, 'strlen');
|
|
}
|
|
|
|
if (null !== $value && ! is_array($value)) {
|
|
throw new ProgrammingError(
|
|
'Got unexpected value, no array: %s',
|
|
var_export($value, 1)
|
|
);
|
|
}
|
|
|
|
$this->value = $value;
|
|
return $this;
|
|
}
|
|
|
|
protected function extractZfInfo(&$attribs = null)
|
|
{
|
|
if ($attribs === null) {
|
|
return;
|
|
}
|
|
|
|
foreach (['id', 'name', 'descriptions'] as $key) {
|
|
if (array_key_exists($key, $attribs)) {
|
|
$this->$key = $attribs[$key];
|
|
unset($attribs[$key]);
|
|
}
|
|
}
|
|
if (array_key_exists('disable', $attribs)) {
|
|
$this->disable($attribs['disable']);
|
|
unset($attribs['disable']);
|
|
}
|
|
if (array_key_exists('value', $attribs)) {
|
|
$this->setValue($attribs['value']);
|
|
unset($attribs['value']);
|
|
}
|
|
if (array_key_exists('inherited', $attribs)) {
|
|
$this->inherited = $attribs['inherited'];
|
|
unset($attribs['inherited']);
|
|
}
|
|
if (array_key_exists('inheritedFrom', $attribs)) {
|
|
$this->inheritedFrom = $attribs['inheritedFrom'];
|
|
unset($attribs['inheritedFrom']);
|
|
}
|
|
|
|
if (array_key_exists('multiOptions', $attribs)) {
|
|
$this->setMultiOptions($attribs['multiOptions']);
|
|
unset($attribs['multiOptions']);
|
|
}
|
|
|
|
if (array_key_exists('hideOptions', $attribs)) {
|
|
$this->hideOptions($attribs['hideOptions']);
|
|
unset($attribs['hideOptions']);
|
|
}
|
|
|
|
if (array_key_exists('sorted', $attribs)) {
|
|
$this->sorted = (bool) $attribs['sorted'];
|
|
unset($attribs['sorted']);
|
|
}
|
|
|
|
if (array_key_exists('description', $attribs)) {
|
|
$this->description = $attribs['description'];
|
|
unset($attribs['description']);
|
|
}
|
|
|
|
if (array_key_exists('suggest', $attribs)) {
|
|
$this->suggestionContext = $attribs['suggest'];
|
|
unset($attribs['suggest']);
|
|
}
|
|
|
|
if (! empty($attribs)) {
|
|
$this->remainingAttribs = $attribs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates an 'extensible set' element.
|
|
*
|
|
* @codingStandardsIgnoreEnd
|
|
*
|
|
* @param string|array $name If a string, the element name. If an
|
|
* array, all other parameters are ignored, and the array elements
|
|
* are used in place of added parameters.
|
|
*
|
|
* @param mixed $value The element value.
|
|
*
|
|
* @param array $attribs Attributes for the element tag.
|
|
*
|
|
* @return string The element XHTML.
|
|
*/
|
|
public static function fromZfDingens($name, $value = null, $attribs = null)
|
|
{
|
|
$el = new static($name);
|
|
$el->extractZfInfo($attribs);
|
|
$el->setValue($value);
|
|
return $el->render();
|
|
}
|
|
|
|
protected function assemble()
|
|
{
|
|
$this->addChosenOptions();
|
|
$this->addAddMore();
|
|
|
|
if ($this->isSorted()) {
|
|
$this->getAttributes()->add('class', 'sortable');
|
|
}
|
|
if (null !== $this->description) {
|
|
$this->addDescription($this->description);
|
|
}
|
|
}
|
|
|
|
private function eventuallyAddAutosuggestion(BaseHtmlElement $element)
|
|
{
|
|
if ($this->suggestionContext !== null) {
|
|
$attrs = $element->getAttributes();
|
|
$attrs->add('class', 'director-suggest');
|
|
$attrs->set([
|
|
'data-suggestion-context' => $this->suggestionContext,
|
|
]);
|
|
}
|
|
|
|
return $element;
|
|
}
|
|
|
|
private function hasAvailableMultiOptions()
|
|
{
|
|
return count($this->multiOptions) > 1 || strlen(key($this->multiOptions));
|
|
}
|
|
|
|
private function addAddMore()
|
|
{
|
|
$cnt = $this->chosenOptionCount;
|
|
|
|
if ($this->multiOptions) {
|
|
if (! $this->hasAvailableMultiOptions()) {
|
|
return;
|
|
}
|
|
$field = Html::tag('select', ['class' => 'autosubmit']);
|
|
$more = $this->inherited === null
|
|
? $this->translate('- add more -')
|
|
: $this->getInheritedInfo();
|
|
$field->add(Html::tag('option', [
|
|
'value' => '',
|
|
'tabindex' => '-1'
|
|
], $more));
|
|
|
|
foreach ($this->multiOptions as $key => $label) {
|
|
if ($key === null) {
|
|
$key = '';
|
|
}
|
|
if (is_array($label)) {
|
|
$optGroup = Html::tag('optgroup', ['label' => $key]);
|
|
foreach ($label as $grpKey => $grpLabel) {
|
|
$optGroup->add(
|
|
Html::tag('option', ['value' => $grpKey], $grpLabel)
|
|
);
|
|
}
|
|
$field->add($optGroup);
|
|
} else {
|
|
$option = Html::tag('option', ['value' => $key], $label);
|
|
$field->add($option);
|
|
}
|
|
}
|
|
} else {
|
|
$field = Html::tag('input', [
|
|
'type' => 'text',
|
|
'placeholder' => $this->inherited === null
|
|
? $this->translate('Add a new one...')
|
|
: $this->getInheritedInfo(),
|
|
]);
|
|
}
|
|
$field->addAttributes([
|
|
'id' => $this->id . $this->suffix($cnt),
|
|
'name' => $this->name . '[]',
|
|
]);
|
|
$this->eventuallyAddAutosuggestion(
|
|
$this->addRemainingAttributes(
|
|
$this->eventuallyDisable($field)
|
|
)
|
|
);
|
|
if ($cnt !== 0) { // TODO: was === 0?!
|
|
$field->getAttributes()->add('class', 'extend-set');
|
|
}
|
|
|
|
if ($this->suggestionContext === null) {
|
|
$this->add(Html::tag('li', null, [
|
|
$this->createAddNewButton(),
|
|
$field
|
|
]));
|
|
} else {
|
|
$this->add(Html::tag('li', null, [
|
|
$this->newInlineButtons(
|
|
$this->renderDropDownButton()
|
|
),
|
|
$field
|
|
]));
|
|
}
|
|
}
|
|
|
|
private function getInheritedInfo()
|
|
{
|
|
if ($this->inheritedFrom === null) {
|
|
return \sprintf(
|
|
$this->translate('%s (inherited)'),
|
|
$this->stringifyInheritedValue()
|
|
);
|
|
} else {
|
|
return \sprintf(
|
|
$this->translate('%s (inherited from %s)'),
|
|
$this->stringifyInheritedValue(),
|
|
$this->inheritedFrom
|
|
);
|
|
}
|
|
}
|
|
|
|
private function stringifyInheritedValue()
|
|
{
|
|
if (\is_array($this->inherited)) {
|
|
return \implode(', ', $this->inherited);
|
|
} else {
|
|
return \sprintf(
|
|
$this->translate('%s (not an Array!)'),
|
|
\var_export($this->inherited, 1)
|
|
);
|
|
}
|
|
}
|
|
|
|
private function createAddNewButton()
|
|
{
|
|
return $this->newInlineButtons(
|
|
$this->eventuallyDisable($this->renderAddButton())
|
|
);
|
|
}
|
|
|
|
private function addChosenOptions()
|
|
{
|
|
if (null === $this->value) {
|
|
return;
|
|
}
|
|
$total = count($this->value);
|
|
|
|
foreach ($this->value as $val) {
|
|
if (in_array($val, $this->hideOptions)) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->multiOptions !== null) {
|
|
if ($this->isValidOption($val)) {
|
|
$this->multiOptions = $this->removeOption(
|
|
$this->multiOptions,
|
|
$val
|
|
);
|
|
// TODO:
|
|
// $this->removeOption($val);
|
|
}
|
|
}
|
|
|
|
$text = Html::tag('input', [
|
|
'type' => 'text',
|
|
'name' => $this->name . '[]',
|
|
'id' => $this->id . $this->suffix($this->chosenOptionCount),
|
|
'value' => $val
|
|
]);
|
|
$text->getAttributes()->set([
|
|
'autocomplete' => 'off',
|
|
'autocorrect' => 'off',
|
|
'autocapitalize' => 'off',
|
|
'spellcheck' => 'false',
|
|
]);
|
|
|
|
$this->addRemainingAttributes($this->eventuallyDisable($text));
|
|
$this->add(Html::tag('li', null, [
|
|
$this->getOptionButtons($this->chosenOptionCount, $total),
|
|
$text
|
|
]));
|
|
$this->chosenOptionCount++;
|
|
}
|
|
}
|
|
|
|
private function addRemainingAttributes(BaseHtmlElement $element)
|
|
{
|
|
if ($this->remainingAttribs !== null) {
|
|
$element->getAttributes()->add($this->remainingAttribs);
|
|
}
|
|
|
|
return $element;
|
|
}
|
|
|
|
private function eventuallyDisable(BaseHtmlElement $element)
|
|
{
|
|
if ($this->isDisabled()) {
|
|
$this->disableElement($element);
|
|
}
|
|
|
|
return $element;
|
|
}
|
|
|
|
private function disableElement(BaseHtmlElement $element)
|
|
{
|
|
$element->getAttributes()->set('disabled', 'disabled');
|
|
return $element;
|
|
}
|
|
|
|
private function disableIf(BaseHtmlElement $element, $condition)
|
|
{
|
|
if ($condition) {
|
|
$this->disableElement($element);
|
|
}
|
|
|
|
return $element;
|
|
}
|
|
|
|
private function getOptionButtons($cnt, $total)
|
|
{
|
|
if ($this->isDisabled()) {
|
|
return [];
|
|
}
|
|
$first = $cnt === 0;
|
|
$last = $cnt === $total - 1;
|
|
$name = $this->name;
|
|
$buttons = $this->newInlineButtons();
|
|
if ($this->isSorted()) {
|
|
$buttons->add([
|
|
$this->disableIf($this->renderDownButton($name, $cnt), $last),
|
|
$this->disableIf($this->renderUpButton($name, $cnt), $first)
|
|
]);
|
|
}
|
|
|
|
$buttons->add($this->renderDeleteButton($name, $cnt));
|
|
|
|
return $buttons;
|
|
}
|
|
|
|
protected function newInlineButtons($content = null)
|
|
{
|
|
return Html::tag('span', ['class' => 'inline-buttons'], $content);
|
|
}
|
|
|
|
protected function addDescription($description)
|
|
{
|
|
$this->add(
|
|
Html::tag('p', ['class' => 'description'], $description)
|
|
);
|
|
}
|
|
|
|
private function flattenOptions($options)
|
|
{
|
|
$flat = array();
|
|
|
|
foreach ($options as $key => $option) {
|
|
if (is_array($option)) {
|
|
foreach ($option as $k => $o) {
|
|
$flat[] = $k;
|
|
}
|
|
} else {
|
|
$flat[] = $key;
|
|
}
|
|
}
|
|
|
|
return $flat;
|
|
}
|
|
|
|
private function removeOption($options, $option)
|
|
{
|
|
$unset = array();
|
|
foreach ($options as $key => & $value) {
|
|
if (is_array($value)) {
|
|
$value = $this->removeOption($value, $option);
|
|
if (empty($value)) {
|
|
$unset[] = $key;
|
|
}
|
|
} elseif ($key === $option) {
|
|
$unset[] = $key;
|
|
}
|
|
}
|
|
|
|
foreach ($unset as $key) {
|
|
unset($options[$key]);
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
private function suffix($cnt)
|
|
{
|
|
if ($cnt === 0) {
|
|
return '';
|
|
} else {
|
|
return '_' . $cnt;
|
|
}
|
|
}
|
|
|
|
private function renderDropDownButton()
|
|
{
|
|
return $this->createRelatedAction(
|
|
'drop-down',
|
|
$this->name,
|
|
$this->translate('Show available options'),
|
|
'down-open'
|
|
);
|
|
}
|
|
|
|
private function renderAddButton()
|
|
{
|
|
return $this->createRelatedAction(
|
|
'add',
|
|
// This would interfere with how PHP resolves _POST arrays. So we
|
|
// use a fake name for now, that way the button will be ignored and
|
|
// behave similar to an auto-submission
|
|
'X_' . $this->name,
|
|
$this->translate('Add a new entry'),
|
|
'plus'
|
|
);
|
|
}
|
|
|
|
private function renderDeleteButton($name, $cnt)
|
|
{
|
|
return $this->createRelatedAction(
|
|
'remove',
|
|
$name . '_' . $cnt,
|
|
$this->translate('Remove this entry'),
|
|
'cancel'
|
|
);
|
|
}
|
|
|
|
private function renderUpButton($name, $cnt)
|
|
{
|
|
return $this->createRelatedAction(
|
|
'move-up',
|
|
$name . '_' . $cnt,
|
|
$this->translate('Move up'),
|
|
'up-big'
|
|
);
|
|
}
|
|
|
|
private function renderDownButton($name, $cnt)
|
|
{
|
|
return $this->createRelatedAction(
|
|
'move-down',
|
|
$name . '_' . $cnt,
|
|
$this->translate('Move down'),
|
|
'down-big'
|
|
);
|
|
}
|
|
|
|
protected function makeActionName($name, $action)
|
|
{
|
|
return $name . '__' . str_replace('-', '_', strtoupper($action));
|
|
}
|
|
|
|
protected function createRelatedAction(
|
|
$action,
|
|
$name,
|
|
$title,
|
|
$icon
|
|
) {
|
|
$input = Html::tag('input', [
|
|
'type' => 'submit',
|
|
'class' => ['related-action', 'action-' . $action],
|
|
'name' => $this->makeActionName($name, $action),
|
|
'value' => IconHelper::instance()->iconCharacter($icon),
|
|
'title' => $title
|
|
]);
|
|
|
|
return $input;
|
|
}
|
|
}
|