implement not strictly linked multiselect tree by using sl-tree selection="single" and sl-tree-item.selection:

- instead of showing checkboxes, we use the sl-tree-item.selection marker (blue left border) to show the multi-selection and sl-tree sl-selection-change event to set the value accordingly
- implement Et2Tree.setSubChecked(_id, _value) to allow apps to (un)check a hierarchy onclick of parent, still allowing to (un)select single children
- also change several tree methods to return the updateComplete promise to use in mail app.js instead of window.setInterval() to wait for tree loading
This commit is contained in:
ralf 2024-04-22 16:52:47 +02:00
parent d1b3786b2a
commit 731a9d91af
5 changed files with 114 additions and 95 deletions

View File

@ -408,7 +408,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
return this._currentSlTreeItem return this._currentSlTreeItem
} }
getDomNode(_id): SlTreeItem getDomNode(_id): SlTreeItem|null
{ {
return this.shadowRoot.querySelector("sl-tree-item[id='" + _id + "'"); return this.shadowRoot.querySelector("sl-tree-item[id='" + _id + "'");
} }
@ -487,19 +487,20 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
* @param {string} _id ID of the node * @param {string} _id ID of the node
* @param {Object} [data] If provided, the item is refreshed directly with * @param {Object} [data] If provided, the item is refreshed directly with
* the provided data instead of asking the server * the provided data instead of asking the server
* @return void * @return Promise
*/ */
refreshItem(_id, data) refreshItem(_id, data)
{ {
/* TODO currently always ask the sever
if (typeof data != "undefined" && data != null) if (typeof data != "undefined" && data != null)
{ {
//TODO currently always ask the sever
//data seems never to be used //data seems never to be used
this.refreshItem(_id, null) this.refreshItem(_id, null)
} else } else*/
{ {
let item = this.getNode(_id) let item = this.getNode(_id)
this.handleLazyLoading(item).then((result) => { return this.handleLazyLoading(item).then((result) => {
item.item = [...result.item] item.item = [...result.item]
this.requestUpdate("_selectOptions") this.requestUpdate("_selectOptions")
}) })
@ -578,6 +579,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
* @param _id * @param _id
* @param _newItemId * @param _newItemId
* @param _label * @param _label
* @return Promise
*/ */
public renameItem(_id, _newItemId, _label) public renameItem(_id, _newItemId, _label)
{ {
@ -602,6 +604,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
if (typeof _label != 'undefined') this.setLabel(_newItemId, _label); if (typeof _label != 'undefined') this.setLabel(_newItemId, _label);
this.requestUpdate() this.requestUpdate()
return this.updatedComplete();
} }
public focusItem(_id) public focusItem(_id)
@ -610,7 +613,13 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
item.focused = true item.focused = true
} }
public openItem(_id) /**
* Open an item, which might trigger lazy-loading
*
* @param string _id
* @return Promise
*/
public openItem(_id : string)
{ {
let item = this.getNode(_id); let item = this.getNode(_id);
if(item) if(item)
@ -618,6 +627,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
item.open = 1; item.open = 1;
} }
this.requestUpdate(); this.requestUpdate();
return this.updateComplete;
} }
/** /**
@ -647,6 +657,38 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
} }
} }
/**
* Set or unset checkbox of given node and all it's children based on given value
*
* @param _id
* @param _value "toggle" means the current nodes value, as the toggle already happened by default
* @return boolean false if _id was not found
*/
setSubChecked(_id : string, _value : boolean|"toggle")
{
const node = this.getDomNode(_id);
if (!node) return false;
if (_value !== 'toggle')
{
node.selected = _value;
}
Array.from(node.querySelectorAll('sl-tree-item')).forEach((item : SlTreeItem) => {
item.selected = node.selected;
});
// set selectedNodes and value
this.selectedNodes = [];
this.value = [];
Array.from(this._tree.querySelectorAll('sl-tree-item')).forEach((item : SlTreeItem) => {
if (item.selected)
{
this.selectedNodes.push(item);
this.value.push(item.id);
}
});
return true;
}
getUserData(_nodeId, _name) getUserData(_nodeId, _name)
{ {
return this.getNode(_nodeId)?.userdata?.find(elem => elem.name === _name)?.content return this.getNode(_nodeId)?.userdata?.find(elem => elem.name === _name)?.content
@ -731,22 +773,57 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
${this.styleTemplate()} ${this.styleTemplate()}
<sl-tree <sl-tree
part="tree" part="tree"
.selection=${this.multiple ? "multiple" : "single"} .selection=${/* implement unlinked multiple: this.multiple ? "multiple" :*/ "single"}
@sl-selection-change=${ @sl-selection-change=${
(event: any) => { (event: any) => {
this._previousOption = this._currentOption ?? (this.value.length ? this.getNode(this.value) : null); this._previousOption = this._currentOption ?? (this.value.length ? this.getNode(this.value[0]) : null);
this._currentOption = this.getNode(event.detail.selection[0].id) ?? this.optionSearch(event.detail.selection[0].id, this._selectOptions, 'id', 'item'); this._currentOption = this.getNode(event.detail.selection[0].id) ?? this.optionSearch(event.detail.selection[0].id, this._selectOptions, 'id', 'item');
const ids = event.detail.selection.map(i => i.id); const ids = event.detail.selection.map(i => i.id);
// implemented unlinked multiple
if (this.multiple)
{
const idx = this.value.indexOf(ids[0]);
if (idx < 0)
{
this.value.push(ids[0]);
}
else
{
this.value.splice(idx, 1);
}
// sync tree-items selected attribute with this.value
this.selectedNodes = [];
Array.from(this._tree.querySelectorAll('sl-tree-item')).forEach((item : SlTreeItem) =>
{
if(this.value.includes(item.id))
{
item.setAttribute("selected", "");
this.selectedNodes.push(item);
}
else
{
item.removeAttribute("selected");
}
});
this._tree.requestUpdate();
}
else
{
this.value = this.multiple ? ids ?? [] : ids[0] ?? ""; this.value = this.multiple ? ids ?? [] : ids[0] ?? "";
}
event.detail.previous = this._previousOption?.id; event.detail.previous = this._previousOption?.id;
this._currentSlTreeItem = event.detail.selection[0]; this._currentSlTreeItem = event.detail.selection[0];
/* implemented unlinked-multiple
if(this.multiple) if(this.multiple)
{ {
this.selectedNodes = event.detail.selection this.selectedNodes = event.detail.selection
} }*/
if(typeof this.onclick == "function") if(typeof this.onclick == "function")
{ {
// wait for the update, so app founds DOM in the expected state
this._tree.updateComplete.then(() => {
this.onclick(event.detail.selection[0].id, this, event.detail.previous) this.onclick(event.detail.selection[0].id, this, event.detail.previous)
});
} }
} }
} }
@ -939,22 +1016,6 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
} }
} }
} }
private createTree()
{
// widget.input = document.querySelector("et2-tree");
// // Allow controlling icon size by CSS
// widget.input.def_img_x = "";
// widget.input.def_img_y = "";
//
// // to allow "," in value, eg. folder-names, IF value is specified as array
// widget.input.dlmtr = ':}-*(';
// @ts-ignore from static get properties
}
} }
customElements.define("et2-tree", Et2Tree); customElements.define("et2-tree", Et2Tree);

View File

@ -63,6 +63,9 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
/** The component's help text. If you need to display HTML, use the `help-text` slot instead. */ /** The component's help text. If you need to display HTML, use the `help-text` slot instead. */
@property({attribute: 'help-text'}) helpText = ""; @property({attribute: 'help-text'}) helpText = "";
@property({type: String})
autoloading: string = "" //description: "JSON URL or menuaction to be called for nodes marked with child=1, but not having children, getSelectedNode() contains node-id"
/** /**
* Indicates whether the dropdown is open. You can toggle this attribute to show and hide the tree, or you can * Indicates whether the dropdown is open. You can toggle this attribute to show and hide the tree, or you can
* use the `show()` and `hide()` methods and this attribute will reflect the open state. * use the `show()` and `hide()` methods and this attribute will reflect the open state.
@ -636,6 +639,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
._selectOptions=${this.select_options} ._selectOptions=${this.select_options}
.actions=${this.actions} .actions=${this.actions}
.styleTemplate=${() => this.styleTemplate()} .styleTemplate=${() => this.styleTemplate()}
.autoloading="${this.autoloading}"
@sl-selection-change=${this.handleTreeChange} @sl-selection-change=${this.handleTreeChange}
> >

View File

@ -690,10 +690,8 @@ app.classes.mail = AppJS.extend(
break; break;
case 'add': case 'add':
const current_id = tree.getValue(); const current_id = tree.getValue();
tree.refreshItem(0); // refresh root
// ToDo: tree.refreshItem() and openItem() should return a promise
// need to wait tree is refreshed: current and new id are there AND current folder is selected again // need to wait tree is refreshed: current and new id are there AND current folder is selected again
const interval = window.setInterval(() => { tree.refreshItem(0).then(() => {
if (tree.getNode(_id) && tree.getNode(current_id)) if (tree.getNode(_id) && tree.getNode(current_id))
{ {
if (!tree.getSelectedNode()) if (!tree.getSelectedNode())
@ -702,24 +700,21 @@ app.classes.mail = AppJS.extend(
} }
else else
{ {
window.clearInterval(interval);
// open new account // open new account
tree.openItem(_id, true);
// need to wait new folders are loaded AND current folder is selected again // need to wait new folders are loaded AND current folder is selected again
const open_interval = window.setInterval(() => { tree.openItem(_id, true).then(() => {
if (tree.getNode(_id + '::INBOX')) { if (tree.getNode(_id + '::INBOX')) {
if (!tree.getSelectedNode()) { if (!tree.getSelectedNode()) {
tree.reSelectItem(current_id); tree.reSelectItem(current_id);
} else { } else {
window.clearInterval(open_interval);
this.mail_changeFolder(_id + '::INBOX', tree, current_id); this.mail_changeFolder(_id + '::INBOX', tree, current_id);
tree.reSelectItem(_id + '::INBOX'); tree.reSelectItem(_id + '::INBOX');
} }
} }
}, 200); });
} }
} }
}, 200); });
break; break;
default: // null default: // null
} }
@ -4548,24 +4543,6 @@ app.classes.mail = AppJS.extend(
console.log(_data); console.log(_data);
}, },
/**
* Submit on apply button and save current tree state
*
* @param {type} _egw
* @param {type} _widget
* @returns {undefined}
*/
subscription_apply: function (_egw, _widget)
{
var tree = etemplate2.getByApplication('mail')[0].widgetContainer.getWidgetById('foldertree');
if (tree)
{
tree.input._xfullXML = true;
this.subscription_treeLastState = tree.input.serializeTreeToJSON();
}
this.et2._inst.submit(_widget);
},
/** /**
* Popup the subscription dialog * Popup the subscription dialog
* *
@ -4611,16 +4588,26 @@ app.classes.mail = AppJS.extend(
}, },
/** /**
* Onclick for node/foldername in subscription popup * Onclick for foldertree to (un)select children
* *
* Used to (un)check node including all children * Used to (un)check node including all children
* *
* @param {string} _id id of clicked node * @param {string} _id id of clicked node
* @param {et2_tree} _widget reference to tree widget * @param {et2_tree} _widget reference to tree widget
* @param {PoinerEvent} _ev
*/ */
subscribe_onclick: function(_id, _widget) foldertree_subselect: function(_id, _widget, _ev)
{
const node = _widget.getNode(_id);
// do we need to autoload the subitems first
if (node.child && !node.item.length)
{
_widget.refreshItem(_id).then(() =>_widget.setSubChecked(_id, "toggle"));
}
else
{ {
_widget.setSubChecked(_id, "toggle"); _widget.setSubChecked(_id, "toggle");
}
}, },
/** /**
@ -5555,6 +5542,7 @@ app.classes.mail = AppJS.extend(
}, },
/** /**
* Range selection for old dhtmlx tree currently NOT used
* *
* @param {type} _ids * @param {type} _ids
* @param {type} _widget * @param {type} _widget
@ -5572,7 +5560,7 @@ app.classes.mail = AppJS.extend(
* *
* @param {string} _a start node id * @param {string} _a start node id
* @param {string} _b end node id * @param {string} _b end node id
* @param {string} _branch totall node ids in the level * @param {string} _branch total node ids in the level
*/ */
var rangeSelector = function(_a,_b, _branch) var rangeSelector = function(_a,_b, _branch)
{ {
@ -5611,38 +5599,6 @@ app.classes.mail = AppJS.extend(
} }
}, },
/**
* Set enable/disable checkbox
*
* @param {object} _widget tree widget
* @param {string} _itemId item tree id
* @param {boolean} _stat - status to be set on checkbox true/false
*/
folderMgmt_setCheckbox: function (_widget, _itemId, _stat)
{
if (_widget)
{
_widget.input.setCheck(_itemId, _stat);
_widget.input.setSubChecked(_itemId,_stat);
}
},
/**
*
* @param {type} _id
* @param {type} _widget
* @TODO: Implement onCheck handler in order to select or deselect subItems
* of a checked parent node
*/
folderMgmt_onCheck: function (_id, _widget)
{
var selected = _widget.value;
if (selected && selected.split(_widget.input.dlmtr).length > 5)
{
egw.message(egw.lang('If you would like to select multiple folders in one action, you can hold ctrl key then select a folder as start range and another folder within a same level as end range, all folders in between will be selected or unselected based on their current status.'), 'success');
}
},
/** /**
* Delete button handler * Delete button handler
* triggers longTask dialog and send delete operation url * triggers longTask dialog and send delete operation url

View File

@ -6,8 +6,8 @@
<et2-description value="Folder Management" class="mail_folder_management_header"></et2-description> <et2-description value="Folder Management" class="mail_folder_management_header"></et2-description>
</et2-hbox> </et2-hbox>
<et2-hbox class="treeContainer"> <et2-hbox class="treeContainer">
<et2-tree id="tree" multiple="true" autoloading="mail_ui::ajax_folderMgmtTree_autoloading" multimarking="strict" <et2-tree id="tree" multiple="true" autoloading="mail_ui::ajax_folderMgmtTree_autoloading"
oncheck="app.mail.folderMgmt_onCheck" onselect="app.mail.folderMgmt_onSelect" highlighting="true"></et2-tree> x-onselect="app.mail.folderMgmt_onSelect" onclick="app.mail.foldertree_subselect"></et2-tree>
</et2-hbox> </et2-hbox>
<et2-hbox class="dialogFooterToolbar"> <et2-hbox class="dialogFooterToolbar">
<et2-button statustext="Delete" label="Delete" id="button[delete]" onclick="app.mail.folderMgmt_deleteBtn"></et2-button> <et2-button statustext="Delete" label="Delete" id="button[delete]" onclick="app.mail.folderMgmt_deleteBtn"></et2-button>

View File

@ -6,9 +6,7 @@
<et2-description value="Subscription folders" class="mail_subscription_header"></et2-description> <et2-description value="Subscription folders" class="mail_subscription_header"></et2-description>
</et2-hbox> </et2-hbox>
<et2-hbox class="treeContainer"> <et2-hbox class="treeContainer">
<et2-tree id="foldertree" multiple="true" autoloading="mail_ui::ajax_tree_autoloading" <et2-tree id="foldertree" multiple="true" onclick="app.mail.foldertree_subselect"></et2-tree>
multimarking="strict" highlighting="true" oncheck="app.mail.folderMgmt_onCheck"
onselect="app.mail.folderMgmt_onSelect"></et2-tree>
</et2-hbox> </et2-hbox>
<et2-hbox class="dialogFooterToolbar"> <et2-hbox class="dialogFooterToolbar">
<et2-button statustext="Saves subscription changes" label="Save" id="button[save]"></et2-button> <et2-button statustext="Saves subscription changes" label="Save" id="button[save]"></et2-button>