Skip to content

Commit

Permalink
[TASK] Make ajax-data-handler visibility toggle explicit
Browse files Browse the repository at this point in the history
Instead of parsing parameters and string replacing
parts of it we are now passing all nessesary parts
as data attributes.

Resolves: #101855
Releases: main, 12.4
Change-Id: I779f9d10f0dd7df6d89fc7b19aa9a36db1bbe3f2
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80993
Tested-by: Benjamin Kott <benjamin.kott@outlook.com>
Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com>
Tested-by: Benjamin Franzke <ben@bnf.dev>
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: core-ci <typo3@b13.com>
  • Loading branch information
benjaminkott authored and bnf committed Sep 12, 2023
1 parent bbf5ae1 commit c4e4a32
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 83 deletions.
135 changes: 73 additions & 62 deletions Build/Sources/TypeScript/backend/ajax-data-handler.ts
Expand Up @@ -24,7 +24,7 @@ import Notification from './notification';
import RegularEvent from '@typo3/core/event/regular-event';

enum Identifiers {
hide = '.t3js-record-hide',
hide = 'button[data-datahandler-action="visibility"]',
delete = '.t3js-record-delete',
icon = '.t3js-icon',
}
Expand Down Expand Up @@ -106,23 +106,9 @@ class AjaxDataHandler {
// @todo: Many extensions rely on this behavior but it's misplaced in AjaxDataHandler. Move into recordlist.ts and deprecate in v11.
private initialize(): void {
// HIDE/UNHIDE: click events for all action icons to hide/unhide
new RegularEvent('click', (e: Event, anchorElement: HTMLElement): void => {
new RegularEvent('click', (e: Event, element: HTMLButtonElement): void => {
e.preventDefault();

const iconElement = anchorElement.querySelector(Identifiers.icon);
const rowElement = anchorElement.closest('tr[data-uid]');
const params = anchorElement.dataset.params;

// add a spinner
this._showSpinnerIcon(iconElement);

// make the AJAX call to toggle the visibility
this.process(params).then((result: ResponseInterface): void => {
if (!result.hasErrors) {
// adjust overlay icon
this.toggleRow(rowElement);
}
});
this.handleVisibilityToggle(element);
}).delegateTo(document, Identifiers.hide);

// DELETE: click events for all action icons to delete
Expand Down Expand Up @@ -153,55 +139,80 @@ class AjaxDataHandler {
}).delegateTo(document, Identifiers.delete);
}

/**
* Toggle row visibility after record has been changed
*/
private toggleRow(rowElement: Element): void {
const anchorElement = rowElement.querySelector(Identifiers.hide) as HTMLElement;
const table = (anchorElement.closest('table[data-table]') as HTMLTableElement).dataset.table;
const params = anchorElement.dataset.params;
let nextParams;
let nextState;
let iconName;

if (anchorElement.dataset.state === 'hidden') {
nextState = 'visible';
nextParams = params.replace('=0', '=1');
iconName = 'actions-edit-hide';
} else {
nextState = 'hidden';
nextParams = params.replace('=1', '=0');
iconName = 'actions-edit-unhide';
}
anchorElement.dataset.state = nextState;
anchorElement.dataset.params = nextParams;
private handleVisibilityToggle(element: HTMLButtonElement): void
{
const rowElement = element.closest('tr[data-uid]');

const iconElement = anchorElement.querySelector(Identifiers.icon);
Icons.getIcon(iconName, Icons.sizes.small).then((icon: string): void => {
iconElement.replaceWith(document.createRange().createContextualFragment(icon));
});
// Show spinner
const iconElement = element.querySelector(Identifiers.icon);
this._showSpinnerIcon(iconElement);

// Set overlay for the record icon
const recordIcon = rowElement.querySelector('.col-icon ' + Identifiers.icon);
if (nextState === 'hidden') {
Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').then((icon: string): void => {
const iconFragment = document.createRange().createContextualFragment(icon);
recordIcon.append(iconFragment.querySelector('.icon-overlay'));
});
} else {
recordIcon.querySelector('.icon-overlay').remove();
}
// Get Settings from element
const settings = {
table: element.dataset.datahandlerTable,
uid: element.dataset.datahandlerUid,
field: element.dataset.datahandlerField,
visible: (element.dataset.datahandlerStatus === 'visible')
};

const params = {
data: {
[settings.table]: {
[settings.uid]: {
[settings.field]: settings.visible
? element.dataset.datahandlerHiddenValue
: element.dataset.datahandlerVisibleValue
}
}
}
};

const animationEvent = new RegularEvent('animationend', (): void => {
rowElement.classList.remove('record-pulse');
animationEvent.release();
});
animationEvent.bindTo(rowElement);
rowElement.classList.add('record-pulse');
// Submit Data
this.process(params).then((result: ResponseInterface): void => {
if (!result.hasErrors) {
// Inverse current state
settings.visible = !(settings.visible);
element.setAttribute('data-datahandler-status', settings.visible ? 'visible' : 'hidden');

const elementLabel = settings.visible
? element.dataset.datahandlerVisibleLabel
: element.dataset.datahandlerHiddenLabel;
element.setAttribute('title', elementLabel);

const elementIconIdentifier = settings.visible
? element.dataset.datahandlerVisibleIcon
: element.dataset.datahandlerHiddenIcon;
const iconElement = element.querySelector(Identifiers.icon);
Icons.getIcon(elementIconIdentifier, Icons.sizes.small).then((icon: string): void => {
iconElement.replaceWith(document.createRange().createContextualFragment(icon));
});

if (table === 'pages') {
AjaxDataHandler.refreshPageTree();
}
// Set overlay for the record icon
const recordIcon = rowElement.querySelector('.col-icon ' + Identifiers.icon);
if (settings.visible) {
recordIcon.querySelector('.icon-overlay').remove();
} else {

Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').then((icon: string): void => {
const iconFragment = document.createRange().createContextualFragment(icon);
recordIcon.append(iconFragment.querySelector('.icon-overlay'));
});
}

// Animate row
const animationEvent = new RegularEvent('animationend', (): void => {
rowElement.classList.remove('record-pulse');
animationEvent.release();
});
animationEvent.bindTo(rowElement);
rowElement.classList.add('record-pulse');

// Refresh Pagetree
if (settings.table === 'pages') {
AjaxDataHandler.refreshPageTree();
}
}
});
}

/**
Expand Down
54 changes: 34 additions & 20 deletions typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
Expand Up @@ -1531,29 +1531,43 @@ public function makeControl($table, $row)
if (!$permsEdit || $isDeletePlaceHolder || $this->isRecordCurrentBackendUser($table, $row)) {
$hideAction = $this->spaceIcon;
} else {
$hideTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($table === 'pages' ? 'Page' : '')));
$unhideTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($table === 'pages' ? 'Page' : '')));
$visibleTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($table === 'pages' ? 'Page' : ''));
$visibleIcon = 'actions-edit-hide';
$visibleValue = '0';
$hiddenTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($table === 'pages' ? 'Page' : ''));
$hiddenIcon = 'actions-edit-unhide';
$hiddenValue = '1';
if ($row[$hiddenField] ?? false) {
$params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=0';
$hideAction = '<button type="button"'
. ' class="btn btn-default t3js-record-hide"'
. ' data-state="hidden"'
. ' data-params="' . htmlspecialchars($params) . '"'
. ' data-toggle-title="' . $hideTitle . '"'
. ' title="' . $unhideTitle . '">'
. $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render()
. '</button>';
$titleLabel = $hiddenTitle;
$iconIdentifier = $hiddenIcon;
$status = 'hidden';
} else {
$params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=1';
$hideAction = '<button type="button"'
. ' class="btn btn-default t3js-record-hide"'
. ' data-state="visible"'
. ' data-params="' . htmlspecialchars($params) . '"'
. ' data-toggle-title="' . $unhideTitle . '"'
. ' title="' . $hideTitle . '">'
. $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render()
. '</button>';
$titleLabel = $visibleTitle;
$iconIdentifier = $visibleIcon;
$status = 'visible';
}
$attributesString = GeneralUtility::implodeAttributes(
[
'class' => 'btn btn-default',
'type' => 'button',
'title' => $titleLabel,
'data-datahandler-action' => 'visibility',
'data-datahandler-table' => $table,
'data-datahandler-uid' => $rowUid,
'data-datahandler-field' => $hiddenField,
'data-datahandler-status' => $status,
'data-datahandler-visible-label' => $visibleTitle,
'data-datahandler-visible-value' => $visibleValue,
'data-datahandler-visible-icon' => $visibleIcon,
'data-datahandler-hidden-label' => $hiddenTitle,
'data-datahandler-hidden-value' => $hiddenValue,
'data-datahandler-hidden-icon' => $hiddenIcon,
],
true
);
$hideAction = '<button ' . $attributesString . '>'
. $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)
. '</button>';
}
$this->addActionToCellGroup($cells, $hideAction, 'hide');
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c4e4a32

Please sign in to comment.