diff --git a/Build/Sources/TypeScript/backend/tree/tree.ts b/Build/Sources/TypeScript/backend/tree/tree.ts index a3d90ce96c64bffe7344b27e6e1d39284081f230..21a2b6fa8bb1c3b662476f3dcf715d61fa56a104 100644 --- a/Build/Sources/TypeScript/backend/tree/tree.ts +++ b/Build/Sources/TypeScript/backend/tree/tree.ts @@ -322,13 +322,13 @@ export class Tree extends LitElement { }]).pop(); if (parentNode) { - if (target.hasChildren && !target.expanded) { - await this.showChildren(target); + if (parentNode.hasChildren && !parentNode.expanded) { + await this.showChildren(parentNode); } - if (!target.hasChildren) { - target.hasChildren = true; - target.expanded = true; + if (!parentNode.hasChildren) { + parentNode.hasChildren = true; + parentNode.expanded = true; } } @@ -342,9 +342,17 @@ export class Tree extends LitElement { public async removeNode(node: TreeNodeInterface) { const index = this.nodes.indexOf(node); + const parentNode = this.getParentNode(node); if (index > -1) { this.nodes.splice(index, 1); } + this.requestUpdate(); + this.updateComplete.then(() => { + if (parentNode.expanded && parentNode.hasChildren && this.getNodeChildren(parentNode).length === 0) { + parentNode.hasChildren = false; + parentNode.expanded = false; + } + }); } public filter(searchTerm?: string|null): void { diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js index ace5876850adde18db271ce625e7762d771e8677..536322e82f53d2d03d7f2402389acc4597a0374a 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,o);else for(var d=e.length-1;d>=0;d--)(n=e[d])&&(r=(s<3?n(r):s>3?n(t,i,r):n(t,i))||r);return s>3&&r&&Object.defineProperty(t,i,r),r};import{html,LitElement,nothing}from"lit";import{property,state,query}from"lit/decorators.js";import{repeat}from"lit/directives/repeat.js";import{styleMap}from"lit/directives/style-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{TreeNodeCommandEnum,TreeNodePositionEnum}from"@typo3/backend/tree/tree-node.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";import{KeyTypesEnum as KeyTypes}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/icon-element.js";import ClientStorage from"@typo3/backend/storage/client.js";import{DataTransferTypes}from"@typo3/backend/enum/data-transfer-types.js";import Severity from"@typo3/backend/severity.js";export class Tree extends LitElement{constructor(){super(...arguments),this.setup=null,this.settings={showIcons:!1,width:300,dataUrl:"",filterUrl:"",defaultProperties:{},expandUpToLevel:null,actions:[]},this.nodes=[],this.currentScrollPosition=0,this.currentVisibleHeight=0,this.searchTerm=null,this.loading=!1,this.hoveredNode=null,this.nodeDragAllowed=!1,this.isOverRoot=!1,this.nodeDragPosition=null,this.nodeDragMode=null,this.draggingNode=null,this.nodeHeight=32,this.indentWidth=20,this.displayNodes=[],this.focusedNode=null,this.editingNode=null,this.openNodeTimeout={targetNode:null,timeout:null},this.unfilteredNodes="",this.muteErrorNotifications=!1,this.networkErrorTitle=top.TYPO3.lang.tree_networkError,this.networkErrorMessage=top.TYPO3.lang.tree_networkErrorDescription,this.allowNodeEdit=!1,this.allowNodeDrag=!1,this.allowNodeSorting=!1}getNodeFromElement(e){return null!==e&&"treeId"in e.dataset?this.getNodeByTreeIdentifier(e.dataset.treeId):null}getElementFromNode(e){return this.querySelector('[data-tree-id="'+this.getNodeTreeIdentifier(e)+'"]')}hideChildren(e){e.expanded=!1,this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}async showChildren(e){e.expanded=!0,await this.loadChildren(e),this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}getDataUrl(e=null){return null===e?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&depth="+e.depth}getFilterUrl(){return this.settings.filterUrl+"&q="+this.searchTerm}async loadData(){this.loading=!0,this.nodes=this.prepareNodes(await this.fetchData()),this.loading=!1}async fetchData(e=null){try{const t=await new AjaxRequest(this.getDataUrl(e)).get({cache:"no-cache"});let i=await t.resolve();if(!Array.isArray(i))return[];null!==e&&(i=i.filter((t=>t.identifier!==e.identifier)),i.unshift(e)),i=this.enhanceNodes(i),null!==e&&i.shift();const o=await Promise.all(i.map((async e=>{const t=e.__parents.join("_"),o=i.find((e=>e.__treeIdentifier===t))||null,n=null===o||o.expanded;if(!e.loaded&&e.hasChildren&&e.expanded&&n){const t=await this.fetchData(e);return e.loaded=!0,[e,...t]}return[e]})));return o.flat()}catch(e){return this.errorNotification(e),[]}}async loadChildren(e){try{if(e.loaded)return void await Promise.all(this.nodes.filter((t=>t.__parents.join("_")===e.__treeIdentifier&&!t.loaded&&t.hasChildren&&t.expanded)).map((e=>this.loadChildren(e))));e.__loading=!0;const t=await this.fetchData(e),i=this.nodes.indexOf(e)+1;let o=0;for(let t=i;t<this.nodes.length&&!(this.nodes[t].depth<=e.depth);++t)o++;this.nodes.splice(i,o,...t),e.__loading=!1,e.loaded=!0}catch(t){throw this.errorNotification(t),e.__loading=!1,t}}getIdentifier(){return this.id??this.setup.id}getLocalStorageIdentifier(){return"tree-state-"+this.getIdentifier()}getNodeStatus(e){return(JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{})[e.__treeIdentifier]??{expanded:!1}}saveNodeStatus(e){const t=JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{};t[e.__treeIdentifier]={expanded:e.expanded},ClientStorage.set(this.getLocalStorageIdentifier(),JSON.stringify(t))}refreshOrFilterTree(){""!==this.searchTerm?this.filter(this.searchTerm):this.loadData()}selectFirstNode(){const e=this.getFirstNode();this.selectNode(e,!0),this.focusNode(e)}selectNode(e,t=!0){this.isNodeSelectable(e)&&(this.resetSelectedNodes(),e.checked=!0,this.dispatchEvent(new CustomEvent("typo3:tree:node-selected",{detail:{node:e,propagate:t}})))}async focusNode(e){this.focusedNode=e;const t=this.getElementFromNode(this.focusedNode);t?t.focus():(this.requestUpdate(),this.updateComplete.then((()=>{this.getElementFromNode(this.focusedNode)?.focus()})))}async editNode(e){this.isNodeEditable(e)&&(this.editingNode=e,this.requestUpdate(),this.updateComplete.then((()=>{const e=this.getElementFromNode(this.editingNode)?.querySelector(".node-edit");e&&(e.focus(),e.select())})))}async deleteNode(e){e.deletable?this.handleNodeDelete(e):console.error("The Node cannot be deleted.")}async moveNode(e,t,i){this.handleNodeMove(e,t,i)}async addNode(e,t,i){let o=this.nodes.indexOf(t);const n=i===TreeNodePositionEnum.INSIDE?t:this.getParentNode(t),s=this.enhanceNodes([n,{...e,depth:n?n.depth+1:0}]).pop();n&&(t.hasChildren&&!t.expanded&&await this.showChildren(t),t.hasChildren||(t.hasChildren=!0,t.expanded=!0)),i!==TreeNodePositionEnum.INSIDE&&i!==TreeNodePositionEnum.AFTER||o++,this.nodes.splice(o,0,s),this.handleNodeAdd(s,t,i)}async removeNode(e){const t=this.nodes.indexOf(e);t>-1&&this.nodes.splice(t,1)}filter(e){"string"==typeof e&&(this.searchTerm=e),this.searchTerm&&this.settings.filterUrl?(this.loading=!0,new AjaxRequest(this.getFilterUrl()).get({cache:"no-cache"}).then((e=>e.resolve())).then((e=>{const t=Array.isArray(e)?e:[];t.length>0&&(""===this.unfilteredNodes&&(this.unfilteredNodes=JSON.stringify(this.nodes)),this.nodes=this.enhanceNodes(t))})).catch((e=>{throw this.errorNotification(e),e})).then((()=>{this.loading=!1}))):(this.resetFilter(),this.loading=!1)}resetFilter(){if(this.searchTerm="",this.unfilteredNodes.length>0){const e=this.getSelectedNodes()[0];if(void 0===e)return void this.loadData();this.nodes=this.enhanceNodes(JSON.parse(this.unfilteredNodes)),this.unfilteredNodes="";const t=this.getNodeByTreeIdentifier(e.__treeIdentifier);t?this.selectNode(t,!1):this.loadData()}else this.loadData()}errorNotification(e=null){if(!this.muteErrorNotifications)if(Array.isArray(e))e.forEach((e=>{Notification.error(e.title,e.message)}));else{let t=this.networkErrorTitle;e&&e.target&&(e.target.status||e.target.statusText)&&(t+=" - "+(e.target.status||"")+" "+(e.target.statusText||"")),Notification.error(t,this.networkErrorMessage)}}getSelectedNodes(){return this.nodes.filter((e=>e.checked))}getNodeByTreeIdentifier(e){return this.nodes.find((t=>t.__treeIdentifier===e))}getNodeDragStatusIcon(){return this.nodeDragMode===TreeNodeCommandEnum.DELETE?"actions-delete":this.nodeDragMode===TreeNodeCommandEnum.NEW?"actions-add":this.nodeDragPosition===TreeNodePositionEnum.BEFORE?"apps-pagetree-drag-move-above":this.nodeDragPosition===TreeNodePositionEnum.INSIDE?"apps-pagetree-drag-move-into":this.nodeDragPosition===TreeNodePositionEnum.AFTER?"apps-pagetree-drag-move-below":"actions-ban"}prepareNodes(e){const t=new CustomEvent("typo3:tree:nodes-prepared",{detail:{nodes:e},bubbles:!1});return this.dispatchEvent(t),t.detail.nodes}enhanceNodes(e){const t=e.reduce(((e,t)=>{if(!0===t.__processed)return[...e,t];(t=Object.assign({},this.settings.defaultProperties,t)).__parents=[];const i=t.depth>0?e.findLast((e=>e.depth<t.depth)):null;i&&(t.__parents=[...i.__parents,i.identifier]),t.__treeIdentifier=t.identifier,t.__loading=!1,t.__treeParents=[],i&&(t.__treeIdentifier=i.__treeIdentifier+"_"+t.__treeIdentifier,t.__treeParents=[...i.__treeParents,i.__treeIdentifier]),t.expanded=!0===t.expanded||(null!==this.settings.expandUpToLevel?t.depth<this.settings.expandUpToLevel:Boolean(this.getNodeStatus(t).expanded)),t.__processed=!0;const o=this;return[...e,new Proxy(t,{set:(e,t,i)=>(e[t]!==i&&(e[t]=i,o.requestUpdate()),!0)})]}),[]);return 1===t.filter((e=>0===e.depth)).length&&(t[0].expanded=!0),t}createRenderRoot(){return this}render(){const e=this.loading?html` +var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,o);else for(var d=e.length-1;d>=0;d--)(n=e[d])&&(r=(s<3?n(r):s>3?n(t,i,r):n(t,i))||r);return s>3&&r&&Object.defineProperty(t,i,r),r};import{html,LitElement,nothing}from"lit";import{property,state,query}from"lit/decorators.js";import{repeat}from"lit/directives/repeat.js";import{styleMap}from"lit/directives/style-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{TreeNodeCommandEnum,TreeNodePositionEnum}from"@typo3/backend/tree/tree-node.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";import{KeyTypesEnum as KeyTypes}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/icon-element.js";import ClientStorage from"@typo3/backend/storage/client.js";import{DataTransferTypes}from"@typo3/backend/enum/data-transfer-types.js";import Severity from"@typo3/backend/severity.js";export class Tree extends LitElement{constructor(){super(...arguments),this.setup=null,this.settings={showIcons:!1,width:300,dataUrl:"",filterUrl:"",defaultProperties:{},expandUpToLevel:null,actions:[]},this.nodes=[],this.currentScrollPosition=0,this.currentVisibleHeight=0,this.searchTerm=null,this.loading=!1,this.hoveredNode=null,this.nodeDragAllowed=!1,this.isOverRoot=!1,this.nodeDragPosition=null,this.nodeDragMode=null,this.draggingNode=null,this.nodeHeight=32,this.indentWidth=20,this.displayNodes=[],this.focusedNode=null,this.editingNode=null,this.openNodeTimeout={targetNode:null,timeout:null},this.unfilteredNodes="",this.muteErrorNotifications=!1,this.networkErrorTitle=top.TYPO3.lang.tree_networkError,this.networkErrorMessage=top.TYPO3.lang.tree_networkErrorDescription,this.allowNodeEdit=!1,this.allowNodeDrag=!1,this.allowNodeSorting=!1}getNodeFromElement(e){return null!==e&&"treeId"in e.dataset?this.getNodeByTreeIdentifier(e.dataset.treeId):null}getElementFromNode(e){return this.querySelector('[data-tree-id="'+this.getNodeTreeIdentifier(e)+'"]')}hideChildren(e){e.expanded=!1,this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}async showChildren(e){e.expanded=!0,await this.loadChildren(e),this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}getDataUrl(e=null){return null===e?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&depth="+e.depth}getFilterUrl(){return this.settings.filterUrl+"&q="+this.searchTerm}async loadData(){this.loading=!0,this.nodes=this.prepareNodes(await this.fetchData()),this.loading=!1}async fetchData(e=null){try{const t=await new AjaxRequest(this.getDataUrl(e)).get({cache:"no-cache"});let i=await t.resolve();if(!Array.isArray(i))return[];null!==e&&(i=i.filter((t=>t.identifier!==e.identifier)),i.unshift(e)),i=this.enhanceNodes(i),null!==e&&i.shift();const o=await Promise.all(i.map((async e=>{const t=e.__parents.join("_"),o=i.find((e=>e.__treeIdentifier===t))||null,n=null===o||o.expanded;if(!e.loaded&&e.hasChildren&&e.expanded&&n){const t=await this.fetchData(e);return e.loaded=!0,[e,...t]}return[e]})));return o.flat()}catch(e){return this.errorNotification(e),[]}}async loadChildren(e){try{if(e.loaded)return void await Promise.all(this.nodes.filter((t=>t.__parents.join("_")===e.__treeIdentifier&&!t.loaded&&t.hasChildren&&t.expanded)).map((e=>this.loadChildren(e))));e.__loading=!0;const t=await this.fetchData(e),i=this.nodes.indexOf(e)+1;let o=0;for(let t=i;t<this.nodes.length&&!(this.nodes[t].depth<=e.depth);++t)o++;this.nodes.splice(i,o,...t),e.__loading=!1,e.loaded=!0}catch(t){throw this.errorNotification(t),e.__loading=!1,t}}getIdentifier(){return this.id??this.setup.id}getLocalStorageIdentifier(){return"tree-state-"+this.getIdentifier()}getNodeStatus(e){return(JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{})[e.__treeIdentifier]??{expanded:!1}}saveNodeStatus(e){const t=JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{};t[e.__treeIdentifier]={expanded:e.expanded},ClientStorage.set(this.getLocalStorageIdentifier(),JSON.stringify(t))}refreshOrFilterTree(){""!==this.searchTerm?this.filter(this.searchTerm):this.loadData()}selectFirstNode(){const e=this.getFirstNode();this.selectNode(e,!0),this.focusNode(e)}selectNode(e,t=!0){this.isNodeSelectable(e)&&(this.resetSelectedNodes(),e.checked=!0,this.dispatchEvent(new CustomEvent("typo3:tree:node-selected",{detail:{node:e,propagate:t}})))}async focusNode(e){this.focusedNode=e;const t=this.getElementFromNode(this.focusedNode);t?t.focus():(this.requestUpdate(),this.updateComplete.then((()=>{this.getElementFromNode(this.focusedNode)?.focus()})))}async editNode(e){this.isNodeEditable(e)&&(this.editingNode=e,this.requestUpdate(),this.updateComplete.then((()=>{const e=this.getElementFromNode(this.editingNode)?.querySelector(".node-edit");e&&(e.focus(),e.select())})))}async deleteNode(e){e.deletable?this.handleNodeDelete(e):console.error("The Node cannot be deleted.")}async moveNode(e,t,i){this.handleNodeMove(e,t,i)}async addNode(e,t,i){let o=this.nodes.indexOf(t);const n=i===TreeNodePositionEnum.INSIDE?t:this.getParentNode(t),s=this.enhanceNodes([n,{...e,depth:n?n.depth+1:0}]).pop();n&&(n.hasChildren&&!n.expanded&&await this.showChildren(n),n.hasChildren||(n.hasChildren=!0,n.expanded=!0)),i!==TreeNodePositionEnum.INSIDE&&i!==TreeNodePositionEnum.AFTER||o++,this.nodes.splice(o,0,s),this.handleNodeAdd(s,t,i)}async removeNode(e){const t=this.nodes.indexOf(e),i=this.getParentNode(e);t>-1&&this.nodes.splice(t,1),this.requestUpdate(),this.updateComplete.then((()=>{i.expanded&&i.hasChildren&&0===this.getNodeChildren(i).length&&(i.hasChildren=!1,i.expanded=!1)}))}filter(e){"string"==typeof e&&(this.searchTerm=e),this.searchTerm&&this.settings.filterUrl?(this.loading=!0,new AjaxRequest(this.getFilterUrl()).get({cache:"no-cache"}).then((e=>e.resolve())).then((e=>{const t=Array.isArray(e)?e:[];t.length>0&&(""===this.unfilteredNodes&&(this.unfilteredNodes=JSON.stringify(this.nodes)),this.nodes=this.enhanceNodes(t))})).catch((e=>{throw this.errorNotification(e),e})).then((()=>{this.loading=!1}))):(this.resetFilter(),this.loading=!1)}resetFilter(){if(this.searchTerm="",this.unfilteredNodes.length>0){const e=this.getSelectedNodes()[0];if(void 0===e)return void this.loadData();this.nodes=this.enhanceNodes(JSON.parse(this.unfilteredNodes)),this.unfilteredNodes="";const t=this.getNodeByTreeIdentifier(e.__treeIdentifier);t?this.selectNode(t,!1):this.loadData()}else this.loadData()}errorNotification(e=null){if(!this.muteErrorNotifications)if(Array.isArray(e))e.forEach((e=>{Notification.error(e.title,e.message)}));else{let t=this.networkErrorTitle;e&&e.target&&(e.target.status||e.target.statusText)&&(t+=" - "+(e.target.status||"")+" "+(e.target.statusText||"")),Notification.error(t,this.networkErrorMessage)}}getSelectedNodes(){return this.nodes.filter((e=>e.checked))}getNodeByTreeIdentifier(e){return this.nodes.find((t=>t.__treeIdentifier===e))}getNodeDragStatusIcon(){return this.nodeDragMode===TreeNodeCommandEnum.DELETE?"actions-delete":this.nodeDragMode===TreeNodeCommandEnum.NEW?"actions-add":this.nodeDragPosition===TreeNodePositionEnum.BEFORE?"apps-pagetree-drag-move-above":this.nodeDragPosition===TreeNodePositionEnum.INSIDE?"apps-pagetree-drag-move-into":this.nodeDragPosition===TreeNodePositionEnum.AFTER?"apps-pagetree-drag-move-below":"actions-ban"}prepareNodes(e){const t=new CustomEvent("typo3:tree:nodes-prepared",{detail:{nodes:e},bubbles:!1});return this.dispatchEvent(t),t.detail.nodes}enhanceNodes(e){const t=e.reduce(((e,t)=>{if(!0===t.__processed)return[...e,t];(t=Object.assign({},this.settings.defaultProperties,t)).__parents=[];const i=t.depth>0?e.findLast((e=>e.depth<t.depth)):null;i&&(t.__parents=[...i.__parents,i.identifier]),t.__treeIdentifier=t.identifier,t.__loading=!1,t.__treeParents=[],i&&(t.__treeIdentifier=i.__treeIdentifier+"_"+t.__treeIdentifier,t.__treeParents=[...i.__treeParents,i.__treeIdentifier]),t.expanded=!0===t.expanded||(null!==this.settings.expandUpToLevel?t.depth<this.settings.expandUpToLevel:Boolean(this.getNodeStatus(t).expanded)),t.__processed=!0;const o=this;return[...e,new Proxy(t,{set:(e,t,i)=>(e[t]!==i&&(e[t]=i,o.requestUpdate()),!0)})]}),[]);return 1===t.filter((e=>0===e.depth)).length&&(t[0].expanded=!0),t}createRenderRoot(){return this}render(){const e=this.loading?html` <div class="nodes-loader"> <div class="nodes-loader-inner"> <typo3-backend-icon identifier="spinner-circle" size="medium"></typo3-backend-icon>