Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TermInput: Support reorder by drag and drop #223

Merged
merged 3 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions asset/css/search-base.less
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@
color: var(--search-term-selected-color, @search-term-selected-color);
font-style: italic;
}

[data-drag-initiator] {
cursor: grab;
}

.sortable-drag > label {
border: 1px dashed var(--search-term-drag-border-color, @search-term-drag-border-color);
}

.sortable-ghost {
opacity: .5;
}
}

fieldset:disabled .term-input-area [data-drag-initiator] {
cursor: not-allowed;
}

.invalid-reason {
Expand Down Expand Up @@ -198,24 +214,53 @@
display: flex;
flex-direction: column-reverse;

@itemGap: 1px;

> .terms {
@gap: 1px;
margin-top: @itemGap;

input {
text-overflow: ellipsis;
}
}

> div.terms {
@termsPerRow: 2;

display: flex;
flex-wrap: wrap;
gap: @gap;
margin-top: @gap;
gap: @itemGap;

label {
@termWidth: 100%/@termsPerRow;
@totalGapWidthPerRow: (@termsPerRow - 1) * @gap;
@totalGapWidthPerRow: (@termsPerRow - 1) * @itemGap;

min-width: ~"calc(@{termWidth} - (@{totalGapWidthPerRow} / @{termsPerRow}))";
flex: 1 1 auto;
}
}

input {
text-overflow: ellipsis;
> ol.terms {
padding: 0;
margin-bottom: 0;
list-style-type: none;

li:not(:first-child) {
margin-top: @itemGap;
}

li {
display: flex;
align-items: center;
gap: .25em;

> label {
flex: 1 1 auto;
}

> [data-drag-initiator]::before {
font-size: 1.75em;
margin: 0;
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions asset/css/variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
@search-term-selected-color: @base-gray-light;
@search-term-highlighted-bg: @base-primary-bg;
@search-term-highlighted-color: @default-text-color-inverted;
@search-term-drag-border-color: @base-gray;

@search-condition-remove-bg: @state-critical;
@search-condition-remove-color: @default-text-color-inverted;
Expand Down Expand Up @@ -160,6 +161,7 @@
--search-term-selected-color: var(--base-gray);
--search-term-highlighted-bg: var(--primary-button-bg);
--search-term-highlighted-color: var(--default-text-color-inverted);
--search-term-drag-border-color: var(--base-gray);

--search-condition-remove-bg: var(--base-remove-bg);
--search-condition-remove-color: var(--default-text-color-inverted);
Expand Down
9 changes: 5 additions & 4 deletions asset/js/widget/BaseInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
}

insertRenderedTerm(label) {
let next = this.termContainer.querySelector(`[data-index="${ label.dataset.index + 1 }"]`);
const termIndex = Number(label.dataset.index);
const next = this.termContainer.querySelector(`[data-index="${ termIndex + 1 }"]`);
this.termContainer.insertBefore(label, next);
return label;
}
Expand Down Expand Up @@ -464,10 +465,10 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
// Cut the term's data
let [termData] = this.usedTerms.splice(termIndex, 1);

// Avoid saving the term, it's removed after all
label.firstChild.skipSaveOnBlur = true;

if (updateDOM) {
// Avoid saving the term, it's removed after all
label.firstChild.skipSaveOnBlur = true;

// Remove it from the DOM
this.removeRenderedTerm(label);
}
Expand Down
72 changes: 71 additions & 1 deletion asset/js/widget/TermInput.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
define(["../notjQuery", "../vendor/Sortable", "BaseInput"], function ($, Sortable, BaseInput) {

"use strict";

Expand All @@ -7,6 +7,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
super(input);

this.separator = this.input.dataset.termSeparator || ' ';
this.ordered = 'maintainTermOrder' in this.input.dataset;
this.readOnly = 'readOnlyTerms' in this.input.dataset;
this.ignoreSpaceUntil = null;
}
Expand All @@ -18,6 +19,16 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
$(this.termContainer).on('click', '[data-index] > input', this.onTermClick, this);
}

if (this.ordered) {
$(this.termContainer).on('end', this.onDrop, this);

Sortable.create(this.termContainer, {
scroll: true,
direction: 'vertical',
handle: '[data-drag-initiator]'
});
}

// 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 Down Expand Up @@ -131,6 +142,41 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
return quoted.join(this.separator).trim();
}

addRenderedTerm(label) {
if (! this.ordered) {
return super.addRenderedTerm(label);
}

const listItem = document.createElement('li');
listItem.appendChild(label);
listItem.appendChild($.render('<i data-drag-initiator class="icon fa-bars fa"></i>'));
this.termContainer.appendChild(listItem);
}

insertRenderedTerm(label) {
if (! this.ordered) {
return super.insertRenderedTerm(label);
}

const termIndex = Number(label.dataset.index);
const nextListItemLabel = this.termContainer.querySelector(`[data-index="${ termIndex + 1 }"]`);
const nextListItem = nextListItemLabel?.parentNode || null;
const listItem = document.createElement('li');
listItem.appendChild(label);
listItem.appendChild($.render('<i data-drag-initiator class="icon fa-bars fa"></i>'));
this.termContainer.insertBefore(listItem, nextListItem);

return label;
}

removeRenderedTerm(label) {
if (! this.ordered) {
return super.removeRenderedTerm(label);
}

label.parentNode.remove();
}

complete(input, data) {
data.exclude = this.usedTerms.map(termData => termData.search);

Expand Down Expand Up @@ -159,6 +205,30 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
this.moveFocusForward(termIndex - 1);
}

onDrop(event) {
if (event.to === event.from && event.newIndex === event.oldIndex) {
// The user dropped the term at its previous position
return;
}

// The item is the list item, not the term's label
const label = event.item.firstChild;

// Remove the term from the internal map, but not the DOM, as it's been moved already
const termData = this.removeTerm(label, false);
delete label.dataset.index; // Which is why we have to take it out of the equation for now

let newIndex = 0; // event.newIndex is intentionally not used, as we have our own indexing
if (event.item.previousSibling) {
newIndex = Number(event.item.previousSibling.firstChild.dataset.index) + 1;
}

// This is essentially insertTerm() with custom DOM manipulation
this.reIndexTerms(newIndex, 1, true); // Free up the new index
this.registerTerm(termData, newIndex); // Re-register the term with the new index
label.dataset.index = `${ newIndex }`; // Update the DOM, we didn't do that during removal
}

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

Expand Down
30 changes: 29 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 term order is significant */
protected $ordered = false;

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

Expand Down Expand Up @@ -103,7 +106,31 @@ public function setVerticalTermDirection(bool $state = true): self
*/
public function getTermDirection(): ?string
{
return $this->verticalTermDirection ? 'vertical' : null;
return $this->verticalTermDirection || $this->ordered ? 'vertical' : null;
}

/**
* Set whether term order is significant
*
* @param bool $state
*
* @return $this
*/
public function setOrdered(bool $state = true): self
{
$this->ordered = $state;

return $this;
}

/**
* Get whether term order is significant
*
* @return bool
*/
public function getOrdered(): bool
{
return $this->ordered;
}

/**
Expand Down Expand Up @@ -442,6 +469,7 @@ public function getValueAttribute()
'data-with-multi-completion' => true,
'data-no-auto-submit-on-remove' => true,
'data-term-direction' => $this->getTermDirection(),
'data-maintain-term-order' => $this->getOrdered() && ! $this->getAttribute('disabled')->getValue(),
'data-read-only-terms' => $this->getReadOnly(),
'data-data-input' => '#' . $dataInputId,
'data-term-input' => '#' . $termInputId,
Expand Down
15 changes: 14 additions & 1 deletion src/FormElement/TermInput/TermContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class TermContainer extends BaseHtmlElement
public function __construct(TermInput $input)
{
$this->input = $input;

if ($input->getOrdered()) {
$this->tag = 'ol';
}
}

protected function assemble()
Expand Down Expand Up @@ -58,7 +62,16 @@ protected function assemble()
);
}

$this->addHtml($label);
if ($this->tag === 'ol') {
$this->addHtml(new HtmlElement(
'li',
null,
$label,
new Icon('bars', ['data-drag-initiator' => true])
));
} else {
$this->addHtml($label);
}
}
}
}
Loading