Commit 1d0f8fce authored by Oliver Hader's avatar Oliver Hader Committed by Oliver Hader
Browse files

[TASK] Use GlobalEventHandler and ActionDispatcher instead of inline JS

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: default avatarJosef Glatz <josefglatz@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: default avatarJosef Glatz <josefglatz@gmail.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
parent 8e305dd1
......@@ -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
*
......@@ -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(',');
......@@ -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])');
......@@ -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 || []);
}
}
}
......
......@@ -12,6 +12,7 @@
*/
import documentService = require('TYPO3/CMS/Core/DocumentService');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
......@@ -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 {
......@@ -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;
......@@ -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;
}
......
......@@ -404,7 +404,10 @@ class Clipboard
$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 {
......@@ -426,7 +429,10 @@ class Clipboard
$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)
];
......
......@@ -411,7 +411,6 @@ class TableController extends AbstractWizardController
</tfoot>';
}
$content = '';
$addSubmitOnClick = 'onclick="document.getElementById(\'TableController\').submit();"';
// Implode all table rows into a string, wrapped in table tags.
$content .= '
......@@ -428,7 +427,7 @@ class TableController extends AbstractWizardController
<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>';
......
......@@ -280,6 +280,8 @@ function jumpToUrl(URL) {
$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();
}
......
......@@ -1196,8 +1196,13 @@ class BackendUtility
. '</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;
}
......@@ -2616,15 +2621,21 @@ class BackendUtility
$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 '';
}
......@@ -2665,11 +2676,20 @@ class BackendUtility
$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>
......@@ -2699,18 +2719,22 @@ class BackendUtility
$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"' .
' />';
}
......
......@@ -1603,7 +1603,7 @@ class PageLayoutView implements LoggerAwareInterface
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>';
}
......
......@@ -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"/>
......
<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>
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Backend/InfoWindow","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Backend/Toolbar/ShortcutMenu","TYPO3/CMS/Core/DocumentService"],(function(t,e,n,a,s,i){"use strict";const r={"TYPO3.InfoWindow.showItem":n.showItem.bind(null),"TYPO3.ShortcutMenu.createShortcut":s.createShortcut.bind(s)};class c{static resolveArguments(t){if(t.dataset.dispatchArgs){const e=JSON.parse(t.dataset.dispatchArgs);return e instanceof Array?c.trimItems(e):null}if(t.dataset.dispatchArgsList){const e=t.dataset.dispatchArgsList.split(",");return c.trimItems(e)}return null}static trimItems(t){return t.map(t=>t instanceof String?t.trim():t)}static enrichItems(t,e,n){return t.map(t=>t instanceof Object&&t.$event?t.$target?n:t.$event?e:void 0:t)}constructor(){i.ready().then(()=>this.registerEvents())}registerEvents(){new a("click",this.handleClickEvent.bind(this)).delegateTo(document,"[data-dispatch-action]:not([data-dispatch-immediately])"),document.querySelectorAll("[data-dispatch-action][data-dispatch-immediately]").forEach(this.delegateTo.bind(this))}handleClickEvent(t,e){t.preventDefault(),this.delegateTo(e)}delegateTo(t){const e=t.dataset.dispatchAction,n=c.resolveArguments(t);r[e]&&r[e].apply(null,n||[])}}return new c}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Backend/InfoWindow","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Backend/Toolbar/ShortcutMenu","TYPO3/CMS/Core/DocumentService"],(function(t,e,a,s,i,n){"use strict";class r{constructor(){this.delegates={},this.createDelegates(),n.ready().then(()=>this.registerEvents())}static resolveArguments(t){if(t.dataset.dispatchArgs){const e=t.dataset.dispatchArgs.replace(/&quot;/g,'"'),a=JSON.parse(e);return a instanceof Array?r.trimItems(a):null}if(t.dataset.dispatchArgsList){const e=t.dataset.dispatchArgsList.split(",");return r.trimItems(e)}return null}static trimItems(t){return t.map(t=>t instanceof String?t.trim():t)}static enrichItems(t,e,a){return t.map(t=>t instanceof Object&&t.$event?t.$target?a:t.$event?e:void 0:t)}createDelegates(){this.delegates={"TYPO3.InfoWindow.showItem":a.showItem.bind(null),"TYPO3.ShortcutMenu.createShortcut":i.createShortcut.bind(i)}}registerEvents(){new s("click",this.handleClickEvent.bind(this)).delegateTo(document,"[data-dispatch-action]:not([data-dispatch-immediately])"),document.querySelectorAll("[data-dispatch-action][data-dispatch-immediately]").forEach(this.delegateTo.bind(this))}handleClickEvent(t,e){t.preventDefault(),this.delegateTo(e)}delegateTo(t){const e=t.dataset.dispatchAction,a=r.resolveArguments(t);this.delegates[e]&&this.delegates[e].apply(null,a||[])}}return new r}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Core/DocumentService"],(function(e,t,n){"use strict";return new class{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]'},n.ready().then(()=>this.registerEvents())}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 i=document.querySelector(n);return i instanceof HTMLFormElement&&(i.submit(),!0)}handleNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const i=this.resolveHTMLFormChildElementValue(t),o=t.dataset.navigateValue;return"$data=~s/$value/"===n&&i&&o?(window.location.href=o.replace(/(\$\{value\}|%24%7Bvalue%7D)/gi,i),!0):"$data"===n&&o?(window.location.href=o,!0):!("$value"!==n||!i)&&(window.location.href=i,!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
define(["require","exports","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,n,i){"use strict";return new class{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]'},n.ready().then(()=>this.registerEvents())}registerEvents(){new i("change",this.handleChangeEvent.bind(this)).delegateTo(document,this.options.onChangeSelector),new i("click",this.handleClickEvent.bind(this)).delegateTo(document,this.options.onClickSelector)}handleChangeEvent(e,t){e.preventDefault(),this.handleSubmitAction(e,t)||this.handleNavigateAction(e,t)}handleClickEvent(e,t){e.preventDefault()}handleSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;if("$form"===n&&this.isHTMLFormChildElement(t))return t.form.submit(),!0;const i=document.querySelector(n);return i instanceof HTMLFormElement&&(i.submit(),!0)}handleNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const i=this.resolveHTMLFormChildElementValue(t),a=t.dataset.navigateValue;return"$data=~s/$value/"===n&&a&&null!==i?(window.location.href=a.replace(/(\$\{value\}|%24%7Bvalue%7D)/gi,i),!0):"$data"===n&&a?(window.location.href=a,!0):!("$value"!==n||!i)&&(window.location.href=i,!0)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){const t=e.getAttribute("type");return e instanceof HTMLSelectElement?e.options[e.selectedIndex].value:e instanceof HTMLInputElement&&"checkbox"===t?e.checked?e.value:"":e instanceof HTMLInputElement?e.value:null}}}));
\ No newline at end of file
......@@ -702,9 +702,12 @@ class QueryView
$out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '">'
. $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
$out .= '</div><div class="btn-group" role="group">';
$out .= '<a class="btn btn-default" href="#" onClick="top.TYPO3.InfoWindow.showItem(\'' . $table . '\',' . $row['uid']
. ');return false;">' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render()
. '</a>';
$out .= sprintf(
'<a class="btn btn-default" href="#" data-dispatch-action="%s" data-dispatch-args-list="%s">%s</a>',
'TYPO3.InfoWindow.showItem',
htmlspecialchars($table . ',' . $row['uid']),
$this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render()
);
$out .= '</div>';
} else {
$out .= '<div class="btn-group" role="group">';
......
......@@ -3742,6 +3742,17 @@ class GeneralUtility
);
}
/**
* @param mixed $value
* @param bool $useHtmlEntities
* @return string
*/
public static function jsonEncodeForHtmlAttribute($value, bool $useHtmlEntities = true): string
{
$json = json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
return $useHtmlEntities ? htmlspecialchars($json) : $json;
}
/**
* Set the ApplicationContext
*
......
.. include:: ../../Includes.txt
====================================================================================
Important: #91117 - Use GlobalEventHandler and ActionDispatcher instead of inline JS
====================================================================================
See :issue:`91117`
Description
===========
In order to reduce the amount of inline JavaScript (with the goal to pave the
way towards stronger Content-Security-Policy assignments) lots of inline JavaScript
code parts have been substituted by a declarative syntax - basically using HTML
:html:`data-*` attributes.
The following list collects an overview of common JavaScript snippets and their
corresponding substitute using modules :js:`TYPO3/CMS/Backend/GlobalEventHandler`
and :js:`TYPO3/CMS/Backend/ActionDispatcher`.
`TYPO3/CMS/Backend/GlobalEventHandler`
--------------------------------------
.. code-block:: html
<select onchange="window.location.href=this.options[this.selectedIndex].value;">'
<!-- ... changed to ... -->
<select data-global-event="change" data-action-navigate="$value">'
Navigates to URL once selected drop-down was changed
(`$value` refers to selected value)
.. code-block:: html
<select value="0" name="depth"
onchange="window.location.href='https://example.org/__VAL__'.replace(/__VAL__/, this.options[this.selectedIndex].value);">
<!-- ... changed to ... -->
<select value="0" name="depth" data-global-event="change"
data-action-navigate="$data=~s/$value/" data-navigate-value="https://example.org/${value}">
Navigates to URL once selected drop-down was changed, including selected value
(`$data` refers to value of :html:`data-navigate-value`, `$value` to selected value,
`$data=~s/$value/` replaces literal `${value}` with selected value in `:html:`data-navigate-value`)
.. code-block:: html
<input type="checkbox" onclick="document.getElementById('formIdentifier').submit();">
<!-- ... changed to ... -->
<input type="checkbox" data-global-event="change" data-action-submit="$form">
<!-- ... or (using CSS selector) ... -->
<input type="checkbox" data-global-event="change" data-action-submit="#formIdentifier">
Submits a form once a value has been changed
(`$form` refers to paren form element, using CSS selectors like `#formIdentifier`
is possible as well)
`TYPO3/CMS/Backend/ActionDispatcher`
------------------------------------
.. code-block:: html
<a href="#" onclick="top.TYPO3.InfoWindow.showItem('tt_content', 123); return false;">
<!-- ... changed to ... -->
data-dispatch-action="TYPO3.InfoWindow.showItem" data-dispatch-args-list="be_users,123">
<!-- ... or (using JSON arguments) ... -->
data-dispatch-action="TYPO3.InfoWindow.showItem" data-dispatch-args="[&quot;tt_content&quot;,123]">
Invokes :js:`TYPO3.InfoWindow.showItem` module function to display details for a given
record (of database table `tt_content`, having `uid=123` in the example above)
.. index:: Backend, JavaScript, ext:backend
......@@ -123,6 +123,7 @@ class TableListViewHelper extends AbstractBackendViewHelper
$clickTitleMode = $this->arguments['clickTitleMode'];
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
$pageinfo = BackendUtility::readPageAccess(GeneralUtility::_GP('id'), $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW));
/** @var \TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList $dblist */
......
......@@ -65,8 +65,7 @@ class InfoPageTyposcriptConfigController
public function init($pObj)
{
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$languageService = $this->getLanguageService();
$languageService->includeLLFile('EXT:info/Resources/Private/Language/InfoPageTsConfig.xlf');
$this->getLanguageService()->includeLLFile('EXT:info/Resources/Private/Language/InfoPageTsConfig.xlf');
$this->view = $this->getFluidTemplateObject();
$this->pObj = $pObj;
$this->id = (int)GeneralUtility::_GP('id');
......
......@@ -616,7 +616,7 @@ class RecordListController
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>';
}
......
......@@ -1874,8 +1874,7 @@ class DatabaseRecordList
}
$this->addActionToCellGroup($cells, $editAction, 'edit');
// "Info": (All records)
$onClick = 'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;';
$viewBigAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('showInfo')) . '">'
$viewBigAction = '<a class="btn btn-default" href="#" ' . $this->createShowItemTagAttributes($table . ',' . (int)$row['uid']) . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('showInfo')) . '">'
. $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>';
$this->addActionToCellGroup($cells, $viewBigAction, 'viewBig');
// "Move" wizard link for pages/tt_content elements:
......@@ -3507,9 +3506,8 @@ class DatabaseRecordList
break;
case 'info':
// "Info": (All records)
$code = '<a href="#" onclick="' . htmlspecialchars(
'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;'
) . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
$code = '<a href="#" ' . $this->createShowItemTagAttributes($table . ',' . (int)$row['uid'])
. ' title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
break;
default:
// Output the label now:
......@@ -4044,9 +4042,7 @@ class DatabaseRecordList
} else {
$htmlCode = '<a href="#"';
if ($launchViewParameter !== '') {
$htmlCode .= ' onclick="' . htmlspecialchars(
'top.TYPO3.InfoWindow.showItem(' . $launchViewParameter . '); return false;'
) . '"';
$htmlCode .= ' ' . $this->createShowItemTagAttributes($launchViewParameter);
}
$htmlCode .= ' title="' . htmlspecialchars(
$this->getLanguageService()->sL(
......@@ -4070,6 +4066,20 @@ class DatabaseRecordList
$this->showOnlyTranslatedRecords = $showOnlyTranslatedRecords;
}
/**
* Creates data attributes to be handles in moddule `TYPO3/CMS/Backend/ActionDispatcher`
*
* @param string $arguments
* @return string
*/
protected function createShowItemTagAttributes(string $arguments): string
{
return GeneralUtility::implodeAttributes([
'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
'data-dispatch-args-list' => $arguments,
], true);
}
/**
* Flatten palettes into types showitem
*
......
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