Commit 6de8eea9 authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

[BUGFIX] Use optgroup in SelectMultipleSideBySideElement

Instead of using multiple "fake" <option> elements with
the same value `--div--`, the SelectMultipleSideBySideElement
does now use proper HTML <optgroup> elements for grouping
of the available options. This is in line with other
select elements, e.g. the SelectSingleElement.

Besides the improved HTML markup, this also improves
the UI, since the <optgroup> element is non-selectable.
It is also no longer considered by the filter, which
previously led to confusion, especially when filtering
and having the "dividers" as only options left.

Resolves: #95137
Releases: main, 11.5
Change-Id: Ia51e2623217eb0c7abd6c0cd2e9c4a742686641d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72992


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
parent 8c5aefaa
......@@ -149,7 +149,7 @@ export = (function() {
$fieldEl = FormEngine.getFieldElement(fieldName);
originalFieldEl = $fieldEl.get(0);
if (originalFieldEl === null || value === '--div--') {
if (originalFieldEl === null || value === '--div--' || originalFieldEl instanceof HTMLOptGroupElement) {
return;
}
......@@ -168,10 +168,11 @@ export = (function() {
// If multiple values are not allowed, clear anything that is in the control already
if (!isMultiple) {
$fieldEl.find('option').each((index: number, el: HTMLElement) => {
$availableFieldEl
.find('option[value="' + $.escapeSelector($(el).attr('value')) + '"]')
.removeClass('hidden')
.prop('disabled', false);
const $option = $availableFieldEl.find('option[value="' + $.escapeSelector($(el).attr('value')) + '"]');
if ($option) {
$option.removeClass('hidden').prop('disabled', false);
FormEngine.enableOptGroup($option.get(0));
}
});
$fieldEl.empty();
}
......@@ -198,6 +199,7 @@ export = (function() {
optionEl.closest('select').querySelectorAll('[disabled]').forEach(function (disabledOption: HTMLOptionElement) {
disabledOption.classList.remove('hidden');
disabledOption.disabled = false;
FormEngine.enableOptGroup(disabledOption);
});
}
}
......@@ -218,6 +220,14 @@ export = (function() {
if (addNewValue && typeof optionEl !== 'undefined') {
optionEl.classList.add('hidden');
optionEl.disabled = true;
// In case the disabled option was the last active option and is in an optGroup, also disable the optGroup
const optGroup = <HTMLOptGroupElement>optionEl.parentElement;
if (optGroup instanceof HTMLOptGroupElement
&& optGroup.querySelectorAll('option:not([disabled]):not([hidden]):not(.hidden)').length === 0
) {
optGroup.disabled = true;
optGroup.classList.add('hidden');
}
}
}
......@@ -1156,6 +1166,18 @@ export = (function() {
});
};
/**
* In case the given option is a child of a disabled optGroup, enable the optGroup
*/
FormEngine.enableOptGroup = function (option: HTMLOptionElement): void {
const optGroup = <HTMLOptGroupElement>option.parentElement;
if (optGroup instanceof HTMLOptGroupElement && optGroup.querySelectorAll('option:not([hidden]):not([disabled]):not(.hidden)').length) {
optGroup.hidden = false;
optGroup.disabled = false;
optGroup.classList.remove('hidden');
}
}
/**
* Close current open document
*/
......
......@@ -87,6 +87,7 @@ export abstract class AbstractSortableSelectItems {
if (originalOption !== null) {
originalOption.classList.remove('hidden');
originalOption.disabled = false;
FormEngine.enableOptGroup(originalOption);
}
fieldElement.removeChild(option);
......
......@@ -27,6 +27,20 @@ class SelectBoxFilter {
private filterText: string = '';
private availableOptions: NodeListOf<HTMLOptionElement> = null;
private static toggleOptGroup(option: HTMLOptionElement): void {
const optGroup = <HTMLOptGroupElement>option.parentElement;
if (!(optGroup instanceof HTMLOptGroupElement)) {
return;
}
if (optGroup.querySelectorAll('option:not([hidden]):not([disabled]):not(.hidden)').length === 0) {
optGroup.hidden = true;
} else {
optGroup.hidden = false;
optGroup.disabled = false;
optGroup.classList.remove('hidden');
}
}
constructor(selectElement: HTMLSelectElement) {
this.selectElement = selectElement;
......@@ -62,6 +76,7 @@ class SelectBoxFilter {
const matchFilter = new RegExp(filterText, 'i');
this.availableOptions.forEach((option: HTMLOptionElement): void => {
option.hidden = filterText.length > 0 && option.textContent.match(matchFilter) === null;
SelectBoxFilter.toggleOptGroup(option);
});
}
}
......
......@@ -87,8 +87,9 @@ declare namespace TYPO3 {
public openPopupWindow(mode: string, params: string): JQuery;
public initializeNullNoPlaceholderCheckboxes(): void;
public initializeNullWithPlaceholderCheckboxes(): void;
public requestFormEngineUpdate(askForUpdate: boolean): void
public processOnFieldChange(items: OnFieldChangeItem[]): void
public requestFormEngineUpdate(askForUpdate: boolean): void;
public processOnFieldChange(items: OnFieldChangeItem[]): void;
public enableOptGroup(option: HTMLOptionElement): void;
}
export class MultiStepWizard {
......
......@@ -168,22 +168,59 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
}
}
$selectableItemCounter = 0;
$selectableItemGroupCounter = 0;
$selectableItemGroups = [];
$selectableItemsHtml = [];
// Initialize groups
foreach ($possibleItems as $possibleItem) {
$disabledAttr = '';
$classAttr = '';
$disableAttributes = [];
if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem[1], $selectedItems, true)) {
$disabledAttr = ' disabled="disabled"';
$classAttr = ' class="hidden"';
}
$selectableItemsHtml[] =
'<option value="'
. htmlspecialchars($possibleItem[1])
. '" title="' . htmlspecialchars($possibleItem[0]) . '"'
. $classAttr . $disabledAttr
. '>'
. htmlspecialchars($this->appendValueToLabelInDebugMode($possibleItem[0], $possibleItem[1])) .
'</option>';
$disableAttributes = [
'disabled' => 'disabled',
'class' => 'hidden',
];
}
if ($possibleItem[1] === '--div--') {
if ($selectableItemCounter !== 0) {
$selectableItemGroupCounter++;
}
$selectableItemGroups[$selectableItemGroupCounter]['header']['title'] = $possibleItem[0];
} else {
$selectableItemGroups[$selectableItemGroupCounter]['items'][] = [
'label' => $this->appendValueToLabelInDebugMode($possibleItem[0], $possibleItem[1]),
'attributes' => array_merge(['title' => $possibleItem[0], 'value' => $possibleItem[1]], $disableAttributes),
];
// In case the item is not disabled, enable the group (if any)
if ($disableAttributes === [] && isset($selectableItemGroups[$selectableItemGroupCounter]['header'])) {
$selectableItemGroups[$selectableItemGroupCounter]['header']['disabled'] = false;
}
$selectableItemCounter++;
}
}
// Process groups
foreach ($selectableItemGroups as $selectableItemGroup) {
if (!is_array($selectableItemGroup['items'] ?? false) || $selectableItemGroup['items'] === []) {
continue;
}
$optionGroup = isset($selectableItemGroup['header']);
if ($optionGroup) {
$selectableItemsHtml[] = '<optgroup label="' . htmlspecialchars($selectableItemGroup['header']['title']) . '"' . (($selectableItemGroup['header']['disabled'] ?? true) ? 'class="hidden" disabled="disabled"' : '') . '>';
}
foreach ($selectableItemGroup['items'] as $item) {
$selectableItemsHtml[] = '
<option ' . GeneralUtility::implodeAttributes($item['attributes'], true) . '>
' . htmlspecialchars($item['label']) . '
</option>';
}
if ($optionGroup) {
$selectableItemsHtml[] = '</optgroup>';
}
}
// Html stuff for filter and select filter on top of right side of multi select boxes
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Backend/FormEngine","TYPO3/CMS/Backend/FormEngineValidation"],(function(e,t,o,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AbstractSortableSelectItems=void 0;class r{constructor(){this.registerSortableEventHandler=e=>{const t=e.closest(".form-wizards-wrap").querySelector(".form-wizards-items-aside");null!==t&&t.addEventListener("click",t=>{let i;if(null===(i=t.target.closest(".t3js-btn-option")))return void(t.target.matches(".t3js-btn-option")&&(i=t.target));t.preventDefault();const l=i.dataset.fieldname,s=o.getFieldElement(l).get(0),a=o.getFieldElement(l,"_avail").get(0);i.classList.contains("t3js-btn-moveoption-top")?r.moveOptionToTop(e):i.classList.contains("t3js-btn-moveoption-up")?r.moveOptionUp(e):i.classList.contains("t3js-btn-moveoption-down")?r.moveOptionDown(e):i.classList.contains("t3js-btn-moveoption-bottom")?r.moveOptionToBottom(e):i.classList.contains("t3js-btn-removeoption")&&r.removeOption(e,a),o.updateHiddenFieldValueFromSelect(e,s),o.legacyFieldChangedCb(),n.markFieldAsChanged(a),n.validateField(a)})}}static moveOptionToTop(e){Array.from(e.querySelectorAll(":checked")).reverse().forEach(t=>{e.insertBefore(t,e.firstElementChild)})}static moveOptionToBottom(e){e.querySelectorAll(":checked").forEach(t=>{e.insertBefore(t,null)})}static moveOptionUp(e){const t=Array.from(e.children),o=Array.from(e.querySelectorAll(":checked"));for(let n of o){if(0===t.indexOf(n)&&null===n.previousElementSibling)break;e.insertBefore(n,n.previousElementSibling)}}static moveOptionDown(e){const t=Array.from(e.children).reverse(),o=Array.from(e.querySelectorAll(":checked")).reverse();for(let n of o){if(0===t.indexOf(n)&&null===n.nextElementSibling)break;e.insertBefore(n,n.nextElementSibling.nextElementSibling)}}static removeOption(e,t){e.querySelectorAll(":checked").forEach(o=>{const n=t.querySelector('option[value="'+o.value+'"]');null!==n&&(n.classList.remove("hidden"),n.disabled=!1),e.removeChild(o)})}}t.AbstractSortableSelectItems=r}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Backend/FormEngine","TYPO3/CMS/Backend/FormEngineValidation"],(function(e,t,o,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AbstractSortableSelectItems=void 0;class r{constructor(){this.registerSortableEventHandler=e=>{const t=e.closest(".form-wizards-wrap").querySelector(".form-wizards-items-aside");null!==t&&t.addEventListener("click",t=>{let i;if(null===(i=t.target.closest(".t3js-btn-option")))return void(t.target.matches(".t3js-btn-option")&&(i=t.target));t.preventDefault();const l=i.dataset.fieldname,s=o.getFieldElement(l).get(0),a=o.getFieldElement(l,"_avail").get(0);i.classList.contains("t3js-btn-moveoption-top")?r.moveOptionToTop(e):i.classList.contains("t3js-btn-moveoption-up")?r.moveOptionUp(e):i.classList.contains("t3js-btn-moveoption-down")?r.moveOptionDown(e):i.classList.contains("t3js-btn-moveoption-bottom")?r.moveOptionToBottom(e):i.classList.contains("t3js-btn-removeoption")&&r.removeOption(e,a),o.updateHiddenFieldValueFromSelect(e,s),o.legacyFieldChangedCb(),n.markFieldAsChanged(a),n.validateField(a)})}}static moveOptionToTop(e){Array.from(e.querySelectorAll(":checked")).reverse().forEach(t=>{e.insertBefore(t,e.firstElementChild)})}static moveOptionToBottom(e){e.querySelectorAll(":checked").forEach(t=>{e.insertBefore(t,null)})}static moveOptionUp(e){const t=Array.from(e.children),o=Array.from(e.querySelectorAll(":checked"));for(let n of o){if(0===t.indexOf(n)&&null===n.previousElementSibling)break;e.insertBefore(n,n.previousElementSibling)}}static moveOptionDown(e){const t=Array.from(e.children).reverse(),o=Array.from(e.querySelectorAll(":checked")).reverse();for(let n of o){if(0===t.indexOf(n)&&null===n.nextElementSibling)break;e.insertBefore(n,n.nextElementSibling.nextElementSibling)}}static removeOption(e,t){e.querySelectorAll(":checked").forEach(n=>{const r=t.querySelector('option[value="'+n.value+'"]');null!==r&&(r.classList.remove("hidden"),r.disabled=!1,o.enableOptGroup(r)),e.removeChild(n)})}}t.AbstractSortableSelectItems=r}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,l){"use strict";var i;!function(e){e.fieldContainerSelector=".t3js-formengine-field-group",e.filterTextFieldSelector=".t3js-formengine-multiselect-filter-textfield",e.filterSelectFieldSelector=".t3js-formengine-multiselect-filter-dropdown"}(i||(i={}));return class{constructor(e){this.selectElement=null,this.filterText="",this.availableOptions=null,this.selectElement=e,this.initializeEvents()}initializeEvents(){const e=this.selectElement.closest(".form-wizards-element");null!==e&&(new l("input",e=>{this.filter(e.target.value)}).delegateTo(e,i.filterTextFieldSelector),new l("change",e=>{this.filter(e.target.value)}).delegateTo(e,i.filterSelectFieldSelector))}filter(e){this.filterText=e,null===this.availableOptions&&(this.availableOptions=this.selectElement.querySelectorAll("option"));const t=new RegExp(e,"i");this.availableOptions.forEach(l=>{l.hidden=e.length>0&&null===l.textContent.match(t)})}}}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,l){"use strict";var i;!function(e){e.fieldContainerSelector=".t3js-formengine-field-group",e.filterTextFieldSelector=".t3js-formengine-multiselect-filter-textfield",e.filterSelectFieldSelector=".t3js-formengine-multiselect-filter-dropdown"}(i||(i={}));class n{constructor(e){this.selectElement=null,this.filterText="",this.availableOptions=null,this.selectElement=e,this.initializeEvents()}static toggleOptGroup(e){const t=e.parentElement;t instanceof HTMLOptGroupElement&&(0===t.querySelectorAll("option:not([hidden]):not([disabled]):not(.hidden)").length?t.hidden=!0:(t.hidden=!1,t.disabled=!1,t.classList.remove("hidden")))}initializeEvents(){const e=this.selectElement.closest(".form-wizards-element");null!==e&&(new l("input",e=>{this.filter(e.target.value)}).delegateTo(e,i.filterTextFieldSelector),new l("change",e=>{this.filter(e.target.value)}).delegateTo(e,i.filterSelectFieldSelector))}filter(e){this.filterText=e,null===this.availableOptions&&(this.availableOptions=this.selectElement.querySelectorAll("option"));const t=new RegExp(e,"i");this.availableOptions.forEach(l=>{l.hidden=e.length>0&&null===l.textContent.match(t),n.toggleOptGroup(l)})}}return n}));
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment