Skip to content

Commit

Permalink
imp: Improve the selector of collections
Browse files Browse the repository at this point in the history
  • Loading branch information
marienfressinaud committed Oct 28, 2022
2 parents 16a41d5 + 368a375 commit 02b12c7
Show file tree
Hide file tree
Showing 19 changed files with 722 additions and 250 deletions.
Binary file modified locales/fr_FR/LC_MESSAGES/main.mo
Binary file not shown.
214 changes: 119 additions & 95 deletions locales/fr_FR/LC_MESSAGES/main.po

Large diffs are not rendered by default.

237 changes: 184 additions & 53 deletions src/assets/javascripts/controllers/collections_selector_controller.js
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;
}
};
37 changes: 26 additions & 11 deletions src/assets/stylesheets/components/cards.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,12 @@
padding: var(--space-small);

flex-grow: 1;

background-color: var(--color-white);
}

.card--illustrated .card__body {
padding-top: calc(70px + var(--space-small));
padding-top: calc(25px + var(--space-small));

color: var(--color-white);
text-decoration: none;
Expand All @@ -181,20 +183,29 @@
background-image:
linear-gradient(
to bottom,
transparent 30px,
rgba(0, 0, 0, 0.4) 70px,
rgba(0, 0, 0, 0.6) 90px,
transparent 0,
rgba(0, 0, 0, 0.4) 25px,
rgba(0, 0, 0, 0.6) 45px,
rgba(0, 0, 0, 0.8)
);

transition: background-color 0.3s ease-in-out;
}

.card--illustrated-alt .card__body {
background-image: none;
.card--illustrated .card__body--large {
padding-top: calc(70px + var(--space-small));

background-image:
linear-gradient(
to bottom,
transparent 30px,
rgba(0, 0, 0, 0.4) 70px,
rgba(0, 0, 0, 0.6) 90px,
rgba(0, 0, 0, 0.8)
);
}

.card--illustrated .card__body--large {
.card--illustrated .card__body--larger {
padding-top: calc(156px + var(--space-small));

background-image:
Expand All @@ -207,15 +218,19 @@
);
}

.card--illustrated .card__body:hover,
.card--illustrated .card__body:focus {
.card--illustrated-alt .card__body {
background-image: none;
}

.card--illustrated a.card__body:hover,
.card--illustrated a.card__body:focus {
color: var(--color-white);

background-color: transparent;
}

.card--illustrated-alt .card__body:hover,
.card--illustrated-alt .card__body:focus {
.card--illustrated-alt a.card__body:hover,
.card--illustrated-alt a.card__body:focus {
background-color: rgba(0, 0, 0, 0.3);
}

Expand Down
13 changes: 10 additions & 3 deletions src/assets/stylesheets/components/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,23 @@ select:not([multiple]) {
appearance: none;
}

select:hover,
select:focus {
select:not([disabled]):hover,
select:not([disabled]):focus {
background-color: var(--color-turquoise-1);
}

select:focus {
select:not([disabled]):focus {
border-color: var(--color-border-active);
}

select[disabled] {
color: var(--color-text-secondary);

border-color: var(--color-border-disabled);
}

fieldset {
min-width: auto;
margin-top: var(--space-medium);
margin-bottom: var(--space-medium);
padding: var(--space-small);
Expand Down

0 comments on commit 02b12c7

Please sign in to comment.