2024-02-08 21:17:55 +01:00
import { SlTreeItem } from "@shoelace-style/shoelace" ;
import { egw } from "../../jsapi/egw_global" ;
2024-02-23 18:49:16 +01:00
import { find_select_options , SelectOption } from "../Et2Select/FindSelectOptions" ;
2024-02-08 21:17:55 +01:00
import { Et2WidgetWithSelectMixin } from "../Et2Select/Et2WidgetWithSelectMixin" ;
import { css , html , LitElement , nothing , PropertyValues , TemplateResult } from "lit" ;
import { repeat } from "lit/directives/repeat.js" ;
2024-02-13 17:33:07 +01:00
import { query } from "lit/decorators/query.js" ;
2024-02-08 21:17:55 +01:00
import shoelace from "../Styles/shoelace" ;
import { property } from "lit/decorators/property.js" ;
import { state } from "lit/decorators/state.js" ;
2024-02-12 21:55:32 +01:00
import { egw_getActionManager , egw_getAppObjectManager } from "../../egw_action/egw_action" ;
2024-02-08 21:17:55 +01:00
import { et2_action_object_impl } from "../et2_core_DOMWidget" ;
import { EgwActionObject } from "../../egw_action/EgwActionObject" ;
import { EgwAction } from "../../egw_action/EgwAction" ;
import { EgwDragDropShoelaceTree } from "../../egw_action/EgwDragDropShoelaceTree" ;
2024-02-23 18:49:16 +01:00
export type TreeItemData = SelectOption & {
2024-02-08 21:17:55 +01:00
focused? : boolean ;
2024-02-14 01:15:23 +01:00
// Has children, but they may not be provided in item
2024-02-08 21:17:55 +01:00
child : Boolean | 1 ,
data? : Object , //{sieve:true,...} or {acl:true} or other
id : string ,
im0 : String ,
im1 : String ,
im2 : String ,
2024-02-14 01:15:23 +01:00
// Child items
2024-02-08 21:17:55 +01:00
item : TreeItemData [ ] ,
checked? : Boolean ,
nocheckbox : number | Boolean ,
open : 0 | 1 ,
parent : String ,
text : String ,
tooltip : String ,
userdata : any [ ]
2024-03-07 14:14:06 +01:00
//here we can store the number of unread messages, if there are any
2024-03-07 16:25:07 +01:00
badge? : String ;
2024-02-08 21:17:55 +01:00
}
/ * *
* @event { { id : String , item :SlTreeItem } } sl - expand emmited when tree item expands
* //TODO add for other events
* /
export class Et2Tree extends Et2WidgetWithSelectMixin ( LitElement )
{
/ * *
* Limit server searches to 100 results , matches Link : : DEFAULT_NUM_ROWS
* @type { number }
* /
static RESULT_LIMIT : number = 100 ;
//does not work because it would need to be run on the shadow root
@query ( "sl-tree-item[selected]" ) selected : SlTreeItem ;
@property ( { type : Boolean } )
multiple : Boolean = false ;
@property ( { type : String } )
leafIcon : String ;
@property ( { type : String } )
collapsedIcon : String ;
@property ( { type : String } )
openIcon : String ;
@property ( { type : Function } )
onclick ; // description: "JS code which gets executed when clicks on text of a node"
2024-02-12 23:17:27 +01:00
//onselect and oncheck only appear in multiselectTree
// @property()
// onselect // description: "Javascript executed when user selects a node"
// @property()
// oncheck // description: "Javascript executed when user checks a node"
@property ( { type : Boolean } )
highlighting : Boolean = false // description: "Add highlighting class on hovered over item, highlighting is disabled by default"
2024-02-08 21:17:55 +01:00
@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"
@property ( )
onopenstart //description: "Javascript function executed when user opens a node: function(_id, _widget, _hasChildren) returning true to allow opening!"
@property ( )
onopenend //description: "Javascript function executed when opening a node is finished: function(_id, _widget, _hasChildren)"
@property ( { type : String } )
imagePath : String = egw ( ) . webserverUrl + "/api/templates/default/images/dhtmlxtree/" //TODO we will need a different path here! maybe just rename the path?
// description: "Directory for tree structure images, set on server-side to 'dhtmlx' subdir of templates image-directory"
@property ( )
2024-02-14 18:42:17 +01:00
value = [ ]
2024-02-08 21:17:55 +01:00
protected autoloading_url : any ;
// private selectOptions: TreeItemData[] = [];
@state ( )
protected _selectOptions : TreeItemData [ ]
@state ( )
2024-02-12 23:17:27 +01:00
protected _currentOption : TreeItemData
2024-02-08 21:17:55 +01:00
@state ( )
protected _previousOption : TreeItemData
@state ( )
protected _currentSlTreeItem : SlTreeItem ;
2024-02-21 20:45:50 +01:00
@state ( )
selectedNodes : SlTreeItem [ ]
2024-02-12 23:17:27 +01:00
private input : any = null ;
private _actionManager : EgwAction ;
2024-02-08 21:17:55 +01:00
2024-02-13 17:33:07 +01:00
private get _tree() { return this . shadowRoot . querySelector ( 'sl-tree' ) ? ? null } ;
2024-02-08 21:17:55 +01:00
constructor ( )
{
super ( ) ;
2024-02-12 21:55:32 +01:00
this . _selectOptions = [ ] ;
2024-02-14 01:15:23 +01:00
this . _optionTemplate = this . _optionTemplate . bind ( this ) ;
2024-02-21 20:45:50 +01:00
this . selectedNodes = [ ] ;
2024-02-08 21:17:55 +01:00
}
2024-02-16 00:16:25 +01:00
firstUpdated ( )
{
// This is somehow required to set the autoload URL properly?
// TODO: Make this not needed, either this.autoload_url should be properly set or go away in favour of using this.autoload
this . createTree ( ) ;
// Check if top level should be autoloaded
if ( this . autoloading && ! this . _selectOptions ? . length )
{
this . handleLazyLoading ( { item : this._selectOptions } ) . then ( ( results ) = >
{
this . _selectOptions = results ? . item ? ? [ ] ;
this . requestUpdate ( "_selectOptions" ) ;
} )
}
2024-02-27 19:22:55 +01:00
// Actions can't be initialized without being connected to InstanceManager
this . _initActions ( ) ;
2024-02-16 00:16:25 +01:00
}
2024-02-12 23:17:27 +01:00
//Sl-Trees handle their own onClick events
_handleClick ( _ev )
{
}
2024-02-08 21:17:55 +01:00
static get styles ( )
{
return [
shoelace ,
// @ts-ignore
. . . super . styles ,
css `
: host {
-- sl - spacing - large : 1rem ;
}
: : part ( expand - button ) {
padding : 0 ;
}
2024-02-16 19:57:22 +01:00
/* Stop icon from shrinking if there's not enough space */
sl - tree - item sl - icon {
flex : 0 0 1 em ;
}
: : part ( label ) {
overflow : hidden ;
}
: : part ( label ) : hover {
2024-02-12 23:17:27 +01:00
text - decoration : underline ;
}
2024-02-16 19:57:22 +01:00
. tree - item__label {
overflow : hidden ;
white - space : nowrap ;
text - overflow : ellipsis ;
}
2024-02-21 16:13:51 +01:00
sl - tree - item {
background - color : white ;
}
sl - tree - item . drop - hover {
background - color : # 0 a5ca5 ;
}
sl - tree - item . drop - hover sl - tree - item {
background - color : white ;
}
2024-02-12 23:17:27 +01:00
`
2024-02-21 16:13:51 +01:00
//todo bg color on drop-hover should take precedence over selected color change
2024-02-08 21:17:55 +01:00
]
}
private _actions : object
get actions ( )
{
return this . _actions
}
/ * *
* Set Actions on the widget
*
* Each action is defined as an object :
*
* move : {
* type : "drop" ,
* acceptedTypes : "mail" ,
* icon : "move" ,
* caption : "Move to"
* onExecute : javascript : mail_move "
* }
*
* This will turn the widget into a drop target for "mail" drag types . When "mail" drag types are dropped ,
* the global function mail_move ( egwAction action , egwActionObject sender ) will be called . The ID of the
* dragged "mail" will be in sender . id , some information about the sender will be in sender . context . The
* etemplate2 widget involved can typically be found in action . parent . data . widget , so your handler
* can operate in the widget context easily . The location varies depending on your action though . It
* might be action . parent . parent . data . widget
*
* To customise how the actions are handled for a particular widget , override _link_actions ( ) . It handles
* the more widget - specific parts .
*
* @param { object } actions { ID : { attributes . . } + } map of egw action information
* @see api / src / Etemplate / Widget / Nextmatch . php egw_actions ( ) method
* /
@property ( { type : Object } )
set actions ( actions : object )
{
this . _actions = actions
if ( this . id == "" || typeof this . id == "undefined" )
{
2024-02-27 19:22:55 +01:00
if ( this . isConnected )
{
window . egw ( ) . debug ( "warn" , "Widget should have an ID if you want actions" , this ) ;
}
// No id because we're not done yet, try again later
2024-02-08 21:17:55 +01:00
return ;
}
2024-02-27 19:22:55 +01:00
if ( this . isConnected )
{
this . _initActions ( ) ;
}
}
public loadFromXML ( )
{
let new_options = [ ] ;
if ( this . id )
{
new_options = < TreeItemData [ ] > < unknown > find_select_options ( this ) [ 1 ] ;
}
if ( new_options ? . length )
{
this . _selectOptions = new_options ;
}
}
/ * *
* Initialize the action manager and add some actions to it
* @private
* /
private _initActions ( )
{
2024-02-08 21:17:55 +01:00
// Only look 1 level deep
// @ts-ignore exists from Et2Widget
var gam = egw_getActionManager ( this . egw ( ) . appName , true , 1 ) ;
2024-02-27 19:22:55 +01:00
if ( typeof this . _actionManager != "object" )
2024-02-08 21:17:55 +01:00
{
// @ts-ignore exists from Et2Widget
2024-02-27 19:22:55 +01:00
if ( this . getInstanceManager ( ) && gam . getActionById ( this . getInstanceManager ( ) . uniqueId , 1 ) !== null )
2024-02-08 21:17:55 +01:00
{
// @ts-ignore exists from Et2Widget
gam = gam . getActionById ( this . getInstanceManager ( ) . uniqueId , 1 ) ;
}
2024-02-27 19:22:55 +01:00
if ( gam . getActionById ( this . id , 1 ) != null )
2024-02-08 21:17:55 +01:00
{
this . _actionManager = gam . getActionById ( this . id , 1 ) ;
2024-02-27 19:22:55 +01:00
}
else
2024-02-08 21:17:55 +01:00
{
this . _actionManager = gam . addAction ( "actionManager" , this . id ) ;
}
}
// @ts-ignore egw() exists on this
2024-02-27 19:22:55 +01:00
this . _actionManager . updateActions ( this . actions , this . egw ( ) . appName ) ;
2024-02-08 21:17:55 +01:00
// @ts-ignore
2024-02-27 19:22:55 +01:00
if ( this . options . default_execute )
{
this . _actionManager . setDefaultExecute ( this . options . default_execute ) ;
}
2024-02-08 21:17:55 +01:00
// Put a reference to the widget into the action stuff, so we can
// easily get back to widget context from the action handler
this . _actionManager . data = { widget : this } ;
}
2024-02-13 17:33:07 +01:00
/** Sets focus on the control. */
focus ( options? : FocusOptions )
{
this . _tree . focus ( ) ;
}
/** Removes focus from the control. */
blur ( )
{
this . _tree . blur ( ) ;
}
2024-02-08 21:17:55 +01:00
/ * *
* @deprecated assign to onopenstart
* @param _handler
* /
public set_onopenstart ( _handler : any )
{
this . onopenstart = _handler
this . installHandler ( "onopenstart" , _handler )
}
2024-02-12 23:17:27 +01:00
2024-02-08 21:17:55 +01:00
/ * *
* @deprecated assign to onopenend
* @param _handler
* /
public set_onopenend ( _handler : any )
{
this . onopenend = _handler
this . installHandler ( 'onopenend' , _handler ) ;
}
/ * *
* @deprecated assign to onclick
* @param _handler
* /
public set_onclick ( _handler : Function )
{
this . installHandler ( 'onclick' , _handler ) ;
}
/ * *
* @deprecated assign to onselect
* @param _handler
* /
public set_onselect ( _handler : any )
{
this . onselect = _handler ;
}
2024-03-07 16:25:07 +01:00
public set_badge ( _id : string , _value : string )
2024-03-07 14:14:06 +01:00
{
2024-03-07 16:25:07 +01:00
this . getNode ( _id ) . badge = _value ;
2024-03-07 14:14:06 +01:00
this . requestUpdate ( ) ;
}
2024-02-08 21:17:55 +01:00
2024-02-21 09:54:20 +01:00
/ * *
* @return currently selected Item or First Item , if no selection was made yet
* /
2024-02-08 21:17:55 +01:00
public getSelectedItem ( ) : TreeItemData
{
2024-03-06 17:20:07 +01:00
return this . _currentOption || ( this . _selectOptions ? this . _selectOptions [ 0 ] : null ) ;
2024-02-08 21:17:55 +01:00
}
2024-02-21 20:45:50 +01:00
/ * *
* getValue , retrieves the Ids of the selected Items
* @return string or object or null
* /
getValue ( )
{
2024-03-06 17:20:07 +01:00
if ( this . multiple )
{
let res :string [ ] = [ ] ;
if ( this . selectedNodes ? . length )
2024-02-21 20:45:50 +01:00
{
2024-03-06 17:20:07 +01:00
for ( const selectedNode of this . selectedNodes )
{
res . push ( selectedNode . id ) ;
}
2024-02-21 20:45:50 +01:00
}
2024-03-06 17:20:07 +01:00
return res ;
2024-03-06 16:27:19 +01:00
}
2024-03-06 17:20:07 +01:00
return this . getSelectedItem ( ) ? . id ;
2024-02-21 20:45:50 +01:00
}
2024-02-08 21:17:55 +01:00
/ * *
* getSelectedNode , retrieves the full node of the selected Item
* @return { SlTreeItem } full SlTreeItem
* /
getSelectedNode ( ) : SlTreeItem
{
return this . _currentSlTreeItem
}
2024-02-08 22:20:11 +01:00
getDomNode ( _id ) : SlTreeItem
2024-02-08 21:17:55 +01:00
{
return this . shadowRoot . querySelector ( "sl-tree-item[id='" + _id + "'" ) ;
}
/ * *
2024-02-08 22:20:11 +01:00
* return the Item with given _id , was called getDomNode ( _id ) in dhtmlxTree
2024-02-08 21:17:55 +01:00
* @param _id
* /
2024-02-08 22:20:11 +01:00
public getNode ( _id : string ) : TreeItemData
2024-02-08 21:17:55 +01:00
{
2024-02-21 09:54:20 +01:00
if ( _id == undefined ) { debugger ; }
2024-02-16 16:59:11 +01:00
// TODO: Look into this._search(), find out why it doesn't always succeed
2024-02-27 19:22:55 +01:00
return this . _search ( _id , this . _selectOptions ) ? ? this . optionSearch ( _id , this . _selectOptions , 'value' , 'children' )
2024-02-08 21:17:55 +01:00
}
/ * *
* set the text of item with given id to new label
* @param _id
* @param _label
* @param _tooltip
* /
setLabel ( _id , _label , _tooltip ? )
{
2024-02-08 22:20:11 +01:00
let tooltip = _tooltip || ( this . getNode ( _id ) && this . getNode ( _id ) . tooltip ? this . getNode ( _id ) . tooltip : "" ) ;
let i = this . getNode ( _id )
2024-02-08 21:17:55 +01:00
i . tooltip = tooltip
i . text = _label
}
/ * *
* getLabel , gets the Label of of an item by id
* @param _id ID of the node
* @return _label
* /
getLabel ( _id )
{
2024-02-08 22:20:11 +01:00
return this . getNode ( _id ) ? . text ;
2024-02-08 21:17:55 +01:00
}
/ * *
* getSelectedLabel , retrieves the Label of the selected Item
* @return string or null
* /
getSelectedLabel ( )
{
return this . getSelectedItem ( ) ? . text
}
/ * *
* deleteItem , deletes an item by id
* @param _id ID of the node
* @param _selectParent select the parent node true / false TODO unused atm
* @return void
* /
deleteItem ( _id , _selectParent )
{
this . _deleteItem ( _id , this . _selectOptions )
// Update action
// since the action ID has to = this.id, getObjectById() won't work
let treeObj = ( < EgwActionObject > < unknown > egw_getAppObjectManager ( false ) ) . getObjectById ( this . id ) ;
for ( let i = 0 ; i < treeObj . children . length ; i ++ )
{
if ( treeObj . children [ i ] . id == _id )
{
treeObj . children . splice ( i , 1 ) ;
}
}
this . requestUpdate ( ) ;
}
/ * *
* Updates a leaf of the tree by requesting new information from the server using the
* autoloading attribute .
*
* @param { string } _id ID of the node
* @param { Object } [ data ] If provided , the item is refreshed directly with
* the provided data instead of asking the server
* @return void
* /
refreshItem ( _id , data )
{
if ( typeof data != "undefined" && data != null )
{
//TODO currently always ask the sever
//data seems never to be used
this . refreshItem ( _id , null )
} else
{
2024-02-08 22:20:11 +01:00
let item = this . getNode ( _id )
2024-02-08 21:17:55 +01:00
this . handleLazyLoading ( item ) . then ( ( result ) = > {
item . item = [ . . . result . item ]
this . requestUpdate ( "_selectOptions" )
} )
}
}
/ * *
* Does nothing
* @param _id
* @param _style
* /
setStyle ( _id , _style )
{
2024-03-07 14:14:06 +01:00
var temp = this . getDomNode ( _id ) . defaultSlot ;
2024-02-12 23:17:27 +01:00
if ( ! temp ) return 0 ;
if ( ! temp . style . cssText )
temp . setAttribute ( "style" , _style ) ;
else
temp . style . cssText = temp . style . cssText + ";" + _style ;
2024-02-08 21:17:55 +01:00
}
/ * *
* getTreeNodeOpenItems TODO
*
2024-03-07 14:14:06 +01:00
* @param { string } _nodeID the nodeID where to start from ( initial node ) 0 means for all items
2024-02-08 21:17:55 +01:00
* @param { string } mode the mode to run in : "forced" fakes the initial node openState to be open
* @return { object } structured array of node ids : array ( message - ids )
* /
2024-03-07 14:14:06 +01:00
getTreeNodeOpenItems ( _nodeID : string | 0 , mode? : string )
{
//let z:string[] = this.input.getSubItems(_nodeID).split(this.input.dlmtr);
let subItems =
( _nodeID == 0 ) ?
this . _selectOptions . map ( option = > this . getDomNode ( option . id ) ) : //NodeID == 0 means that we want all tree Items
this . getDomNode ( _nodeID ) . getChildrenItems ( ) ; // otherwise get the subItems of the given Node
let oS : boolean ;
let PoS : 0 | 1 | - 1 ;
let rv : string [ ] ;
let returnValue = ( _nodeID == 0 ) ? [ ] : [ _nodeID ] ; // do not keep 0 in the return value...
// it is not needed and only throws a php warning later TODO check with ralf what happens in mail_ui.inc.php with ajax_setFolderStatus
let modetorun = "none" ;
if ( mode )
{
modetorun = mode ;
}
PoS = ( _nodeID == 0 ) ? 1 : ( this . getDomNode ( _nodeID ) . expanded ? 1 : 0 )
if ( modetorun == "forced" ) PoS = 1 ;
if ( PoS == 1 )
{
for ( const item of subItems )
{
//oS = this.input.getOpenState(z[i]);
oS = item . expanded // iff current item is expanded go deeper
//if (oS == -1) {returnValue.push(z[i]);}
//if (oS == 0) {returnValue.push(z[i]);}
if ( ! oS )
{
returnValue . push ( item . id )
}
//if (oS == 1)
else
{
rv = this . getTreeNodeOpenItems ( item . id ) ;
for ( const recId of rv )
{
returnValue . push ( recId ) ;
}
}
}
}
//alert(returnValue.join('#,#'));
return returnValue ;
2024-02-08 21:17:55 +01:00
}
/ * *
* @param _id
* @param _newItemId
* @param _label
* /
public renameItem ( _id , _newItemId , _label )
{
2024-02-08 22:20:11 +01:00
this . getNode ( _id ) . id = _newItemId
2024-02-08 21:17:55 +01:00
// Update action
// since the action ID has to = this.id, getObjectById() won't work
2024-02-12 23:17:27 +01:00
let treeObj : EgwActionObject = egw_getAppObjectManager ( false ) . getObjectById ( this . id ) ;
for ( const actionObject of treeObj . children )
{
if ( actionObject . id == _id )
{
2024-02-08 21:17:55 +01:00
actionObject . id = _newItemId ;
2024-02-12 23:17:27 +01:00
if ( actionObject . iface )
{
actionObject . iface . id = _newItemId
}
2024-02-08 21:17:55 +01:00
break
}
}
if ( typeof _label != 'undefined' ) this . setLabel ( _newItemId , _label ) ;
this . requestUpdate ( )
}
public focusItem ( _id )
{
2024-02-08 22:20:11 +01:00
let item = this . getNode ( _id )
2024-02-08 21:17:55 +01:00
item . focused = true
}
2024-02-15 22:32:40 +01:00
public openItem ( _id )
{
let item = this . getNode ( _id ) ;
if ( item )
{
2024-03-07 14:14:06 +01:00
item . open = 1 ;
2024-02-15 22:32:40 +01:00
}
this . requestUpdate ( ) ;
}
2024-02-08 21:17:55 +01:00
/ * *
* hasChildren
*
* @param _id ID of the node
* @return the number of childelements
* /
hasChildren ( _id )
{
2024-02-08 22:20:11 +01:00
return this . getNode ( _id ) . item ? . length ;
2024-02-08 21:17:55 +01:00
}
/ * *
* reSelectItem , reselects an item by id
* @param _id ID of the node
* /
reSelectItem ( _id )
{
this . _previousOption = this . _currentOption
2024-02-08 22:20:11 +01:00
this . _currentOption = this . getNode ( _id ) ;
const node : SlTreeItem = this . getDomNode ( _id )
2024-02-08 21:17:55 +01:00
if ( node )
{
this . _currentSlTreeItem = node ;
node . selected = true
}
}
getUserData ( _nodeId , _name )
{
2024-02-14 19:18:15 +01:00
return this . getNode ( _nodeId ) ? . userdata ? . find ( elem = > elem . name === _name ) ? . content
2024-02-08 21:17:55 +01:00
}
2024-02-28 22:59:39 +01:00
/ * *
* Overridable , add style
* @returns { TemplateResult < 1 > }
* /
styleTemplate ( )
{
return html ` ` ;
}
2024-02-08 21:17:55 +01:00
//this.selectOptions = find_select_options(this)[1];
_optionTemplate ( selectOption : TreeItemData ) : TemplateResult < 1 >
{
/ *
if collapsed . . opended ? leaf ?
* /
2024-02-23 18:49:16 +01:00
let img : String = selectOption . icon ? ? selectOption . im0 ? ? selectOption . im1 ? ? selectOption . im2 ;
2024-02-08 21:17:55 +01:00
if ( img )
{
//sl-icon images need to be svgs if there is a png try to find the corresponding svg
img = img . endsWith ( ".png" ) ? img . replace ( ".png" , ".svg" ) : img ;
}
2024-02-16 00:16:25 +01:00
// Check to see if node is marked as open with no children. If autoloadable, load the children
const expandState = ( this . calculateExpandState ( selectOption ) ) ;
2024-02-23 08:53:13 +01:00
// lazy iff "child" is set and "item" is empty or item does not exist in the first place
const lazy = ( selectOption . item ? . length === 0 && selectOption . child ) || ( selectOption . child && ! selectOption . item )
2024-02-16 00:16:25 +01:00
if ( expandState && this . autoloading && lazy )
{
this . updateComplete . then ( ( ) = >
{
this . getDomNode ( selectOption . id ) ? . dispatchEvent ( new CustomEvent ( "sl-lazy-load" ) ) ;
} )
}
2024-02-26 23:19:41 +01:00
const value = selectOption . value ? ? selectOption . id ;
2024-02-16 00:16:25 +01:00
2024-02-08 21:17:55 +01:00
return html `
< sl - tree - item
2024-02-14 01:15:23 +01:00
part = "item"
2024-02-28 22:59:39 +01:00
exportparts = "checkbox, label, item:item-item"
2024-02-08 21:17:55 +01:00
id = $ { selectOption . id }
2024-02-14 01:15:23 +01:00
title = $ { selectOption . tooltip || nothing }
2024-02-28 22:59:39 +01:00
class = $ { selectOption . class || nothing }
2024-02-26 23:19:41 +01:00
? selected = $ { typeof this . value == "string" && this . value == value || Array . isArray ( this . value ) && this . value . includes ( value ) }
2024-02-16 00:16:25 +01:00
? expanded = $ { expandState }
? lazy = $ { lazy }
2024-02-08 21:17:55 +01:00
? focused = $ { selectOption . focused || nothing }
@sl - lazy - load = $ { ( event ) = > {
2024-02-15 21:43:52 +01:00
// No need for this to bubble up, we'll handle it (otherwise the parent leaf will load too)
event . stopPropagation ( ) ;
2024-02-08 21:17:55 +01:00
this . handleLazyLoading ( selectOption ) . then ( ( result ) = > {
2024-02-15 21:43:52 +01:00
// TODO: We already have the right option in context. Look into this.getNode(), find out why it's there. It doesn't do a deep search.
2024-02-16 16:59:11 +01:00
const parentNode = selectOption ? ? this . getNode ( selectOption . id ) ? ? this . optionSearch ( selectOption . id , this . _selectOptions , 'id' , 'item' ) ;
2024-02-15 21:43:52 +01:00
parentNode . item = [ . . . result . item ]
2024-02-08 21:17:55 +01:00
this . requestUpdate ( "_selectOptions" )
} )
} }
>
< sl - icon src = "${img ?? nothing}" > < / s l - i c o n >
2024-03-07 14:14:06 +01:00
< span class = "tree-item__label" >
$ { selectOption . label ? ? selectOption . text }
< / span >
2024-03-07 16:25:07 +01:00
$ { selectOption . badge ?
2024-03-07 14:18:55 +01:00
html `
2024-03-07 16:25:07 +01:00
< sl - badge pill variant = "danger" > $ { selectOption . badge } < / s l - b a d g e >
2024-03-07 14:18:55 +01:00
` : nothing}
2024-02-08 21:17:55 +01:00
2024-02-23 18:49:16 +01:00
$ { selectOption . children ? repeat ( selectOption . children , this . _optionTemplate ) : ( selectOption . item ? repeat ( selectOption . item , this . _optionTemplate ) : nothing ) }
2024-02-08 21:17:55 +01:00
< / s l - t r e e - i t e m > `
}
public render ( ) : unknown
{
return html `
2024-02-28 22:59:39 +01:00
$ { this . styleTemplate ( ) }
2024-02-08 21:17:55 +01:00
< sl - tree
2024-02-28 22:59:39 +01:00
part = "tree"
2024-02-14 01:15:23 +01:00
. selection = $ { this . multiple ? "multiple" : "single" }
2024-02-08 21:17:55 +01:00
@sl - selection - change = $ {
( event : any ) = > {
2024-02-14 19:29:08 +01:00
this . _previousOption = this . _currentOption ? ? ( this . value . length ? this . getNode ( this . value ) : null ) ;
2024-02-16 16:59:11 +01:00
this . _currentOption = this . getNode ( event . detail . selection [ 0 ] . id ) ? ? this . optionSearch ( event . detail . selection [ 0 ] . id , this . _selectOptions , 'id' , 'item' ) ;
2024-02-15 19:54:57 +01:00
const ids = event . detail . selection . map ( i = > i . id ) ;
this . value = this . multiple ? ids ? ? [ ] : ids [ 0 ] ? ? "" ;
2024-02-14 19:29:08 +01:00
event . detail . previous = this . _previousOption ? . id ;
2024-02-08 21:17:55 +01:00
this . _currentSlTreeItem = event . detail . selection [ 0 ] ;
2024-02-21 20:45:50 +01:00
if ( this . multiple )
{
this . selectedNodes = event . detail . selection
}
2024-02-13 18:41:27 +01:00
if ( typeof this . onclick == "function" )
{
this . onclick ( event . detail . selection [ 0 ] . id , this , event . detail . previous )
}
2024-02-08 21:17:55 +01:00
}
}
@sl - expand = $ {
( event ) = > {
event . detail . id = event . target . id
event . detail . item = event . target
2024-02-12 23:17:27 +01:00
this . onopenstart ( event . detail . id , this , 1 )
2024-02-08 21:17:55 +01:00
}
}
@sl - after - expand = $ {
( event ) = > {
event . detail . id = event . target . id
event . detail . item = event . target
2024-02-12 23:17:27 +01:00
this . onopenend ( event . detail . id , this , - 1 )
2024-02-08 21:17:55 +01:00
}
}
>
2024-02-14 01:15:23 +01:00
$ { repeat ( this . _selectOptions , this . _optionTemplate ) }
2024-02-08 21:17:55 +01:00
< / s l - t r e e >
` ;
}
handleLazyLoading ( _item : TreeItemData )
{
let requestLink = egw ( ) . link ( egw ( ) . ajaxUrl ( egw ( ) . decodePath ( this . autoloading_url ) ) ,
{
id : _item.id
} )
let result : Promise < TreeItemData > = egw ( ) . request ( requestLink , [ ] )
return result
. then ( ( results ) = > {
_item = results ;
return results ;
} ) ;
}
/ * *
*
*
* /
_link_actions ( actions )
{
2024-02-27 19:22:55 +01:00
if ( this . actions && ! this . _actionManager )
{
// ActionManager creation was missed
this . actions = this . _actions ;
}
2024-02-08 21:17:55 +01:00
// Get the top level element for the tree
let objectManager = egw_getAppObjectManager ( true ) ;
let widget_object = objectManager . getObjectById ( this . id ) ;
if ( widget_object == null )
{
// Add a new container to the object manager which will hold the widget
// objects
widget_object = objectManager . insertObject ( false , new EgwActionObject (
//@ts-ignore
this . id , objectManager , ( new et2_action_object_impl ( this , this ) ) . getAOI ( ) ,
this . _actionManager || objectManager . manager . getActionById ( this . id ) || objectManager . manager
) ) ;
} else
{
// @ts-ignore
widget_object . setAOI ( ( new et2_action_object_impl ( this , this ) ) . getAOI ( ) ) ;
}
// Delete all old objects
widget_object . clear ( ) ;
widget_object . unregisterActions ( ) ;
// Go over the widget & add links - this is where we decide which actions are
// 'allowed' for this widget at this time
var action_links = this . _get_action_links ( actions ) ;
//Drop target enabeling
if ( typeof this . _selectOptions != 'undefined' )
{
let self : Et2Tree = this
// Iterate over the options (leaves) and add action to each one
let apply_actions = function ( treeObj : EgwActionObject , option : TreeItemData ) {
// Add a new action object to the object manager
2024-02-27 19:22:55 +01:00
let id = option . value ? ? ( typeof option . value == 'number' ? String ( option . id ) : option . id ) ;
2024-02-08 21:17:55 +01:00
// @ts-ignore
2024-02-27 19:22:55 +01:00
let obj : EgwActionObject = treeObj . addObject ( id , new EgwDragDropShoelaceTree ( self , id ) ) ;
2024-02-08 21:17:55 +01:00
obj . updateActionLinks ( action_links ) ;
2024-02-27 19:22:55 +01:00
const children = option . children ? ? option . item ? ? [ ] ;
for ( let i = 0 ; i < children . length ; i ++ )
2024-02-08 21:17:55 +01:00
{
2024-02-27 19:22:55 +01:00
apply_actions . call ( this , treeObj , children [ i ] ) ;
2024-02-08 21:17:55 +01:00
}
} ;
for ( const selectOption of this . _selectOptions )
{
apply_actions . call ( this , widget_object , selectOption )
}
}
widget_object . updateActionLinks ( action_links ) ;
}
/ * *
* Get all action - links / id ' s of 1 . - level actions from a given action object
*
* This can be overwritten to not allow all actions , by not returning them here .
*
* @param actions
* @returns { Array }
* /
_get_action_links ( actions )
{
var action_links = [ ] ;
for ( var i in actions )
{
var action = actions [ i ] ;
action_links . push ( typeof action . id != 'undefined' ? action.id : i ) ;
}
return action_links ;
}
2024-02-12 23:17:27 +01:00
protected updated ( _changedProperties : PropertyValues )
{
2024-02-15 16:13:09 +01:00
this . _link_actions ( this . actions )
2024-02-12 23:17:27 +01:00
super . updated ( _changedProperties ) ;
}
2024-02-21 09:54:20 +01:00
/ * *
*
* @param _id to search for
* @param data { TreeItemData [ ] } structure to search in
* @return { TreeItemData } node with the given _id or null
* @private
* /
2024-02-12 23:17:27 +01:00
private _search ( _id : string , data : TreeItemData [ ] ) : TreeItemData
{
let res : TreeItemData = null
2024-02-21 09:54:20 +01:00
if ( _id == undefined )
{
return null
}
2024-02-12 23:17:27 +01:00
for ( const value of data )
{
if ( value . id === _id )
{
res = value
return res
2024-02-16 16:59:11 +01:00
}
2024-03-07 14:46:00 +01:00
else if ( _id ? . startsWith ( value . id ) && typeof value . item !== "undefined" )
2024-02-12 23:17:27 +01:00
{
res = this . _search ( _id , value . item )
}
}
return res
}
private calculateExpandState = ( selectOption : TreeItemData ) = > {
2024-02-14 01:15:23 +01:00
2024-02-12 23:17:27 +01:00
if ( selectOption . open )
{
return true
}
2024-02-28 16:59:11 +01:00
// TODO: Move this mail-specific stuff into mail
2024-02-14 01:15:23 +01:00
if ( selectOption . id && ( selectOption . id . endsWith ( "INBOX" ) || selectOption . id == window . egw . preference ( "ActiveProfileID" , "mail" ) ) )
{
return true
}
2024-02-28 16:59:11 +01:00
return false ;
2024-02-12 23:17:27 +01:00
}
2024-02-08 21:17:55 +01:00
private _deleteItem ( _id , list )
{
for ( let i = 0 ; i < list . length ; i ++ )
{
const value = list [ i ] ;
if ( value . id === _id )
{
list . splice ( i , 1 )
} else if ( _id . startsWith ( value . id ) )
{
this . _deleteItem ( _id , value . item )
}
}
}
private installHandler ( _name : String , _handler : Function )
{
if ( this . input == null ) this . createTree ( ) ;
// automatic convert onChange event to oncheck or onSelect depending on multiple is used or not
// if (_name == "onchange") {
// _name = this.options.multiple ? "oncheck" : "onselect"
// }
// let handler = _handler;
// let widget = this;
// this.input.attachEvent(_name, function(_id){
// let args = jQuery.makeArray(arguments);
// // splice in widget as 2. parameter, 1. is new node-id, now 3. is old node id
// args.splice(1, 0, widget);
// // try to close mobile sidemenu after clicking on node
// if (egwIsMobile() && typeof args[2] == 'string') framework.toggleMenu('on');
// return handler.apply(this, args);
// });
}
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
if ( this . autoloading )
{
// @ts-ignore from static get properties
let url = this . autoloading ;
if ( url . charAt ( 0 ) != '/' && url . substr ( 0 , 4 ) != 'http' )
{
url = '/json.php?menuaction=' + url ;
}
this . autoloading_url = url ;
}
}
}
2024-02-14 10:19:41 +01:00
customElements . define ( "et2-tree" , Et2Tree ) ;