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'] : []));
};