Skip to content

Commit

Permalink
[TASK] Use GlobalEventHandler and ActionDispatcher instead of inline JS
Browse files Browse the repository at this point in the history
This change aims to reduce the amount of inline JavaScript by
removing `onchange` or `onclick` events and dynamically created
JavaScript code/settings.

* adjusts invocations of top.TYPO3.InfoWindow.showItem
* adjusts low-level inline `onchange` and `onclick` events

Both JavaScript modules `TYPO3/CMS/Backend/GlobalEventHandler` and
`TYPO3/CMS/Backend/ActionDispatcher` are required to actually handle
these new triggers and correpsonding events - that's why they are
loaded in `ModuleTemplate` and deprecated `DocumentTemplate`.

Resolves: #91117
Releases: master
Change-Id: Ie7012445d09c3aee253548cb3057c8e9e4b86809
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64242
Tested-by: Josef Glatz <josefglatz@gmail.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Josef Glatz <josefglatz@gmail.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
ohader committed Apr 22, 2020
1 parent 8e305dd commit 1d0f8fc
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 63 deletions.
Expand Up @@ -16,11 +16,6 @@ import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
import shortcutMenu = require('TYPO3/CMS/Backend/Toolbar/ShortcutMenu');
import documentService = require('TYPO3/CMS/Core/DocumentService');

const delegates: {[key: string]: Function} = {
'TYPO3.InfoWindow.showItem': InfoWindow.showItem.bind(null),
'TYPO3.ShortcutMenu.createShortcut': shortcutMenu.createShortcut.bind(shortcutMenu),
};

/**
* Module: TYPO3/CMS/Backend/ActionDispatcher
*
Expand All @@ -32,9 +27,14 @@ const delegates: {[key: string]: Function} = {
* data-dispatch-args="[$quot;tt_content&quot;,123]"
*/
class ActionDispatcher {
private delegates: {[key: string]: Function} = {};

private static resolveArguments(element: HTMLElement): null | string[] {
if (element.dataset.dispatchArgs) {
const args = JSON.parse(element.dataset.dispatchArgs);
// `&quot;` is the only literal of a PHP `json_encode` that needs to be substituted
// all other payload values are expected to be serialized to unicode literals
const json = element.dataset.dispatchArgs.replace(/&quot;/g, '"');
const args = JSON.parse(json);
return args instanceof Array ? ActionDispatcher.trimItems(args) : null;
} else if (element.dataset.dispatchArgsList) {
const args = element.dataset.dispatchArgsList.split(',');
Expand Down Expand Up @@ -67,9 +67,17 @@ class ActionDispatcher {
}

public constructor() {
this.createDelegates();
documentService.ready().then((): void => this.registerEvents());
}

private createDelegates(): void {
this.delegates = {
'TYPO3.InfoWindow.showItem': InfoWindow.showItem.bind(null),
'TYPO3.ShortcutMenu.createShortcut': shortcutMenu.createShortcut.bind(shortcutMenu),
};
}

private registerEvents(): void {
new RegularEvent('click', this.handleClickEvent.bind(this))
.delegateTo(document, '[data-dispatch-action]:not([data-dispatch-immediately])');
Expand All @@ -85,8 +93,8 @@ class ActionDispatcher {
private delegateTo(target: HTMLElement): void {
const action = target.dataset.dispatchAction;
const args = ActionDispatcher.resolveArguments(target);
if (delegates[action]) {
delegates[action].apply(null, args || []);
if (this.delegates[action]) {
this.delegates[action].apply(null, args || []);
}
}
}
Expand Down
Expand Up @@ -12,6 +12,7 @@
*/

import documentService = require('TYPO3/CMS/Core/DocumentService');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');

type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;

Expand Down Expand Up @@ -45,22 +46,20 @@ class GlobalEventHandler {
};

private registerEvents(): void {
document.querySelectorAll(this.options.onChangeSelector).forEach((element: HTMLElement) => {
document.addEventListener('change', this.handleChangeEvent.bind(this));
});
document.querySelectorAll(this.options.onClickSelector).forEach((element: HTMLElement) => {
document.addEventListener('click', this.handleClickEvent.bind(this));
});
new RegularEvent('change', this.handleChangeEvent.bind(this))
.delegateTo(document, this.options.onChangeSelector);
new RegularEvent('click', this.handleClickEvent.bind(this))
.delegateTo(document, this.options.onClickSelector);
}

private handleChangeEvent(evt: Event): void {
const resolvedTarget = evt.target as HTMLElement;
private handleChangeEvent(evt: Event, resolvedTarget: HTMLElement): void {
evt.preventDefault();
this.handleSubmitAction(evt, resolvedTarget)
|| this.handleNavigateAction(evt, resolvedTarget);
}

private handleClickEvent(evt: Event): void {
const resolvedTarget = evt.currentTarget as HTMLElement;
private handleClickEvent(evt: Event, resolvedTarget: HTMLElement): void {
evt.preventDefault();
}

private handleSubmitAction(evt: Event, resolvedTarget: HTMLElement): boolean {
Expand Down Expand Up @@ -88,7 +87,7 @@ class GlobalEventHandler {
}
const value = this.resolveHTMLFormChildElementValue(resolvedTarget);
const navigateValue = resolvedTarget.dataset.navigateValue;
if (action === '$data=~s/$value/' && value && navigateValue) {
if (action === '$data=~s/$value/' && navigateValue && value !== null) {
// replacing `${value}` and its URL encoded representation
window.location.href = navigateValue.replace(/(\$\{value\}|%24%7Bvalue%7D)/gi, value);
return true;
Expand All @@ -111,8 +110,11 @@ class GlobalEventHandler {
}

private resolveHTMLFormChildElementValue(element: HTMLElement): string | null {
const type: string = element.getAttribute('type');
if (element instanceof HTMLSelectElement) {
return element.options[element.selectedIndex].value;
} else if (element instanceof HTMLInputElement && type === 'checkbox') {
return element.checked ? element.value : '';
} else if (element instanceof HTMLInputElement) {
return element.value;
}
Expand Down
10 changes: 8 additions & 2 deletions typo3/sysext/backend/Classes/Clipboard/Clipboard.php
Expand Up @@ -404,7 +404,10 @@ public function getContentFromTab($pad)
$this->getBackendUser()->uc['titleLen']
)), $fileObject->getName()),
'thumb' => $thumb,
'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($v) . '); return false;'),
'infoDataDispatch' => [
'action' => 'TYPO3.InfoWindow.showItem',
'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, $v], false),
],
'removeLink' => $this->removeUrl('_FILE', GeneralUtility::shortMD5($v))
];
} else {
Expand All @@ -426,7 +429,10 @@ public function getContentFromTab($pad)
$table,
$rec
), $this->getBackendUser()->uc['titleLen'])), $rec, $table),
'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', \'' . (int)$uid . '\'); return false;'),
'infoDataDispatch' => [
'action' => 'TYPO3.InfoWindow.showItem',
'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, (int)$uid], false),
],
'removeLink' => $this->removeUrl($table, $uid)
];

Expand Down
Expand Up @@ -411,7 +411,6 @@ protected function getTableWizard(array $configuration): string
</tfoot>';
}
$content = '';
$addSubmitOnClick = 'onclick="document.getElementById(\'TableController\').submit();"';
// Implode all table rows into a string, wrapped in table tags.
$content .= '
Expand All @@ -428,7 +427,7 @@ protected function getTableWizard(array $configuration): string
<div class="checkbox">
<input type="hidden" name="TABLE[textFields]" value="0" />
<label for="textFields">
<input type="checkbox" ' . $addSubmitOnClick . ' name="TABLE[textFields]" id="textFields" value="1"' . ($this->inputStyle ? ' checked="checked"' : '') . ' />
<input type="checkbox" data-global-event="change" data-action-submit="$form" name="TABLE[textFields]" id="textFields" value="1"' . ($this->inputStyle ? ' checked="checked"' : '') . ' />
' . $this->getLanguageService()->getLL('table_smallFields') . '
</label>
</div>';
Expand Down
2 changes: 2 additions & 0 deletions typo3/sysext/backend/Classes/Template/DocumentTemplate.php
Expand Up @@ -280,6 +280,8 @@ protected function initPageRenderer()
$this->pageRenderer->enableConcatenateJavascript();
$this->pageRenderer->enableCompressCss();
$this->pageRenderer->enableCompressJavascript();
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) {
$this->pageRenderer->enableDebugMode();
}
Expand Down
64 changes: 44 additions & 20 deletions typo3/sysext/backend/Classes/Utility/BackendUtility.php
Expand Up @@ -1196,8 +1196,13 @@ public static function thumbCode(
. '</span>';
}
if ($linkInfoPopup) {
$onClick = 'top.TYPO3.InfoWindow.showItem(\'_FILE\',\'' . (int)$fileObject->getUid() . '\'); return false;';
$thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $imgTag . '</a> ';
// @todo Should we add requireJsModule again (should be loaded in most/all cases)
// loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
$attributes = GeneralUtility::implodeAttributes([
'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(),
], true);
$thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> ';
} else {
$thumbData .= $imgTag;
}
Expand Down Expand Up @@ -2616,15 +2621,21 @@ public static function getFuncMenu(
$dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
$dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
if (!empty($options)) {
$onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value;';
return '
<!-- Function Menu of module -->
<select class="form-control" name="' . $elementName . '" onchange="' . htmlspecialchars($onChange) . '" data-menu-identifier="' . htmlspecialchars($dataMenuIdentifier) . '">
' . implode('
', $options) . '
</select>
';
// @todo Should we add requireJsModule again (should be loaded in most/all cases)
// loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
$attributes = GeneralUtility::implodeAttributes([
'name' => $elementName,
'class' => 'form-control',
'data-menu-identifier' => $dataMenuIdentifier,
'data-global-event' => 'change',
'data-action-navigate' => '$data=~s/$value/',
'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
], true);
return sprintf(
'<select %s>%s</select>select>',
$attributes,
implode('', $options)
);
}
return '';
}
Expand Down Expand Up @@ -2665,11 +2676,20 @@ public static function getDropdownMenu(
$dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
$dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
if (!empty($options)) {
// @todo Should we add requireJsModule again (should be loaded in most/all cases)
// loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
$onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value;';
$attributes = GeneralUtility::implodeAttributes([
'name' => $elementName,
'data-menu-identifier' => $dataMenuIdentifier,
'data-global-event' => 'change',
'data-action-navigate' => '$data=~s/$value/',
'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
], true);
return '
<div class="form-group">
<!-- Function Menu of module -->
<select class="form-control input-sm" name="' . htmlspecialchars($elementName) . '" onchange="' . htmlspecialchars($onChange) . '" data-menu-identifier="' . htmlspecialchars($dataMenuIdentifier) . '">
<select class="form-control input-sm" ' . $attributes . '>
' . implode(LF, $options) . '
</select>
</div>
Expand Down Expand Up @@ -2699,18 +2719,22 @@ public static function getFuncCheck(
$addParams = '',
$tagParams = ''
) {
// @todo Should we add requireJsModule again (should be loaded in most/all cases)
// loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
$scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
$onClick = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+(this.checked?1:0);';

$attributes = GeneralUtility::implodeAttributes([
'type' => 'checkbox',
'class' => 'checkbox',
'name' => $elementName,
'value' => 1,
'data-global-event' => 'change',
'data-action-navigate' => '$data=~s/$value/',
'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
], true);
return
'<input' .
' type="checkbox"' .
' class="checkbox"' .
' name="' . $elementName . '"' .
'<input ' . $attributes .
($currentValue ? ' checked="checked"' : '') .
' onclick="' . htmlspecialchars($onClick) . '"' .
($tagParams ? ' ' . $tagParams : '') .
' value="1"' .
' />';
}

Expand Down
2 changes: 1 addition & 1 deletion typo3/sysext/backend/Classes/View/PageLayoutView.php
Expand Up @@ -1603,7 +1603,7 @@ public function languageSelector($id)

return '<div class="form-inline form-inline-spaced">'
. '<div class="form-group">'
. '<select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">'
. '<select class="form-control input-sm" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">'
. $output
. '</select></div></div>';
}
Expand Down
Expand Up @@ -22,9 +22,11 @@
</f:if>
</td>
<td class="col-control nowrap">
<f:if condition="{content.infoLink}">
<f:if condition="{content.infoDataDispatch}">
<div class="btn-group">
<a class="btn btn-default" href="#" onclick="{content.infoLink}"
<a class="btn btn-default" href="#"
data-dispatch-action="{content.infoDataDispatch.action}"
data-dispatch-args="{content.infoDataDispatch.args}"
title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info')}">
<f:format.raw>
<core:icon identifier="actions-document-info" alternativeMarkupIdentifier="inline"/>
Expand Down
@@ -1,7 +1,7 @@
<f:if condition="{context.newLanguageOptions}">
<div class="form-inline form-inline-spaced">
<div class="form-group">
<select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">'
<select class="form-control input-sm" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">'
<f:for each="{context.newLanguageOptions}" as="languageName" key="url">
<option value="{url}">{languageName}</option>
</f:for>
Expand Down

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

0 comments on commit 1d0f8fc

Please sign in to comment.