From 3389dc60567acdcef8cb2016e22388a4bcf826e0 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Sat, 10 Oct 2015 09:34:17 +0200
Subject: [PATCH] [FEATURE] JavaScript-based icon API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A new JavaScript-based icon API is introduced with this patch.
As first step, some spinner icons used in the topbar items are
replaced with the icon API.

Resolves: #70583
Releases: master
Change-Id: I24c0649df5ddce2eb8f3191137a01fa7db29209a
Reviewed-on: http://review.typo3.org/43961
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
---
 .../Configuration/Backend/AjaxRoutes.php      |   6 +
 .../Resources/Public/JavaScript/Icons.js      | 157 ++++++++++++++++++
 .../JavaScript/Toolbar/ClearCacheMenu.js      |  18 +-
 .../Public/JavaScript/Toolbar/ShortcutMenu.js |  18 +-
 .../Toolbar/SystemInformationMenu.js          |  18 +-
 .../core/Classes/Imaging/IconFactory.php      |  34 ++++
 ...re-70583-IntroducedIconAPIInJavaScript.rst |  67 ++++++++
 .../Public/JavaScript/Toolbar/OpendocsMenu.js |  17 +-
 8 files changed, 299 insertions(+), 36 deletions(-)
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Icons.js
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-70583-IntroducedIconAPIInJavaScript.rst

diff --git a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
index c7977e3708af..6d0f6886ff5a 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 000000000000..5abcbd71facb
--- /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 1a599a7882e2..35e5715508b0 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 0ce6b7b4cb85..a52053513eb5 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 02a3f048859b..8461cc55d87e 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 fdd4a79f7d17..a10b1dd09f08 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 000000000000..8f725621adec
--- /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 aa22dffc4bd1..442400fcbbb1 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);
 			}
 		});
 	};
-- 
GitLab