From 5d6b1066fddc96c14b16f7a579cf9330f2b63e76 Mon Sep 17 00:00:00 2001 From: JiaJia Ji Date: Mon, 17 Jun 2024 11:22:26 +0200 Subject: [PATCH] [Improvement]: Split and lazy load icon library (#567) * split in tabs,small tweaks and opmitizations lazy loading avoid loading pimcore js files, copy the copytoClipboard function there replace .click with on('click') sort alphabetically by iso code, but should be by country name and translate in base of user language but not worth to change it further refactor, reducing flags to 248 since we consider only the country code * improve dataobject class icon selectors --------- Co-authored-by: markus-moser --- CHANGELOG.md | 3 +- public/js/pimcore/iconlibrary.js | 87 ++++++++ public/js/pimcore/layout/toolbar.js | 16 +- public/js/pimcore/object/classes/class.js | 67 +++++-- .../pimcore/object/classes/layout/layout.js | 2 +- .../Admin/DataObject/ClassController.php | 65 +++++- src/Controller/Admin/MiscController.php | 61 ++++-- src/Tool.php | 7 +- src/Twig/Extension/AdminExtension.php | 11 ++ templates/admin/index/index.html.twig | 2 +- templates/admin/misc/icon_list.html.twig | 187 +++++++++--------- translations/admin_ext.en.yaml | 7 + 12 files changed, 379 insertions(+), 136 deletions(-) create mode 100644 public/js/pimcore/iconlibrary.js 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 %} -
- + {% 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