Skip to content

Commit 2c9639b

Browse files
committed
TermInput: Mark invalid terms in read only mode as such
The browser's validation API doesn't run for readonly inputs, so we have to always check the constraint.
1 parent 90e4845 commit 2c9639b

File tree

4 files changed

+74
-2
lines changed

4 files changed

+74
-2
lines changed

asset/css/search-base.less

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626
.search-bar,
2727
.term-input-area {
28-
[data-index] input:invalid {
28+
[data-index] input:invalid,
29+
[data-index] input.invalid {
2930
background-color: var(--search-term-invalid-bg, @search-term-invalid-bg);
3031
color: var(--search-term-invalid-color, @search-term-invalid-color);
3132
}
@@ -41,6 +42,23 @@
4142
}
4243
}
4344

45+
.invalid-reason {
46+
padding: .25em;
47+
.rounded-corners(.25em);
48+
border: 1px solid black;
49+
font-weight: bold;
50+
background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg);
51+
52+
opacity: 0;
53+
visibility: hidden;
54+
transition: opacity 2s, visibility 2s;
55+
&.visible {
56+
opacity: 1;
57+
visibility: visible;
58+
transition: none;
59+
}
60+
}
61+
4462
.search-suggestions {
4563
background: var(--suggestions-bg, @suggestions-bg);
4664
color: var(--suggestions-color, @suggestions-color);
@@ -236,6 +254,12 @@
236254
display: revert;
237255
}
238256
}
257+
258+
.invalid-reason {
259+
position: absolute;
260+
top: 85%;
261+
left: .5em;
262+
}
239263
}
240264
}
241265
}

asset/css/variables.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
@search-term-selected-bg: @base-disabled;
6161
@search-term-invalid-bg: @state-critical;
6262
@search-term-invalid-color: @default-text-color-inverted;
63+
@search-term-invalid-reason-bg: @base-gray-lighter;
6364
@search-term-disabled-bg: @base-disabled;
6465
@search-term-selected-color: @base-gray-light;
6566
@search-term-highlighted-bg: @base-primary-bg;
@@ -154,6 +155,7 @@
154155
--search-term-selected-bg: var(--base-disabled);
155156
--search-term-invalid-bg: var(--base-remove-bg);
156157
--search-term-invalid-color: var(--default-text-color-inverted);
158+
--search-term-invalid-reason-bg: var(--base-gray-lighter);
157159
--search-term-disabled-bg: var(--base-gray-light);
158160
--search-term-selected-color: var(--base-gray);
159161
--search-term-highlighted-bg: var(--primary-button-bg);

asset/js/widget/TermInput.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
3232
this.ignoreSpaceUntil = null;
3333
}
3434

35+
registerTerm(termData, termIndex = null) {
36+
termIndex = super.registerTerm(termData, termIndex);
37+
38+
if (this.readOnly) {
39+
const label = this.termContainer.querySelector(`[data-index="${ termIndex }"]`);
40+
if (label) {
41+
// The label only exists in DOM at this time if it was transmitted
42+
// by the server. So it's safe to assume that it needs validation
43+
this.validate(label.firstChild);
44+
}
45+
}
46+
47+
return termIndex;
48+
}
49+
3550
readPartialTerm(input) {
3651
let value = super.readPartialTerm(input);
3752
if (value && this.ignoreSpaceUntil && value[0] === this.ignoreSpaceUntil) {
@@ -75,6 +90,33 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
7590
return super.hasSyntaxError(input);
7691
}
7792

93+
checkValidity(input) {
94+
if (! this.readOnly) {
95+
return super.checkValidity(input);
96+
}
97+
98+
// Readonly terms don't participate in constraint validation, so we have to do it ourselves
99+
return ! (input.pattern && ! input.value.match(input.pattern));
100+
}
101+
102+
reportValidity(element) {
103+
if (! this.readOnly) {
104+
return super.reportValidity(element);
105+
}
106+
107+
// Once invalid, it stays invalid since it's readonly
108+
element.classList.add('invalid');
109+
if (element.dataset.invalidMsg) {
110+
const reason = element.parentNode.querySelector(':scope > .invalid-reason');
111+
if (! reason.matches('.visible')) {
112+
element.title = element.dataset.invalidMsg;
113+
reason.textContent = element.dataset.invalidMsg;
114+
reason.classList.add('visible');
115+
setTimeout(() => reason.classList.remove('visible'), 5000);
116+
}
117+
}
118+
}
119+
78120
termsToQueryString(terms) {
79121
let quoted = [];
80122
for (const termData of terms) {
@@ -101,6 +143,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
101143
if (this.readOnly) {
102144
label.firstChild.readOnly = true;
103145
label.appendChild($.render('<i class="icon fa-trash fa"></i>'));
146+
label.appendChild($.render('<span class="invalid-reason"></span>'));
104147
}
105148

106149
return label;

src/FormElement/TermInput/TermContainer.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ protected function assemble()
5252
)
5353
);
5454
if ($this->input->getReadOnly()) {
55-
$label->addHtml(new Icon('trash'));
55+
$label->addHtml(
56+
new Icon('trash'),
57+
new HtmlElement('span', Attributes::create(['class' => 'invalid-reason']))
58+
);
5659
}
5760

5861
$this->addHtml($label);

0 commit comments

Comments
 (0)