Commit fb74d662 authored by Oliver Bartsch's avatar Oliver Bartsch Committed by Christian Kuhn
Browse files

[FEATURE] Add filter to columns selector in recordlist

To ease the use of the new columns selector,
especially when dealing with tables, having a
large number of columns (e.g. pages or tt_content),
a filter is added to the columns selector action bar.

The remaining actions, such as "check all" are
then bound to the current filter result. This allows
to quickly select a specific group of columns.

Resolves: #94680
Releases: master
Change-Id: I348fcb9b043a2dd5697248be7aebd322c25cdf3f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70169

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: default avatarGuido Schmechel <guido.schmechel@brandung.de>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 3c78adc6
......@@ -567,8 +567,10 @@ $form-toggle-checked-bg-image: url("data:image/svg+xml, <svg xmlns='http://www.w
background: $white;
a,
button {
margin: 0.25rem 0.25rem 0.25rem 0;
button,
input,
.input-group-addon {
margin: 0.25rem 0;
}
}
......
......@@ -22,8 +22,10 @@ import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import Notification = require('TYPO3/CMS/Backend/Notification');
enum Selectors {
columnSelectors = '.t3js-record-column-selector',
columnSelectorActionsSelector = '.t3js-record-column-selector-actions'
columnsSelector = '.t3js-record-column-selector',
columnsContainerSelector = '.t3js-column-selector-container',
columnsFilterSelector = 'input[name="columns-filter"]',
columnsSelectorActionsSelector = '.t3js-record-column-selector-actions'
}
enum SelectorActions {
......@@ -56,27 +58,80 @@ class ColumnSelectorButton extends LitElement {
@property({type: String}) close: string = lll('button.close') || 'Close';
@property({type: String}) error: string = 'Could not update columns';
private static toggleSelectors(
columnSelectors: NodeListOf<HTMLInputElement>,
/**
* Toggle selector actions state (enabled or disabled) depending
* on the columns state (checked, unchecked, displayed or hidden)
*
* @param columns The columns
* @param selectAll The "select all" action button
* @param selectNone The "select none" action button
* @param initialize Whether this is the initialize call - don't check hidden
* state as all columns are displayed on initialization
* @private
*/
private static toggleSelectorActions(
columns: NodeListOf<HTMLInputElement>,
selectAll: HTMLButtonElement,
selectNone: HTMLButtonElement
selectNone: HTMLButtonElement,
initialize: boolean = false
) {
selectAll.classList.add('disabled')
for (let i=0; i < columnSelectors.length; i++) {
if (!columnSelectors[i].disabled && !columnSelectors[i].checked) {
for (let i=0; i < columns.length; i++) {
if (!columns[i].disabled
&& !columns[i].checked
&& (initialize || !ColumnSelectorButton.isColumnHidden(columns[i]))
) {
selectAll.classList.remove('disabled')
break;
}
}
selectNone.classList.add('disabled')
for (let i=0; i < columnSelectors.length; i++) {
if (!columnSelectors[i].disabled && columnSelectors[i].checked) {
for (let i=0; i < columns.length; i++) {
if (!columns[i].disabled
&& columns[i].checked
&& (initialize || !ColumnSelectorButton.isColumnHidden(columns[i]))
) {
selectNone.classList.remove('disabled')
break;
}
}
}
/**
* Check if the given column is hidden by looking at it's container element
*
* @param column The column to check for
* @private
*/
private static isColumnHidden(column: HTMLInputElement): boolean {
return column.closest(Selectors.columnsContainerSelector)?.classList.contains('hidden');
}
/**
* Check each column if it matches the current search term.
* If not, hide its outer container to not break the grid.
*
* @param columnsFilter The columns filter
* @param columns The columns to check
* @private
*/
private static filterColumns(columnsFilter: HTMLInputElement, columns: NodeListOf<HTMLInputElement>): void {
columns.forEach((column: HTMLInputElement) => {
const columnContainer: HTMLDivElement = column.closest(Selectors.columnsContainerSelector);
if (!column.disabled && columnContainer !== null) {
const filterValue: string = columnContainer.querySelector('.form-check-label-text')?.textContent;
if (filterValue && filterValue.length) {
columnContainer.classList.toggle(
'hidden',
columnsFilter.value !== '' && !RegExp(columnsFilter.value, 'i').test(
filterValue.trim().replace(/\[\]/g, '').replace(/\s+/g, ' ')
)
);
}
}
});
}
public constructor() {
super();
this.addEventListener('click', (e: Event): void => {
......@@ -151,30 +206,53 @@ class ColumnSelectorButton extends LitElement {
return;
}
// Prevent the form from being submitted as the form data will be send via an ajax request
form.addEventListener('submit', (e: Event): void => {e.preventDefault()});
form.addEventListener('submit', (e: Event): void => { e.preventDefault() });
const columnSelectors: NodeListOf<HTMLInputElement> = currentModal.querySelectorAll(Selectors.columnSelectors);
const columnSelectorActions: HTMLDivElement = currentModal.querySelector(Selectors.columnSelectorActionsSelector);
const selectAll: HTMLButtonElement = columnSelectorActions.querySelector('button[data-action="' + SelectorActions.all + '"]');
const selectNone: HTMLButtonElement = columnSelectorActions.querySelector('button[data-action="' + SelectorActions.none + '"]');
const columns: NodeListOf<HTMLInputElement> = currentModal.querySelectorAll(Selectors.columnsSelector);
const columnsFilter: HTMLInputElement = currentModal.querySelector(Selectors.columnsFilterSelector);
const columnsSelectorActions: HTMLDivElement = currentModal.querySelector(Selectors.columnsSelectorActionsSelector);
const selectAll: HTMLButtonElement = columnsSelectorActions.querySelector('button[data-action="' + SelectorActions.all + '"]');
const selectNone: HTMLButtonElement = columnsSelectorActions.querySelector('button[data-action="' + SelectorActions.none + '"]');
if (selectAll === null || selectNone === null || !columnSelectors.length) {
if (!columns.length || columnsFilter === null || selectAll === null || selectNone === null) {
// Return in case required elements do not exist in the modal content
return;
}
// Initialize select-all / select-none buttons
ColumnSelectorButton.toggleSelectors(columnSelectors, selectAll, selectNone);
// First initialize select-all / select-none buttons
ColumnSelectorButton.toggleSelectorActions(columns, selectAll, selectNone, true);
// Add event listener for each column selector to toggle the selector actions after change
columnSelectors.forEach((column: HTMLInputElement) => {
// Add event listener for each column to toggle the selector actions after change
columns.forEach((column: HTMLInputElement) => {
column.addEventListener('change', (): void => {
ColumnSelectorButton.toggleSelectors(columnSelectors, selectAll, selectNone);
ColumnSelectorButton.toggleSelectorActions(columns, selectAll, selectNone);
});
});
// Add event listener for keydown event for the columns filter, so we
// can catch the "Escape" key, which would otherwise close the modal.
columnsFilter.addEventListener('keydown', (e: KeyboardEvent): void => {
const target = e.target as HTMLInputElement;
if (e.code === 'Escape') {
e.stopImmediatePropagation();
target.value = '';
}
});
// Add event listener for keydown event for the columns filter, allowing the "live filtering"
columnsFilter.addEventListener('keyup', (e: KeyboardEvent): void => {
ColumnSelectorButton.filterColumns(e.target as HTMLInputElement, columns);
ColumnSelectorButton.toggleSelectorActions(columns, selectAll, selectNone);
});
// Catch browser specific "search" event, triggered on clicking the "clear" button
columnsFilter.addEventListener('search', (e: Event): void => {
ColumnSelectorButton.filterColumns(e.target as HTMLInputElement, columns);
ColumnSelectorButton.toggleSelectorActions(columns, selectAll, selectNone);
});
// Add event listener for selector actions
columnSelectorActions.addEventListener('click', (e: Event): void => {
columnsSelectorActions.addEventListener('click', (e: Event): void => {
e.preventDefault();
const target: HTMLElement = e.target as HTMLElement;
......@@ -186,22 +264,22 @@ class ColumnSelectorButton extends LitElement {
// Perform requested action
switch (target.dataset.action) {
case SelectorActions.toggle:
columnSelectors.forEach((column: HTMLInputElement) => {
if (!column.disabled) {
columns.forEach((column: HTMLInputElement) => {
if (!column.disabled && !ColumnSelectorButton.isColumnHidden(column)) {
column.checked = !column.checked;
}
});
break;
case SelectorActions.all:
columnSelectors.forEach((column: HTMLInputElement) => {
if (!column.disabled) {
columns.forEach((column: HTMLInputElement) => {
if (!column.disabled && !ColumnSelectorButton.isColumnHidden(column)) {
column.checked = true;
}
});
break;
case SelectorActions.none:
columnSelectors.forEach((column: HTMLInputElement) => {
if (!column.disabled) {
columns.forEach((column: HTMLInputElement) => {
if (!column.disabled && !ColumnSelectorButton.isColumnHidden(column)) {
column.checked = false;
}
});
......@@ -211,8 +289,8 @@ class ColumnSelectorButton extends LitElement {
Notification.warning('Unknown selector action');
}
// After performing the action always toggle selectors
ColumnSelectorButton.toggleSelectors(columnSelectors, selectAll, selectNone);
// After performing the action always toggle selector actions
ColumnSelectorButton.toggleSelectorActions(columns, selectAll, selectNone);
});
}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
.. include:: ../../Includes.txt
==============================================
Feature: #94680 - Show columns selector filter
==============================================
See :issue:`94680`
Description
===========
In :issue:`94474`, the column selector in the record list, formerly known
as "field selector", got improved by adding a couple of actions, such
as "check all" and by moving the selection into a modal instead of a dropdown.
However, since there are tables, e.g. `pages` or `tt_content`, which
contain a lot of columns, it could sometimes still be unnecessarily hard
to find a specific column in such a list.
Therefore, the columns selectors' action bar has been extended for
a new filter, which can be used to quickly find the desired column
in such large lists.
When the filter is active - at least one character was entered - all other
actions are bound to the current filter result. This means, when using the
"check all" action, while the list is filtered, the action is only applied
to the currently visible items. This comes in handy in case a group of
columns, sharing the same name (e.g. "backend layouts" in `pages`) should
be selected.
Impact
======
It's now possible to filter the list of columns in the "Show column selector"
of the recordlist module.
.. index:: Backend, ext:recordlist
......@@ -18,6 +18,9 @@
<trans-unit id="updateColumnView.error" resname="updateColumnView.error">
<source>Could not update columns</source>
</trans-unit>
<trans-unit id="columnsFilter" resname="columnsFilter">
<source>Filter by:</source>
</trans-unit>
<trans-unit id="error.linkHandlerTitleMissing" resname="error.linkHandlerTitleMissing">
<source>[title missing]</source>
</trans-unit>
......
<form id="columnSelectorForm">
<div class="sticky-form-actions border-bottom t3js-record-column-selector-actions">
<button type="button" class="btn btn-default disabled" data-action="select-all">
<core:icon identifier="actions-check-square"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll" />
</button>
<button type="button" class="btn btn-default disabled" data-action="select-none">
<core:icon identifier="actions-square"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll" />
</button>
<button type="button" class="btn btn-default" data-action="select-toggle">
<core:icon identifier="actions-document-select"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection" />
</button>
<div class="sticky-form-actions border-bottom">
<div class="row">
<div class="col-5">
<div class="input-group">
<span class="input-group-addon" id="columns-filter-label"><f:translate key="LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:columnsFilter" /></span>
<input type="search" name="columns-filter" class="form-control" value="" aria-labelledby="columns-filter-label" placeholder="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enterSearchString')}">
</div>
</div>
<div class="col-7">
<div class="row row-cols-auto justify-content-end g-2 t3js-record-column-selector-actions">
<div class="col">
<button type="button" class="btn btn-default disabled" data-action="select-all">
<core:icon identifier="actions-check-square"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll" />
</button>
</div>
<div class="col">
<button type="button" class="btn btn-default disabled" data-action="select-none">
<core:icon identifier="actions-square"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll" />
</button>
</div>
<div class="col">
<button type="button" class="btn btn-default" data-action="select-toggle">
<core:icon identifier="actions-document-select"/> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row pt-5">
<f:for each="{columns}" as="column">
<div class="col-6">
<div class="col-6 t3js-column-selector-container">
<div class="mt-2 form-check form-check-type-icon-toggle {f:if(condition: column.disabled, then: 'disabled')}">
<input
type="checkbox"
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=this&&this.__decorate||function(e,t,o,r){var l,c=arguments.length,s=c<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,r);else for(var n=e.length-1;n>=0;n--)(l=e[n])&&(s=(c<3?l(s):c>3?l(t,o,s):l(t,o))||s);return c>3&&s&&Object.defineProperty(t,o,s),s},__importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","lit","lit/decorators","TYPO3/CMS/Backend/Enum/Severity","TYPO3/CMS/Backend/Severity","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Core/lit-helper","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Backend/Notification"],(function(e,t,o,r,l,c,s,n,a,i){"use strict";var d,u,p;Object.defineProperty(t,"__esModule",{value:!0}),a=__importDefault(a),function(e){e.columnSelectors=".t3js-record-column-selector",e.columnSelectorActionsSelector=".t3js-record-column-selector-actions"}(u||(u={})),function(e){e.toggle="select-toggle",e.all="select-all",e.none="select-none"}(p||(p={}));let h=d=class extends o.LitElement{constructor(){super(),this.title="Show columns",this.ok=n.lll("button.ok")||"Update",this.close=n.lll("button.close")||"Close",this.error="Could not update columns",this.addEventListener("click",e=>{e.preventDefault(),this.showColumnSelectorModal()})}static toggleSelectors(e,t,o){t.classList.add("disabled");for(let o=0;o<e.length;o++)if(!e[o].disabled&&!e[o].checked){t.classList.remove("disabled");break}o.classList.add("disabled");for(let t=0;t<e.length;t++)if(!e[t].disabled&&e[t].checked){o.classList.remove("disabled");break}}render(){return o.html`<slot></slot>`}showColumnSelectorModal(){this.url&&this.target&&s.advanced({content:this.url,title:this.title,severity:l.SeverityEnum.notice,size:s.sizes.medium,type:s.types.ajax,buttons:[{text:this.close,active:!0,btnClass:"btn-default",name:"cancel",trigger:()=>s.dismiss()},{text:this.ok,btnClass:"btn-"+c.getCssClass(l.SeverityEnum.info),name:"update",trigger:()=>this.proccessSelection(s.currentModal[0])}],ajaxCallback:()=>this.handleModalContentLoaded(s.currentModal[0])})}proccessSelection(e){const t=e.querySelector("form");null!==t?new a.default(TYPO3.settings.ajaxUrls.record_show_columns).post("",{body:new FormData(t)}).then(async e=>{const t=await e.resolve();!0===t.success?(this.ownerDocument.location.href=this.target,this.ownerDocument.location.reload(!0)):i.error(t.message||"No update was performed"),s.dismiss()}).catch(()=>{this.abortSelection()}):this.abortSelection()}handleModalContentLoaded(e){const t=e.querySelector("form");if(null===t)return;t.addEventListener("submit",e=>{e.preventDefault()});const o=e.querySelectorAll(u.columnSelectors),r=e.querySelector(u.columnSelectorActionsSelector),l=r.querySelector('button[data-action="'+p.all+'"]'),c=r.querySelector('button[data-action="'+p.none+'"]');null!==l&&null!==c&&o.length&&(d.toggleSelectors(o,l,c),o.forEach(e=>{e.addEventListener("change",()=>{d.toggleSelectors(o,l,c)})}),r.addEventListener("click",e=>{e.preventDefault();const t=e.target;if("BUTTON"===t.nodeName&&t.dataset.action){switch(t.dataset.action){case p.toggle:o.forEach(e=>{e.disabled||(e.checked=!e.checked)});break;case p.all:o.forEach(e=>{e.disabled||(e.checked=!0)});break;case p.none:o.forEach(e=>{e.disabled||(e.checked=!1)});break;default:i.warning("Unknown selector action")}d.toggleSelectors(o,l,c)}}))}abortSelection(){i.error(this.error),s.dismiss()}};__decorate([r.property({type:String})],h.prototype,"url",void 0),__decorate([r.property({type:String})],h.prototype,"target",void 0),__decorate([r.property({type:String})],h.prototype,"title",void 0),__decorate([r.property({type:String})],h.prototype,"ok",void 0),__decorate([r.property({type:String})],h.prototype,"close",void 0),__decorate([r.property({type:String})],h.prototype,"error",void 0),h=d=__decorate([r.customElement("typo3-recordlist-column-selector-button")],h)}));
\ No newline at end of file
var __decorate=this&&this.__decorate||function(e,t,o,r){var l,n=arguments.length,s=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,r);else for(var c=e.length-1;c>=0;c--)(l=e[c])&&(s=(n<3?l(s):n>3?l(t,o,s):l(t,o))||s);return n>3&&s&&Object.defineProperty(t,o,s),s},__importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","lit","lit/decorators","TYPO3/CMS/Backend/Enum/Severity","TYPO3/CMS/Backend/Severity","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Core/lit-helper","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Backend/Notification"],(function(e,t,o,r,l,n,s,c,i,a){"use strict";var d,u,p;Object.defineProperty(t,"__esModule",{value:!0}),i=__importDefault(i),function(e){e.columnsSelector=".t3js-record-column-selector",e.columnsContainerSelector=".t3js-column-selector-container",e.columnsFilterSelector='input[name="columns-filter"]',e.columnsSelectorActionsSelector=".t3js-record-column-selector-actions"}(u||(u={})),function(e){e.toggle="select-toggle",e.all="select-all",e.none="select-none"}(p||(p={}));let m=d=class extends o.LitElement{constructor(){super(),this.title="Show columns",this.ok=c.lll("button.ok")||"Update",this.close=c.lll("button.close")||"Close",this.error="Could not update columns",this.addEventListener("click",e=>{e.preventDefault(),this.showColumnSelectorModal()})}static toggleSelectorActions(e,t,o,r=!1){t.classList.add("disabled");for(let o=0;o<e.length;o++)if(!e[o].disabled&&!e[o].checked&&(r||!d.isColumnHidden(e[o]))){t.classList.remove("disabled");break}o.classList.add("disabled");for(let t=0;t<e.length;t++)if(!e[t].disabled&&e[t].checked&&(r||!d.isColumnHidden(e[t]))){o.classList.remove("disabled");break}}static isColumnHidden(e){var t;return null===(t=e.closest(u.columnsContainerSelector))||void 0===t?void 0:t.classList.contains("hidden")}static filterColumns(e,t){t.forEach(t=>{var o;const r=t.closest(u.columnsContainerSelector);if(!t.disabled&&null!==r){const t=null===(o=r.querySelector(".form-check-label-text"))||void 0===o?void 0:o.textContent;t&&t.length&&r.classList.toggle("hidden",""!==e.value&&!RegExp(e.value,"i").test(t.trim().replace(/\[\]/g,"").replace(/\s+/g," ")))}})}render(){return o.html`<slot></slot>`}showColumnSelectorModal(){this.url&&this.target&&s.advanced({content:this.url,title:this.title,severity:l.SeverityEnum.notice,size:s.sizes.medium,type:s.types.ajax,buttons:[{text:this.close,active:!0,btnClass:"btn-default",name:"cancel",trigger:()=>s.dismiss()},{text:this.ok,btnClass:"btn-"+n.getCssClass(l.SeverityEnum.info),name:"update",trigger:()=>this.proccessSelection(s.currentModal[0])}],ajaxCallback:()=>this.handleModalContentLoaded(s.currentModal[0])})}proccessSelection(e){const t=e.querySelector("form");null!==t?new i.default(TYPO3.settings.ajaxUrls.record_show_columns).post("",{body:new FormData(t)}).then(async e=>{const t=await e.resolve();!0===t.success?(this.ownerDocument.location.href=this.target,this.ownerDocument.location.reload(!0)):a.error(t.message||"No update was performed"),s.dismiss()}).catch(()=>{this.abortSelection()}):this.abortSelection()}handleModalContentLoaded(e){const t=e.querySelector("form");if(null===t)return;t.addEventListener("submit",e=>{e.preventDefault()});const o=e.querySelectorAll(u.columnsSelector),r=e.querySelector(u.columnsFilterSelector),l=e.querySelector(u.columnsSelectorActionsSelector),n=l.querySelector('button[data-action="'+p.all+'"]'),s=l.querySelector('button[data-action="'+p.none+'"]');o.length&&null!==r&&null!==n&&null!==s&&(d.toggleSelectorActions(o,n,s,!0),o.forEach(e=>{e.addEventListener("change",()=>{d.toggleSelectorActions(o,n,s)})}),r.addEventListener("keydown",e=>{const t=e.target;"Escape"===e.code&&(e.stopImmediatePropagation(),t.value="")}),r.addEventListener("keyup",e=>{d.filterColumns(e.target,o),d.toggleSelectorActions(o,n,s)}),r.addEventListener("search",e=>{d.filterColumns(e.target,o),d.toggleSelectorActions(o,n,s)}),l.addEventListener("click",e=>{e.preventDefault();const t=e.target;if("BUTTON"===t.nodeName&&t.dataset.action){switch(t.dataset.action){case p.toggle:o.forEach(e=>{e.disabled||d.isColumnHidden(e)||(e.checked=!e.checked)});break;case p.all:o.forEach(e=>{e.disabled||d.isColumnHidden(e)||(e.checked=!0)});break;case p.none:o.forEach(e=>{e.disabled||d.isColumnHidden(e)||(e.checked=!1)});break;default:a.warning("Unknown selector action")}d.toggleSelectorActions(o,n,s)}}))}abortSelection(){a.error(this.error),s.dismiss()}};__decorate([r.property({type:String})],m.prototype,"url",void 0),__decorate([r.property({type:String})],m.prototype,"target",void 0),__decorate([r.property({type:String})],m.prototype,"title",void 0),__decorate([r.property({type:String})],m.prototype,"ok",void 0),__decorate([r.property({type:String})],m.prototype,"close",void 0),__decorate([r.property({type:String})],m.prototype,"error",void 0),m=d=__decorate([r.customElement("typo3-recordlist-column-selector-button")],m)}));
\ No newline at end of file
Markdown is supported
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