diff options
Diffstat (limited to '')
-rw-r--r-- | library/Director/Web/Form/IplElement/ExtensibleSetElement.php | 570 |
1 files changed, 570 insertions, 0 deletions
diff --git a/library/Director/Web/Form/IplElement/ExtensibleSetElement.php b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php new file mode 100644 index 0000000..a4dbb20 --- /dev/null +++ b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php @@ -0,0 +1,570 @@ +<?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; + } +} |