Skip to content
Snippets Groups Projects
Commit 9561422c authored by Oliver Hader's avatar Oliver Hader Committed by Oliver Hader
Browse files

[TASK] Reduce inline `onchange` events in backend scope

This change aims to reduce the amount of inline JavaScript by
removing `onchange` events.

* extracts inline JavaScript from templates
* introduces `TYPO3/CMS/Backend/GlobalEventHandler` module
* adjusts PHP type-hints to their correct & actual values

Resolves: #91052
Releases: master
Change-Id: If92391bd48f89df57fbb0ed6f8b0936da0e1a49d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64191


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: default avatarBenni Mack <benni@typo3.org>
Reviewed-by: default avatarOliver Hader <oliver.hader@typo3.org>
parent 0bdfd8d7
Branches
Tags
No related merge requests found
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
/**
* Module: TYPO3/CMS/Backend/GlobalEventHandler
*
* + `data-global-event="change"`
* + `data-action-submit="..."` submits form data
* + `$form` parent form element of current element is submitted
* + `<any CSS selector>` queried element is submitted (if implementing HTMLFormElement)
* + `data-action-navigate="..."` navigates to URL
* + `$value` URL taken from value of current element
* + `data-global-event="click"`
* + @todo
*
* @example
* <form action="..." id="...">
* <input name="name" value="value" data-global-event="change" data-action-submit="$form">
* </form>
*/
class GlobalEventHandler {
private options = {
onChangeSelector: '[data-global-event="change"]',
onClickSelector: '[data-global-event="click"]',
};
constructor() {
this.onReady(() => {
this.registerEvents();
});
};
private onReady(callback: Function): void {
if (document.readyState === 'complete') {
callback.call(this);
} else {
const delegate = () => {
window.removeEventListener('load', delegate);
document.removeEventListener('DOMContentLoaded', delegate);
callback.call(this);
};
window.addEventListener('load', delegate);
document.addEventListener('DOMContentLoaded', delegate);
}
}
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));
});
}
private handleChangeEvent(evt: Event): void {
const resolvedTarget = evt.target as HTMLElement;
this.handleSubmitAction(evt, resolvedTarget)
|| this.handleNavigateAction(evt, resolvedTarget);
}
private handleClickEvent(evt: Event): void {
const resolvedTarget = evt.currentTarget as HTMLElement;
}
private handleSubmitAction(evt: Event, resolvedTarget: HTMLElement): boolean {
const action: string = resolvedTarget.dataset.actionSubmit;
if (!action) {
return false;
}
// @example [data-action-submit]="$form"
if (action === '$form' && this.isHTMLFormChildElement(resolvedTarget)) {
(resolvedTarget as HTMLFormChildElement).form.submit();
return true;
}
const formCandidate = document.querySelector(action);
if (formCandidate instanceof HTMLFormElement) {
formCandidate.submit();
return true;
}
return false;
}
private handleNavigateAction(evt: Event, resolvedTarget: HTMLElement): boolean {
const action: string = resolvedTarget.dataset.actionNavigate;
if (!action) {
return false;
}
const value = this.resolveHTMLFormChildElementValue(resolvedTarget);
if (action === '$value' && value) {
window.location.href = value;
return true;
}
return false;
}
private isHTMLFormChildElement(element: HTMLElement): boolean {
return element instanceof HTMLSelectElement
|| element instanceof HTMLInputElement
|| element instanceof HTMLTextAreaElement;
}
private resolveHTMLFormChildElementValue(element: HTMLElement): string | null {
if (element instanceof HTMLSelectElement) {
return element.options[element.selectedIndex].value;
} else if (element instanceof HTMLInputElement) {
return element.value;
}
return null;
}
}
export = new GlobalEventHandler();
......@@ -309,6 +309,7 @@ class ModuleTemplate
$this->pageRenderer->enableConcatenateJavascript();
$this->pageRenderer->enableCompressCss();
$this->pageRenderer->enableCompressJavascript();
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
$languageCode = $this->pageRenderer->getLanguage() === 'default' ? 'en' : $this->pageRenderer->getLanguage();
$this->pageRenderer->setHtmlTag('<html lang="' . htmlspecialchars($languageCode) . '">');
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) {
......
<f:if condition="{menu.menuItems -> f:count()} > 1">
{menu.label} <select class="form-control t3-js-jumpMenuBox" name="{menu.identifier}" data-menu-identifier="{menu.dataIdentifier}" onchange="if(this.options[this.selectedIndex].value){window.location.href=(this.options[this.selectedIndex].value);}">
{menu.label} <select class="form-control t3-js-jumpMenuBox"
name="{menu.identifier}"
data-menu-identifier="{menu.dataIdentifier}"
data-global-event="change" data-action-navigate="$value">
<f:for each="{menu.menuItems}" as="menuItem">
<option value="{menuItem.href}" {f:if(condition: '{menuItem.active}', then: ' selected="selected"')}>{menuItem.title}</option>
</f:for>
......
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],(function(e,t){"use strict";return new class{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]'},this.onReady(()=>{this.registerEvents()})}onReady(e){if("complete"===document.readyState)e.call(this);else{const t=()=>{window.removeEventListener("load",t),document.removeEventListener("DOMContentLoaded",t),e.call(this)};window.addEventListener("load",t),document.addEventListener("DOMContentLoaded",t)}}registerEvents(){document.querySelectorAll(this.options.onChangeSelector).forEach(e=>{document.addEventListener("change",this.handleChangeEvent.bind(this))}),document.querySelectorAll(this.options.onClickSelector).forEach(e=>{document.addEventListener("click",this.handleClickEvent.bind(this))})}handleChangeEvent(e){const t=e.target;this.handleSubmitAction(e,t)||this.handleNavigateAction(e,t)}handleClickEvent(e){e.currentTarget}handleSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;if("$form"===n&&this.isHTMLFormChildElement(t))return t.form.submit(),!0;const o=document.querySelector(n);return o instanceof HTMLFormElement&&(o.submit(),!0)}handleNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const o=this.resolveHTMLFormChildElementValue(t);return!("$value"!==n||!o)&&(window.location.href=o,!0)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){return e instanceof HTMLSelectElement?e.options[e.selectedIndex].value:e instanceof HTMLInputElement?e.value:null}}}));
\ No newline at end of file
......@@ -15,10 +15,11 @@
namespace TYPO3\CMS\Belog\Controller;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Belog\Domain\Model\Constraint;
use TYPO3\CMS\Belog\Domain\Model\LogEntry;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
......@@ -69,11 +70,6 @@ class BackendLogController extends ActionController
*/
protected $logEntryRepository;
/**
* @var BackendTemplateView
*/
protected $view;
/**
* @param \TYPO3\CMS\Belog\Domain\Repository\LogEntryRepository $logEntryRepository
*/
......@@ -95,6 +91,8 @@ class BackendLogController extends ActionController
}
$constraintConfiguration = $this->arguments->getArgument('constraint')->getPropertyMappingConfiguration();
$constraintConfiguration->allowAllProperties();
GeneralUtility::makeInstance(PageRenderer::class)
->loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
}
/**
......
......@@ -20,7 +20,7 @@
<f:form.select
property="userOrGroup"
options="{userGroups}"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-users"
/>
......@@ -32,7 +32,7 @@
property="number"
options="{settings.selectableNumberOfLogEntries}"
optionLabelPrefix="LLL:EXT:belog/Resources/Private/Language/locallang.xlf:"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-max"
/>
......@@ -45,7 +45,7 @@
<f:form.select
property="workspaceUid"
options="{workspaces}"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-workspaces"
/>
......@@ -59,7 +59,7 @@
<f:form.select
property="depth"
options="{pageDepths}"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-depth"
/>
......@@ -72,7 +72,7 @@
property="timeFrame"
options="{settings.selectableTimeFrames}"
optionLabelPrefix="LLL:EXT:belog/Resources/Private/Language/locallang.xlf:"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-time"
/>
......@@ -84,7 +84,7 @@
property="action"
options="{settings.selectableActions}"
optionLabelPrefix="LLL:EXT:belog/Resources/Private/Language/locallang.xlf:"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
class="form-control input-sm"
id="belog-action"
/>
......@@ -96,7 +96,7 @@
<f:form.checkbox
property="groupByPage"
value="1"
additionalAttributes="{onchange : 'submit()'}"
additionalAttributes="{data-global-event='change', data-action-submit: '$form'}"
id="belog-group"
/>
</div>
......
......@@ -10,8 +10,8 @@
<f:form.select name="mode" options="{
overview:'{f:translate(key:\'administration.statistics.view.overview\')}',
content:'{f:translate(key:\'administration.statistics.view.content\')}'
}" value="{mode}" additionalAttributes="{onchange:'this.form.submit();'}" />
<f:form.select name="depth" options="{levelTranslations}" value="{depth}" additionalAttributes="{onchange:'this.form.submit();'}" />
}" value="{mode}" additionalAttributes="{data-global-event='change', data-action-submit: '$form'}" />
<f:form.select name="depth" options="{levelTranslations}" value="{depth}" additionalAttributes="{data-global-event='change', data-action-submit: '$form'}" />
</div>
</div>
</f:form>
......
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