hpv-tree-action
Version:
Zero-dependency JavaScript library for building interactive treeviews with hierarchical permissions, lazy loading, event-driven updates, and advanced search capabilities.
1 lines • 15.5 kB
JavaScript
class EventEmitter{constructor(){this._events={}}on(e,t){return this._events[e]||(this._events[e]=[]),this._events[e].push(t),this}off(e,t){return this._events[e]&&-1!==(t=this._events[e].indexOf(t))&&this._events[e].splice(t,1),this}emit(e,...t){if(!this._events[e])return!1;var i=this._events[e].slice();for(let e=0;e<i.length;e++)i[e].apply(this,t);return!0}once(i,o){let r=this;return this.on(i,function e(...t){r.off(i,e),o.apply(r,t)}),this}}class TreeNode{constructor(e,t,i={}){this.id=e,this.name=t,this.isFolder=i.isFolder??!1,this.lazyLoad=i.lazyLoad??!1,this.isVisible=i.isVisible??!0,this.loaded=void 0!==i.loaded?i.loaded:!this.lazyLoad,this.collapsed=!0,this.loading=!1,this.level=i.level??0,this.children=[],this.parent=null,this.operationState={},this.availableOperations=i.availableOperations??[],i.initialStates&&this.setInitialStates(i.initialStates)}setInitialStates(e){this.operationState={...this.operationState,...e}}addChild(e){return e.level=this.level+1,(e.parent=this).children.push(e),e}hasOperationMixedState(t){if(0===this.children.length)return!1;let i=this.children.find(e=>e.availableOperations.includes(t))?.operationState[t]||"unselected";return this.children.some(e=>e.availableOperations.includes(t)&&(e.operationState[t]||"unselected")!==i)}setState(e,t){if(this.availableOperations.includes(e))return this.operationState[e]=t,this;throw new Error(`Operation '${e}' is not available for this node`)}getDescendantNodes(){var e,t=[];t.push(...this.children);for(e of this.children)t.push(...e.getDescendantNodes());return t}getAscendantNodes(){var e=[];let t=this.parent;for(;null!==t;)e.push(t),t=t.parent;return e}findNearestParentByOp(t){return this.getAscendantNodes().find(e=>e.availableOperations.includes(t))||null}}class TreeAction extends EventEmitter{static EVENTS={NODE:{COLLAPSE:"nodeCollapse",EXPAND:"nodeExpand"},TREE:{UPDATE:"treeUpdate",DATA_LOAD:"dataLoad",EXPORT:"exportData",SEARCH_START:"searchStart",SEARCH_COMPLETE:"searchComplete"}};static ACTIONS=[{code:"C",tooltip:"Create"},{code:"R",tooltip:"Read"},{code:"U",tooltip:"Update"},{code:"D",tooltip:"Delete"}];constructor(e={}){if(super(),!e.actionClickHandler)throw new Error("actionClickHandler is required in TreeAction constructor");this.operationTypes=e.operations??TreeAction.ACTIONS,this.currentSearchQuery=null,this.actionClickHandler=e.actionClickHandler,this.childrenLoader=e.childrenLoader||null,this.rootNode=new TreeNode("root","Root",{isFolder:!0,level:0,availableOperations:this.operationTypes.map(e=>e.code),lazyLoad:!1,loaded:!0}),e.initialData&&this.loadTreeFromJSON(e.initialData)}toggleCollapse(e){e.isFolder&&(e.collapsed=!e.collapsed,this.emit(e.collapsed?TreeAction.EVENTS.NODE.COLLAPSE:TreeAction.EVENTS.NODE.EXPAND,e),e.collapsed||!e.lazyLoad||e.loaded||e.loading||this.loadNodeChildren(e),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode))}loadNodeChildren(t){t.loading||!t.lazyLoad||t.loaded||(t.loading=!0,this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode),t.children=[],Promise.resolve().then(()=>this.childrenLoader?this.childrenLoader(t,this.currentSearchQuery):null).then(()=>{this.currentSearchQuery||(t.loaded=!0),t.loading=!1,this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}).catch(e=>{console.error("Error loading children:",e),t.loading=!1,this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}))}exportTreeAsJSON(){var e={operations:this.operationTypes,tree:this._serializeNode(this.rootNode)},e=JSON.stringify(e);return this.emit(TreeAction.EVENTS.TREE.EXPORT,e),e}loadTreeFromJSON(e){if("string"==typeof e)try{e=JSON.parse(e)}catch(e){return console.error("Invalid JSON format",e),this.emit(TreeAction.EVENTS.TREE.DATA_LOAD,{success:!1,error:e}),!1}return e.operations&&(this.operationTypes=e.operations),e.tree&&(this.rootNode=this._deserializeNode(e.tree)),this.emit(TreeAction.EVENTS.TREE.DATA_LOAD,{success:!0,operations:this.operationTypes,tree:this.rootNode}),!0}_serializeNode(e){var t={id:e.id,name:e.name,isFolder:e.isFolder,level:e.level,operationState:{...e.operationState},availableOperations:[...e.availableOperations],lazyLoad:e.lazyLoad,loaded:e.loaded,collapsed:e.collapsed,isVisible:e.isVisible};return e.children&&0<e.children.length&&(t.children=e.children.map(e=>this._serializeNode(e))),t}_deserializeNode(e,t=null){let i=new TreeNode(e.id,e.name,{isFolder:e.isFolder,level:e.level,availableOperations:e.availableOperations,lazyLoad:e.lazyLoad,loaded:e.loaded,initialStates:e.operationState,isVisible:e.isVisible});return i.parent=t,i.loaded=e.loaded,i.collapsed=void 0===e.collapsed||e.collapsed,e.children&&0<e.children.length&&e.children.forEach(e=>{e=this._deserializeNode(e,i);i.addChild(e)}),i}expandToLevel(t){this._traverseTree(this.rootNode,e=>{e.level<t&&e.isFolder&&(e.collapsed=!1,!e.lazyLoad||e.loaded||e.loading||this.loadNodeChildren(e))}),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}collapseToLevel(t){this._traverseTree(this.rootNode,e=>{e.level>=t&&e.isFolder&&(e.collapsed=!0)}),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}deleteTemporaryNodes(){this._traverseTree(this.rootNode,e=>{e.isFolder&&e.lazyLoad&&!e.loaded&&(e.collapsed=!0,e.children=[])}),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}_traverseTree(e,t){t(e),e.children&&e.children.forEach(e=>{this._traverseTree(e,t)})}addNewOperationType(t,e){return this.operationTypes.some(e=>e.code===t)?(console.error(`Operation type ${t} already exists`),!1):(this.operationTypes.push({code:t,tooltip:e}),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode),!0)}removeOperationType(t){var e=this.operationTypes.findIndex(e=>e.code===t);return-1!==e&&(this.operationTypes.splice(e,1),this._removeOperationFromNode(this.rootNode,t),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode),!0)}_removeOperationFromNode(e,t){var i=e.availableOperations.indexOf(t);-1!==i&&e.availableOperations.splice(i,1),t in e.operationState&&delete e.operationState[t],e.children.forEach(e=>{this._removeOperationFromNode(e,t)})}_findNodeById(e,t){if(e.id===t)return e;if(e.children)for(var i of e.children){i=this._findNodeById(i,t);if(i)return i}return null}getTreeData(){return this._serializeNode(this.rootNode)}getOperationTypes(){return[...this.operationTypes]}getNodeById(e){return this._findNodeById(this.rootNode,e)}sortNodesByName(e=this.rootNode){e.isFolder&&0<e.children.length&&(e.children.sort((e,t)=>e.name.localeCompare(t.name)),e.children.forEach(e=>this.sortNodesByName(e))),e===this.rootNode&&this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}resetNodesVisibility(){this._traverseTree(this.rootNode,e=>{e.isVisible=!0}),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}markPathAsVisible(e){let t=e;for(;t;)t.isVisible=!0,t=t.parent}markChildrenVisible(e){e.children&&e.children.forEach(e=>{e.isVisible=!0,e.isFolder&&this.markChildrenVisible(e)})}async search(a){if(a){this.currentSearchQuery=a,this.emit(TreeAction.EVENTS.TREE.SEARCH_START),this._traverseTree(this.rootNode,e=>{e.isVisible=!1});let o=[],r=async t=>{t.isFolder&&t.lazyLoad&&!t.loaded&&(t.loading=!0,e=Promise.resolve().then(()=>this.childrenLoader?this.childrenLoader(t,a):null).then(()=>{t.loading=!1}),o.push(e),await e);var e=t.name.toLowerCase(),i=a.toLowerCase();if(e.includes(i)){this.markPathAsVisible(t);let e=t;for(;e.parent;)e.parent.isFolder&&(e.parent.collapsed=!1),e=e.parent;t.isFolder&&(t.collapsed=!1,this.markChildrenVisible(t))}t.isFolder&&t.children&&await Promise.all(t.children.map(e=>r(e)))};try{await r(this.rootNode),await Promise.all(o),this.emit(TreeAction.EVENTS.TREE.SEARCH_COMPLETE),this.emit(TreeAction.EVENTS.TREE.UPDATE,this.rootNode)}catch(e){console.error("Error during search:",e)}}else this.currentSearchQuery=null,this.resetNodesVisibility(),this.deleteTemporaryNodes()}}class TreeActionUI{constructor(e,t={}){this.treeAction=e,this.containerId=t.containerId||"tree-container",this.loadingText=t.loadingText||"Loading data...",this.operationsTitle=t.operationsTitle||"Node / Operations",this.componentOrder=t.componentOrder||["header","levelControls","searchControls","treeContainer","legend"],this.searchPlaceholderText=t.searchPlaceholderText||"Search by node name...",this.searchButtonText=t.searchButtonText||"Search",this.clearButtonText=t.clearButtonText||"Clear",this.expandToLevelText=t.expandToLevelText||"Expand to Level",this.collapseToLevelText=t.collapseToLevelText||"Collapse to Level",this.expandAllText=t.expandAllText||"Expand All",this.collapseAllText=t.collapseAllText||"Collapse All",this.levelPlaceholderText=t.levelPlaceholderText||"Level",this.folderIconText=t.folderIconText||"📁",this.fileIconText=t.fileIconText||"📄",this.collapsedIconText=t.collapsedIconText||"▶",this.expandedIconText=t.expandedIconText||"▼",this.operationUnavailableText=t.operationUnavailableText||"(Not available)",this.showSearchControls=t.showSearchControls??!0,this.showTreeActionHeader=t.showTreeActionHeader??!0,this.showLevelControls=t.showLevelControls??!0,this.showLegend=t.showLegend??!0,this.legend=t.legend||{unselected:{text:"Not Selected",bg:"#fff",border:"#ccc"},disabled:{text:"Not Available",bg:"#eee",border:"#ccc"}},this.maxHeight=t.maxHeight||null,this.init()}init(){var e;document.getElementById(this.containerId)||((e=document.createElement("div")).id=this.containerId,document.body.appendChild(e)),this._setupEventListeners(),this.render()}_setupEventListeners(){this.treeAction.on(TreeAction.EVENTS.NODE.COLLAPSE,()=>this.render()),this.treeAction.on(TreeAction.EVENTS.NODE.EXPAND,()=>this.render()),this.treeAction.on(TreeAction.EVENTS.TREE.UPDATE,()=>this.render()),this.treeAction.on(TreeAction.EVENTS.TREE.DATA_LOAD,()=>this.render())}render(){var o=document.getElementById(this.containerId);if(o){let e=0;var r=o.querySelector(".tree-container");r&&(e=r.scrollTop),o.innerHTML="";let t=document.createElement("div"),i=(t.className="tree-action-wrapper",{header:()=>this.showTreeActionHeader?this._createOperationsHeader():null,levelControls:()=>this.showLevelControls?this._createLevelControls():null,searchControls:()=>this.showSearchControls?this._createSearchControls():null,treeContainer:()=>{var e=document.createElement("div");return e.className="tree-container",this.maxHeight&&(e.style.maxHeight="number"==typeof this.maxHeight?this.maxHeight+"px":this.maxHeight,e.style.overflowY="auto"),this._renderNode(this.treeAction.rootNode,e),e},legend:()=>this.showLegend?this._createLegend():null});this.componentOrder.forEach(e=>{e=i[e]?.();e&&t.appendChild(e)}),o.appendChild(t);r=o.querySelector(".tree-container");r&&(r.scrollTop=e)}else console.error(`Container with ID "${this.containerId}" not found.`)}_createSearchControls(){var e=document.createElement("div");e.className="search-controls";let t=document.createElement("input");t.type="text",t.className="search-input",t.placeholder=this.searchPlaceholderText,this.treeAction.currentSearchQuery&&(t.value=this.treeAction.currentSearchQuery);var i=document.createElement("button"),o=(i.textContent=this.searchButtonText,i.className="tree-action-button",i.addEventListener("click",()=>{this.treeAction.search(t.value)}),t.addEventListener("keyup",e=>{"Enter"===e.key&&this.treeAction.search(t.value)}),document.createElement("button"));return o.textContent=this.clearButtonText,o.className="tree-action-button",o.addEventListener("click",()=>{t.value="",this.treeAction.search()}),e.appendChild(t),e.appendChild(i),e.appendChild(o),e}_createOperationsHeader(){var e=document.createElement("div"),t=(e.className="tree-action-header",document.createElement("div")),i=(t.className="operations-list",document.createElement("div"));return i.className="operations-title",i.textContent=this.operationsTitle,t.appendChild(i),e.appendChild(t),e}_createLegend(){let r=document.createElement("div");return r.className="action-legend",Object.entries(this.legend).forEach(([,e])=>{var t=document.createElement("div"),i=(t.className="legend-item",document.createElement("div")),o=(i.className="legend-color",i.style.background=e.bg,i.style.borderColor=e.border,document.createElement("span"));o.textContent=e.text,t.appendChild(i),t.appendChild(o),r.appendChild(t)}),r}_createLevelControls(){var e=document.createElement("div");e.className="level-controls";let t=document.createElement("input");t.type="number",t.min="0",t.value="0",t.className="level-input",t.placeholder=this.levelPlaceholderText;var i=document.createElement("button"),o=(i.textContent=this.expandToLevelText,i.className="tree-action-button",i.addEventListener("click",()=>{var e=parseInt(t.value,10);!isNaN(e)&&0<=e&&this.treeAction.expandToLevel(e)}),document.createElement("button")),r=(o.textContent=this.collapseToLevelText,o.className="tree-action-button",o.addEventListener("click",()=>{var e=parseInt(t.value,10);!isNaN(e)&&0<=e&&this.treeAction.collapseToLevel(e)}),document.createElement("button")),a=(r.textContent=this.expandAllText,r.className="tree-action-button",r.addEventListener("click",()=>{this.treeAction.expandToLevel(Number.MAX_SAFE_INTEGER)}),document.createElement("button"));return a.textContent=this.collapseAllText,a.className="tree-action-button",a.addEventListener("click",()=>{this.treeAction.collapseToLevel(0)}),e.appendChild(t),e.appendChild(i),e.appendChild(o),e.appendChild(r),e.appendChild(a),e}_renderNode(r,e){if(r.isVisible){var i=document.createElement("div"),a=(i.className="tree-node",i.dataset.nodeId=r.id,i.style.paddingLeft=20*r.level+"px",document.createElement("span")),n=(a.className="toggle-indicator",r.isFolder?(a.textContent=r.collapsed?this.collapsedIconText:this.expandedIconText,a.addEventListener("click",()=>this.treeAction.toggleCollapse(r))):a.textContent=" ",document.createElement("span")),s=(n.className="node-icon",n.textContent=r.isFolder?this.folderIconText:this.fileIconText,document.createElement("span"));if(s.className="node-name",r.lazyLoad&&s.classList.add("node-name-lazy"),this.treeAction.currentSearchQuery){let t=this.treeAction.currentSearchQuery.toLowerCase();var l,d,c=r.name,h=c.toLowerCase().indexOf(t);-1!==h?(s.classList.add("match"),l=c.substring(0,h),d=c.substring(h,h+t.length),h=c.substring(h+t.length),s.innerHTML=l+`<span class="match-text">${d}</span>`+h):(r.isFolder&&r.children.some(e=>e.isVisible&&(e.name.toLowerCase().includes(t)||e.isFolder&&e.children.some(e=>e.isVisible)))&&s.classList.add("contains-match"),s.textContent=c)}else s.textContent=r.name;let o=document.createElement("div"),t=(o.className="node-operations",this.treeAction.operationTypes.forEach(e=>{var t,i;r.availableOperations.includes(e.code)?((t=document.createElement("button")).className="operation-button",t.dataset.operation=e.code,t.title=e.tooltip,t.textContent=e.code,i=r.operationState[e.code]||"unselected",t.classList.add("state-"+i),t.addEventListener("click",()=>{this.treeAction.actionClickHandler(r,e.code),this.render()}),o.appendChild(t)):((i=document.createElement("span")).className="operation-button state-disabled",i.textContent=e.code,i.title=e.tooltip+" "+this.operationUnavailableText,o.appendChild(i))}),i.appendChild(a),i.appendChild(n),i.appendChild(s),i.appendChild(o),e.appendChild(i),document.createElement("div"));t.className="node-children",t.style.display=r.collapsed?"none":"block",r.isFolder&&r.loading&&!r.collapsed&&((l=document.createElement("div")).className="loading-indicator",l.textContent=this.loadingText,t.appendChild(l)),!r.collapsed&&0<r.children.length&&r.children.forEach(e=>{this._renderNode(e,t)}),e.appendChild(t)}}}