diff --git a/asset/css/compat.less b/asset/css/compat.less index 7eef5b00..ecb6823e 100644 --- a/asset/css/compat.less +++ b/asset/css/compat.less @@ -74,7 +74,11 @@ form.icinga-form .control-group { form.icinga-form .control-group { > .term-input-area { flex: 1 1 auto; + width: auto; + &.vertical { + width: 0; + } input[type="text"] { flex: unset; @@ -82,3 +86,14 @@ form.icinga-form .control-group { } } } + +// SearchBar styles + +.icinga-module.module-icingadb .controls .search-bar .filter-input-area { + label { + &::after, + input { + padding: 0 .5em; + } + } +} diff --git a/asset/css/search-base.less b/asset/css/search-base.less index c63fab51..fb74305f 100644 --- a/asset/css/search-base.less +++ b/asset/css/search-base.less @@ -1,7 +1,7 @@ // Style .search-bar .filter-input-area, -.term-input-area { +.term-input-area:not(.vertical) { // Scrollbar style // Firefox @@ -116,7 +116,7 @@ // Layout .search-bar .filter-input-area, -.term-input-area { +.term-input-area:not(.vertical) { overflow: auto hidden; overflow-x: overlay; // Not invalid, but proprietary feature by chrome/webkit display: flex; @@ -134,7 +134,7 @@ &::after, input { width: auto; - padding: 0 .5em; + padding: .25em .5em; resize: none; } @@ -174,6 +174,33 @@ } } +.term-input-area.vertical { + display: flex; + flex-direction: column-reverse; + + > .terms { + @gap: 1px; + @termsPerRow: 2; + + display: flex; + flex-wrap: wrap; + gap: @gap; + margin-top: @gap; + + label { + @termWidth: 100%/@termsPerRow; + @totalGapWidthPerRow: (@termsPerRow - 1) * @gap; + + min-width: ~"calc(@{termWidth} - (@{totalGapWidthPerRow} / @{termsPerRow}))"; + flex: 1 1 auto; + + input { + text-overflow: ellipsis; + } + } + } +} + .term-input-area { label input:focus { @labelPad: 7/12em; diff --git a/asset/js/widget/BaseInput.js b/asset/js/widget/BaseInput.js index 0985fa20..cceaf3fa 100644 --- a/asset/js/widget/BaseInput.js +++ b/asset/js/widget/BaseInput.js @@ -524,6 +524,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) { } togglePlaceholder() { + if (this.isTermDirectionVertical()) { + return; + } + let placeholder = ''; if (! this.hasTerms()) { @@ -619,6 +623,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) { ); } + isTermDirectionVertical() { + return this.input.dataset.termDirection === 'vertical'; + } + moveFocusForward(from = null) { let toFocus; @@ -774,7 +782,9 @@ define(["../notjQuery", "Completer"], function ($, Completer) { case 'Backspace': removedTerms = this.clearSelectedTerms(); - if (termIndex >= 0 && ! input.value) { + if (this.isTermDirectionVertical()) { + // pass + } else if (termIndex >= 0 && ! input.value) { let removedTerm = this.removeTerm(input.parentNode); if (removedTerm !== false) { input = this.moveFocusBackward(termIndex); @@ -806,7 +816,7 @@ define(["../notjQuery", "Completer"], function ($, Completer) { case 'Delete': removedTerms = this.clearSelectedTerms(); - if (termIndex >= 0 && ! input.value) { + if (! this.isTermDirectionVertical() && termIndex >= 0 && ! input.value) { let removedTerm = this.removeTerm(input.parentNode); if (removedTerm !== false) { input = this.moveFocusForward(termIndex - 1); @@ -845,6 +855,26 @@ define(["../notjQuery", "Completer"], function ($, Completer) { this.moveFocusForward(); } break; + case 'ArrowUp': + if (this.isTermDirectionVertical() + && input.selectionStart === 0 + && this.hasTerms() + && (this.completer === null || ! this.completer.isBeingCompleted(input)) + ) { + event.preventDefault(); + this.moveFocusBackward(); + } + break; + case 'ArrowDown': + if (this.isTermDirectionVertical() + && input.selectionStart === input.value.length + && this.hasTerms() + && (this.completer === null || ! this.completer.isBeingCompleted(input)) + ) { + event.preventDefault(); + this.moveFocusForward(); + } + break; case 'a': if ((event.ctrlKey || event.metaKey) && ! this.readPartialTerm(input)) { this.selectTerms(); diff --git a/asset/js/widget/FilterInput.js b/asset/js/widget/FilterInput.js index 99431851..c84c7fee 100644 --- a/asset/js/widget/FilterInput.js +++ b/asset/js/widget/FilterInput.js @@ -1082,6 +1082,10 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { return termData; } + isTermDirectionVertical() { + return false; + } + highlightTerm(label, highlightedBy = null) { label.classList.add('highlighted'); diff --git a/src/FormElement/TermInput.php b/src/FormElement/TermInput.php index e946aa3b..99a666b9 100644 --- a/src/FormElement/TermInput.php +++ b/src/FormElement/TermInput.php @@ -37,6 +37,9 @@ class TermInput extends FieldsetElement /** @var Url The suggestion url */ protected $suggestionUrl; + /** @var bool Whether term direction is vertical */ + protected $verticalTermDirection = false; + /** @var array Changes to transmit to the client */ protected $changes = []; @@ -76,6 +79,30 @@ public function getSuggestionUrl(): ?Url return $this->suggestionUrl; } + /** + * Set whether term direction should be vertical + * + * @param bool $state + * + * @return $this + */ + public function setVerticalTermDirection(bool $state = true): self + { + $this->verticalTermDirection = $state; + + return $this; + } + + /** + * Get the desired term direction + * + * @return ?string + */ + public function getTermDirection(): ?string + { + return $this->verticalTermDirection ? 'vertical' : null; + } + /** * Set terms * @@ -386,6 +413,7 @@ public function getValueAttribute() 'data-term-separator' => ',', 'data-enrichment-type' => 'terms', 'data-no-auto-submit-on-remove' => true, + 'data-term-direction' => $this->getTermDirection(), 'data-data-input' => '#' . $dataInputId, 'data-term-input' => '#' . $termInputId, 'data-term-container' => '#' . $termContainer->getAttribute('id')->getValue(), @@ -407,7 +435,7 @@ public function getValueAttribute() $mainInput->prependWrapper((new HtmlElement( 'div', - Attributes::create(['class' => 'term-input-area']), + Attributes::create(['class' => ['term-input-area', $this->getTermDirection()]]), $termContainer, new HtmlElement('label', null, $mainInput) )));