-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
imp: Improve the selector of collections
- Loading branch information
Showing
19 changed files
with
722 additions
and
250 deletions.
There are no files selected for viewing
Binary file not shown.
Large diffs are not rendered by default.
Oops, something went wrong.
237 changes: 184 additions & 53 deletions
237
src/assets/javascripts/controllers/collections_selector_controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,238 @@ | ||
import { Controller } from '@hotwired/stimulus'; | ||
|
||
import _ from 'js/l10n.js'; | ||
import icon from 'js/icon.js'; | ||
|
||
export default class extends Controller { | ||
static get targets () { | ||
return ['data', 'list', 'select']; | ||
return [ | ||
'dataCollections', | ||
'dataNewCollections', | ||
'selectGroup', | ||
'inputGroup', | ||
'collectionCards', | ||
'select', | ||
'input', | ||
'collectionTemplate', | ||
]; | ||
} | ||
|
||
connect () { | ||
this.inputTarget.addEventListener('keydown', this.trapEscape.bind(this)); | ||
this.inputTarget.addEventListener('keydown', this.trapEnter.bind(this)); | ||
|
||
this.refreshList(); | ||
this.refreshSelect(); | ||
} | ||
|
||
refreshList () { | ||
let html = ''; | ||
for (const option of this.dataTarget.selectedOptions) { | ||
html += this._item(option); | ||
this.collectionCardsTarget.innerHTML = ''; | ||
for (const option of this.dataCollectionsTarget.selectedOptions) { | ||
this.collectionCardsTarget.appendChild( | ||
this.collectionNode(option.value, { | ||
name: option.text, | ||
imageFilename: option.dataset.illustration, | ||
isPublic: 'public' in option.dataset, | ||
by: option.dataset.by, | ||
}, false) | ||
); | ||
} | ||
|
||
for (const input of this.dataNewCollectionsTarget.children) { | ||
this.collectionCardsTarget.appendChild( | ||
this.collectionNode(input.value, { | ||
name: input.value, | ||
imageFilename: '', | ||
isPublic: false, | ||
by: null, | ||
}, true) | ||
); | ||
} | ||
this.listTarget.innerHTML = html; | ||
} | ||
|
||
refreshSelect () { | ||
// remove all the options except the first one ('Attach a collection') | ||
while (this.selectTarget.options.length > 1) { | ||
this.selectTarget.remove(1); | ||
} | ||
// Reset the options and optgroups of the select | ||
this.selectTarget.innerHTML = ''; | ||
|
||
const newOption = document.createElement('option'); | ||
newOption.text = _('Open the list'); | ||
newOption.disabled = true; | ||
newOption.selected = true; | ||
this.selectTarget.add(newOption); | ||
|
||
// readd options that have not been selected yet | ||
for (const option of this.dataTarget.options) { | ||
// read options that have not been selected yet | ||
const optionsNoGroup = this.dataCollectionsTarget.querySelectorAll('select > option'); | ||
for (const option of optionsNoGroup) { | ||
if (!option.selected) { | ||
const newOption = new Option(option.text, option.value); | ||
const newOption = document.createElement('option'); | ||
newOption.value = option.value; | ||
newOption.text = option.text; | ||
if ('public' in option.dataset) { | ||
newOption.text += _(' (public)'); | ||
} | ||
this.selectTarget.add(newOption); | ||
} | ||
} | ||
|
||
// force the selection of the first option | ||
this.selectTarget.options[0].selected = true; | ||
// same with the options in optgroups | ||
const groups = this.dataCollectionsTarget.querySelectorAll('select > optgroup'); | ||
for (const group of groups) { | ||
const newOptGroup = document.createElement('optgroup'); | ||
newOptGroup.label = group.label; | ||
|
||
let groupIsEmpty = true; | ||
const groupOptions = group.querySelectorAll('optgroup > option'); | ||
for (const option of groupOptions) { | ||
if (!option.selected) { | ||
const newOption = document.createElement('option'); | ||
newOption.value = option.value; | ||
newOption.text = option.text; | ||
if ('by' in option.dataset) { | ||
newOption.text += ` (${option.dataset.by})`; | ||
} | ||
if ('public' in option.dataset) { | ||
newOption.text += _(' (public)'); | ||
} | ||
newOptGroup.append(newOption); | ||
groupIsEmpty = false; | ||
} | ||
} | ||
|
||
if (!groupIsEmpty) { | ||
this.selectTarget.add(newOptGroup); | ||
} | ||
} | ||
|
||
// hide the select input if all collections have been selected | ||
if (this.selectTarget.options.length === 1) { | ||
this.selectTarget.style.display = 'none'; | ||
this.selectTarget.disabled = true; | ||
} else { | ||
this.selectTarget.style.display = 'block'; | ||
this.selectTarget.disabled = false; | ||
} | ||
|
||
// make the select required if no options have been selected and data | ||
// target have been marked as required. | ||
if (this.dataTarget.selectedOptions.length === 0) { | ||
this.selectTarget.required = this.dataTarget.required; | ||
if (this.dataCollectionsTarget.selectedOptions.length === 0) { | ||
this.selectTarget.required = this.dataCollectionsTarget.required; | ||
} else { | ||
this.selectTarget.required = false; | ||
} | ||
} | ||
|
||
showInput () { | ||
this.inputGroupTarget.hidden = false; | ||
this.selectGroupTarget.hidden = true; | ||
this.inputTarget.focus(); | ||
} | ||
|
||
showSelect () { | ||
this.selectGroupTarget.hidden = false; | ||
this.inputGroupTarget.hidden = true; | ||
this.selectTarget.focus(); | ||
} | ||
|
||
setFocus () { | ||
if (this.selectGroupTarget.hidden) { | ||
this.inputTarget.focus(); | ||
} else { | ||
this.selectTarget.focus(); | ||
} | ||
} | ||
|
||
attach (event) { | ||
event.preventDefault(); | ||
|
||
const value = event.target.value; | ||
for (const option of this.dataTarget.options) { | ||
if (option.value === value) { | ||
option.selected = true; | ||
this.refreshList(); | ||
this.refreshSelect(); | ||
this.selectTarget.focus(); | ||
break; | ||
if (this.selectGroupTarget.hidden) { | ||
const value = this.inputTarget.value; | ||
if (value !== '') { | ||
const input = document.createElement('input'); | ||
input.type = 'hidden'; | ||
input.name = 'new_collection_names[]'; | ||
input.value = value; | ||
this.dataNewCollectionsTarget.append(input); | ||
} | ||
this.inputTarget.value = ''; | ||
} else { | ||
const value = event.target.value; | ||
for (const option of this.dataCollectionsTarget.options) { | ||
if (option.value === value) { | ||
option.selected = true; | ||
this.refreshSelect(); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
this.refreshList(); | ||
this.setFocus(); | ||
} | ||
|
||
detach (event) { | ||
event.preventDefault(); | ||
|
||
const value = event.currentTarget.getAttribute('data-value'); | ||
for (const option of this.dataTarget.selectedOptions) { | ||
if (option.value === value) { | ||
option.selected = false; | ||
this.refreshList(); | ||
this.refreshSelect(); | ||
this.selectTarget.focus(); | ||
break; | ||
const isNew = event.currentTarget.getAttribute('data-is-new'); | ||
|
||
if (isNew === 'true') { | ||
for (const input of this.dataNewCollectionsTarget.children) { | ||
if (input.value === value) { | ||
input.remove(); | ||
break; | ||
} | ||
} | ||
} else { | ||
for (const option of this.dataCollectionsTarget.selectedOptions) { | ||
if (option.value === value) { | ||
option.selected = false; | ||
this.refreshSelect(); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
this.refreshList(); | ||
this.setFocus(); | ||
} | ||
|
||
trapEscape (event) { | ||
if (event.key === 'Escape') { | ||
event.stopPropagation(); // avoid to close the modal | ||
event.preventDefault(); | ||
this.showSelect(); | ||
} | ||
} | ||
|
||
trapEnter (event) { | ||
if (event.key === 'Enter') { | ||
event.preventDefault(); | ||
this.attach(event); | ||
} | ||
} | ||
|
||
_item (option) { | ||
return ` | ||
<li class="collections-selector__item"> | ||
<span class="collections-selector__item-label"> | ||
${option.text} | ||
</span> | ||
<button | ||
class="collections-selector__unselect button--smaller button--ghost" | ||
type="button" | ||
data-action="collections-selector#detach" | ||
data-value="${option.value}" | ||
title="${_('Unselect this collection')}" | ||
aria-label="${_('Unselect')}" | ||
> | ||
${icon('times')} | ||
</button> | ||
</li> | ||
`; | ||
collectionNode (value, collection, isNew) { | ||
const item = this.collectionTemplateTarget.content.firstElementChild.cloneNode(true); | ||
|
||
item.querySelector('[data-target="name"]').textContent = collection.name; | ||
|
||
if (collection.imageFilename) { | ||
item.style.backgroundImage = `url('${collection.imageFilename}')`; | ||
} | ||
|
||
if (collection.by) { | ||
item.querySelector('[data-target="by"]').textContent = collection.by; | ||
} | ||
|
||
if (!collection.isPublic) { | ||
item.querySelector('[data-target="isPublic"]').remove(); | ||
} | ||
|
||
if (!isNew) { | ||
item.querySelector('[data-target="isNew"]').remove(); | ||
} | ||
|
||
const unselectButton = item.querySelector('[data-target="unselect"]'); | ||
unselectButton.setAttribute('data-value', value); | ||
unselectButton.setAttribute('data-is-new', isNew); | ||
|
||
return item; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.