using bootstrap icons as stock icons for categories (former api/images) and allow to search arbitrary bootstrap icons when adding/editing categories

also fixed error editing categories in the root ('0' is not allowed validation message)
This commit is contained in:
ralf 2024-09-05 18:00:18 +02:00
parent 1f7325b648
commit 51e7269d88
5 changed files with 60 additions and 44 deletions

View File

@ -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;

View File

@ -26,10 +26,7 @@
</row>
<row>
<et2-description value="Icon" for="data[icon]"></et2-description>
<et2-hbox cellpadding="0" cellspacing="0" >
<et2-select id="data[icon]" onchange="app.admin.change_icon(widget);" emptyLabel="None"></et2-select>
<et2-image src="icon_url" id="icon_url" class="leftPad5"></et2-image>
</et2-hbox>
<et2-select id="data[icon]" emptyLabel="None" search="true" searchUrl="preferences.preferences_categories_ui.ajax_search"></et2-select>
</row>
<row disabled="@appname=phpgw">
<et2-description value="Application"></et2-description>

View File

@ -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;

View File

@ -549,7 +549,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & 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;}}

View File

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