mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-22 07:53:39 +01:00
Just get it working WIP
- Fix category tree structure - Switch on tree multiple probably lots of bugs still, looks like we may have to do click on tree = add / remove and not show the value after all
This commit is contained in:
parent
6fa102dfc5
commit
62d9c222b6
@ -148,7 +148,7 @@
|
|||||||
</columns>
|
</columns>
|
||||||
<rows>
|
<rows>
|
||||||
<row valign="top">
|
<row valign="top">
|
||||||
<tree-cat id="cat_id_tree" options="13,,width:99%"/>
|
<et2-tree-cat id="cat_id_tree" multiple="true" placeholder="Category"/>
|
||||||
<et2-select-cat id="cat_id" width="100%" height="195" multiple="true" placeholder="Category"></et2-select-cat>
|
<et2-select-cat id="cat_id" width="100%" height="195" multiple="true" placeholder="Category"></et2-select-cat>
|
||||||
<et2-description></et2-description>
|
<et2-description></et2-description>
|
||||||
<grid width="100%">
|
<grid width="100%">
|
||||||
|
@ -58,7 +58,6 @@ export class Et2MultiselectTree extends Et2Tree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_optionTemplate(selectOption: TreeItemData): TemplateResult<1> {
|
_optionTemplate(selectOption: TreeItemData): TemplateResult<1> {
|
||||||
this._currentOption = selectOption
|
|
||||||
let img: String = selectOption.im0 ?? selectOption.im1 ?? selectOption.im2;
|
let img: String = selectOption.im0 ?? selectOption.im1 ?? selectOption.im2;
|
||||||
if (img) {
|
if (img) {
|
||||||
//sl-icon images need to be svgs if there is a png try to find the corresponding svg
|
//sl-icon images need to be svgs if there is a png try to find the corresponding svg
|
||||||
@ -70,7 +69,8 @@ export class Et2MultiselectTree extends Et2Tree {
|
|||||||
<sl-tree-item
|
<sl-tree-item
|
||||||
|
|
||||||
id=${selectOption.id}
|
id=${selectOption.id}
|
||||||
?lazy=${this._currentOption.item?.length === 0 && this._currentOption.child}
|
?selected=${this.value.includes(selectOption.id)}
|
||||||
|
?lazy=${selectOption.item?.length === 0 && selectOption.child}
|
||||||
|
|
||||||
@sl-lazy-load=${(event) => {
|
@sl-lazy-load=${(event) => {
|
||||||
this.handleLazyLoading(selectOption).then((result) => {
|
this.handleLazyLoading(selectOption).then((result) => {
|
||||||
@ -82,8 +82,8 @@ export class Et2MultiselectTree extends Et2Tree {
|
|||||||
>
|
>
|
||||||
<sl-icon src="${img ?? nothing}"></sl-icon>
|
<sl-icon src="${img ?? nothing}"></sl-icon>
|
||||||
|
|
||||||
${this._currentOption.text}
|
${selectOption.text}
|
||||||
${repeat(this._currentOption.item, this._optionTemplate.bind(this))}
|
${(selectOption.item) ? html`${repeat(selectOption.item, this._optionTemplate.bind(this))}` : nothing}
|
||||||
</sl-tree-item>`
|
</sl-tree-item>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +100,10 @@ export class Et2MultiselectTree extends Et2Tree {
|
|||||||
}
|
}
|
||||||
this.selectedNodes = event.detail.selection;
|
this.selectedNodes = event.detail.selection;
|
||||||
//TODO look at what signature is expected here
|
//TODO look at what signature is expected here
|
||||||
this.onchange(event,this)
|
if(typeof this.onclick == "function")
|
||||||
|
{
|
||||||
|
this.onclick(event.detail.selection[0].id, this, event.detail.previous)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,14 @@ import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree"
|
|||||||
|
|
||||||
export type TreeItemData = {
|
export type TreeItemData = {
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
|
// Has children, but they may not be provided in item
|
||||||
child: Boolean | 1,
|
child: Boolean | 1,
|
||||||
data?: Object,//{sieve:true,...} or {acl:true} or other
|
data?: Object,//{sieve:true,...} or {acl:true} or other
|
||||||
id: string,
|
id: string,
|
||||||
im0: String,
|
im0: String,
|
||||||
im1: String,
|
im1: String,
|
||||||
im2: String,
|
im2: String,
|
||||||
|
// Child items
|
||||||
item: TreeItemData[],
|
item: TreeItemData[],
|
||||||
checked?: Boolean,
|
checked?: Boolean,
|
||||||
nocheckbox: number | Boolean,
|
nocheckbox: number | Boolean,
|
||||||
@ -98,6 +100,8 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this._selectOptions = [];
|
this._selectOptions = [];
|
||||||
|
|
||||||
|
this._optionTemplate = this._optionTemplate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Sl-Trees handle their own onClick events
|
//Sl-Trees handle their own onClick events
|
||||||
@ -592,7 +596,11 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<sl-tree-item
|
<sl-tree-item
|
||||||
|
part="item"
|
||||||
|
exportparts="checkbox"
|
||||||
id=${selectOption.id}
|
id=${selectOption.id}
|
||||||
|
title=${selectOption.tooltip || nothing}
|
||||||
|
?selected=${this.value.includes(selectOption.id)}
|
||||||
?expanded=${(this.calculateExpandState(selectOption))}
|
?expanded=${(this.calculateExpandState(selectOption))}
|
||||||
?lazy=${selectOption.item?.length === 0 && selectOption.child}
|
?lazy=${selectOption.item?.length === 0 && selectOption.child}
|
||||||
?focused=${selectOption.focused || nothing}
|
?focused=${selectOption.focused || nothing}
|
||||||
@ -607,7 +615,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
<sl-icon src="${img ?? nothing}"></sl-icon>
|
<sl-icon src="${img ?? nothing}"></sl-icon>
|
||||||
|
|
||||||
${selectOption.text}
|
${selectOption.text}
|
||||||
${selectOption.item ? repeat(selectOption.item, this._optionTemplate.bind(this)) : ""}
|
${selectOption.item ? repeat(selectOption.item, this._optionTemplate) : nothing}
|
||||||
</sl-tree-item>`
|
</sl-tree-item>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,6 +624,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
{
|
{
|
||||||
return html`
|
return html`
|
||||||
<sl-tree
|
<sl-tree
|
||||||
|
.selection=${this.multiple ? "multiple" : "single"}
|
||||||
@sl-selection-change=${
|
@sl-selection-change=${
|
||||||
(event: any) => {
|
(event: any) => {
|
||||||
this._previousOption = this._currentOption
|
this._previousOption = this._currentOption
|
||||||
@ -646,7 +655,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
}
|
}
|
||||||
|
|
||||||
>
|
>
|
||||||
${repeat(this._selectOptions, this._optionTemplate.bind(this))}
|
${repeat(this._selectOptions, this._optionTemplate)}
|
||||||
</sl-tree>
|
</sl-tree>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -750,7 +759,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
|
|
||||||
protected updated(_changedProperties: PropertyValues)
|
protected updated(_changedProperties: PropertyValues)
|
||||||
{
|
{
|
||||||
this._link_actions(this.actions)
|
// this._link_actions(this.actions)
|
||||||
super.updated(_changedProperties);
|
super.updated(_changedProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -772,15 +781,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private calculateExpandState = (selectOption: TreeItemData) => {
|
private calculateExpandState = (selectOption: TreeItemData) => {
|
||||||
if (selectOption.id.endsWith("INBOX") || selectOption.id == window.egw.preference("ActiveProfileID", "mail"))
|
|
||||||
{
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (selectOption.open)
|
if (selectOption.open)
|
||||||
{
|
{
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (
|
if(this._selectOptions.length > 1 &&
|
||||||
this._selectOptions[0] == selectOption &&
|
this._selectOptions[0] == selectOption &&
|
||||||
(this._selectOptions.find((selectOption) => {
|
(this._selectOptions.find((selectOption) => {
|
||||||
return selectOption.open
|
return selectOption.open
|
||||||
@ -790,7 +796,10 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
{
|
{
|
||||||
return true //open the first item, if no item is opened
|
return true //open the first item, if no item is opened
|
||||||
}
|
}
|
||||||
|
if(selectOption.id && (selectOption.id.endsWith("INBOX") || selectOption.id == window.egw.preference("ActiveProfileID", "mail")))
|
||||||
|
{
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@ -855,8 +864,5 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("et2-tree", Et2Tree);
|
customElements.define("et2-tree", Et2Tree);
|
||||||
customElements.define("et2-tree-cat", class extends Et2Tree
|
|
||||||
{
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,15 +44,12 @@ export default css`
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.1rem 0.5rem;
|
|
||||||
|
|
||||||
background-color: var(--sl-input-background-color);
|
background-color: var(--sl-input-background-color);
|
||||||
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
|
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
|
||||||
|
|
||||||
border-radius: var(--sl-input-border-radius-medium);
|
border-radius: var(--sl-input-border-radius-medium);
|
||||||
font-size: var(--sl-input-font-size-medium);
|
font-size: var(--sl-input-font-size-medium);
|
||||||
min-height: var(--sl-input-height-medium);
|
|
||||||
max-height: calc(var(--height, 5) * var(--sl-input-height-medium));
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
@ -107,6 +104,9 @@ export default css`
|
|||||||
.tree-dropdown__tags {
|
.tree-dropdown__tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.1rem 0.5rem;
|
||||||
|
min-height: var(--sl-input-height-medium);
|
||||||
|
max-height: calc(var(--height, 5) * var(--sl-input-height-medium));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* End tags */
|
/* End tags */
|
||||||
@ -157,14 +157,17 @@ export default css`
|
|||||||
border-radius: var(--sl-border-radius-medium);
|
border-radius: var(--sl-border-radius-medium);
|
||||||
padding-block: var(--sl-spacing-x-small);
|
padding-block: var(--sl-spacing-x-small);
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
z-index: var(--sl-z-index-dropdown);
|
z-index: var(--sl-z-index-dropdown);
|
||||||
|
|
||||||
/* Make sure it adheres to the popup's auto size */
|
/* Make sure it adheres to the popup's auto size */
|
||||||
|
height: auto;
|
||||||
max-width: var(--auto-size-available-width);
|
max-width: var(--auto-size-available-width);
|
||||||
|
}
|
||||||
|
|
||||||
/* This doesn't work for some reason, it's overwritten somewhere */
|
et2-tree::part(checkbox) {
|
||||||
--size: 1.8em;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
@ -90,7 +90,8 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
new_value = new_value.split(",")
|
new_value = new_value.split(",")
|
||||||
}
|
}
|
||||||
const oldValue = this.__value;
|
const oldValue = this.__value;
|
||||||
this.__value = <string[]>new_value;
|
// Filter to make sure there are no trailing commas
|
||||||
|
this.__value = <string[]>new_value.filter(v => v);
|
||||||
this.requestUpdate("value", oldValue);
|
this.requestUpdate("value", oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,19 +317,41 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
{
|
{
|
||||||
// Find the tag value and remove it from current value
|
// Find the tag value and remove it from current value
|
||||||
let valueArray = this.getValueAsArray();
|
let valueArray = this.getValueAsArray();
|
||||||
|
const oldValue = valueArray.slice(0);
|
||||||
const index = valueArray.indexOf(value);
|
const index = valueArray.indexOf(value);
|
||||||
valueArray.splice(index, 1);
|
valueArray.splice(index, 1);
|
||||||
this.value = valueArray;
|
this.value = valueArray;
|
||||||
this.requestUpdate("value");
|
this.requestUpdate("value", oldValue);
|
||||||
this.dispatchEvent(new Event("change", {bubbles: true}));
|
// TODO: Clean up this scope violation
|
||||||
|
// sl-tree-item is not getting its selected attribute updated
|
||||||
|
Array.from(this._tree._tree.querySelectorAll('sl-tree-item')).forEach(e =>
|
||||||
|
{
|
||||||
|
if(this.value.includes(e.id))
|
||||||
|
{
|
||||||
|
e.setAttribute("selected", "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.removeAttribute("selected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._tree.requestUpdate();
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTreeChange(event)
|
handleTreeChange(event)
|
||||||
{
|
{
|
||||||
const oldValue = this.value;
|
const oldValue = this.value;
|
||||||
this.value = this._tree.getValue();
|
this.value = event?.detail?.selection?.map(i => i.id) ?? [];
|
||||||
this.requestUpdate("value", oldValue);
|
this.requestUpdate("value", oldValue);
|
||||||
|
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||||
|
});
|
||||||
if(!this.multiple)
|
if(!this.multiple)
|
||||||
{
|
{
|
||||||
this.hide();
|
this.hide();
|
||||||
@ -394,14 +417,21 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
tagsTemplate()
|
tagsTemplate()
|
||||||
{
|
{
|
||||||
const value = this.getValueAsArray();
|
const value = this.getValueAsArray();
|
||||||
return html`${keyed(this._valueUID, map(value, (value, index) => this.tagTemplate(this.optionSearch(value, this.select_options))))}`;
|
return html`${keyed(this._valueUID, map(value, (value, index) =>
|
||||||
|
{
|
||||||
|
// Deal with value that is not in options
|
||||||
|
const option = this.optionSearch(value, this.select_options);
|
||||||
|
return option ? this.tagTemplate(option) : nothing;
|
||||||
|
}))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
tagTemplate(option : TreeItemData)
|
tagTemplate(option : TreeItemData)
|
||||||
{
|
{
|
||||||
const readonly = (this.readonly || option && typeof (option.disabled) != "undefined" && option.disabled);
|
const readonly = (this.readonly || option && typeof (option.disabled) != "undefined" && option.disabled);
|
||||||
const isEditable = false && !readonly;
|
const isEditable = false && !readonly;
|
||||||
const image = this.iconTemplate(option?.option ?? option);
|
const image = option ? this.iconTemplate(option?.option ?? option) : null;
|
||||||
|
const isValid = true;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<et2-tag
|
<et2-tag
|
||||||
part="tag"
|
part="tag"
|
||||||
@ -414,7 +444,7 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
"
|
"
|
||||||
class=${"tree_tag " + option.class ?? ""}
|
class=${"tree_tag " + option.class ?? ""}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
?pill=${this.pill}
|
variant=${isValid ? nothing : "danger"}
|
||||||
size=${this.size || "medium"}
|
size=${this.size || "medium"}
|
||||||
?removable=${!readonly}
|
?removable=${!readonly}
|
||||||
?readonly=${readonly}
|
?readonly=${readonly}
|
||||||
@ -468,7 +498,6 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
'tree-dropdown--focused': this.hasFocus,
|
'tree-dropdown--focused': this.hasFocus,
|
||||||
'tree-dropdown--placeholder-visible': isPlaceholderVisible,
|
'tree-dropdown--placeholder-visible': isPlaceholderVisible,
|
||||||
})}
|
})}
|
||||||
strategy="fixed"
|
|
||||||
flip
|
flip
|
||||||
shift
|
shift
|
||||||
sync="width"
|
sync="width"
|
||||||
@ -504,7 +533,7 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
multiple=${this.multiple}
|
multiple=${this.multiple}
|
||||||
?readonly=${this.readonly}
|
?readonly=${this.readonly}
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
.value=${this.value}
|
value=${this.value}
|
||||||
._selectOptions=${this.select_options}
|
._selectOptions=${this.select_options}
|
||||||
|
|
||||||
@sl-selection-change=${this.handleTreeChange}
|
@sl-selection-change=${this.handleTreeChange}
|
||||||
@ -517,3 +546,7 @@ export class Et2TreeDropdown extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("et2-tree-dropdown", Et2TreeDropdown);
|
customElements.define("et2-tree-dropdown", Et2TreeDropdown);
|
||||||
|
|
||||||
|
customElements.define("et2-tree-cat", class extends Et2TreeDropdown
|
||||||
|
{
|
||||||
|
});
|
@ -473,34 +473,8 @@ class Tree extends Etemplate\Widget
|
|||||||
$categories = new Api\Categories('',$type3);
|
$categories = new Api\Categories('',$type3);
|
||||||
}
|
}
|
||||||
$cat2path=array();
|
$cat2path=array();
|
||||||
foreach((array)$categories->return_sorted_array(0,False,'','','',!$type,0,true) as $cat)
|
|
||||||
{
|
|
||||||
$s = stripslashes($cat['name']);
|
|
||||||
|
|
||||||
if ($cat['app_name'] == 'phpgw' || $cat['owner'] == '-1')
|
static::processCategory(0, $options, $categories, !$type);
|
||||||
{
|
|
||||||
$s .= ' ♦';
|
|
||||||
}
|
|
||||||
$cat2path[$cat['id']] = $path = ($cat['parent'] ? $cat2path[$cat['parent']].'/' : '').(string)$cat['id'];
|
|
||||||
|
|
||||||
// 1D array
|
|
||||||
$options[] = $cat + array(
|
|
||||||
'text' => $s,
|
|
||||||
'path' => $path,
|
|
||||||
|
|
||||||
/*
|
|
||||||
These ones to play nice when a user puts a tree & a selectbox with the same
|
|
||||||
ID on the form (addressbook edit):
|
|
||||||
if tree overwrites selectbox options, selectbox will still work
|
|
||||||
*/
|
|
||||||
'value' => $cat['id'],
|
|
||||||
'label' => $s,
|
|
||||||
'title' => $cat['description']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tree in array
|
|
||||||
//$options[$cat['parent']][] = $cat;
|
|
||||||
}
|
|
||||||
// change cat-ids to pathes and preserv unavailible cats (eg. private user-cats)
|
// change cat-ids to pathes and preserv unavailible cats (eg. private user-cats)
|
||||||
if ($value)
|
if ($value)
|
||||||
{
|
{
|
||||||
@ -533,4 +507,39 @@ class Tree extends Etemplate\Widget
|
|||||||
//error_log(__METHOD__."('$widget_type', '$legacy_options', no_lang=".array2string($no_lang).', readonly='.array2string($readonly).", value=$value) returning ".array2string($options));
|
//error_log(__METHOD__."('$widget_type', '$legacy_options', no_lang=".array2string($no_lang).', readonly='.array2string($readonly).", value=$value) returning ".array2string($options));
|
||||||
return $options;
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function processCategory($cat_id, &$options, &$categories, $globals)
|
||||||
|
{
|
||||||
|
foreach((array)$categories->return_array($cat_id ? 'subs' : 'mains', 0, false, '', 'ASC', '', $globals, $cat_id) as $cat)
|
||||||
|
{
|
||||||
|
|
||||||
|
$s = stripslashes($cat['name']);
|
||||||
|
|
||||||
|
if($cat['app_name'] == 'phpgw' || $cat['owner'] == '-1')
|
||||||
|
{
|
||||||
|
$s .= ' ♦';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1D array
|
||||||
|
$category = $cat + array(
|
||||||
|
'text' => $s,
|
||||||
|
|
||||||
|
/*
|
||||||
|
These ones to play nice when a user puts a tree & a selectbox with the same
|
||||||
|
ID on the form (addressbook edit):
|
||||||
|
if tree overwrites selectbox options, selectbox will still work
|
||||||
|
*/
|
||||||
|
'value' => $cat['id'],
|
||||||
|
'label' => $s,
|
||||||
|
'title' => $cat['description']
|
||||||
|
);
|
||||||
|
if(!empty($cat['children']))
|
||||||
|
{
|
||||||
|
$category['item'] = [];
|
||||||
|
unset($cat['children']);
|
||||||
|
static::processCategory($cat['id'], $category['item'], $categories, $globals);
|
||||||
|
}
|
||||||
|
$options[] = $category;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user