diff --git a/admin/inc/class.admin_categories.inc.php b/admin/inc/class.admin_categories.inc.php index 69deb3bc89..6523e8df3e 100644 --- a/admin/inc/class.admin_categories.inc.php +++ b/admin/inc/class.admin_categories.inc.php @@ -128,7 +128,10 @@ class admin_categories $readonlys['__ALL__'] = true; $readonlys['button[cancel]'] = false; } - $content['base_url'] = self::icon_url(); + if (!empty($content['data']['icon'])) + { + $content['data']['icon'] = preg_replace('/\.(png|svg|jpe?g|gif)$/i', '', $content['data']['icon']); + } } elseif ($content['button'] || $content['delete']) { @@ -258,12 +261,9 @@ class admin_categories } $content['msg'] = $msg; if(!$content['appname']) $content['appname'] = $appname; - if($content['data']['icon']) - { - $content['icon_url'] = Api\Image::find('vfs',$content['data']['icon']) ?: self::icon_url($content['data']['icon']); - } + if (!$content['parent']) $content['parent'] = ''; - $sel_options['icon'] = self::get_icons(); + $sel_options['icon'] = self::get_icons($content['data']['icon']); $sel_options['owner'] = array(); // User's category - add current value to be able to preserve owner @@ -333,51 +333,61 @@ class admin_categories ),2); } - /** - * Return URL of an icon, or base url with trailing slash - * - * @param string $icon = '' filename - * @return string url - */ - static function icon_url($icon='') - { - return $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$icon; - } - /** * Return icons from /api/images * * @return array filename => label */ - static function get_icons() + static function get_icons(string $_icon=null) { - $icons = array(); - if (file_exists($image_dir=EGW_SERVER_ROOT.self::ICON_PATH) && ($dir = dir($image_dir))) + $stock_icon = false; + $icons = []; + foreach(Api\Image::map() as $app => $images) { - $matches = null; - while(($file = $dir->read())) + if (!in_array($app, ['global', 'vfs'])) continue; + + foreach($images as $image => $icon) { - if (preg_match('/^(.*)\\.(png|gif|jpe?g)$/i',$file,$matches)) + if ($app === 'vfs' || str_starts_with($image, 'images/')) { - $icons[$file] = ucfirst($matches[1]); + if ($app !== 'vfs') $image = substr($image, 7); + $icons[] = ['value' => $image, 'label' => ucfirst($image), 'icon' => $icon]; + if ($_icon === $image) $stock_icon = true; } } - $dir->close(); } - - // Get custom icons - $map = Api\Image::map(); - if(array_key_exists('vfs', $map)) + // add arbitrary icons + if ($_icon && !$stock_icon && ($icon = Api\Image::find('vfs', $_icon))) { - foreach($map['vfs'] as $name => $path) - { - $icons[$name] = $name; - } + $icons[] = ['value' => $_icon, 'label' => ucfirst($_icon), 'icon' => $icon]; } - asort($icons); + uasort($icons, static function ($a, $b) { + return strnatcasecmp($a['label'], $b['label']); + }); return $icons; } + /** + * Search bootstrap icons + * + * @param string $pattern + * @throws Api\Json\Exception + */ + public function ajax_search(string $pattern) + { + $pattern = strtolower($pattern); + $icons = []; + foreach(Api\Image::map()['bootstrap'] ?? [] as $image => $icon) + { + if (strpos($image, $pattern) !== false) + { + $icons[] = ['value' => $image, 'label' => $image, 'icon' => $icon]; + } + if (count($icons) > 100) break; + } + Api\Json\Response::get()->data($icons); + } + /** * query rows for the nextmatch widget * @@ -430,7 +440,12 @@ class admin_categories $row['level_spacer'] = str_repeat('    ',$row['level']); } - if ($row['data']['icon']) $row['icon_url'] = Api\Image::find('vfs',$row['data']['icon']) ?: self::icon_url($row['data']['icon']); + if (!empty($row['data']['icon'])) + { + $row['data']['icon'] = preg_replace('/\.(png|svg|jpe?g|gif)$/i', '', $row['data']['icon']); + $row['icon_url'] = Api\Image::find('', 'images/'.$row['data']['icon']) ?: + Api\Image::find('vfs', $row['data']['icon']); + } $row['subs'] = $row['children'] ? count($row['children']) : 0; diff --git a/admin/templates/default/categories.edit.xet b/admin/templates/default/categories.edit.xet index 54c92f9120..150f3bfbd8 100644 --- a/admin/templates/default/categories.edit.xet +++ b/admin/templates/default/categories.edit.xet @@ -26,10 +26,7 @@ - - - - + diff --git a/api/categories.php b/api/categories.php index 308a4ba30c..86e64d6795 100644 --- a/api/categories.php +++ b/api/categories.php @@ -50,7 +50,9 @@ foreach($categories as $cat) } if (!empty($cat['data']['icon'])) { - $content .= ".cat_{$cat['id']} .cat_icon { background-image: url('". admin_categories::icon_url($cat['data']['icon']) ."');} /*{$cat['name']}*/\n"; + $icon = preg_replace('/\.(png|svg|jpe?g|gif)$/i', '', $cat['data']['icon']); + $content .= ".cat_{$cat['id']} .cat_icon { background-image: url('". ( + Api\Image::find('', 'images/'.$icon) ?: Api\Image::find('vfs', $icon)) ."');} /*{$cat['name']}*/\n"; } } @@ -78,4 +80,4 @@ if (in_array('gzip', explode(',',$_SERVER['HTTP_ACCEPT_ENCODING'])) && function_ // Content-Lenght header is important, otherwise browsers dont cache! Header('Content-Length: '.bytes($content)); -echo $content; +echo $content; \ No newline at end of file diff --git a/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts index d70df481a4..328f5d7fbf 100644 --- a/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts +++ b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts @@ -549,7 +549,7 @@ export class Et2TreeDropdown extends SearchMixin & Et2InputWidg autocomplete="off" ?disabled=${this.disabled} ?readonly=${this.readonly} - placeholder="${this.hasFocus || this.value.length > 0 || this.disabled || this.readonly ? "" : this.placeholder || this.emptyLabel}" + placeholder="${this.hasFocus || this.value.length > 0 || this.disabled || this.readonly ? "" : this.egw().lang(this.placeholder || this.emptyLabel)}" tabindex="0" @keydown=${this.handleSearchKeyDown} @blur=${() => {this.hasFocus = false;}} diff --git a/api/src/Etemplate/Widget/Select.php b/api/src/Etemplate/Widget/Select.php index a2637182da..f97699e2b0 100644 --- a/api/src/Etemplate/Widget/Select.php +++ b/api/src/Etemplate/Widget/Select.php @@ -150,7 +150,8 @@ class Select extends Etemplate\Widget $widget_type = substr($widget_type, 4); } $multiple = $this->attrs['multiple'] || $this->getElementAttribute($form_name, 'multiple') || $this->getElementAttribute($form_name, 'rows') > 1; - $allowFreeEntries = $this->attrs['allowFreeEntries'] || $this->getElementAttribute($form_name, 'allowFreeEntries'); + $allowFreeEntries = $this->attrs['allowFreeEntries'] || $this->getElementAttribute($form_name, 'allowFreeEntries') || + $this->attrs['searchUrl'] || $this->getElementAttribute($form_name, 'searchUrl'); $ok = true; if (!$this->is_readonly($cname, $form_name)) @@ -771,7 +772,8 @@ class Select extends Etemplate\Widget }))), //add different class per level to allow different styling for each category level: 'class' => "cat_level" . $cat['level'] . " cat_{$cat['id']}", - 'icon' => !empty($cat['data']['icon']) ? \admin_categories::icon_url($cat['data']['icon']) : null, + 'icon' => empty($cat['data']['icon']) ? null : + (Api\Image::find('', 'images/'.$cat['data']['icon']) ?: Api\Image::find('vfs', $cat['data']['icon'])), // send cat-date too ]+(is_array($cat['data']) ? $cat['data'] : [])); };