summaryrefslogtreecommitdiffstats
path: root/library/Director/Web/Form/IplElement/ExtensibleSetElement.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Director/Web/Form/IplElement/ExtensibleSetElement.php570
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;
+ }
+}