diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ActionDispatcher.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ActionDispatcher.ts index 3c35d23c9c9def07fd40b7f843b05e22daa218e5..c311607226394e1be44c5a15b5b4bc4059a8e6f2 100644 --- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ActionDispatcher.ts +++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ActionDispatcher.ts @@ -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",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); + // `"` 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(/"/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 || []); } } } diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts index 25188a5839dc2d6f9ed925ccf33e79b2d8c079b6..4676ee345bb9550fc28fbd884e9bbfe557c4d48d 100644 --- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts +++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts @@ -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; } diff --git a/typo3/sysext/backend/Classes/Clipboard/Clipboard.php b/typo3/sysext/backend/Classes/Clipboard/Clipboard.php index 87aeb9fd0ef9707b8af1b0ee44f3df466fca43f4..70dc61e11f491592652a4c253b3319c9b847f817 100644 --- a/typo3/sysext/backend/Classes/Clipboard/Clipboard.php +++ b/typo3/sysext/backend/Classes/Clipboard/Clipboard.php @@ -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) ]; diff --git a/typo3/sysext/backend/Classes/Controller/Wizard/TableController.php b/typo3/sysext/backend/Classes/Controller/Wizard/TableController.php index 896d3bccd04022b128ea5e23d560350a6f0ba936..73aef3f5b380ee090322956c2217f30ef9852eb8 100644 --- a/typo3/sysext/backend/Classes/Controller/Wizard/TableController.php +++ b/typo3/sysext/backend/Classes/Controller/Wizard/TableController.php @@ -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>'; diff --git a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php index 8fce986b738d1a7adf4aa8bf99341aa23516c17f..db36014aff15cee4a05d91d3694b6e83c23bb10f 100644 --- a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php +++ b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php @@ -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(); } diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index d1c1b53972b43a04fc71ea037fefd41fb98fe018..76531fc4786019584bbabd8382e9e2902adae67b 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -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"' . ' />'; } diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php index cd1c81dd52defdb82a2287aebfe27eb8c4fc9dad..d8c6143140f6c1fe967de579004229f59a19c605 100644 --- a/typo3/sysext/backend/Classes/View/PageLayoutView.php +++ b/typo3/sysext/backend/Classes/View/PageLayoutView.php @@ -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>'; } diff --git a/typo3/sysext/backend/Resources/Private/Partials/Clipboard/TabContent.html b/typo3/sysext/backend/Resources/Private/Partials/Clipboard/TabContent.html index 02f460ee7e4d9b64fb47b438047f59b9bed48de8..40ddc3ab1f5f168d61525ef55a697aed1c5ad6eb 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/Clipboard/TabContent.html +++ b/typo3/sysext/backend/Resources/Private/Partials/Clipboard/TabContent.html @@ -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"/> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html index 172d073b07bb5ed4dbb098aade23da40ac8681c6..335b46096949dad98940e009fc67aa38a4e384e9 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html @@ -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> diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ActionDispatcher.js b/typo3/sysext/backend/Resources/Public/JavaScript/ActionDispatcher.js index b8199599761d8b64943410beb00898d656fb5682..2ce07a873ad137a50770a345eba15e45425ed6a0 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/ActionDispatcher.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/ActionDispatcher.js @@ -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(/"/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 diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js b/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js index 326c2c9de5ad0b7e164180f162d936f76124c286..f7c769d996ced87cc60978300affc7245a5a7093 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js @@ -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 diff --git a/typo3/sysext/core/Classes/Database/QueryView.php b/typo3/sysext/core/Classes/Database/QueryView.php index 690008c0bfd842eb086dada13f4fd9d508051073..4cda41cde72179267564ec9a7103ec3da0a55525 100644 --- a/typo3/sysext/core/Classes/Database/QueryView.php +++ b/typo3/sysext/core/Classes/Database/QueryView.php @@ -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">'; diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php index de4216ad7ad1c8cf650bfedd1d500c0f8fd30f31..34f2f633ad7a0775c47f1f9dd2cb9c00a1fd85cd 100644 --- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php +++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php @@ -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 * diff --git a/typo3/sysext/core/Documentation/Changelog/10.4.x/Important-91117-UseGlobalEventHandlerAndActionDispatcherInsteadOfInlineJS.rst b/typo3/sysext/core/Documentation/Changelog/10.4.x/Important-91117-UseGlobalEventHandlerAndActionDispatcherInsteadOfInlineJS.rst new file mode 100644 index 0000000000000000000000000000000000000000..f3fb631aaa494becdb66aa74f31c95b7a53a90a0 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/10.4.x/Important-91117-UseGlobalEventHandlerAndActionDispatcherInsteadOfInlineJS.rst @@ -0,0 +1,76 @@ +.. 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="["tt_content",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 diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Be/TableListViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Be/TableListViewHelper.php index a889b62cb85fb7067fcada76648ebc88e5837deb..6ca9b3f07ada02d9f48a7ba114ecffba963b5532 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Be/TableListViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Be/TableListViewHelper.php @@ -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 */ diff --git a/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php b/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php index 3193de31515b0c7416e8fa01bff563b9e62fd34b..a1582fcedcf62311eaf5fca01aadeef34bbf58d7 100644 --- a/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php +++ b/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php @@ -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'); diff --git a/typo3/sysext/recordlist/Classes/Controller/RecordListController.php b/typo3/sysext/recordlist/Classes/Controller/RecordListController.php index 19fb1924baf198931bd2344e3010297e361e74bf..e1d2e63a6ef5aaef8bbbd46807a9c11bdff8bd34 100644 --- a/typo3/sysext/recordlist/Classes/Controller/RecordListController.php +++ b/typo3/sysext/recordlist/Classes/Controller/RecordListController.php @@ -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>'; } diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php index 50226a621f70e11533e4eb152fde2f2a9a7b7265..26cd4161b32d809d64dec82cde00e3259885e2bc 100644 --- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php +++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php @@ -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 *