Commit de89f5b5 authored by Oliver Bartsch's avatar Oliver Bartsch Committed by Andreas Fernandez
Browse files

[BUGFIX] Replace all-embracing form in recordlist

All tables were previously wrapped by one form in
the recordlist. Since #94218 this leads to problems
because all tables now define another form for the
"Show columns" selector. Nested forms are invalid
markup. Therefore, effectively only the first table
was wrapped by the general form. The remaining
tables were not longer wrapped into a form, leading
to problems when using the clipboard actions.

This is now fixed by replacing the all-embracing
form with dedicated forms for each displayed table.

Besides fixing the mentioned problems, this change
results in further improvements:

* The forms are now dedicated to their table
* The "Show columns" selector does not longer
need to define a form itself
* When updating the "Show columns" selection
or when executing a clipboard action, the recordlist
jumps to the correct table after reload, using an
anchor tag
* The clipboard related JavaScript can be simplified
in an follow-up patch

Resolves: #94302
Related: #94218
Releases: master
Change-Id: Ib57abfe7bc9a2c9b6b3919a9ec78c25a87b8f08a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69453

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent 4610cc85
......@@ -14,7 +14,7 @@
class ClipboardComponent {
private static setCheckboxValue(checkboxName: string, check: boolean): void {
const fullName = 'CBC[' + checkboxName + ']';
const checkboxElement: HTMLInputElement = document.querySelector('form[name="dblistForm"] [name="' + fullName + '"]');
const checkboxElement: HTMLInputElement = document.querySelector('input[name="' + fullName + '"]');
if (checkboxElement !== null) {
checkboxElement.checked = check;
}
......
......@@ -228,7 +228,7 @@ class Recordlist {
private getCheckboxState(CBname: string): boolean {
const fullName = 'CBC[' + CBname + ']';
const checkbox: HTMLInputElement = document.querySelector('form[name="dblistForm"] [name="' + fullName + '"]');
const checkbox: HTMLInputElement = document.querySelector('input[name="' + fullName + '"]');
return checkbox !== null ? checkbox.checked : false;
}
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],(function(e,t){"use strict";class c{static setCheckboxValue(e,t){const c="CBC["+e+"]",s=document.querySelector('form[name="dblistForm"] [name="'+c+'"]');null!==s&&(s.checked=t)}constructor(){this.registerCheckboxTogglers()}registerCheckboxTogglers(){const e=".t3js-toggle-all-checkboxes";document.addEventListener("click",t=>{let s,o=t.target;if(!o.matches(e)){let t=o.closest(e);if(null===t)return;o=t}t.preventDefault(),"checked"in o.dataset&&"none"!==o.dataset.checked?(o.dataset.checked="none",s=!1):(o.dataset.checked="all",s=!0);const n=o.dataset.checkboxesNames.split(",");for(let e of n)c.setCheckboxValue(e,s)})}}return new c}));
\ No newline at end of file
define(["require","exports"],(function(e,t){"use strict";class c{static setCheckboxValue(e,t){const c="CBC["+e+"]",s=document.querySelector('input[name="'+c+'"]');null!==s&&(s.checked=t)}constructor(){this.registerCheckboxTogglers()}registerCheckboxTogglers(){const e=".t3js-toggle-all-checkboxes";document.addEventListener("click",t=>{let s,n=t.target;if(!n.matches(e)){let t=n.closest(e);if(null===t)return;n=t}t.preventDefault(),"checked"in n.dataset&&"none"!==n.dataset.checked?(n.dataset.checked="none",s=!1):(n.dataset.checked="all",s=!0);const o=n.dataset.checkboxesNames.split(",");for(let e of o)c.setCheckboxValue(e,s)})}}return new c}));
\ No newline at end of file
......@@ -85,7 +85,12 @@ class SearchCest
$I->click('.t3js-live-search-show-all', self::$topBarModuleSelector);
$I->switchToContentFrame();
$I->waitForElementVisible('form[name="dblistForm"]');
$I->canSee('fileadmin');
// Search word is transferred to the recordlist search form
$I->seeInField('#search_field', 'fileadmin');
// Correct table and element is displayed
$I->waitForElementVisible('form[name="list-table-form-sys_file_storage"]');
$I->canSee('fileadmin', 'form[name="list-table-form-sys_file_storage"] a');
}
}
......@@ -330,9 +330,7 @@ class RecordListController
if ($beforeOutput) {
$body .= '<div class="row">' . $beforeOutput . '</div>';
}
$body .= '<form action="' . htmlspecialchars($dblist->listURL()) . '" method="post" name="dblistForm">';
$body .= $output;
$body .= '<input type="hidden" name="cmd_table" /><input type="hidden" name="cmd" /></form>';
// If a listing was produced, create the page footer with search form etc:
if ($tableOutput) {
// Adding checkbox options for extended listing and clipboard display:
......
......@@ -752,16 +752,20 @@ class DatabaseRecordList
$dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded';
return '
<div class="panel panel-space panel-default recordlist" id="t3-table-' . htmlspecialchars($tableIdentifier) . '">
<div class="panel-heading">
' . $tableHeader . '
</div>
<div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($tableIdentifier) . '">
<div class="table-fit">
<table data-table="' . htmlspecialchars($tableIdentifier) . '" class="table table-striped table-hover">
' . $columnsOutput . $rowOutput . '
</table>
<form action="' . htmlspecialchars($this->listURL()) . '#t3-table-' . htmlspecialchars($tableIdentifier) . '" method="post" name="list-table-form-' . htmlspecialchars($tableIdentifier) . '">
<input type="hidden" name="cmd_table" />
<input type="hidden" name="cmd" />
<div class="panel-heading">
' . $tableHeader . '
</div>
</div>
<div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($tableIdentifier) . '">
<div class="table-fit">
<table data-table="' . htmlspecialchars($tableIdentifier) . '" class="table table-striped table-hover">
' . $columnsOutput . $rowOutput . '
</table>
</div>
</div>
</form>
</div>
';
}
......@@ -1990,7 +1994,6 @@ class DatabaseRecordList
return $this->getFluidTemplateObject('ColumnSelector.html')
->assignMultiple([
'formUrl' => $this->listURL(),
'table' => $table,
'tableIdentifier' => $this->showOnlyTranslatedRecords ? ($table . '_translated') : $table,
'allChecked' => $checkAllChecked,
......@@ -2018,10 +2021,13 @@ class DatabaseRecordList
*/
public function linkClipboardHeaderIcon($icon, $table, $cmd, $warning = '', $title = '')
{
$jsCode = 'document.dblistForm.cmd.value=' . GeneralUtility::quoteJSvalue($cmd)
. ';document.dblistForm.cmd_table.value='
. GeneralUtility::quoteJSvalue($table)
. ';document.dblistForm.submit();';
$tableIdentifier = $this->showOnlyTranslatedRecords ? ($table . '_translated') : $table;
$formName = 'list-table-form-' . htmlspecialchars($tableIdentifier);
// @todo Replace this inline JavaScript by a custom elements or some kind of trigger class
$jsCode = 'document.querySelector(\'form[name="' . $formName . '"]\').cmd.value=' . GeneralUtility::quoteJSvalue($cmd)
. ';document.querySelector(\'form[name="' . $formName . '"]\').cmd_table.value=' . GeneralUtility::quoteJSvalue($table)
. ';document.querySelector(\'form[name="' . $formName . '"]\').submit();';
$attributes = [];
if ($title !== '') {
......@@ -2041,11 +2047,7 @@ class DatabaseRecordList
$attributes['onclick'] = $jsCode . 'return false;';
}
$attributesString = '';
foreach ($attributes as $key => $value) {
$attributesString .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
return $tag[0] . $attributesString . '>' . $icon . $tag[1];
return $tag[0] . GeneralUtility::implodeAttributes($attributes, true) . '>' . $icon . $tag[1];
}
/**
......
......@@ -4,50 +4,44 @@
<f:translate key="LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:showColumns" />
</button>
<div class="dropdown-menu dropdown-menu-end pt-0 field-selector-dropdown" aria-labelledby="selectcolumns-{tableIdentifier}">
<f:comment>
The below form and the anchor do only have effect outside of Recordlist (e.g. DatabaseBrowser) since
Recordlist already defines an outer form and nested forms are not supported.
</f:comment>
<form action="{formUrl}#t3-table-{tableIdentifier}" method="post" name="field-selector-form-{tableIdentifier}">
<div class="pt-2 pb-2 mb-2 row align-items-center border-bottom field-selector-header">
<div class="col">
<div class="form-check">
<input type="checkbox" class="form-check-input recordlist-select-allcolumns" id="select-allcolumns-{tableIdentifier}" {f:if(condition: allChecked, then: ' checked')} />
<label class="form-check-label" for="select-allcolumns-{tableIdentifier}">
<f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall" />
</label>
</div>
</div>
<div class="col text-end">
<button type="submit" class="btn btn-primary">
<f:translate key="LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:updateColumnView" />
</button>
<div class="pt-2 pb-2 mb-2 row align-items-center border-bottom field-selector-header">
<div class="col">
<div class="form-check">
<input type="checkbox" class="form-check-input recordlist-select-allcolumns" id="select-allcolumns-{tableIdentifier}" {f:if(condition: allChecked, then: ' checked')} />
<label class="form-check-label" for="select-allcolumns-{tableIdentifier}">
<f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall" />
</label>
</div>
</div>
<f:for each="{checkboxes}" as="checkbox">
<div class="mb-2">
<div class="form-check">
<input
type="checkbox"
id="select-column-{tableIdentifier}-{checkbox.fieldName}"
name="displayFields[{table}][]"
class="form-check-input recordlist-select-column"
value="{checkbox.fieldName}"
{f:if(condition:'{checkbox.checked}', then:' checked')}
{f:if(condition:'{checkbox.disabled}', then: 'disabled')}
/>
<f:if condition="{checkbox.disabled}">
<input type="hidden" name="displayFields[{table}][]" value="{checkbox.fieldName}" />
<div class="col text-end">
<button type="submit" class="btn btn-primary">
<f:translate key="LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:updateColumnView" />
</button>
</div>
</div>
<f:for each="{checkboxes}" as="checkbox">
<div class="mb-2">
<div class="form-check">
<input
type="checkbox"
id="select-column-{tableIdentifier}-{checkbox.fieldName}"
name="displayFields[{table}][]"
class="form-check-input recordlist-select-column"
value="{checkbox.fieldName}"
{f:if(condition:'{checkbox.checked}', then:' checked')}
{f:if(condition:'{checkbox.disabled}', then: 'disabled')}
/>
<f:if condition="{checkbox.disabled}">
<input type="hidden" name="displayFields[{table}][]" value="{checkbox.fieldName}" />
</f:if>
<label class="form-check-label" for="select-column-{tableIdentifier}-{checkbox.fieldName}">
<f:if condition="{checkbox.fieldLabel}">
<span class="me-2"><f:translate key="{checkbox.fieldLabel}">{checkbox.fieldLabel}</f:translate></span>
</f:if>
<label class="form-check-label" for="select-column-{tableIdentifier}-{checkbox.fieldName}">
<f:if condition="{checkbox.fieldLabel}">
<span class="me-2"><f:translate key="{checkbox.fieldLabel}">{checkbox.fieldLabel}</f:translate></span>
</f:if>
<span class="text-muted text-monospace">[{checkbox.fieldName}]</span>
</label>
</div>
<span class="text-muted text-monospace">[{checkbox.fieldName}]</span>
</label>
</div>
</f:for>
</form>
</div>
</f:for>
</div>
</div>
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Backend/Icons","TYPO3/CMS/Backend/Storage/Persistent","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Backend/Tooltip","TYPO3/CMS/Core/DocumentService"],(function(t,e,a,i,l,n,s,d){"use strict";a=__importDefault(a);return new class{constructor(){this.identifier={entity:".t3js-entity",toggle:".t3js-toggle-recordlist",localize:".t3js-action-localize",icons:{collapse:"actions-view-list-collapse",expand:"actions-view-list-expand",editMultiple:".t3js-record-edit-multiple"}},this.toggleClick=t=>{t.preventDefault();const e=a.default(t.currentTarget),n=e.data("table"),s=a.default(e.data("bs-target")),d="expanded"===s.data("state"),r=e.find(".collapseIcon"),o=d?this.identifier.icons.expand:this.identifier.icons.collapse;i.getIcon(o,i.sizes.small).done(t=>{r.html(t)});let c={};l.isset("moduleData.list")&&(c=l.get("moduleData.list"));const u={};u[n]=d?1:0,a.default.extend(c,u),l.set("moduleData.list",c).done(()=>{s.data("state",d?"collapsed":"expanded")})},this.onEditMultiple=t=>{let e,i,l,n,s;t.preventDefault(),e=a.default(t.currentTarget).closest("[data-table]"),0!==e.length&&(n=a.default(t.currentTarget).data("uri"),i=e.data("table"),l=e.find(this.identifier.entity+'[data-uid][data-table="'+i+'"]').map((t,e)=>a.default(e).data("uid")).toArray().join(","),s=n.match(/{[^}]+}/g),a.default.each(s,(t,e)=>{const s=e.substr(1,e.length-2).split(":");let d;switch(s.shift()){case"entityIdentifiers":d=l;break;case"T3_THIS_LOCATION":d=T3_THIS_LOCATION;break;default:return}a.default.each(s,(t,e)=>{"editList"===e&&(d=this.editList(i,d))}),n=n.replace(e,d)}),window.location.href=n)},this.disableButton=t=>{a.default(t.currentTarget).prop("disable",!0).addClass("disabled")},this.deleteRow=t=>{const e=a.default(`table[data-table="${t.table}"]`),i=e.find(`tr[data-uid="${t.uid}"]`),l=e.closest(".panel"),n=l.find(".panel-heading"),s=e.find(`[data-l10nparent="${t.uid}"]`),d=a.default().add(i).add(s);if(d.fadeTo("slow",.4,()=>{d.slideUp("slow",()=>{d.remove(),0===e.find("tbody tr").length&&l.slideUp("slow")})}),"0"===i.data("l10nparent")||""===i.data("l10nparent")){const t=Number(n.find(".t3js-table-total-items").html());n.find(".t3js-table-total-items").text(t-1)}"pages"===t.table&&top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))},this.registerPaginationEvents=()=>{document.querySelectorAll(".t3js-recordlist-paging").forEach(t=>{t.addEventListener("keyup",e=>{e.preventDefault();let a=parseInt(t.value,10);a<parseInt(t.min,10)&&(a=parseInt(t.min,10)),a>parseInt(t.max,10)&&(a=parseInt(t.max,10)),"Enter"===e.key&&a!==parseInt(t.dataset.currentpage,10)&&(window.location.href=t.dataset.currenturl+a.toString())})})},this.registerColumnSelectorEvents=()=>{document.querySelectorAll(".recordlist-select-allcolumns").forEach(t=>{t.addEventListener("change",e=>{t.closest("form").querySelectorAll(".recordlist-select-column").forEach(t=>{t.disabled||(t.checked=!t.checked)})})})},a.default(document).on("click",this.identifier.toggle,this.toggleClick),a.default(document).on("click",this.identifier.icons.editMultiple,this.onEditMultiple),a.default(document).on("click",this.identifier.localize,this.disableButton),d.ready().then(()=>{s.initialize(".table-fit a[title]"),this.registerPaginationEvents()}),d.ready().then(()=>{this.registerColumnSelectorEvents()}),new n("typo3:datahandler:process",this.handleDataHandlerResult.bind(this)).bindTo(document)}editList(t,e){const a=[];let i=0,l=e.indexOf(",");for(;-1!==l;)this.getCheckboxState(t+"|"+e.substr(i,l-i))&&a.push(e.substr(i,l-i)),i=l+1,l=e.indexOf(",",i);return this.getCheckboxState(t+"|"+e.substr(i))&&a.push(e.substr(i)),a.length>0?a.join(","):e}handleDataHandlerResult(t){const e=t.detail.payload;e.hasErrors||"datahandler"!==e.component&&"delete"===e.action&&this.deleteRow(e)}getCheckboxState(t){const e="CBC["+t+"]",a=document.querySelector('form[name="dblistForm"] [name="'+e+'"]');return null!==a&&a.checked}}}));
\ No newline at end of file
var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Backend/Icons","TYPO3/CMS/Backend/Storage/Persistent","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Backend/Tooltip","TYPO3/CMS/Core/DocumentService"],(function(t,e,a,i,l,n,s,d){"use strict";a=__importDefault(a);return new class{constructor(){this.identifier={entity:".t3js-entity",toggle:".t3js-toggle-recordlist",localize:".t3js-action-localize",icons:{collapse:"actions-view-list-collapse",expand:"actions-view-list-expand",editMultiple:".t3js-record-edit-multiple"}},this.toggleClick=t=>{t.preventDefault();const e=a.default(t.currentTarget),n=e.data("table"),s=a.default(e.data("bs-target")),d="expanded"===s.data("state"),r=e.find(".collapseIcon"),o=d?this.identifier.icons.expand:this.identifier.icons.collapse;i.getIcon(o,i.sizes.small).done(t=>{r.html(t)});let c={};l.isset("moduleData.list")&&(c=l.get("moduleData.list"));const u={};u[n]=d?1:0,a.default.extend(c,u),l.set("moduleData.list",c).done(()=>{s.data("state",d?"collapsed":"expanded")})},this.onEditMultiple=t=>{let e,i,l,n,s;t.preventDefault(),e=a.default(t.currentTarget).closest("[data-table]"),0!==e.length&&(n=a.default(t.currentTarget).data("uri"),i=e.data("table"),l=e.find(this.identifier.entity+'[data-uid][data-table="'+i+'"]').map((t,e)=>a.default(e).data("uid")).toArray().join(","),s=n.match(/{[^}]+}/g),a.default.each(s,(t,e)=>{const s=e.substr(1,e.length-2).split(":");let d;switch(s.shift()){case"entityIdentifiers":d=l;break;case"T3_THIS_LOCATION":d=T3_THIS_LOCATION;break;default:return}a.default.each(s,(t,e)=>{"editList"===e&&(d=this.editList(i,d))}),n=n.replace(e,d)}),window.location.href=n)},this.disableButton=t=>{a.default(t.currentTarget).prop("disable",!0).addClass("disabled")},this.deleteRow=t=>{const e=a.default(`table[data-table="${t.table}"]`),i=e.find(`tr[data-uid="${t.uid}"]`),l=e.closest(".panel"),n=l.find(".panel-heading"),s=e.find(`[data-l10nparent="${t.uid}"]`),d=a.default().add(i).add(s);if(d.fadeTo("slow",.4,()=>{d.slideUp("slow",()=>{d.remove(),0===e.find("tbody tr").length&&l.slideUp("slow")})}),"0"===i.data("l10nparent")||""===i.data("l10nparent")){const t=Number(n.find(".t3js-table-total-items").html());n.find(".t3js-table-total-items").text(t-1)}"pages"===t.table&&top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))},this.registerPaginationEvents=()=>{document.querySelectorAll(".t3js-recordlist-paging").forEach(t=>{t.addEventListener("keyup",e=>{e.preventDefault();let a=parseInt(t.value,10);a<parseInt(t.min,10)&&(a=parseInt(t.min,10)),a>parseInt(t.max,10)&&(a=parseInt(t.max,10)),"Enter"===e.key&&a!==parseInt(t.dataset.currentpage,10)&&(window.location.href=t.dataset.currenturl+a.toString())})})},this.registerColumnSelectorEvents=()=>{document.querySelectorAll(".recordlist-select-allcolumns").forEach(t=>{t.addEventListener("change",e=>{t.closest("form").querySelectorAll(".recordlist-select-column").forEach(t=>{t.disabled||(t.checked=!t.checked)})})})},a.default(document).on("click",this.identifier.toggle,this.toggleClick),a.default(document).on("click",this.identifier.icons.editMultiple,this.onEditMultiple),a.default(document).on("click",this.identifier.localize,this.disableButton),d.ready().then(()=>{s.initialize(".table-fit a[title]"),this.registerPaginationEvents()}),d.ready().then(()=>{this.registerColumnSelectorEvents()}),new n("typo3:datahandler:process",this.handleDataHandlerResult.bind(this)).bindTo(document)}editList(t,e){const a=[];let i=0,l=e.indexOf(",");for(;-1!==l;)this.getCheckboxState(t+"|"+e.substr(i,l-i))&&a.push(e.substr(i,l-i)),i=l+1,l=e.indexOf(",",i);return this.getCheckboxState(t+"|"+e.substr(i))&&a.push(e.substr(i)),a.length>0?a.join(","):e}handleDataHandlerResult(t){const e=t.detail.payload;e.hasErrors||"datahandler"!==e.component&&"delete"===e.action&&this.deleteRow(e)}getCheckboxState(t){const e="CBC["+t+"]",a=document.querySelector('input[name="'+e+'"]');return null!==a&&a.checked}}}));
\ 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