diff --git a/asset/css/search-base.less b/asset/css/search-base.less
index 60253f49..d6f0e3b6 100644
--- a/asset/css/search-base.less
+++ b/asset/css/search-base.less
@@ -25,7 +25,8 @@
.search-bar,
.term-input-area {
- [data-index] input:invalid {
+ [data-index] input:invalid,
+ [data-index] input.invalid {
background-color: var(--search-term-invalid-bg, @search-term-invalid-bg);
color: var(--search-term-invalid-color, @search-term-invalid-color);
}
@@ -41,6 +42,23 @@
}
}
+.invalid-reason {
+ padding: .25em;
+ .rounded-corners(.25em);
+ border: 1px solid black;
+ font-weight: bold;
+ background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg);
+
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 2s, visibility 2s;
+ &.visible {
+ opacity: 1;
+ visibility: visible;
+ transition: none;
+ }
+}
+
.search-suggestions {
background: var(--suggestions-bg, @suggestions-bg);
color: var(--suggestions-color, @suggestions-color);
@@ -236,6 +254,12 @@
display: revert;
}
}
+
+ .invalid-reason {
+ position: absolute;
+ top: 85%;
+ left: .5em;
+ }
}
}
}
diff --git a/asset/css/variables.less b/asset/css/variables.less
index a500c5e9..d1328fe6 100644
--- a/asset/css/variables.less
+++ b/asset/css/variables.less
@@ -60,6 +60,7 @@
@search-term-selected-bg: @base-disabled;
@search-term-invalid-bg: @state-critical;
@search-term-invalid-color: @default-text-color-inverted;
+@search-term-invalid-reason-bg: @base-gray-lighter;
@search-term-disabled-bg: @base-disabled;
@search-term-selected-color: @base-gray-light;
@search-term-highlighted-bg: @base-primary-bg;
@@ -154,6 +155,7 @@
--search-term-selected-bg: var(--base-disabled);
--search-term-invalid-bg: var(--base-remove-bg);
--search-term-invalid-color: var(--default-text-color-inverted);
+ --search-term-invalid-reason-bg: var(--base-gray-lighter);
--search-term-disabled-bg: var(--base-gray-light);
--search-term-selected-color: var(--base-gray);
--search-term-highlighted-bg: var(--primary-button-bg);
diff --git a/asset/js/widget/TermInput.js b/asset/js/widget/TermInput.js
index 7e8e891e..d405ae77 100644
--- a/asset/js/widget/TermInput.js
+++ b/asset/js/widget/TermInput.js
@@ -32,6 +32,21 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
this.ignoreSpaceUntil = null;
}
+ registerTerm(termData, termIndex = null) {
+ termIndex = super.registerTerm(termData, termIndex);
+
+ if (this.readOnly) {
+ const label = this.termContainer.querySelector(`[data-index="${ termIndex }"]`);
+ if (label) {
+ // The label only exists in DOM at this time if it was transmitted
+ // by the server. So it's safe to assume that it needs validation
+ this.validate(label.firstChild);
+ }
+ }
+
+ return termIndex;
+ }
+
readPartialTerm(input) {
let value = super.readPartialTerm(input);
if (value && this.ignoreSpaceUntil && value[0] === this.ignoreSpaceUntil) {
@@ -75,6 +90,33 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
return super.hasSyntaxError(input);
}
+ checkValidity(input) {
+ if (! this.readOnly) {
+ return super.checkValidity(input);
+ }
+
+ // Readonly terms don't participate in constraint validation, so we have to do it ourselves
+ return ! (input.pattern && ! input.value.match(input.pattern));
+ }
+
+ reportValidity(element) {
+ if (! this.readOnly) {
+ return super.reportValidity(element);
+ }
+
+ // Once invalid, it stays invalid since it's readonly
+ element.classList.add('invalid');
+ if (element.dataset.invalidMsg) {
+ const reason = element.parentNode.querySelector(':scope > .invalid-reason');
+ if (! reason.matches('.visible')) {
+ element.title = element.dataset.invalidMsg;
+ reason.textContent = element.dataset.invalidMsg;
+ reason.classList.add('visible');
+ setTimeout(() => reason.classList.remove('visible'), 5000);
+ }
+ }
+ }
+
termsToQueryString(terms) {
let quoted = [];
for (const termData of terms) {
@@ -101,6 +143,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
if (this.readOnly) {
label.firstChild.readOnly = true;
label.appendChild($.render(''));
+ label.appendChild($.render(''));
}
return label;
diff --git a/src/FormElement/TermInput/TermContainer.php b/src/FormElement/TermInput/TermContainer.php
index 5d3643ab..6bcf4dbb 100644
--- a/src/FormElement/TermInput/TermContainer.php
+++ b/src/FormElement/TermInput/TermContainer.php
@@ -52,7 +52,10 @@ protected function assemble()
)
);
if ($this->input->getReadOnly()) {
- $label->addHtml(new Icon('trash'));
+ $label->addHtml(
+ new Icon('trash'),
+ new HtmlElement('span', Attributes::create(['class' => 'invalid-reason']))
+ );
}
$this->addHtml($label);