diff --git a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php index c7977e3708afd343a1ee4a23f479b4312bdae925..6d0f6886ff5a04c3cc349bd1e5c87eafeebb7445 100644 --- a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php +++ b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php @@ -186,5 +186,11 @@ return [ 'online_media_create' => [ 'path' => '/online-media/create', 'target' => Controller\OnlineMediaController::class . '::createAction' + ], + + // Get icon from IconFactory + 'icons_get' => [ + 'path' => '/icons/get', + 'target' => \TYPO3\CMS\Core\Imaging\IconFactory::class . '::processAjaxRequest' ] ]; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Icons.js b/typo3/sysext/backend/Resources/Public/JavaScript/Icons.js new file mode 100644 index 0000000000000000000000000000000000000000..5abcbd71facb722eb769b18e49b5b69ad38efcee --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Icons.js @@ -0,0 +1,157 @@ +/* + * 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 DocumentHeader source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Uses the icon API of the core to fetch icons via AJAX. + */ +define(['jquery'], function($) { + 'use strict'; + + var Icons = { + cache: {}, + sizes: { + small: 'small', + default: 'default', + large: 'large', + overlay: 'overlay' + }, + states: { + default: 'default', + disabled: 'disabled' + } + }; + + /** + * Get the icon by its identifier. + * + * @param {string} identifier + * @param {string} size + * @param {string} overlayIdentifier + * @param {string} state + * @return {Promise<Array>} + */ + Icons.getIcon = function(identifier, size, overlayIdentifier, state) { + return $.when.apply($, Icons.fetch([[identifier, size, overlayIdentifier, state]])); + }; + + /** + * Fetches multiple icons by passing the parameters of getIcon() for each requested + * icon as array. + * + * @param {Array} icons + * @return {Promise<Array>} + */ + Icons.getIcons = function(icons) { + if (!icons instanceof Array) { + throw 'Icons must be an array of multiple definitions.'; + } + return $.when.apply($, Icons.fetch(icons)); + }; + + /** + * Performs the AJAX request to fetch the icon. + * + * @param {Array} icons + * @return {Array} + * @private + */ + Icons.fetch = function(icons) { + var promises = [], + requestedIcons = {}; + + for (var i = 0; i < icons.length; ++i) { + /** + * Icon keys: + * + * 0: identifier + * 1: size + * 2: overlayIdentifier + * 3: state + */ + var icon = icons[i]; + icon[1] = icon[1] || Icons.sizes.default; + icon[3] = icon[3] || Icons.states.default; + + var cacheIdentifier = icon.join('_'); + if (Icons.isCached(cacheIdentifier)) { + promises.push(Icons.getFromCache(cacheIdentifier)); + } else { + requestedIcons[icon[0]] = { + cacheIdentifier: cacheIdentifier, + icon: icon + }; + } + } + + if (Object.keys(requestedIcons).length > 0) { + promises.push( + $.ajax({ + url: TYPO3.settings.ajaxUrls['icons_get'], + data: { + requestedIcons: JSON.stringify( + $.map(requestedIcons, function(o) { + return [o['icon']]; + }) + ) + }, + success: function(data) { + $.each(data, function(identifier, markup) { + var cacheIdentifier = requestedIcons[identifier].cacheIdentifier, + cacheEntry = {}; + + cacheEntry[identifier] = markup; + Icons.putInCache(cacheIdentifier, cacheEntry); + }); + } + }) + ); + } + + return promises; + }; + + /** + * Check whether icon was fetched already + * + * @param {String} cacheIdentifier + * @returns {Boolean} + * @private + */ + Icons.isCached = function(cacheIdentifier) { + return typeof Icons.cache[cacheIdentifier] !== 'undefined'; + }; + + /** + * Get icon from cache + * + * @param {String} cacheIdentifier + * @returns {String} + * @private + */ + Icons.getFromCache = function(cacheIdentifier) { + return Icons.cache[cacheIdentifier]; + }; + + /** + * Put icon into cache + * + * @param {String} cacheIdentifier + * @param {Object} markup + * @private + */ + Icons.putInCache = function(cacheIdentifier, markup) { + Icons.cache[cacheIdentifier] = markup; + }; + + return Icons; +}); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ClearCacheMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ClearCacheMenu.js index 1a599a7882e246c9f4a29d7466f5f6a5bd88b17b..35e5715508b02a73ab5722bb0dbcc7685b164a53 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ClearCacheMenu.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ClearCacheMenu.js @@ -15,16 +15,13 @@ * main functionality for clearing caches via the top bar * reloading the clear cache icon */ -define(['jquery'], function($) { +define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) { var ClearCacheMenu = { - $spinnerElement: $('<span>', { - 'class': 'fa fa-circle-o-notch fa-spin' - }), options: { containerSelector: '#typo3-cms-backend-backend-toolbaritems-clearcachetoolbaritem', menuItemSelector: '.dropdown-menu a', - toolbarIconSelector: '.dropdown-toggle i.fa' + toolbarIconSelector: '.dropdown-toggle span.icon' } }; @@ -52,16 +49,19 @@ define(['jquery'], function($) { // Close clear cache menu $(ClearCacheMenu.options.containerSelector).removeClass('open'); - var $toolbarItemIcon = $(ClearCacheMenu.options.toolbarIconSelector, ClearCacheMenu.options.containerSelector); + var $toolbarItemIcon = $(ClearCacheMenu.options.toolbarIconSelector, ClearCacheMenu.options.containerSelector), + $existingIcon = $toolbarItemIcon.clone(); + + Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(icons) { + $toolbarItemIcon.replaceWith(icons['spinner-circle-light']); + }); - var $spinnerIcon = ClearCacheMenu.$spinnerElement.clone(); - var $existingIcon = $toolbarItemIcon.replaceWith($spinnerIcon); $.ajax({ url: ajaxUrl, type: 'post', cache: false, success: function() { - $spinnerIcon.replaceWith($existingIcon); + $(ClearCacheMenu.options.toolbarIconSelector, ClearCacheMenu.options.containerSelector).replaceWith($existingIcon); } }); }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js index 0ce6b7b4cb85db78d5915ca3d0ed35c5c8971f0c..a52053513eb521fca5ef8d0be0bc0dddf95a62e5 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js @@ -15,15 +15,12 @@ * shortcut menu logic to add new shortcut, remove a shortcut * and edit a shortcut */ -define(['jquery', 'TYPO3/CMS/Backend/Modal'], function($, Modal) { +define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Icons'], function($, Modal, Icons) { var ShortcutMenu = { - $spinnerElement: $('<span>', { - class: 'fa fa-circle-o-notch fa-spin' - }), options: { containerSelector: '#typo3-cms-backend-backend-toolbaritems-shortcuttoolbaritem', - toolbarIconSelector: '.dropdown-toggle .fa', + toolbarIconSelector: '.dropdown-toggle span.icon', toolbarMenuSelector: '.dropdown-menu', shortcutItemSelector: '.dropdown-menu .shortcut', shortcutDeleteSelector: '.shortcut-delete', @@ -107,9 +104,12 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal'], function($, Modal) { // @todo: translations Modal.confirm('Create bookmark', confirmationText) .on('confirm.button.ok', function() { - var $toolbarItemIcon = $(ShortcutMenu.options.toolbarIconSelector, ShortcutMenu.options.containerSelector); - var $spinner = ShortcutMenu.$spinnerElement.clone(); - var $existingItem = $toolbarItemIcon.replaceWith($spinner); + var $toolbarItemIcon = $(ShortcutMenu.options.toolbarIconSelector, ShortcutMenu.options.containerSelector), + $existingIcon = $toolbarItemIcon.clone(); + + Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(icons) { + $toolbarItemIcon.replaceWith(icons['spinner-circle-light']); + }); $.ajax({ url: TYPO3.settings.ajaxUrls['shortcut_create'], @@ -122,7 +122,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal'], function($, Modal) { cache: false }).done(function() { ShortcutMenu.refreshMenu(); - $spinner.replaceWith($existingItem); + $(ShortcutMenu.options.toolbarIconSelector, ShortcutMenu.options.containerSelector).replaceWith($existingIcon); if (typeof shortcutButton === 'object') { $(shortcutButton).addClass('active'); $(shortcutButton).attr('title', null); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/SystemInformationMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/SystemInformationMenu.js index 02a3f048859b8d2054644b24420711180263edd1..8461cc55d87ef09d6f9fce28a64d65d8b8b1814a 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/SystemInformationMenu.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/SystemInformationMenu.js @@ -14,21 +14,18 @@ /** * System information menu handler */ -define(['jquery', 'TYPO3/CMS/Backend/Storage'], function($, Storage) { +define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Storage'], function($, Icons, Storage) { 'use strict'; var SystemInformationMenu = { identifier: { containerSelector: '#typo3-cms-backend-backend-toolbaritems-systeminformationtoolbaritem', - toolbarIconSelector: '.dropdown-toggle span.t3-icon', + toolbarIconSelector: '.dropdown-toggle span.icon', menuContainerSelector: '.dropdown-menu', moduleLinks: '.t3js-systeminformation-module' }, elements: { - $counter: $('#t3js-systeminformation-counter'), - $spinnerElement: $('<span>', { - 'class': 't3-icon fa fa-circle-o-notch spinner fa-spin' - }) + $counter: $('#t3js-systeminformation-counter') } }; @@ -44,8 +41,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Storage'], function($, Storage) { */ SystemInformationMenu.updateMenu = function() { var $toolbarItemIcon = $(SystemInformationMenu.identifier.toolbarIconSelector, SystemInformationMenu.identifier.containerSelector), - $spinnerIcon = SystemInformationMenu.elements.$spinnerElement.clone(), - $existingIcon = $toolbarItemIcon.replaceWith($spinnerIcon), + $existingIcon = $toolbarItemIcon.clone(), $menuContainer = $(SystemInformationMenu.identifier.containerSelector).find(SystemInformationMenu.identifier.menuContainerSelector); // hide the menu if it's active @@ -53,6 +49,10 @@ define(['jquery', 'TYPO3/CMS/Backend/Storage'], function($, Storage) { $menuContainer.click(); } + Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(icons) { + $toolbarItemIcon.replaceWith(icons['spinner-circle-light']); + }); + $.ajax({ url: TYPO3.settings.ajaxUrls['systeminformation_render'], type: 'post', @@ -60,7 +60,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Storage'], function($, Storage) { success: function(data) { $menuContainer.html(data); SystemInformationMenu.updateCounter(); - $spinnerIcon.replaceWith($existingIcon); + $(SystemInformationMenu.identifier.toolbarIconSelector, SystemInformationMenu.identifier.containerSelector).replaceWith($existingIcon); SystemInformationMenu.initialize(); } diff --git a/typo3/sysext/core/Classes/Imaging/IconFactory.php b/typo3/sysext/core/Classes/Imaging/IconFactory.php index fdd4a79f7d1771461970052617139d46eaf72c2e..a10b1dd09f08c705cdc3b772265b9807f39b2d7a 100644 --- a/typo3/sysext/core/Classes/Imaging/IconFactory.php +++ b/typo3/sysext/core/Classes/Imaging/IconFactory.php @@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Imaging; * The TYPO3 project - inspiring people to share! */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\FolderInterface; use TYPO3\CMS\Core\Resource\InaccessibleFolder; @@ -74,6 +76,38 @@ class IconFactory $this->iconRegistry = $iconRegistry ? $iconRegistry : GeneralUtility::makeInstance(IconRegistry::class); } + /** + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return string + * @internal + */ + public function processAjaxRequest(ServerRequestInterface $request, ResponseInterface $response) + { + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + $requestedIcons = json_decode( + isset($parsedBody['requestedIcons']) + ? $parsedBody['requestedIcons'] + : $queryParams['requestedIcons'], + true + ); + + $icons = []; + for ($i = 0, $count = count($requestedIcons); $i < $count; ++$i) { + list($identifier, $size, $overlayIdentifier, $iconState) = $requestedIcons[$i]; + if (empty($overlayIdentifier)) { + $overlayIdentifier = null; + } + $iconState = IconState::cast($iconState); + $icons[$identifier] = $this->getIcon($identifier, $size, $overlayIdentifier, $iconState)->render(); + } + $response->getBody()->write( + json_encode($icons) + ); + return $response; + } + /** * @param string $identifier * @param string $size "large", "small" or "default", see the constants of the Icon class diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-70583-IntroducedIconAPIInJavaScript.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-70583-IntroducedIconAPIInJavaScript.rst new file mode 100644 index 0000000000000000000000000000000000000000..8f725621adec7562b9ec7d61d7e5660840d2a614 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-70583-IntroducedIconAPIInJavaScript.rst @@ -0,0 +1,67 @@ +=================================================== +Feature: #70583 - Introduced Icon API in JavaScript +=================================================== + +Description +=========== + +A JavaScript-based icon API based on the PHP API has been introduced. The methods ``getIcon()`` +and ``getIcons()`` can be called in an RequireJS module. + +When imported in a RequireJS module, a developer can fetch icons via JavaScript with the same parameters as in PHP. +The methods ``getIcon()`` and ``getIcons()`` return ``Promise`` objects. + +Importing +========= + +.. code-block:: javascript + + define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) { + }); + + +Get icons +========= + +A single icon can be fetched by ``getIcon()`` which takes four parameters: + +.. container:: table-row + + identifier + The icon identifier. + + size + The size of the icon. Please use the properties of the ``Icons.sizes`` object. + + overlayIdentifier + An overlay identifier rendered on the icon. + + state + The state of the icon. Please use the properties of the ``Icons.states`` object. + + +Multiple icons can be fetched by ``getIcons()``. This function takes a multidimensional array as parameter, +holding the parameters used by ``getIcon()`` for each icon. + +To use the fetched icons, chain the ``done()`` method to the promise. + +Examples +-------- + +.. code-block:: javascript + + // Get a single icon + Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(icons) { + $toolbarItemIcon.replaceWith(icons['spinner-circle-light']); + }); + + // Get multiple icons + Icons.getIcons([ + ['apps-filetree-folder-default', Icons.sizes.large], + ['actions-edit-delete', Icons.sizes.small, null, Icons.states.disabled], + ['actions-system-cache-clear-impact-medium'] + ]).done(function(icons) { + // icons['apps-filetree-folder-default'] + // icons['actions-edit-delete'] + // icons['actions-system-cache-clear-impact-medium'] + }); diff --git a/typo3/sysext/opendocs/Resources/Public/JavaScript/Toolbar/OpendocsMenu.js b/typo3/sysext/opendocs/Resources/Public/JavaScript/Toolbar/OpendocsMenu.js index aa22dffc4bd100307cd638b248296dfa9b2e3cf8..442400fcbbb19fa3c5b28a71df1861ddac3621d9 100644 --- a/typo3/sysext/opendocs/Resources/Public/JavaScript/Toolbar/OpendocsMenu.js +++ b/typo3/sysext/opendocs/Resources/Public/JavaScript/Toolbar/OpendocsMenu.js @@ -16,19 +16,16 @@ * - navigating to the documents * - updating the menu */ -define(['jquery'], function($) { +define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) { var OpendocsMenu = { - $spinnerElement: $('<span>', { - 'class': 'fa fa-circle-o-notch fa-spin' - }), options: { containerSelector: '#typo3-cms-opendocs-backend-toolbaritems-opendocstoolbaritem', hashDataAttributeName: 'opendocsidentifier', closeSelector: '.dropdown-list-link-close', menuContainerSelector: '.dropdown-menu', menuItemSelector: '.dropdown-menu li a', - toolbarIconSelector: '.dropdown-toggle i.fa', + toolbarIconSelector: '.dropdown-toggle span.icon', openDocumentsItemsSelector: 'li.opendoc', counterSelector: '#tx-opendocs-counter' } @@ -52,10 +49,12 @@ define(['jquery'], function($) { * Displays the menu and does the AJAX call to the TYPO3 backend */ OpendocsMenu.updateMenu = function() { - var $toolbarItemIcon = $(OpendocsMenu.options.toolbarIconSelector, OpendocsMenu.options.containerSelector); + var $toolbarItemIcon = $(OpendocsMenu.options.toolbarIconSelector, OpendocsMenu.options.containerSelector), + $existingIcon = $toolbarItemIcon.clone(); - var $spinnerIcon = OpendocsMenu.$spinnerElement.clone(); - var $existingIcon = $toolbarItemIcon.replaceWith($spinnerIcon); + Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function(icons) { + $toolbarItemIcon.replaceWith(icons['spinner-circle-light']); + }); $.ajax({ url: TYPO3.settings.ajaxUrls['opendocs_menu'], @@ -64,7 +63,7 @@ define(['jquery'], function($) { success: function(data) { $(OpendocsMenu.options.containerSelector).find(OpendocsMenu.options.menuContainerSelector).html(data); OpendocsMenu.updateNumberOfDocs(); - $spinnerIcon.replaceWith($existingIcon); + $(OpendocsMenu.options.toolbarIconSelector, OpendocsMenu.options.containerSelector).replaceWith($existingIcon); } }); };