Skip to content

Commit

Permalink
Read only term input (#222)
Browse files Browse the repository at this point in the history
  • Loading branch information
nilmerg authored Jun 12, 2024
2 parents 0721dd0 + b8d7b5b commit 64b335f
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 17 deletions.
55 changes: 54 additions & 1 deletion asset/css/search-base.less
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -209,6 +227,41 @@
outline-width: 3px;
outline-offset: ~"calc(-@{labelPad} + 3px)";
}

&.read-only {
[data-index] {
position: relative;

input {
padding-left: 1.5em;
text-align: center;
cursor: pointer;

&:disabled {
cursor: default;
}

+ i {
position: absolute;
display: none;
top: .5em;
left: .5em;
}

&:not(:disabled):hover + i,
&:not(:disabled):focus + i {
display: revert;
}
}

.invalid-reason {
position: absolute;
z-index: 1;
top: 85%;
left: .5em;
}
}
}
}

.search-suggestions {
Expand Down
2 changes: 2 additions & 0 deletions asset/css/variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
42 changes: 33 additions & 9 deletions asset/js/widget/BaseInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
class BaseInput {
constructor(input) {
this.input = input;
this.readOnly = false;
this.disabled = false;
this.separator = '';
this.usedTerms = [];
Expand Down Expand Up @@ -55,9 +56,12 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
$(this.termContainer).on('input', '[data-label]', this.onInput, this);
$(this.termContainer).on('keydown', '[data-label]', this.onKeyDown, this);
$(this.termContainer).on('keyup', '[data-label]', this.onKeyUp, this);
$(this.termContainer).on('focusout', '[data-index]', this.onTermFocusOut, this);
$(this.termContainer).on('focusin', '[data-index]', this.onTermFocus, this);

if (! this.readOnly) {
$(this.termContainer).on('focusout', '[data-index]', this.onTermFocusOut, this);
}

// Copy/Paste
$(this.input).on('paste', this.onPaste, this);
$(this.input).on('copy', this.onCopyAndCut, this);
Expand Down Expand Up @@ -664,7 +668,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
toFocus = this.input;
}

toFocus.selectionStart = toFocus.selectionEnd = 0;
if (! this.readOnly) {
toFocus.selectionStart = toFocus.selectionEnd = 0;
}

$(toFocus).focus();

return toFocus;
Expand All @@ -687,7 +694,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
toFocus = this.input;
}

toFocus.selectionStart = toFocus.selectionEnd = toFocus.value.length;
if (! this.readOnly) {
toFocus.selectionStart = toFocus.selectionEnd = toFocus.value.length;
}

$(toFocus).focus();

return toFocus;
Expand Down Expand Up @@ -801,7 +811,12 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
case 'Backspace':
removedTerms = this.clearSelectedTerms();

if (this.isTermDirectionVertical()) {
if (this.readOnly) {
if (termIndex >= 0) {
removedTerms[termIndex] = this.removeTerm(input.parentNode);
this.moveFocusForward(termIndex - 1);
}
} else if (this.isTermDirectionVertical()) {
// pass
} else if (termIndex >= 0 && ! input.value) {
let removedTerm = this.removeTerm(input.parentNode);
Expand Down Expand Up @@ -835,7 +850,12 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
case 'Delete':
removedTerms = this.clearSelectedTerms();

if (! this.isTermDirectionVertical() && termIndex >= 0 && ! input.value) {
if (this.readOnly) {
if (termIndex >= 0) {
removedTerms[termIndex] = this.removeTerm(input.parentNode);
this.moveFocusForward(termIndex - 1);
}
} else if (! this.isTermDirectionVertical() && termIndex >= 0 && ! input.value) {
let removedTerm = this.removeTerm(input.parentNode);
if (removedTerm !== false) {
input = this.moveFocusForward(termIndex - 1);
Expand Down Expand Up @@ -863,20 +883,20 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
}
break;
case 'ArrowLeft':
if (input.selectionStart === 0 && this.hasTerms()) {
if (this.hasTerms() && (this.readOnly || input.selectionStart === 0)) {
event.preventDefault();
this.moveFocusBackward();
}
break;
case 'ArrowRight':
if (input.selectionStart === input.value.length && this.hasTerms()) {
if (this.hasTerms() && (this.readOnly || input.selectionStart === input.value.length)) {
event.preventDefault();
this.moveFocusForward();
}
break;
case 'ArrowUp':
if (this.isTermDirectionVertical()
&& input.selectionStart === 0
&& (this.readOnly || input.selectionStart === 0)
&& this.hasTerms()
&& (this.completer === null || ! this.completer.isBeingCompleted(input))
) {
Expand All @@ -886,7 +906,7 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
break;
case 'ArrowDown':
if (this.isTermDirectionVertical()
&& input.selectionStart === input.value.length
&& (this.readOnly || input.selectionStart === input.value.length)
&& this.hasTerms()
&& (this.completer === null || ! this.completer.isBeingCompleted(input))
) {
Expand Down Expand Up @@ -974,6 +994,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) {

this.deselectTerms();

if (this.readOnly) {
return;
}

if (! this.hasSyntaxError(input) && (
this.completer === null || ! this.completer.isBeingCompleted(input, false)
)) {
Expand Down
65 changes: 65 additions & 0 deletions asset/js/widget/TermInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
super(input);

this.separator = this.input.dataset.termSeparator || ' ';
this.readOnly = 'readOnlyTerms' in this.input.dataset;
this.ignoreSpaceUntil = null;
}

bind() {
super.bind();

if (this.readOnly) {
$(this.termContainer).on('click', '[data-index] > input', this.onTermClick, this);
}

// TODO: Compatibility only. Remove as soon as possible once Web 2.12 (?) is out.
// Or upon any other update which lets Web trigger a real submit upon auto submit.
$(this.input.form).on('change', 'select.autosubmit', this.onSubmit, this);
Expand All @@ -27,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) {
Expand Down Expand Up @@ -70,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) {
Expand All @@ -90,10 +137,28 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
super.complete(input, data);
}

renderTerm(termData, termIndex) {
const label = super.renderTerm(termData, termIndex);

if (this.readOnly) {
label.firstChild.readOnly = true;
label.appendChild($.render('<i class="icon fa-trash fa"></i>'));
label.appendChild($.render('<span class="invalid-reason"></span>'));
}

return label;
}

/**
* Event listeners
*/

onTermClick(event) {
let termIndex = Number(event.target.parentNode.dataset.index);
this.removeTerm(event.target.parentNode);
this.moveFocusForward(termIndex - 1);
}

onSubmit(event) {
super.onSubmit(event);

Expand Down
34 changes: 33 additions & 1 deletion src/FormElement/TermInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class TermInput extends FieldsetElement
/** @var bool Whether term direction is vertical */
protected $verticalTermDirection = false;

/** @var bool Whether registered terms are read-only */
protected $readOnly = false;

/** @var array Changes to transmit to the client */
protected $changes = [];

Expand Down Expand Up @@ -103,6 +106,30 @@ public function getTermDirection(): ?string
return $this->verticalTermDirection ? 'vertical' : null;
}

/**
* Set whether registered terms are read-only
*
* @param bool $state
*
* @return $this
*/
public function setReadOnly(bool $state = true): self
{
$this->readOnly = $state;

return $this;
}

/**
* Get whether registered terms are read-only
*
* @return bool
*/
public function getReadOnly(): bool
{
return $this->readOnly;
}

/**
* Set terms
*
Expand Down Expand Up @@ -415,6 +442,7 @@ public function getValueAttribute()
'data-with-multi-completion' => true,
'data-no-auto-submit-on-remove' => true,
'data-term-direction' => $this->getTermDirection(),
'data-read-only-terms' => $this->getReadOnly(),
'data-data-input' => '#' . $dataInputId,
'data-term-input' => '#' . $termInputId,
'data-term-container' => '#' . $termContainer->getAttribute('id')->getValue(),
Expand All @@ -436,7 +464,11 @@ public function getValueAttribute()

$mainInput->prependWrapper((new HtmlElement(
'div',
Attributes::create(['class' => ['term-input-area', $this->getTermDirection()]]),
Attributes::create(['class' => [
'term-input-area',
$this->getTermDirection(),
$this->getReadOnly() ? 'read-only' : null
]]),
$termContainer,
new HtmlElement('label', null, $mainInput)
)));
Expand Down
Loading

0 comments on commit 64b335f

Please sign in to comment.