From a162a04519503c96c0330763ddcc528ec27fb4ba Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <ben@bnf.dev> Date: Tue, 29 Aug 2023 06:15:32 +0200 Subject: [PATCH] [TASK] Migrate helper entry point scripts to TypeScript Now that our build chain targets ESM modules, we can use our TypeScript toolchain to also build utility entry point scripts, allowing to make use of TypeScript checking and centralize asset management. Resolves: #101790 Related: #101783 Releases: main, 12.4 Change-Id: I2b4e89114022a611a0e061d16d012e4b389dc750 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80830 Tested-by: Benjamin Franzke <ben@bnf.dev> Reviewed-by: Benjamin Franzke <ben@bnf.dev> Tested-by: core-ci <typo3@b13.com> --- .../backend/close-current-window.ts | 2 + .../Sources/TypeScript/backend/helper.ts | 10 +- .../core/java-script-item-handler.ts | 2 +- .../core/java-script-item-processor.ts | 4 +- .../TypeScript/core/referrer-refresh.ts | 2 + .../TypeScript/core/requirejs-loader.ts | 231 ++++++++++++++++++ .../TypeScript/form/frontend/date-picker.ts | 19 +- .../TypeScript/frontend/default_frontend.ts | 82 +++++++ .../TypeScript/install/init-install.ts | 2 +- .../TypeScript/install/init-installer.ts | 2 +- Build/tsconfig.json | 3 + Build/types/TYPO3/index.d.ts | 1 + .../backend/Resources/Public/Html/Close.html | 2 +- .../backend/Resources/Public/Html/Close.js | 2 - .../Public/JavaScript/close-current-window.js | 13 + .../Resources/Public/JavaScript/helper.js | 13 + .../Http/Security/ReferrerEnforcer.php | 2 +- .../Public/JavaScript/ReferrerRefresh.js | 2 - .../Public/JavaScript/referrer-refresh.js | 13 + .../Public/JavaScript/requirejs-loader.js | 222 +---------------- .../ViewHelpers/Form/DatePickerViewHelper.php | 2 +- .../Yaml/FormElements/DatePicker.yaml | 2 +- .../Public/JavaScript/frontend/date-picker.js | 13 + .../Public/JavaScript/default_frontend.js | 106 +------- 24 files changed, 421 insertions(+), 331 deletions(-) create mode 100644 Build/Sources/TypeScript/backend/close-current-window.ts rename typo3/sysext/backend/Resources/Public/JavaScript/Helper.js => Build/Sources/TypeScript/backend/helper.ts (87%) create mode 100644 Build/Sources/TypeScript/core/referrer-refresh.ts create mode 100644 Build/Sources/TypeScript/core/requirejs-loader.ts rename typo3/sysext/form/Resources/Public/JavaScript/Frontend/DatePicker.js => Build/Sources/TypeScript/form/frontend/date-picker.ts (52%) create mode 100644 Build/Sources/TypeScript/frontend/default_frontend.ts delete mode 100644 typo3/sysext/backend/Resources/Public/Html/Close.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/close-current-window.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/helper.js delete mode 100644 typo3/sysext/core/Resources/Public/JavaScript/ReferrerRefresh.js create mode 100644 typo3/sysext/core/Resources/Public/JavaScript/referrer-refresh.js create mode 100644 typo3/sysext/form/Resources/Public/JavaScript/frontend/date-picker.js diff --git a/Build/Sources/TypeScript/backend/close-current-window.ts b/Build/Sources/TypeScript/backend/close-current-window.ts new file mode 100644 index 000000000000..cb17d31d3d04 --- /dev/null +++ b/Build/Sources/TypeScript/backend/close-current-window.ts @@ -0,0 +1,2 @@ +self.close(); +window.opener.location.reload(); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Helper.js b/Build/Sources/TypeScript/backend/helper.ts similarity index 87% rename from typo3/sysext/backend/Resources/Public/JavaScript/Helper.js rename to Build/Sources/TypeScript/backend/helper.ts index 4750af36f7e5..f500196eaa9e 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Helper.js +++ b/Build/Sources/TypeScript/backend/helper.ts @@ -13,13 +13,7 @@ /** * @internal Use in TYPO3 core only, API can change at any time! */ -(function() { - "use strict"; - - if (!document.currentScript) { - return false; - } - +if (document.currentScript) { const scriptElement = document.currentScript; switch (scriptElement.dataset.action) { case 'window.close': @@ -27,4 +21,4 @@ break; default: } -})(); +} diff --git a/Build/Sources/TypeScript/core/java-script-item-handler.ts b/Build/Sources/TypeScript/core/java-script-item-handler.ts index 4e0b5fbf6244..84317561a0ad 100644 --- a/Build/Sources/TypeScript/core/java-script-item-handler.ts +++ b/Build/Sources/TypeScript/core/java-script-item-handler.ts @@ -22,7 +22,7 @@ if (document.currentScript) { const textContent = scriptElement.textContent.replace(/^\s*\/\*\s*|\s*\*\/\s*/g, ''); const items = JSON.parse(textContent); - const moduleImporter = (moduleName: string) => import(moduleName).catch(() => (window as any).importShim(moduleName)); + const moduleImporter = (moduleName: string) => import(moduleName).catch(() => window.importShim(moduleName)); moduleImporter('@typo3/core/java-script-item-processor.js').then(({ JavaScriptItemProcessor }) => { const processor = new JavaScriptItemProcessor(); diff --git a/Build/Sources/TypeScript/core/java-script-item-processor.ts b/Build/Sources/TypeScript/core/java-script-item-processor.ts index bfce48f8423c..132f0e442800 100644 --- a/Build/Sources/TypeScript/core/java-script-item-processor.ts +++ b/Build/Sources/TypeScript/core/java-script-item-processor.ts @@ -48,9 +48,9 @@ export interface JavaScriptItem { */ let useShim = false; -const moduleImporter = (moduleName: string): Promise<any> => { +const moduleImporter = (moduleName: string): Promise<unknown> => { if (useShim) { - return (window as any).importShim(moduleName); + return window.importShim(moduleName); } else { return import(moduleName).catch(() => { // Consider that importmaps are not supported and use shim from now on diff --git a/Build/Sources/TypeScript/core/referrer-refresh.ts b/Build/Sources/TypeScript/core/referrer-refresh.ts new file mode 100644 index 000000000000..6caeb42ec826 --- /dev/null +++ b/Build/Sources/TypeScript/core/referrer-refresh.ts @@ -0,0 +1,2 @@ +document.querySelectorAll('a#referrer-refresh') + .forEach((element: HTMLAnchorElement) => element.click()); diff --git a/Build/Sources/TypeScript/core/requirejs-loader.ts b/Build/Sources/TypeScript/core/requirejs-loader.ts new file mode 100644 index 000000000000..cc232e45a499 --- /dev/null +++ b/Build/Sources/TypeScript/core/requirejs-loader.ts @@ -0,0 +1,231 @@ +interface RequireConfig { + // Custom property by TYPO3 + typo3BaseUrl?: string; +} + +interface InternalRequireContext { + config: RequireConfig; + contextName: string; + completeLoad(moduleName: string): void; + configure(config: RequireConfig): void; + nameToUrl(moduleName: string, ext?: string, skipExt?: boolean): string; + onError(err: RequireError, errback?: (err: RequireError) => void): void; +} + +interface Require { + load(context: InternalRequireContext, name: string, url: string): void; +} + +(function(req: Require) { + /** + * Determines whether moduleName is configured in requirejs paths + * (this code was taken from RequireJS context.nameToUrl). + * + * @see context.nameToUrl + * @see https://github.com/requirejs/requirejs/blob/2.3.3/require.js#L1650-L1670 + * + * @param {Object} config the require context to find state. + * @param {String} moduleName the name of the module. + * @return {boolean} + */ + const inPath = function(config: RequireConfig, moduleName: string): boolean { + let i, parentModule, parentPath; + const paths = config.paths; + const syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + parentPath = paths[parentModule]; + if (parentPath) { + return true; + } + } + return false; + }; + + /** + * @return {XMLHttpRequest} + */ + const createXhr = function(): XMLHttpRequest { + if (typeof XMLHttpRequest !== 'undefined') { + return new XMLHttpRequest(); + } else { + return new ActiveXObject('Microsoft.XMLHTTP') as XMLHttpRequest; + } + }; + + /** + * Fetches RequireJS configuration from server via XHR call. + * + * @param {object} config + * @param {string} name + * @param {function} success + * @param {function} error + */ + const fetchConfiguration = function( + config: RequireConfig, + name: string, + success: (responseData: unknown) => void, + error: (status: number, error: Error) => void + ) { + // cannot use jQuery here which would be loaded via RequireJS... + const xhr = createXhr(); + xhr.onreadystatechange = function() { + if (this.readyState !== 4) { + return; + } + try { + if (this.status === 200) { + success(JSON.parse(xhr.responseText)); + } else { + error(this.status, new Error(xhr.statusText)); + } + } catch (err) { + error(this.status, err); + } + }; + xhr.open('GET', config.typo3BaseUrl + (config.typo3BaseUrl.indexOf('?') === -1 ? '?' : '&' ) + 'name=' + encodeURIComponent(name)); + xhr.send(); + }; + + /** + * Adds aspects to RequireJS configuration keys paths and packages. + */ + const addToConfiguration = function(config: RequireConfig, data: Partial<RequireConfig>, context: InternalRequireContext) { + if (data.shim && data.shim instanceof Object) { + if (typeof config.shim === 'undefined') { + config.shim = {}; + } + Object.keys(data.shim).forEach(function(moduleName) { + config.shim[moduleName] = data.shim[moduleName]; + }); + } + if (data.paths && data.paths instanceof Object) { + if (typeof config.paths === 'undefined') { + config.paths = {}; + } + Object.keys(data.paths).forEach(function(moduleName) { + config.paths[moduleName] = data.paths[moduleName]; + }); + } + if (data.packages && data.packages instanceof Array) { + if (typeof config.packages === 'undefined') { + config.packages = []; + } + data.packages.forEach(function (packageName) { + // Note: config.packages is defined as {} in + // node_modules/@types/requirejs/index.d.ts + // but it is an array + (config.packages as Array<string>).push(packageName); + }); + } + context.configure(config); + }; + + // keep reference to RequireJS default loader + const originalLoad = req.load; + + /** + * Fallback to importShim() after import() + * failed the first time (considering + * importmaps are not supported by the browser). + */ + let useShim = false; + + const moduleImporter = (moduleName: string): Promise<unknown> => { + if (useShim) { + return window.importShim(moduleName) + } else { + return import(moduleName).catch(() => { + // Consider that import-maps are not available and use shim from now on + useShim = true; + return moduleImporter(moduleName) + }) + } + }; + + const importMap: Record<string, unknown> = (() => { + try { + return JSON.parse(document.querySelector('script[type="importmap"]').innerHTML).imports || {}; + } catch (e) { + return {} + } + })(); + + const isDefinedInImportMap = (moduleName: string): boolean => { + if (moduleName in importMap) { + return true + } + + const moduleParts = moduleName.split('/'); + for (let i = 1; i < moduleParts.length; ++i) { + const prefix = moduleParts.slice(0, i).join('/') + '/'; + if (prefix in importMap) { + return true + } + } + + return false; + } + + /** + * Does the request to load a module for the browser case. + * Make this a separate function to allow other environments + * to override it. + * + * @param {Object} context the require context to find state. + * @param {String} name the name of the module. + * @param {Object} url the URL to the module. + */ + req.load = function(context: InternalRequireContext, name: string, url: string): void { + + /* Shim to load module via ES6 if available, fallback to original loading otherwise */ + const esmName = name in importMap ? name : name.replace(/^TYPO3\/CMS\//, '@typo3/').replace(/[A-Z]+/g, str => '-' + str.toLowerCase()).replace(/(\/|^)-/g, '$1') + '.js'; + if (isDefinedInImportMap(esmName)) { + const importPromise = moduleImporter(esmName); + importPromise.catch(function(e) { + const error = new Error('Failed to load ES6 module ' + esmName) as RequireError; + (error as any).contextName = context.contextName; + error.requireModules = [name]; + error.originalError = e; + context.onError(error); + }); + importPromise.then(function(module) { + define(name, function() { + return typeof module === 'object' && 'default' in module ? module.default : module; + }); + context.completeLoad(name); + }); + return; + } + + if ( + inPath(context.config, name) || + url.charAt(0) === '/' || + (context.config.typo3BaseUrl as unknown as boolean) === false + ) { + originalLoad.call(req, context, name, url); + return; + } + + fetchConfiguration( + context.config, + name, + function(data) { + addToConfiguration(context.config, data, context); + url = context.nameToUrl(name); + // result cannot be returned since nested in two asynchronous calls + originalLoad.call(req, context, name, url); + }, + function(status, err) { + const error = new Error('requirejs fetchConfiguration for ' + name + ' failed [' + status + ']') as RequireError; + (error as any).contextName = context.contextName; + error.requireModules = [name]; + error.originalError = err; + context.onError(error); + } + ); + }; +})(window.requirejs); diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Frontend/DatePicker.js b/Build/Sources/TypeScript/form/frontend/date-picker.ts similarity index 52% rename from typo3/sysext/form/Resources/Public/JavaScript/Frontend/DatePicker.js rename to Build/Sources/TypeScript/form/frontend/date-picker.ts index 17197c8bfa5c..b077c6a03992 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Frontend/DatePicker.js +++ b/Build/Sources/TypeScript/form/frontend/date-picker.ts @@ -18,16 +18,19 @@ * * Scope: frontend */ -if ('undefined' !== typeof $) { - $(function() { - $('input[data-t3-form-datepicker]').each(function () { - $(this).datepicker({ - dateFormat: $(this).data('format') - }).on('keydown', function(e) { +interface JQuery { // eslint-disable-line @typescript-eslint/no-unused-vars + datepicker(optionsOrInstruction: object | string, value?: string): JQuery; +} +if (typeof $ !== 'undefined') { // eslint-disable-line no-restricted-globals + $(function(jQuery: JQueryStatic) { // eslint-disable-line no-restricted-globals + jQuery('input[data-t3-form-datepicker]').each(function (this: HTMLInputElement) { + jQuery(this).datepicker({ + dateFormat: jQuery(this).data('format') + }).on('keydown', function(this: HTMLInputElement, e: JQueryEventObject) { // By using "backspace" or "delete", you can clear the datepicker again. - if(e.keyCode === 8 || e.keyCode === 46) { + if (e.keyCode === 8 || e.keyCode === 46) { e.preventDefault(); - $(this).datepicker('setDate', ''); + jQuery(this).datepicker('setDate', ''); } }); }); diff --git a/Build/Sources/TypeScript/frontend/default_frontend.ts b/Build/Sources/TypeScript/frontend/default_frontend.ts new file mode 100644 index 000000000000..ebb1fe729ed4 --- /dev/null +++ b/Build/Sources/TypeScript/frontend/default_frontend.ts @@ -0,0 +1,82 @@ +/* + * 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! + */ +(function() { + function decryptCharcode(charCode: number, start: number, end: number, offset: number): string { + charCode = charCode + offset; + if (offset > 0 && charCode > end) { + charCode = start + (charCode - end - 1); + } else if (offset < 0 && charCode < start) { + charCode = end - (start - charCode - 1); + } + return String.fromCharCode(charCode); + } + + function decryptString(value: string, offset: number): string { + let result = ''; + for (let i = 0; i < value.length; i++) { + const charCode = value.charCodeAt(i); + if (charCode >= 0x2B && charCode <= 0x3A) { + result += decryptCharcode(charCode,0x2B,0x3A,offset); /* 0-9 . , - + / : */ + } else if (charCode >= 0x40 && charCode <= 0x5A) { + result += decryptCharcode(charCode,0x40,0x5A,offset); /* A-Z @ */ + } else if (charCode >= 0x61 && charCode <= 0x7A) { + result += decryptCharcode(charCode,0x61,0x7A,offset); /* a-z */ + } else { + result += value.charAt(i); + } + } + return result; + } + + function windowOpen(url: string, target: string | null, features: string | null): Window { + const windowRef = window.open(url, target, features); + if (windowRef) { + windowRef.focus(); + } + return windowRef; + } + + function delegateEvent( + event: string, + selector: string, + callback: (evt: Event, targetElement: HTMLElement) => void + ): void { + document.addEventListener(event, function(evt: Event) { + for (let node = evt.target as Node; node; node = node.parentNode !== document ? node.parentNode : null) { + if ('matches' in node) { + const targetElement = node as HTMLElement; + if (targetElement.matches(selector)) { + callback(evt, targetElement); + } + } + } + }); + } + + delegateEvent('click', 'a[data-mailto-token][data-mailto-vector]', function(evt: Event, evtTarget: HTMLElement) { + evt.preventDefault(); + const dataset = evtTarget.dataset; + const value = dataset.mailtoToken; + const offset = parseInt(dataset.mailtoVector, 10) * -1; + document.location.href = decryptString(value, offset); + }); + + delegateEvent('click', 'a[data-window-url]', function(evt: Event, evtTarget: HTMLElement) { + evt.preventDefault(); + const dataset = evtTarget.dataset; + const url = dataset.windowUrl; + const target = dataset.windowTarget || null; + const features = dataset.windowFeatures || null; + windowOpen(url, target, features); + }); +})(); diff --git a/Build/Sources/TypeScript/install/init-install.ts b/Build/Sources/TypeScript/install/init-install.ts index 8885d773aff1..bf3cf81311ab 100644 --- a/Build/Sources/TypeScript/install/init-install.ts +++ b/Build/Sources/TypeScript/install/init-install.ts @@ -1,2 +1,2 @@ self.TYPO3 = <typeof TYPO3>{}; -(window as any).importShim('@typo3/install/install.js'); +window.importShim('@typo3/install/install.js'); diff --git a/Build/Sources/TypeScript/install/init-installer.ts b/Build/Sources/TypeScript/install/init-installer.ts index 86ed81a7a44b..5c54c5b2dcd7 100644 --- a/Build/Sources/TypeScript/install/init-installer.ts +++ b/Build/Sources/TypeScript/install/init-installer.ts @@ -1,2 +1,2 @@ self.TYPO3 = <typeof TYPO3>{}; -(window as any).importShim('@typo3/install/installer.js'); +window.importShim('@typo3/install/installer.js'); diff --git a/Build/tsconfig.json b/Build/tsconfig.json index e2858cd18dda..414a7a711126 100644 --- a/Build/tsconfig.json +++ b/Build/tsconfig.json @@ -45,6 +45,9 @@ "@typo3/form/*": [ "form/*" ], + "@typo3/frontend/*": [ + "frontend/*" + ], "@typo3/impexp/*": [ "impexp/*" ], diff --git a/Build/types/TYPO3/index.d.ts b/Build/types/TYPO3/index.d.ts index 0b5ecbdf91c3..c8924a4c59f9 100644 --- a/Build/types/TYPO3/index.d.ts +++ b/Build/types/TYPO3/index.d.ts @@ -76,6 +76,7 @@ interface Window { require: (moduleName: string) => void; list_frame: Window; CKEditorInspector: any; + importShim(moduleName: string): Promise<unknown>; } /** diff --git a/typo3/sysext/backend/Resources/Public/Html/Close.html b/typo3/sysext/backend/Resources/Public/Html/Close.html index 9a266b1e269d..e5b39a0139dd 100644 --- a/typo3/sysext/backend/Resources/Public/Html/Close.html +++ b/typo3/sysext/backend/Resources/Public/Html/Close.html @@ -5,7 +5,7 @@ <!-- TYPO3 Script ID: typo3/sysext/backend/Resources/Public/Html/Close.html --> <meta charset="utf-8" /> <title>Close</title> - <script src="Close.js"></script> + <script src="../JavaScript/close-current-window.js"></script> </head> <body> </body> diff --git a/typo3/sysext/backend/Resources/Public/Html/Close.js b/typo3/sysext/backend/Resources/Public/Html/Close.js deleted file mode 100644 index 8f711b568bbd..000000000000 --- a/typo3/sysext/backend/Resources/Public/Html/Close.js +++ /dev/null @@ -1,2 +0,0 @@ -self.close(); -window.opener.location.reload(true); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/close-current-window.js b/typo3/sysext/backend/Resources/Public/JavaScript/close-current-window.js new file mode 100644 index 000000000000..9c941edfc025 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/close-current-window.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +"use strict";self.close(),window.opener.location.reload(); \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/helper.js b/typo3/sysext/backend/Resources/Public/JavaScript/helper.js new file mode 100644 index 000000000000..ecd1ed5572cb --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/helper.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +"use strict";if(document.currentScript){if("window.close"===document.currentScript.dataset.action)window.close()} \ No newline at end of file diff --git a/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php b/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php index d347d56ecfe8..556a5bb8c2c9 100644 --- a/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php +++ b/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php @@ -82,7 +82,7 @@ class ReferrerEnforcer $query !== '' ? $query . '&' . $refreshParameter : $refreshParameter ); $scriptUri = $this->resolveAbsoluteWebPath( - 'EXT:core/Resources/Public/JavaScript/ReferrerRefresh.js' + 'EXT:core/Resources/Public/JavaScript/referrer-refresh.js' ); $attributes = ['src' => $scriptUri]; if ($nonce instanceof ConsumableString) { diff --git a/typo3/sysext/core/Resources/Public/JavaScript/ReferrerRefresh.js b/typo3/sysext/core/Resources/Public/JavaScript/ReferrerRefresh.js deleted file mode 100644 index 74256e3a6a5d..000000000000 --- a/typo3/sysext/core/Resources/Public/JavaScript/ReferrerRefresh.js +++ /dev/null @@ -1,2 +0,0 @@ -document.querySelectorAll('a#referrer-refresh') - .forEach((element) => element.click()); diff --git a/typo3/sysext/core/Resources/Public/JavaScript/referrer-refresh.js b/typo3/sysext/core/Resources/Public/JavaScript/referrer-refresh.js new file mode 100644 index 000000000000..d96cc37c0517 --- /dev/null +++ b/typo3/sysext/core/Resources/Public/JavaScript/referrer-refresh.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +"use strict";document.querySelectorAll("a#referrer-refresh").forEach((e=>e.click())); \ No newline at end of file diff --git a/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js b/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js index 77adf3d7b997..d98f7d4573fe 100644 --- a/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js +++ b/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js @@ -1,209 +1,13 @@ -(function(req) { - /** - * Determines whether moduleName is configured in requirejs paths - * (this code was taken from RequireJS context.nameToUrl). - * - * @see context.nameToUrl - * @see https://github.com/requirejs/requirejs/blob/2.3.3/require.js#L1650-L1670 - * - * @param {Object} config the require context to find state. - * @param {String} moduleName the name of the module. - * @return {boolean} - */ - var inPath = function(config, moduleName) { - var i, parentModule, parentPath; - var paths = config.paths; - var syms = moduleName.split('/'); - //For each module name segment, see if there is a path - //registered for it. Start with most specific name - //and work up from it. - for (i = syms.length; i > 0; i -= 1) { - parentModule = syms.slice(0, i).join('/'); - parentPath = paths[parentModule]; - if (parentPath) { - return true; - } - } - return false; - }; - - /** - * @return {XMLHttpRequest} - */ - var createXhr = function() { - if (typeof XMLHttpRequest !== 'undefined') { - return new XMLHttpRequest(); - } else { - return new ActiveXObject('Microsoft.XMLHTTP'); - } - }; - - /** - * Fetches RequireJS configuration from server via XHR call. - * - * @param {object} config - * @param {string} name - * @param {function} success - * @param {function} error - */ - var fetchConfiguration = function(config, name, success, error) { - // cannot use jQuery here which would be loaded via RequireJS... - var xhr = createXhr(); - xhr.onreadystatechange = function() { - if (this.readyState !== 4) { - return; - } - try { - if (this.status === 200) { - success(JSON.parse(xhr.responseText)); - } else { - error(this.status, new Error(xhr.statusText)); - } - } catch (err) { - error(this.status, err); - } - }; - xhr.open('GET', config.typo3BaseUrl + (config.typo3BaseUrl.indexOf('?') === -1 ? '?' : '&' ) + 'name=' + encodeURIComponent(name)); - xhr.send(); - }; - - /** - * Adds aspects to RequireJS configuration keys paths and packages. - * - * @param {object} config - * @param {string} data - * @param {object} context - */ - var addToConfiguration = function(config, data, context) { - if (data.shim && data.shim instanceof Object) { - if (typeof config.shim === 'undefined') { - config.shim = {}; - } - Object.keys(data.shim).forEach(function(moduleName) { - config.shim[moduleName] = data.shim[moduleName]; - }); - } - if (data.paths && data.paths instanceof Object) { - if (typeof config.paths === 'undefined') { - config.paths = {}; - } - Object.keys(data.paths).forEach(function(moduleName) { - config.paths[moduleName] = data.paths[moduleName]; - }); - } - if (data.packages && data.packages instanceof Array) { - if (typeof config.packages === 'undefined') { - config.packages = []; - } - data.packages.forEach(function (packageName) { - config.packages.push(packageName); - }); - } - context.configure(config); - }; - - // keep reference to RequireJS default loader - var originalLoad = req.load; - - /** - * Fallback to importShim() after import() - * failed the first time (considering - * importmaps are not supported by the browser). - */ - let useShim = false; - - const moduleImporter = (moduleName) => { - if (useShim) { - return window.importShim(moduleName) - } else { - return import(moduleName).catch(() => { - // Consider that import-maps are not available and use shim from now on - useShim = true; - return moduleImporter(moduleName) - }) - } - }; - - const importMap = (() => { - try { - return JSON.parse(document.querySelector('script[type="importmap"]').innerHTML).imports || {}; - } catch (e) { - return {} - } - })(); - - const isDefinedInImportMap = (moduleName) => { - if (moduleName in importMap) { - return true - } - - const moduleParts = moduleName.split('/'); - for (let i = 1; i < moduleParts.length; ++i) { - const prefix = moduleParts.slice(0, i).join('/') + '/'; - if (prefix in importMap) { - return true - } - } - - return false; - } - - /** - * Does the request to load a module for the browser case. - * Make this a separate function to allow other environments - * to override it. - * - * @param {Object} context the require context to find state. - * @param {String} name the name of the module. - * @param {Object} url the URL to the module. - */ - req.load = function(context, name, url) { - - /* Shim to load module via ES6 if available, fallback to original loading otherwise */ - const esmName = name in importMap ? name : name.replace(/^TYPO3\/CMS\//, '@typo3/').replace(/[A-Z]+/g, str => '-' + str.toLowerCase()).replace(/(\/|^)-/g, '$1') + '.js'; - if (isDefinedInImportMap(esmName)) { - const importPromise = moduleImporter(esmName); - importPromise.catch(function(e) { - var error = new Error('Failed to load ES6 module ' + esmName); - error.contextName = context.contextName; - error.requireModules = [name]; - error.originalError = e; - context.onError(error); - }); - importPromise.then(function(module) { - define(name, function() { - return typeof module === 'object' && 'default' in module ? module.default : module; - }); - context.completeLoad(name); - }); - return; - } - - if ( - inPath(context.config, name) || - url.charAt(0) === '/' || - context.config.typo3BaseUrl === false - ) { - originalLoad.call(req, context, name, url); - return; - } - - fetchConfiguration( - context.config, - name, - function(data) { - addToConfiguration(context.config, data, context); - url = context.nameToUrl(name); - // result cannot be returned since nested in two asynchronous calls - originalLoad.call(req, context, name, url); - }, - function(status, err) { - var error = new Error('requirejs fetchConfiguration for ' + name + ' failed [' + status + ']'); - error.contextName = context.contextName; - error.requireModules = [name]; - error.originalError = err; - context.onError(error); - } - ); - }; -})(window.requirejs); +/* + * 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! + */ +"use strict";!function(t){const e=function(t,e,n,o){const r="undefined"!=typeof XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");r.onreadystatechange=function(){if(4===this.readyState)try{200===this.status?n(JSON.parse(r.responseText)):o(this.status,new Error(r.statusText))}catch(t){o(this.status,t)}},r.open("GET",t.typo3BaseUrl+(-1===t.typo3BaseUrl.indexOf("?")?"?":"&")+"name="+encodeURIComponent(e)),r.send()},n=t.load;let o=!1;const r=t=>o?window.importShim(t):import(t).catch((()=>(o=!0,r(t)))),i=(()=>{try{return JSON.parse(document.querySelector('script[type="importmap"]').innerHTML).imports||{}}catch(t){return{}}})();t.load=function(o,c,s){const a=c in i?c:c.replace(/^TYPO3\/CMS\//,"@typo3/").replace(/[A-Z]+/g,(t=>"-"+t.toLowerCase())).replace(/(\/|^)-/g,"$1")+".js";if((t=>{if(t in i)return!0;const e=t.split("/");for(let t=1;t<e.length;++t)if(e.slice(0,t).join("/")+"/"in i)return!0;return!1})(a)){const t=r(a);return t.catch((function(t){const e=new Error("Failed to load ES6 module "+a);e.contextName=o.contextName,e.requireModules=[c],e.originalError=t,o.onError(e)})),void t.then((function(t){define(c,(function(){return"object"==typeof t&&"default"in t?t.default:t})),o.completeLoad(c)}))}!function(t,e){let n,o,r;const i=t.paths,c=e.split("/");for(n=c.length;n>0;n-=1)if(o=c.slice(0,n).join("/"),r=i[o],r)return!0;return!1}(o.config,c)&&"/"!==s.charAt(0)&&!1!==o.config.typo3BaseUrl?e(o.config,c,(function(e){!function(t,e,n){e.shim&&e.shim instanceof Object&&(void 0===t.shim&&(t.shim={}),Object.keys(e.shim).forEach((function(n){t.shim[n]=e.shim[n]}))),e.paths&&e.paths instanceof Object&&(void 0===t.paths&&(t.paths={}),Object.keys(e.paths).forEach((function(n){t.paths[n]=e.paths[n]}))),e.packages&&e.packages instanceof Array&&(void 0===t.packages&&(t.packages=[]),e.packages.forEach((function(e){t.packages.push(e)}))),n.configure(t)}(o.config,e,o),s=o.nameToUrl(c),n.call(t,o,c,s)}),(function(t,e){const n=new Error("requirejs fetchConfiguration for "+c+" failed ["+t+"]");n.contextName=o.contextName,n.requireModules=[c],n.originalError=e,o.onError(n)})):n.call(t,o,c,s)}}(window.requirejs); \ No newline at end of file diff --git a/typo3/sysext/form/Classes/ViewHelpers/Form/DatePickerViewHelper.php b/typo3/sysext/form/Classes/ViewHelpers/Form/DatePickerViewHelper.php index f7e7f659586e..48cfa3e03ad6 100644 --- a/typo3/sysext/form/Classes/ViewHelpers/Form/DatePickerViewHelper.php +++ b/typo3/sysext/form/Classes/ViewHelpers/Form/DatePickerViewHelper.php @@ -64,7 +64,7 @@ final class DatePickerViewHelper extends AbstractFormFieldViewHelper $this->registerArgument('previewMode', 'bool', 'Preview mde flag', true, false); $this->registerArgument('dateFormat', 'string', 'The date format', false, 'Y-m-d'); // use the default value if custom templates have not yet adapted this property - $this->registerArgument('datePickerInitializationJavaScriptFile', 'string', 'The JavaScript file to initialize the date picker', false, 'EXT:form/Resources/Public/JavaScript/Frontend/DatePicker.js'); + $this->registerArgument('datePickerInitializationJavaScriptFile', 'string', 'The JavaScript file to initialize the date picker', false, 'EXT:form/Resources/Public/JavaScript/frontend/date-picker.js'); $this->registerUniversalTagAttributes(); } diff --git a/typo3/sysext/form/Configuration/Yaml/FormElements/DatePicker.yaml b/typo3/sysext/form/Configuration/Yaml/FormElements/DatePicker.yaml index 23b7d2879d7b..bcda8c8d72a2 100644 --- a/typo3/sysext/form/Configuration/Yaml/FormElements/DatePicker.yaml +++ b/typo3/sysext/form/Configuration/Yaml/FormElements/DatePicker.yaml @@ -136,7 +136,7 @@ prototypes: timeSelectorMinuteLabel: '' dateFormat: Y-m-d # consider using a different file if the backend is behind an http basic auth - datePickerInitializationJavaScriptFile: EXT:form/Resources/Public/JavaScript/Frontend/DatePicker.js + datePickerInitializationJavaScriptFile: EXT:form/Resources/Public/JavaScript/frontend/date-picker.js enableDatePicker: true displayTimeSelector: false variants: diff --git a/typo3/sysext/form/Resources/Public/JavaScript/frontend/date-picker.js b/typo3/sysext/form/Resources/Public/JavaScript/frontend/date-picker.js new file mode 100644 index 000000000000..b1f0b5df76f6 --- /dev/null +++ b/typo3/sysext/form/Resources/Public/JavaScript/frontend/date-picker.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +"use strict";"undefined"!=typeof $&&$((function(t){t("input[data-t3-form-datepicker]").each((function(){t(this).datepicker({dateFormat:t(this).data("format")}).on("keydown",(function(e){8!==e.keyCode&&46!==e.keyCode||(e.preventDefault(),t(this).datepicker("setDate",""))}))}))})); \ No newline at end of file diff --git a/typo3/sysext/frontend/Resources/Public/JavaScript/default_frontend.js b/typo3/sysext/frontend/Resources/Public/JavaScript/default_frontend.js index 1e360c88d769..324add6268ea 100644 --- a/typo3/sysext/frontend/Resources/Public/JavaScript/default_frontend.js +++ b/typo3/sysext/frontend/Resources/Public/JavaScript/default_frontend.js @@ -1,93 +1,13 @@ -(function() { - /** - * Decoding helper function - * - * @param {number} charCode - * @param {number} start - * @param {number} end - * @param {number} offset - * @return {string} - */ - function decryptCharcode(charCode, start, end, offset) { - charCode = charCode + offset; - if (offset > 0 && charCode > end) { - charCode = start + (charCode - end - 1); - } else if (offset < 0 && charCode < start) { - charCode = end - (start - charCode - 1); - } - return String.fromCharCode(charCode); - } - /** - * Decodes string - * - * @param {string} value - * @param {number} offset - * @return {string} - */ - function decryptString(value, offset) { - var result = ''; - for (var i=0; i < value.length; i++) { - var charCode = value.charCodeAt(i); - if (charCode >= 0x2B && charCode <= 0x3A) { - result += decryptCharcode(charCode,0x2B,0x3A,offset); /* 0-9 . , - + / : */ - } else if (charCode >= 0x40 && charCode <= 0x5A) { - result += decryptCharcode(charCode,0x40,0x5A,offset); /* A-Z @ */ - } else if (charCode >= 0x61 && charCode <= 0x7A) { - result += decryptCharcode(charCode,0x61,0x7A,offset); /* a-z */ - } else { - result += value.charAt(i); - } - } - return result; - } - - /** - * Opens URL in new window. - * - * @param {string} url - * @param {string|null} target - * @param {string|null} features - * @return {Window} - */ - function windowOpen(url, target, features) { - var windowRef = window.open(url, target, features); - if (windowRef) { - windowRef.focus(); - } - return windowRef; - } - - /** - * Delegates event handling to elements - * - * @param {string} event - * @param {string} selector - * @param {function} callback - */ - function delegateEvent(event, selector, callback) { - document.addEventListener(event, function(evt) { - for (var targetElement = evt.target; targetElement && targetElement !== document; targetElement = targetElement.parentNode) { - if (targetElement.matches(selector)) { - callback.call(targetElement, evt, targetElement); - } - } - }); - } - - delegateEvent('click', 'a[data-mailto-token][data-mailto-vector]', function(evt, evtTarget) { - evt.preventDefault(); - var dataset = evtTarget.dataset; - var value = dataset.mailtoToken; - var offset = parseInt(dataset.mailtoVector, 10) * -1; - document.location.href = decryptString(value, offset); - }); - - delegateEvent('click', 'a[data-window-url]', function(evt, evtTarget) { - evt.preventDefault(); - var dataset = evtTarget.dataset; - var url = dataset.windowUrl; - var target = dataset.windowTarget || null; - var features = dataset.windowFeatures || null; - windowOpen(url, target, features); - }); -})(); +/* + * 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! + */ +"use strict";!function(){function t(t,n,e,o){return t+=o,o>0&&t>e?t=n+(t-e-1):o<0&&t<n&&(t=e-(n-t-1)),String.fromCharCode(t)}function n(t,n,e){document.addEventListener(t,(function(t){for(let o=t.target;o;o=o.parentNode!==document?o.parentNode:null)if("matches"in o){const a=o;a.matches(n)&&e(t,a)}}))}n("click","a[data-mailto-token][data-mailto-vector]",(function(n,e){n.preventDefault();const o=e.dataset,a=o.mailtoToken,c=-1*parseInt(o.mailtoVector,10);document.location.href=function(n,e){let o="";for(let a=0;a<n.length;a++){const c=n.charCodeAt(a);o+=c>=43&&c<=58?t(c,43,58,e):c>=64&&c<=90?t(c,64,90,e):c>=97&&c<=122?t(c,97,122,e):n.charAt(a)}return o}(a,c)})),n("click","a[data-window-url]",(function(t,n){t.preventDefault();const e=n.dataset;!function(t,n,e){const o=window.open(t,n,e);o&&o.focus()}(e.windowUrl,e.windowTarget||null,e.windowFeatures||null)}))}(); \ No newline at end of file -- GitLab