• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}