diff --git a/CHANGELOG.md b/CHANGELOG.md index 413c4733f1..454add0417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ - [Composer] Added `phpoffice/phpspreadsheet` requirement (which got moved out from `pimcore/pimcore`) and extended support to `v2`. - [Date & date/time fields] Date & date/time fields are now configured with `date` and `datetime` column type by default. - [Date/time fields] Date/time fields now support the usage without timezone support. - +- [Icons] Overhauled Icon library and icon dropdown selector in class definition editor. + #### v1.4.0 - [DataObject] Password data type algorithms other than `password_hash` are deprecated since `pimcore/pimcore:^11.2` and will be removed in `pimcore/pimcore:^12`. diff --git a/public/js/pimcore/iconlibrary.js b/public/js/pimcore/iconlibrary.js new file mode 100644 index 0000000000..0d7243e658 --- /dev/null +++ b/public/js/pimcore/iconlibrary.js @@ -0,0 +1,87 @@ +/** + * Pimcore + * + * This source file is available under two different licenses: + * - GNU General Public License version 3 (GPLv3) + * - Pimcore Commercial License (PCL) + * Full copyright and license information is available in + * LICENSE.md which is distributed with this source code. + * + * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org) + * @license http://www.pimcore.org/license GPLv3 and PCL + */ + +pimcore.registerNS("pimcore.iconlibrary.panel"); + +/** + * @private + */ +pimcore.iconlibrary.panel = Class.create({ + + initialize: function () { + this.getTabPanel(); + }, + + activate: function () { + const tabPanel = Ext.getCmp("pimcore_panel_tabs"); + tabPanel.setActiveItem("pimcore_iconlibrary_panel"); + }, + + getTabPanel: function () { + if (!this.panel) { + const iconLibraryTab = Ext.create('Ext.tab.Panel', { + region: 'center', + deferredRender: true, + id: "pimcore_icon_library_tabs", + hideMode: "display", + cls: "tab_panel", + height: "100%", + items: [ + { + title: t('color_icons'), + html: '', + }, + { + title: t('white_icons'), + html: '', + }, + { + title: t('twemoji'), + html: '', + }, + { + title: t('flags'), + html: '', + } + ] + }); + + this.panel = new Ext.Panel({ + id: "pimcore_iconlibrary_panel", + title: t("icon_library"), + iconCls: "pimcore_icon_icons", + border: false, + layout: 'border', + closable: true, + items: [ + iconLibraryTab + ], + }); + + const tabPanel = Ext.getCmp("pimcore_panel_tabs"); + tabPanel.add(this.panel); + + + this.panel.on("destroy", function () { + pimcore.globalmanager.remove("iconlibrary"); + }); + + pimcore.layout.refresh(); + } + + return this.panel; + }, +}); + + + diff --git a/public/js/pimcore/layout/toolbar.js b/public/js/pimcore/layout/toolbar.js index 1bab22c42a..26a4c8f15f 100644 --- a/public/js/pimcore/layout/toolbar.js +++ b/public/js/pimcore/layout/toolbar.js @@ -812,9 +812,7 @@ pimcore.layout.toolbar = Class.create({ iconCls: "pimcore_nav_icon_icons", itemId: 'pimcore_menu_settings_icon_library', text: t('icon_library'), - handler: function() { - pimcore.helpers.openGenericIframeWindow("icon-library", Routing.generate('pimcore_admin_misc_iconlist'), "pimcore_icon_icons", t("icon_library")); - } + handler: this.showIconLibrary.bind(this) }); } @@ -1366,6 +1364,16 @@ pimcore.layout.toolbar = Class.create({ } pimcore.globalmanager.add("new_notifications", new pimcore.notification.modal()); - } + }, + + showIconLibrary: function () { + try { + pimcore.globalmanager.get("iconlibrary").activate(); + } + catch (e) { + pimcore.globalmanager.add("iconlibrary", new pimcore.iconlibrary.panel()); + pimcore.globalmanager.get("iconlibrary").activate(); + } + } }); diff --git a/public/js/pimcore/object/classes/class.js b/public/js/pimcore/object/classes/class.js index e0ca2a1b8e..eddda19605 100644 --- a/public/js/pimcore/object/classes/class.js +++ b/public/js/pimcore/object/classes/class.js @@ -22,7 +22,7 @@ pimcore.object.classes.klass = Class.create({ context: "class", uploadRoute: 'pimcore_admin_dataobject_class_importclass', exportRoute: 'pimcore_admin_dataobject_class_exportclass', - + iconCss: ' left center no-repeat; text-indent: 20px', initialize: function (data, parentPanel, reopen, editorPrefix) { this.parentPanel = parentPanel; this.data = data; @@ -701,7 +701,38 @@ pimcore.object.classes.klass = Class.create({ return "Pimcore\\Model\\DataObject\\" + ucfirst(name); }; - var iconStore = new Ext.data.ArrayStore({ + const iconTypes = Ext.create('Ext.data.Store', { + fields: ['text', 'value'], + data: [ + { "text": t('color_icons'), "value": 'color' }, + { "text": t('white_icons'), "value": 'white' }, + { "text": t('twemoji') + ' (1/3)', "value": 'twemoji-1' }, + { "text": t('twemoji') + ' (2/3)', "value": 'twemoji-2' }, + { "text": t('twemoji') + ' (3/3)', "value": 'twemoji-3' }, + { "text": t('twemoji_variants') + ' (1/3)', "value": 'twemoji_variants-1' }, + { "text": t('twemoji_variants') + ' (2/3)', "value": 'twemoji_variants-2' }, + { "text": t('twemoji_variants') + ' (3/3)', "value": 'twemoji_variants-3' }, + ] + }); + + const iconTypeBox = Ext.create('Ext.form.ComboBox', { + store: iconTypes, + width: 180, + displayField: 'text', + valueField: 'value', + emptyText: t('type'), + listeners: { + select: function (elem) { + iconStore.proxy.extraParams = { + 'type' : elem.value, + classId: this.getId(), + }; + iconStore.load(); + }.bind(this) + } + }); + + const iconStore = new Ext.data.ArrayStore({ proxy: { url: Routing.generate('pimcore_admin_dataobject_class_geticons'), type: 'ajax', @@ -723,8 +754,8 @@ pimcore.object.classes.klass = Class.create({ value: this.data.icon, listeners: { "afterrender": function (el) { - el.inputEl.applyStyles("background:url(" + el.getValue() + ") right center no-repeat;"); - } + el.inputEl.applyStyles("background:url(" + el.getValue() + ")" + this.iconCss); + }.bind(this) } }); @@ -872,19 +903,31 @@ pimcore.object.classes.klass = Class.create({ labelWidth: 200 }, items: [ - iconField, + iconField + ] + }, + { + xtype: "fieldcontainer", + layout: "hbox", + fieldLabel: t("icon_tools"), + defaults: { + labelWidth: 200 + }, + items: [ + iconTypeBox, { xtype: "combobox", store: iconStore, - width: 50, + width: 75, valueField: 'value', displayField: 'text', + emptyText: t('select_type_first'), listeners: { select: function (ele, rec, idx) { - var icon = ele.container.down("#iconfield-" + this.getId()); - var newValue = rec.data.value; - icon.component.setValue(newValue); - icon.component.inputEl.applyStyles("background:url(" + newValue + ") right center no-repeat;"); + const icon = Ext.getCmp("iconfield-" + this.getId()); + const newValue = rec.data.value; + icon.setValue(newValue); + icon.inputEl.applyStyles("background:url(" + newValue + ")" + this.iconCss); return newValue; }.bind(this) } @@ -894,7 +937,7 @@ pimcore.object.classes.klass = Class.create({ xtype: "button", tooltip: t("refresh"), handler: function(iconField) { - iconField.inputEl.applyStyles("background:url(" + iconField.getValue() + ") right center no-repeat;"); + iconField.inputEl.applyStyles("background:url(" + iconField.getValue() + ")" + this.iconCss); }.bind(this, iconField) }, { @@ -902,7 +945,7 @@ pimcore.object.classes.klass = Class.create({ iconCls: "pimcore_icon_icons", text: t('icon_library'), handler: function () { - pimcore.helpers.openGenericIframeWindow("icon-library", Routing.generate('pimcore_admin_misc_iconlist'), "pimcore_icon_icons", t("icon_library")); + pimcore.globalmanager.get("layout_toolbar").showIconLibrary(); } } ] diff --git a/public/js/pimcore/object/classes/layout/layout.js b/public/js/pimcore/object/classes/layout/layout.js index d43e796ae7..7798efa68a 100644 --- a/public/js/pimcore/object/classes/layout/layout.js +++ b/public/js/pimcore/object/classes/layout/layout.js @@ -293,7 +293,7 @@ pimcore.object.classes.layout.layout = Class.create({ iconCls: "pimcore_icon_icons", text: t('icon_library'), handler: function () { - pimcore.helpers.openGenericIframeWindow("icon-library", Routing.generate('pimcore_admin_misc_iconlist'), "pimcore_icon_icons", t("icon_library")); + pimcore.globalmanager.get("layout_toolbar").showIconLibrary(); } } ] diff --git a/src/Controller/Admin/DataObject/ClassController.php b/src/Controller/Admin/DataObject/ClassController.php index d8f0b2443e..0a4bb3140f 100644 --- a/src/Controller/Admin/DataObject/ClassController.php +++ b/src/Controller/Admin/DataObject/ClassController.php @@ -1810,14 +1810,27 @@ public function getSelectOptionsUsagesAction(Request $request): Response public function getIconsAction(Request $request, EventDispatcherInterface $eventDispatcher): Response { $classId = $request->get('classId'); + $type = $request->get('type'); + + if (!$type) { + return $this->adminJson([]); + } $iconDir = PIMCORE_WEB_ROOT . '/bundles/pimcoreadmin/img'; - $classIcons = rscandir($iconDir . '/object-icons/'); - $colorIcons = rscandir($iconDir . '/flat-color-icons/'); - $twemoji = rscandir($iconDir . '/twemoji/'); - $icons = array_merge($classIcons, $colorIcons, $twemoji); + $icons = match($type) { + 'color' => rscandir($iconDir . '/flat-color-icons/'), + 'white' => rscandir($iconDir . '/flat-white-icons/'), + 'twemoji-1', 'twemoji-2', 'twemoji-3', + 'twemoji_variants-1', 'twemoji_variants-2', 'twemoji_variants-3' + => rscandir($iconDir . '/twemoji/'), + default => [] + }; + $style = ''; + if ($type === 'white') { + $style = 'background-color:#000'; + } foreach ($icons as &$icon) { $icon = str_replace(PIMCORE_WEB_ROOT, '', $icon); } @@ -1829,11 +1842,51 @@ public function getIconsAction(Request $request, EventDispatcherInterface $event $eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECT_ICONS_PRE_SEND_DATA); $icons = $event->getArgument('icons'); + $startIndex = 0; $result = []; - foreach ($icons as $icon) { + + if (str_starts_with($type, 'twemoji')) { + foreach ($icons as $index => $twemojiIcon) { + $iconBase = basename($twemojiIcon); + + // All the variants (like skin color) have a hyphen in their base name + // Here we remove/unset wheter if the selected icon type is the variant list + $explodeByHyphen = explode('-', $iconBase); + if ( + (!str_starts_with($type, 'twemoji_variants') && isset($explodeByHyphen[1])) || + (str_starts_with($type, 'twemoji_variants') && !isset($explodeByHyphen[1])) + ) { + unset($icons[$index]); + } + } + + $icons = array_values($icons); + $limit = count($icons); + + if (str_ends_with($type, '-1')) { + $limit = floor($limit / 3); + } + if (str_ends_with($type, '-2')) { + $startIndex = floor($limit / 3); + $limit = floor($limit / 3 * 2); + } + if (str_ends_with($type, '-3')) { + $startIndex = floor($limit / 3 * 2); + } + } else { + $limit = count($icons); + } + + for ($i = $startIndex; $i < $limit; $i++) { + $icon = $icons[$i]; $content = file_get_contents(PIMCORE_WEB_ROOT . $icon); $result[] = [ - 'text' => sprintf('', mime_content_type(PIMCORE_WEB_ROOT . $icon), base64_encode($content)), + 'text' => sprintf( + '', + $style, + mime_content_type(PIMCORE_WEB_ROOT . $icon), + base64_encode($content) + ), 'value' => $icon, ]; } diff --git a/src/Controller/Admin/MiscController.php b/src/Controller/Admin/MiscController.php index 14a9db38d5..01a832ed1f 100644 --- a/src/Controller/Admin/MiscController.php +++ b/src/Controller/Admin/MiscController.php @@ -297,34 +297,61 @@ public function iconListAction(Request $request, ?Profiler $profiler): Response $profiler->disable(); } + $type = $request->get('type'); $publicDir = PIMCORE_WEB_ROOT . '/bundles/pimcoreadmin'; $iconDir = $publicDir . '/img'; - $colorIcons = rscandir($iconDir . '/flat-color-icons/'); - $whiteIcons = rscandir($iconDir . '/flat-white-icons/'); - $twemoji = rscandir($iconDir . '/twemoji/'); + $extraInfo = null; + + $icons = match ($type) { + 'color' => rscandir($iconDir . '/flat-color-icons/'), + 'white' => rscandir($iconDir . '/flat-white-icons/'), + 'twemoji' => rscandir($iconDir . '/twemoji/'), + 'flags' => $this->getFlags(), + default => [] + }; + + $source = match ($type) { + 'color', 'white' => + 'based on the ' . + 'Material Design Icons', + 'twemoji' => + 'based on the ' . + 'Twemoji icons', + default => '' + }; + + if ($type === 'twemoji') { + $extraInfo = 'ℹ Click on icon with green border to display all its related variants. Click on the letter to display flags with the clicked initial'; + } + + $iconsCss = file_get_contents($publicDir . '/css/icons.css'); + + return $this->render('@PimcoreAdmin/admin/misc/icon_list.html.twig', [ + 'icons' => $icons, + 'iconsCss' => $iconsCss, + 'type' => $type, + 'extraInfo' => $extraInfo, + 'source' => $source, + ]); + } - //flag icons for locales + private function getFlags(): array + { $locales = Tool::getSupportedLocales(); $languageOptions = []; foreach ($locales as $short => $translation) { if (!empty($short)) { - $languageOptions[] = [ - 'language' => $short, - 'display' => $translation . " ($short)", - 'flag' => AdminTool::getLanguageFlagFile($short, true), - ]; + $flag = AdminTool::getLanguageFlagFile($short, true, false); + if ($flag) { + $languageOptions[] = $flag; + } } } - $iconsCss = file_get_contents($publicDir . '/css/icons.css'); + $languageOptions = array_unique($languageOptions); + sort($languageOptions); - return $this->render('@PimcoreAdmin/admin/misc/icon_list.html.twig', [ - 'colorIcons' => $colorIcons, - 'whiteIcons' => $whiteIcons, - 'twemoji' => $twemoji, - 'languageOptions' => $languageOptions, - 'iconsCss' => $iconsCss, - ]); + return $languageOptions; } /** diff --git a/src/Tool.php b/src/Tool.php index 773ac42a1d..08ff8b55e7 100644 --- a/src/Tool.php +++ b/src/Tool.php @@ -21,7 +21,7 @@ final class Tool /** * @internal */ - public static function getLanguageFlagFile(string $language, bool $absolutePath = true): string + public static function getLanguageFlagFile(string $language, bool $absolutePath = true, bool $includeUnknown = true): string { $basePath = '/bundles/pimcoreadmin/img/flags'; $iconFsBasePath = PIMCORE_WEB_ROOT . $basePath; @@ -45,7 +45,10 @@ public static function getLanguageFlagFile(string $language, bool $absolutePath $countryFsPath = $iconFsBasePath . '/countries/' . $countryCode . '.svg'; $fallbackFsLanguagePath = $iconFsBasePath . '/languages/' . $fallbackLanguageCode . '.svg'; - $iconPath = ($absolutePath === true ? $iconFsBasePath : $basePath) . '/countries/_unknown.svg'; + $iconPath = ''; + if ($includeUnknown) { + $iconPath = ($absolutePath === true ? $iconFsBasePath : $basePath) . '/countries/_unknown.svg'; + } $languageCountryMapping = [ 'aa' => 'er', 'af' => 'za', 'am' => 'et', 'as' => 'in', 'ast' => 'es', 'asa' => 'tz', diff --git a/src/Twig/Extension/AdminExtension.php b/src/Twig/Extension/AdminExtension.php index b3f39870aa..244f40f1d8 100644 --- a/src/Twig/Extension/AdminExtension.php +++ b/src/Twig/Extension/AdminExtension.php @@ -55,6 +55,7 @@ public function getFilters(): array { return [ new TwigFilter('pimcore_inline_icon', [$this, 'inlineIcon']), + new TwigFilter('pimcore_lazy_icon', [$this, 'lazyIcon']), new TwigFilter('pimcore_twemoji_variant_icon', [$this, 'twemojiVariantIcon']), ]; } @@ -163,6 +164,16 @@ public function inlineIcon(string $icon): string ); } + public function lazyIcon(string $icon): string + { + return sprintf( + '', + str_replace(PIMCORE_WEB_ROOT, '', $icon), + basename($icon), + str_replace(PIMCORE_WEB_ROOT, '', $icon) + ); + } + public function twemojiVariantIcon(string $icon): string { return sprintf( diff --git a/templates/admin/index/index.html.twig b/templates/admin/index/index.html.twig index fbb3410f57..9f5e47160d 100644 --- a/templates/admin/index/index.html.twig +++ b/templates/admin/index/index.html.twig @@ -216,7 +216,7 @@ "pimcore/helpers.js", "pimcore/error.js", "pimcore/events.js", - + "pimcore/iconlibrary.js", "pimcore/treenodelocator.js", "pimcore/helpers/generic-grid.js", "pimcore/helpers/quantityValue.js", diff --git a/templates/admin/misc/icon_list.html.twig b/templates/admin/misc/icon_list.html.twig index 616e4d991a..f54f22e8cc 100644 --- a/templates/admin/misc/icon_list.html.twig +++ b/templates/admin/misc/icon_list.html.twig @@ -13,10 +13,10 @@ } .icons { - width:1200px; margin: 0 auto; + display:inline-block; + padding-top: 20px; } - .icon { text-align: center; width:100px; @@ -25,25 +25,30 @@ float: left; font-size: 10px; word-wrap: break-word; - cursor: copy; padding-top: 5px; box-sizing: border-box; } - .icon.black { + .icon img:hover { + cursor: copy; + } + + #white .icon { background-color: #0C0F12; } - .icon.black .label { + #white .icon .label { color: #fff; } + #flags .icon{ + height: 100px; + } + .info { text-align: center; - margin-bottom: 30px; clear: both; font-size: 22px; - padding-top: 50px; } .info small { @@ -66,126 +71,124 @@ display: none; background-color: #eee; } +
- Color Icons -
- based on the Material Design Icons + {% if source is defined %} + {{ source|raw }} + {% endif %}
-
+
ℹ Click on icon to copy path to clipboard.
- {% for icon in colorIcons %} - {% set iconPath = icon|replace({(webRoot): ''}) %} -
- {{ icon | pimcore_inline_icon | raw }} -
- {{ iconPath in iconsCss ? '*' : '' }} - {{ iconPath|basename }} -
-
- {% endfor %} -
- -
- White Icons -
- based on the Material Design Icons -
- -
- {% for icon in whiteIcons %} - {% set iconPath = icon|replace({(webRoot): ''}) %} -
- {{ icon | pimcore_inline_icon | raw }} -
- {{ iconPath in iconsCss ? '*' : '' }} - {{ iconPath|basename }} -
-
- {% endfor %} -
-
- Source (Twemoji) -
+ {% if extraInfo is defined %} +
{{ extraInfo }}
+ {% endif %} -
-
ℹ Click on icon to copy path to clipboard.
-
ℹ Click on icon with green border to display all its related variants. Click on the letter to display flags with the clicked initial
- {% for icon in twemoji %} + {% for icon in icons %} {% set iconPath = icon|replace({(webRoot): ''}) %} {# Any icon with basename that has a dash will be considered as a variant to avoid recurisvely searching for "parent" icon. It happens that all icon that have variants have at least a prefix of 4-5 characters. #} - {% if '-' in iconPath and iconPath|basename|split('-')[0]|length > 3 %} + {% if ('-' in iconPath and iconPath|basename|split('-')[0]|length > 3) and (type =='twemoji') %}
{{ icon | pimcore_twemoji_variant_icon | raw }}
{{ iconPath|basename }}
{% else %}
- {{ icon | pimcore_inline_icon | raw }} -
{{ iconPath|basename }}
+ + {% if type == 'flags' %} + {% set country = iconPath|basename|replace({'.svg': ''}) %} +
+ {% endif %} + + {% if loop.index < 100 %} + {{ icon | pimcore_inline_icon | raw }} + {% else %} + {{ icon | pimcore_lazy_icon | raw }} + {% endif %} + +
+ {{ iconPath in iconsCss ? '*' : '' }} + {{ iconPath|basename }} +
{% endif %} {% endfor %}
-
- Flags -
- - - - - - - - {% for langOpt in languageOptions %} - - - - - - {% endfor %} -
FlagCodeName
{{ langOpt['flag'] | pimcore_inline_icon | raw }}{{ langOpt['language'] }}{{ langOpt['display'] }}
- - - - + src="https://code.jquery.com/jquery-3.7.1.min.js" + integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" + crossorigin="anonymous"> diff --git a/translations/admin_ext.en.yaml b/translations/admin_ext.en.yaml index d414b55a12..d38bd02986 100644 --- a/translations/admin_ext.en.yaml +++ b/translations/admin_ext.en.yaml @@ -15,6 +15,13 @@ controller_action: Controller & Action admin_interface: Admin Interface admin_interface_background: Admin Interface Background icon_library: Icon Library +color_icons: Color Icons +white_icons: White Icons +twemoji: Twemoji +twemoji_variants: Twemoji variants +select_type_first: Please select a type first +icon_tools: Icon Tools +flags: Flags system_performance_stability_warning: >- Please do not perform this action unless you are sure what you are doing.
This action can have a major impact onto the stability