1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import './LitTreeNode.js' 17import { BaseElement, element } from '../BaseElement.js'; 18import { LitTreeNode } from './LitTreeNode.js'; 19 20export interface TreeItemData { 21 key: string; 22 title: string; 23 icon?: string; //节点的自定义图标 设置show-icon才会生效 24 selected?: boolean; //控制是否选择该节点 25 checked?: boolean; 26 children?: Array<TreeItemData> | null | undefined; 27} 28 29@element('lit-tree') 30export class LitTree extends BaseElement { 31 32 private _treeData: Array<TreeItemData> = []; 33 private currentSelectedNode: any; 34 private currentSelectedData: any; 35 private proxyData: any; 36 private nodeList: Array<LitTreeNode> = []; 37 private contextMenu: HTMLDivElement | null | undefined; 38 private srcDragElement: any; 39 private dragDirection: string | null | undefined; 40 41 static get observedAttributes() { 42 return ['show-line', 'show-icon', 'checkable', 'foldable', 'dragable', 'multiple']; //foldable 表示点击目录是否可以折叠 43 } 44 45 set treeData(value: Array<TreeItemData>) { 46 this._treeData = value; 47 this.shadowRoot!.querySelector('#root')!.innerHTML = ''; 48 this.nodeList = []; 49 this.drawTree(this.shadowRoot!.querySelector('#root'), value, true); 50 51 /*双向绑定*/ 52 const handler = { 53 get: (target: any, propkey: any) => { 54 return target[propkey]; 55 }, 56 set: (target: any, propkey: any, value: any, receiver: any) => { 57 if (target[propkey] !== value) { 58 if (!value.children) { 59 value.children = new Proxy([], handler); 60 } else { 61 value.children = new Proxy(value.children, handler); 62 } 63 target[propkey] = value; 64 if (!this.currentSelectedNode) { 65 this._insertNode(null, value); 66 } else { 67 if (this.currentSelectedNode.nextElementSibling) { 68 this._insertNode(this.currentSelectedNode.nextElementSibling, value); 69 } else { 70 this.currentSelectedNode.setAttribute('show-arrow', 'true'); 71 let ul = document.createElement('ul'); 72 // @ts-ignore 73 ul.open = 'true' 74 ul.style.transition = '.3s all'; 75 this.currentSelectedNode.parentElement.append(ul); 76 this.currentSelectedNode.arrow = true; 77 this._insertNode(ul, value); 78 } 79 } 80 } 81 return true; 82 } 83 }; 84 let setProxy = (v: Array<TreeItemData>) => { 85 v.forEach(a => { 86 if (!a.children) { 87 a.children = new Proxy([], handler); 88 } else { 89 a.children = new Proxy(a.children, handler); 90 setProxy(a.children || []); 91 } 92 }) 93 }; 94 setProxy(this._treeData); 95 this.proxyData = new Proxy(this._treeData, handler); 96 } 97 98 set multiple(value: boolean) { 99 if (value) { 100 this.setAttribute('multiple',''); 101 } else { 102 this.removeAttribute('multiple'); 103 } 104 } 105 106 get multiple() { 107 return this.hasAttribute('multiple'); 108 } 109 110 get treeData() { 111 return this.proxyData; 112 } 113 114 set onSelect(fn: any) { 115 this.addEventListener('onSelect', fn); 116 } 117 118 set onChange(fn: any) { 119 this.addEventListener('onChange', fn); 120 } 121 122 set foldable(value: any) { 123 if (value) { 124 this.setAttribute('foldable', ''); 125 } else { 126 this.removeAttribute('foldable'); 127 } 128 } 129 130 //当 custom element首次被插入文档DOM时,被调用。 131 connectedCallback() { 132 this.onclick = ev => { 133 this.contextMenu!.style.display = 'none'; 134 this.currentSelectedData = null; 135 this.currentSelectedNode = null; 136 this.selectedNode(null); 137 } 138 } 139 140 getCheckdKeys() { 141 return Array.from(this.shadowRoot!.querySelectorAll('lit-tree-node[checked]')).map((a: any) => a.data.key); 142 } 143 144 getCheckdNodes() { 145 return Array.from(this.shadowRoot!.querySelectorAll('lit-tree-node[checked]')).map((a: any) => a.data); 146 } 147 148 //展开所有节点 149 expandKeys(keys: Array<string>) { 150 keys.forEach(k => this.shadowRoot!.querySelectorAll(`lit-tree-node[key='${k}']`).forEach((b: any) => b.expand())); 151 } 152 153 //收起所有节点 154 collapseKeys(keys: Array<string>) { 155 keys.forEach(k => this.shadowRoot!.querySelectorAll(`lit-tree-node[key='${k}']`).forEach((b: any) => b.collapse())); 156 } 157 158 checkedKeys(keys: Array<string>) { 159 keys.forEach(k => this.shadowRoot!.querySelectorAll(`lit-tree-node[key='${k}']`).forEach((b: any) => { 160 b.setAttribute('checked', 'true'); 161 b.checkHandler(); 162 })); 163 } 164 165 uncheckedKeys(keys: Array<string>) { 166 keys.forEach(k => this.shadowRoot!.querySelectorAll(`lit-tree-node[key='${k}']`).forEach((b: any) => { 167 b.removeAttribute('checked'); 168 b.removeAttribute('missing'); 169 b.checkHandler(); 170 })); 171 } 172 173 drawTree(parent: any, array: Array<TreeItemData>, topDepth: boolean = false) { 174 let that = this; 175 array.forEach(a => { 176 let li = document.createElement('li'); 177 let node: LitTreeNode = document.createElement('lit-tree-node') as LitTreeNode; 178 node.title = a.title; 179 node.setAttribute('key', a.key); 180 node.topDepth = topDepth; 181 if (this.hasAttribute('dragable')) { 182 node.draggable = true; 183 document.ondragover = function (e) { 184 e.preventDefault(); 185 }; 186 //在拖动目标上触发事件 (源元素) 187 node.ondrag = ev => this.onDrag(ev);//元素正在拖动时触发 188 node.ondragstart = ev => this.onDragStart(ev);//用户开始拖动元素时触发 189 node.ondragend = ev => this.onDragEnd(ev);// 用户完成元素拖动后触发 190 //释放目标时触发的事件: 191 node.ondragenter = ev => this.onDragEnter(ev);//当被鼠标拖动的对象进入其容器范围内时触发此事件 192 node.ondragover = ev => this.onDragOver(ev);//当某被拖动的对象在另一对象容器范围内拖动时触发此事件 193 node.ondragleave = ev => this.onDragLeave(ev);//当被鼠标拖动的对象离开其容器范围内时触发此事件 194 node.ondrop = ev => this.onDrop(ev);//在一个拖动过程中,释放鼠标键时触发此事件 195 } 196 node.selected = a.selected || false; //是否选中行 197 node.checked = a.checked || false;// 是否勾选 198 node.data = a; 199 node.addEventListener('change', (e: any) => { 200 if (e.detail && !this.multiple) { 201 this.nodeList.forEach(item => { 202 item.checked = item.data!.key === node.data!.key; 203 item.data!.checked = item.checked; 204 }); 205 } 206 var litTreeNodes = this.nodeList.filter(it => it.checked); 207 if (litTreeNodes.length === 0) { 208 node.checked = true; 209 node.data!.checked = true; 210 } 211 that.dispatchEvent(new CustomEvent('onChange', {detail: {data: (node as any).data, checked: e.detail}})); 212 }); 213 node.multiple = this.hasAttribute('multiple'); 214 node.checkable = this.getAttribute('checkable') || 'false'; 215 this.nodeList.push(node); 216 // @ts-ignore 217 li.data = a; 218 li.append(node); 219 parent.append(li); 220 let ul = document.createElement('ul'); 221 // @ts-ignore 222 ul.open = 'true'; 223 ul.style.transition = '.3s all'; 224 if (a.children && a.children.length > 0) { 225 if (this.hasAttribute('show-icon')) { 226 if (a.icon) { 227 (node as any).iconName = a.icon; 228 } else { 229 (node as any).iconName = 'folder' 230 } 231 } else { 232 node.iconName = ''; 233 } 234 node.arrow = true; 235 li.append(ul); 236 this.drawTree(ul, a.children); 237 } else { 238 if (this.hasAttribute('show-icon')) { 239 if (a.icon) { 240 node.iconName = a.icon; 241 } else { 242 node.iconName = 'file'; 243 } 244 } else { 245 node.iconName = ''; 246 } 247 node.arrow = false; 248 } 249 li.onclick = (e) => { 250 e.stopPropagation() 251 if (this.hasAttribute('foldable')) { 252 // @ts-ignore 253 if (li.data.children && li.data.children.length > 0) { 254 node.autoExpand(); 255 } else { 256 // @ts-ignore 257 this.dispatchEvent(new CustomEvent('onSelect', {detail: li.data})) 258 this.selectedNode(node); 259 } 260 } else { 261 // @ts-ignore 262 this.dispatchEvent(new CustomEvent('onSelect', {detail: li.data})) 263 this.selectedNode(node); 264 } 265 }; 266 // node 添加右键菜单功能 267 node.oncontextmenu = ev => { 268 ev.preventDefault(); 269 this.selectedNode(node); 270 this.currentSelectedNode = node; 271 this.currentSelectedData = node.data; 272 this.contextMenu!.style.display = 'block'; 273 this.contextMenu!.style.left = ev.pageX + 'px'; 274 this.contextMenu!.style.top = ev.pageY + 'px'; 275 }; 276 }); 277 this.oncontextmenu = ev => { 278 ev.preventDefault(); 279 this.contextMenu!.style.display = 'block'; 280 this.contextMenu!.style.left = ev.pageX + 'px'; 281 this.contextMenu!.style.top = ev.pageY + 'px'; 282 }; 283 } 284 285 //取消所有节点的选中状态 然后选中当前node节点 286 selectedNode(node: LitTreeNode | null | undefined) { 287 this.shadowRoot!.querySelectorAll<LitTreeNode>('lit-tree-node').forEach((a) => { 288 a.selected = false; 289 }) 290 if (node) { 291 node.selected = true; 292 } 293 } 294 295 closeContextMenu() { 296 this.contextMenu!.style.display = 'none'; 297 } 298 299 onDrag(e: MouseEvent) { 300 301 } 302 303 onDragStart(ev: MouseEvent) { 304 this.srcDragElement = ev.target; 305 (ev.target! as LitTreeNode).open = 'true'; 306 (ev.target! as LitTreeNode).autoExpand(); 307 return undefined; 308 } 309 310 onDragEnd(ev: MouseEvent) { 311 this.srcDragElement = null; 312 return undefined; 313 } 314 315 onDragEnter(ev: MouseEvent) { 316 (ev.target as LitTreeNode).style.backgroundColor = '#42b98333'; 317 return undefined; 318 } 319 320 onDragOver(ev: MouseEvent) { 321 let node = ev.target as LitTreeNode; 322 if (this.srcDragElement.data.key === node.data!.key) return; 323 let rect = (ev.currentTarget! as any).getBoundingClientRect(); 324 if (ev.clientX >= rect.left + rect.width / 3 && ev.clientX < rect.left + rect.width) { //bottom-right 325 this.dragDirection = 'bottom-right'; 326 node.drawLine('bottom-right'); 327 } else if (ev.clientY >= rect.top && ev.clientY < rect.top + rect.height / 2) {//上面 328 this.dragDirection = 'top'; 329 node.drawLine('top'); 330 } else if (ev.clientY <= rect.bottom && ev.clientY > rect.top + rect.height / 2) {//下面 331 this.dragDirection = 'bottom'; 332 node.drawLine('bottom'); 333 } 334 return undefined; 335 } 336 337 onDragLeave(ev: MouseEvent) { 338 (ev.target as LitTreeNode).style.backgroundColor = '#ffffff00'; 339 (ev.target as LitTreeNode).drawLine(''); 340 return undefined; 341 } 342 343 onDrop(ev: MouseEvent) { 344 (ev.target as LitTreeNode).style.backgroundColor = '#ffffff00'; 345 (ev.target as LitTreeNode).drawLine(''); 346 //移动的不是node节点 而是上层的li节点 347 let srcData = this.srcDragElement.data;//获取原节点的data数据 348 let dstData = (ev.target as LitTreeNode).data;//获取目标节点的data数据 349 if (srcData.key === dstData!.key) return;//同一个节点不用处理 350 let srcElement = this.srcDragElement.parentElement; 351 let srcParentElement = srcElement.parentElement; 352 let dstElement = (ev.target as LitTreeNode).parentElement; 353 srcElement.parentElement.removeChild(srcElement);//node li ul 从 ul 中移除 li 354 if (this.dragDirection === 'top') { 355 dstElement!.parentElement!.insertBefore(srcElement, dstElement); 356 } else if (this.dragDirection === 'bottom') { 357 dstElement!.parentElement!.insertBefore(srcElement, dstElement!.nextSibling); 358 } else if (this.dragDirection === 'bottom-right') { 359 let ul = dstElement!.querySelector('ul'); 360 if (!ul) { 361 ul = document.createElement('ul'); 362 ul.style.cssText = 'transition: all 0.3s ease 0s;'; 363 dstElement!.appendChild(ul); 364 } 365 dstElement!.querySelector<LitTreeNode>('lit-tree-node')!.arrow = true;//拖动进入子节点,需要开启箭头 366 ul.appendChild(srcElement); 367 } 368 let ul1 = dstElement!.querySelector('ul');//如果拖动后目标节点的子节点没有记录,需要关闭arrow箭头 369 if (ul1) { 370 if (ul1.childElementCount == 0) (ul1.previousElementSibling! as LitTreeNode).arrow = false; 371 } 372 if (srcParentElement.childElementCount === 0) srcParentElement.previousElementSibling.arrow = false;//如果拖动的原节点的父节点没有子节点需要 关闭arrow箭头 373 //拖动调整结构后修改 data树形数据结构 374 this.removeDataNode(this._treeData, srcData); 375 this.addDataNode(this._treeData, srcData, dstData!.key, this.dragDirection!); 376 this.dispatchEvent(new CustomEvent('drop', { 377 detail: { 378 treeData: this._treeData, 379 srcData: srcData, 380 dstData: dstData, 381 type: this.dragDirection 382 } 383 })) 384 ev.stopPropagation(); 385 return undefined; 386 } 387 388 //移除treeData中指定的节点 通过key匹配 389 removeDataNode(arr: Array<TreeItemData>, d: TreeItemData) { 390 let delIndex = arr.findIndex(v => v.key === d.key); 391 if (delIndex > -1) { 392 arr.splice(delIndex, 1); 393 return; 394 } 395 for (let i = 0; i < arr.length; i++) { 396 if (arr[i].children && arr[i].children!.length > 0) { 397 this.removeDataNode(arr[i].children!, d); 398 } 399 } 400 } 401 402 //中array中匹配到key为k的节点, t='bottom-right' 把d加入到该节点的children中去 t='top' 添加到找到的节点前面 t='bottom' 添加到找到的节点后面 403 addDataNode(arr: Array<TreeItemData>, d: TreeItemData, k: string, t: string) { 404 for (let i = 0; i < arr.length; i++) { 405 if (arr[i].key === k) { 406 if (t === 'bottom-right') { 407 if (!arr[i].children) arr[i].children = []; 408 arr[i].children!.push(d); 409 } else if (t === 'top') { 410 arr.splice(i, 0, d); 411 } else if (t === 'bottom') { 412 arr.splice(i + 1, 0, d); 413 } 414 return; 415 } else { 416 if (arr[i].children) this.addDataNode(arr[i].children || [], d, k, t); 417 } 418 } 419 } 420 421 insert(obj: TreeItemData) { 422 if (this.currentSelectedData) { 423 this.currentSelectedData.children.push(obj); 424 } else { 425 this.treeData.push(obj); 426 } 427 } 428 429 _insertNode(parent: any, a: any) { 430 if (!parent) parent = this.shadowRoot!.querySelector('#root'); 431 let li = document.createElement('li'); 432 let insertNode = document.createElement('lit-tree-node') as LitTreeNode; 433 insertNode.title = a.title; 434 insertNode.setAttribute('key', a.key); 435 if (this.hasAttribute('dragable')) { 436 insertNode.draggable = true; 437 document.ondragover = function (e) { 438 e.preventDefault(); 439 } 440 //在拖动目标上触发事件 (源元素) 441 insertNode.ondrag = ev => this.onDrag(ev);//元素正在拖动时触发 442 insertNode.ondragstart = ev => this.onDragStart(ev);//用户开始拖动元素时触发 443 insertNode.ondragend = ev => this.onDragEnd(ev);// 用户完成元素拖动后触发 444 //释放目标时触发的事件: 445 insertNode.ondragenter = ev => this.onDragEnter(ev);//当被鼠标拖动的对象进入其容器范围内时触发此事件 446 insertNode.ondragover = ev => this.onDragOver(ev);//当某被拖动的对象在另一对象容器范围内拖动时触发此事件 447 insertNode.ondragleave = ev => this.onDragLeave(ev);//当被鼠标拖动的对象离开其容器范围内时触发此事件 448 insertNode.ondrop = ev => this.onDrop(ev);//在一个拖动过程中,释放鼠标键时触发此事件 449 } 450 insertNode.selected = a.selected || false; //是否选中行 451 insertNode.checked = a.checked || false;// 是否勾选 452 insertNode.data = a; 453 insertNode.addEventListener('change', (e: any) => { 454 if (e.detail && !this.multiple) { 455 this.nodeList.forEach(node => { 456 node.checked = node.data!.key === insertNode.data!.key; 457 }); 458 } 459 this.dispatchEvent(new CustomEvent('onChange', {detail: {data: insertNode.data, checked: e.detail}})); 460 }) 461 this.nodeList.push(insertNode); 462 insertNode.checkable = this.getAttribute('checkable') || 'false'; 463 insertNode.multiple = this.hasAttribute('multiple'); 464 // @ts-ignore 465 li.data = a; 466 li.append(insertNode); 467 parent.append(li) 468 let ul = document.createElement('ul'); 469 // @ts-ignore 470 ul.open = 'true' 471 ul.style.transition = '.3s all'; 472 if (a.children && a.children.length > 0) { 473 if (this.hasAttribute('show-icon')) { 474 if (a.icon) { 475 insertNode.iconName = a.icon; 476 } else { 477 insertNode.iconName = 'folder'; 478 } 479 } else { 480 insertNode.iconName = ''; 481 } 482 insertNode.arrow = true; 483 li.append(ul); 484 this.drawTree(ul, a.children); 485 } else { 486 if (this.hasAttribute('show-icon')) { 487 if (a.icon) { 488 insertNode.iconName = a.icon; 489 } else { 490 insertNode.iconName = 'file'; 491 } 492 } else { 493 insertNode.iconName = ''; 494 } 495 insertNode.arrow = false; 496 } 497 li.onclick = (e) => { 498 e.stopPropagation() 499 if (this.hasAttribute('foldable')) { 500 // @ts-ignore 501 if (li.data.children && li.data.children.length > 0) { 502 insertNode.autoExpand(); 503 } else { 504 // @ts-ignore 505 this.dispatchEvent(new CustomEvent('onSelect', {detail: li.data})); 506 this.selectedNode(insertNode); 507 } 508 } else { 509 // @ts-ignore 510 this.dispatchEvent(new CustomEvent('onSelect', {detail: li.data})); 511 this.selectedNode(insertNode); 512 } 513 } 514 // node 添加右键菜单功能 515 insertNode.oncontextmenu = (ev) => { 516 ev.preventDefault(); 517 this.selectedNode(insertNode); 518 this.currentSelectedNode = insertNode; 519 this.currentSelectedData = insertNode.data; 520 this.contextMenu!.style.display = 'block'; 521 this.contextMenu!.style.left = ev.pageX + 'px'; 522 this.contextMenu!.style.top = ev.pageY + 'px'; 523 }; 524 } 525 526 initElements(): void { 527 this.contextMenu = this.shadowRoot!.querySelector<HTMLDivElement>('#contextMenu'); 528 } 529 530 initHtml(): string { 531 return ` 532 <style> 533 :host{ 534 display: flex; 535 color:#333; 536 width: 100%; 537 height: 100%; 538 overflow: auto; 539 } 540 :host *{ 541 box-sizing: border-box; 542 } 543 ul,li{ 544 list-style-type: none; 545 position:relative; 546 cursor:pointer; 547 overflow: hidden; 548 } 549 :host([show-line]) ul{ 550 padding-left:10px; 551 border-left: 1px solid #d9d9d9; 552 overflow: hidden; 553 } 554 :host(:not([show-line])) ul{ 555 padding-left: 10px; 556 overflow: hidden; 557 } 558 /*ul>li:after{content:' ';position:absolute;top:50%;left:-45px;width:45px;border:none;border-top:1px solid #d9d9d9;}*/ 559 #root{ 560 width: 100%; 561 height: 100%; 562 } 563 .context-menu { 564 position: absolute; 565 box-shadow: 0 0 10px #00000077; 566 pointer-events: auto; 567 z-index: 999; 568 transition: all .3s; 569 } 570 </style> 571 <ul id="root" style="margin: 0;overflow: hidden"></ul> 572 <div id="contextMenu" class="context-menu" style="display:none"> 573 <slot name="contextMenu"></slot> 574 </div> 575 `; 576 } 577} 578 579if (!customElements.get('lit-tree')) { 580 customElements.define('lit-tree', LitTree); 581}