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 '../icon/LitIcon.js' 17import '../checkbox/LitCheckBox.js' 18import { BaseElement, element } from '../BaseElement.js'; 19import { LitCheckBox } from '../checkbox/LitCheckBox.js'; 20import { LitIcon } from '../icon/LitIcon.js'; 21import { TreeItemData } from './LitTree.js'; 22 23@element('lit-tree-node') 24export class LitTreeNode extends BaseElement { 25 26 private arrowElement: HTMLSpanElement | null | undefined; 27 private itemElement: HTMLDivElement | null | undefined; 28 private checkboxElement: LitCheckBox | null | undefined; 29 private iconElement: LitIcon | null | undefined; 30 private _data: TreeItemData | null | undefined; 31 32 static get observedAttributes() { 33 return ['icon-name', 'icon-size', 'color', 'path', 'title', 'arrow', 'checkable', 'selected', 'checked', 'missing', 'multiple', 'top-depth']; 34 } 35 36 get checkable() { 37 return this.getAttribute('checkable') || 'false'; 38 } 39 40 set data(value: TreeItemData | null | undefined) { 41 this._data = value; 42 } 43 44 get data() { 45 return this._data; 46 } 47 48 set checkable(value) { 49 if (value === 'true') { 50 this.setAttribute('checkable', 'true'); 51 } else { 52 this.setAttribute('checkable', 'false'); 53 } 54 } 55 56 set multiple(value: boolean) { 57 if (value) { 58 this.setAttribute('multiple', ''); 59 } else { 60 this.removeAttribute('multiple'); 61 } 62 } 63 64 get multiple() { 65 return this.hasAttribute('multiple'); 66 } 67 68 69 get iconName() { 70 return this.getAttribute('icon-name') || ''; 71 } 72 73 set iconName(value) { 74 this.setAttribute('icon-name', value); 75 } 76 77 get topDepth() { 78 return this.hasAttribute('top-depth'); 79 } 80 81 set topDepth(value) { 82 if (value) { 83 this.setAttribute('top-depth', ''); 84 } else { 85 this.removeAttribute('top-depth'); 86 } 87 } 88 89 get arrow() { 90 return this.hasAttribute('arrow'); 91 } 92 93 set arrow(value) { 94 if (value) { 95 this.setAttribute('arrow', 'true'); 96 } else { 97 this.removeAttribute('arrow'); 98 } 99 } 100 101 get open() { 102 return this.getAttribute('open') || 'true'; 103 } 104 105 set open(value) { 106 this.setAttribute('open', value); 107 } 108 109 get selected() { 110 return this.hasAttribute('selected'); 111 } 112 113 set selected(value) { 114 if (value) { 115 this.setAttribute('selected', ''); 116 } else { 117 this.removeAttribute('selected'); 118 } 119 } 120 121 get checked() { 122 return this.hasAttribute('checked'); 123 } 124 125 set checked(value) { 126 if (value === null || !value) { 127 this.removeAttribute('checked'); 128 } else { 129 this.setAttribute('checked', ''); 130 } 131 } 132 133 initElements(): void { 134 this.arrowElement = this.shadowRoot!.querySelector<HTMLSpanElement>('#arrow'); 135 this.iconElement = this.shadowRoot!.querySelector<LitIcon>('#icon'); 136 this.itemElement = this.shadowRoot!.querySelector<HTMLDivElement>('#item'); 137 this.checkboxElement = this.shadowRoot!.querySelector<LitCheckBox>('#checkbox'); 138 this.arrowElement!.onclick = (e) => { 139 e.stopPropagation(); 140 this.autoExpand(); 141 } 142 this.checkboxElement!.onchange = (e: any) => { 143 e.stopPropagation(); 144 this.onChange(e.detail.checked); 145 return false; 146 } 147 //这里需要给checkbox 添加onclick时间 并停止冒泡,不然onchange事件会触发父节点中的 onclick事件 148 this.checkboxElement!.onclick = (e) => { 149 e.stopPropagation(); 150 }; 151 this.itemElement!.onclick = (e) => { 152 e.stopPropagation(); 153 this.onChange(!this.data?.checked); 154 }; 155 } 156 157 onChange(checked: boolean) { 158 this.checked = checked; 159 this.data!.checked = checked; 160 this.checkHandler(); 161 this.dispatchEvent(new CustomEvent('change', {detail: checked})); 162 } 163 164 initHtml(): string { 165 return ` 166 <style> 167 :host{ 168 display: flex; 169 margin: 0; 170 align-items: center; 171 } 172 :host(:hover) #item{ 173 background-color: #f5f5f5; 174 border-radius: 4px; 175 } 176 177 :host(:not([arrow])) #arrow{ 178 display: none; 179 } 180 181 :host(:not([arrow])) #item{ 182 margin-left: 15px; 183 } 184 185 :host([top-depth]) #item{ 186 margin-left: 0; 187 } 188 189 #title{ 190 padding: 4px 6px; 191 } 192 193 #arrow{ 194 width: 0; 195 height: 0; 196 border-top: 8px solid #262626; 197 border-bottom: 0px solid transparent; 198 border-left: 5px solid transparent; 199 border-right: 5px solid transparent; 200 transition: all .3s ; 201 transform: translateX(-50%) rotateZ(0deg); 202 margin-left: 5px; 203 } 204 205 #icon{ 206 display: none; 207 margin-left: 5px; 208 } 209 210 /*画拖动辅助线*/ 211 #item[line-top]{ 212 position: relative; 213 width: 100%; 214 } 215 216 #item[line-top]::before{ 217 content: ''; 218 position: absolute; 219 top: 5px; 220 left: 0; 221 transform: translateY(-50%); 222 width: 6px; 223 height: 6px; 224 overflow: visible; 225 z-index: 999; 226 background-color: #fff; 227 border-radius: 6px; 228 border: 2px solid #42b983; 229 } 230 #item[line-top]::after{ 231 content: ''; 232 overflow: visible; 233 position: absolute; 234 z-index: 999; 235 top: 4px; 236 left: 10px; 237 width: 100%; 238 height: 2px; 239 background-color: #42b983; 240 } 241 242 #item[line-bottom]{ 243 position: relative; 244 width: 100%; 245 } 246 #item[line-bottom]::before{ 247 content: ''; 248 position: absolute; 249 bottom: 5px; 250 left: 0; 251 transform: translateY(50%); 252 width: 6px; 253 height: 6px; 254 overflow: visible; 255 z-index: 999; 256 background-color: #fff; 257 border-radius: 6px; 258 border: 2px solid #42b983; 259 } 260 #item[line-bottom]::after{ 261 content: ''; 262 overflow: visible; 263 position: absolute; 264 z-index: 999; 265 bottom: 4px; 266 left: 10px; 267 width: 100%; 268 height: 2px; 269 background-color: #42b983; 270 } 271 #item[line-bottom-right]{ 272 position: relative; 273 width: 100%; 274 } 275 #item[line-bottom-right]::before{ 276 content: ''; 277 position: absolute; 278 bottom: 5px; 279 left: 20px; 280 transform: translateY(50%); 281 width: 6px; 282 height: 6px; 283 overflow: visible; 284 z-index: 999; 285 background-color: #fff; 286 border-radius: 6px; 287 border: 2px solid #42b983; 288 } 289 #item[line-bottom-right]::after{ 290 content: ''; 291 overflow: visible; 292 position: absolute; 293 z-index: 999; 294 bottom: 4px; 295 left: 30px; 296 width: 100%; 297 height: 2px; 298 background-color: #42b983; 299 } 300 :host([missing]) #checkbox{ 301 position: relative; 302 } 303 304 :host([missing]) #checkbox::after{ 305 content: ''; 306 width: calc(100% - 20px); 307 height: calc(100% - 8px); 308 box-sizing: border-box; 309 top: 0; 310 left: 0; 311 margin: 4px; 312 background-color: #3391FF; 313 position: absolute; 314 } 315 316 </style> 317 <span id="arrow" style="margin-right: 2px"></span> 318 <div id="item" style="display: flex;align-items: center;padding-left: 2px"> 319 <lit-check-box id="checkbox"></lit-check-box> 320 <lit-icon id="icon" name="${this.iconName}"></lit-icon> 321 <span id="title">${this.title}</span> 322 </div> 323 ` 324 } 325 326 //当 custom element首次被插入文档DOM时,被调用。 327 connectedCallback() { 328 if (this.hasAttribute('checked')) this.checkboxElement!.checked = true; 329 this.checkHandler(); 330 } 331 332 checkHandler() { 333 if (this.checked) { 334 this.removeAttribute('missing'); 335 } 336 if (this.hasAttribute('multiple')) { 337 if (this.nextElementSibling) { 338 if (this.checked) { 339 this.nextElementSibling.querySelectorAll('lit-tree-node').forEach((a: any) => { 340 a.checked = true; 341 a.removeAttribute('missing'); 342 }); 343 } else { 344 this.nextElementSibling.querySelectorAll('lit-tree-node').forEach((a: any) => a.checked = false); 345 } 346 } 347 let setCheckStatus = (element: any) => { 348 if (element.parentElement.parentElement.previousElementSibling && element.parentElement.parentElement.previousElementSibling.tagName === 'LIT-TREE-NODE') { 349 let allChecked = Array.from(element.parentElement.parentElement.querySelectorAll('lit-tree-node')).every((item: any) => item.checked); 350 let someChecked = Array.from(element.parentElement.parentElement.querySelectorAll('lit-tree-node')).some((item: any, index, array) => item.checked); 351 if (allChecked === true) { 352 element.parentElement.parentElement.previousElementSibling.checked = true; 353 element.parentElement.parentElement.previousElementSibling.removeAttribute('missing'); 354 } else if (someChecked) { 355 element.parentElement.parentElement.previousElementSibling.setAttribute('missing', '') 356 element.parentElement.parentElement.previousElementSibling.removeAttribute('checked') 357 } else { 358 element.parentElement.parentElement.previousElementSibling.removeAttribute('missing') 359 element.parentElement.parentElement.previousElementSibling.removeAttribute('checked') 360 } 361 setCheckStatus(element.parentElement.parentElement.previousElementSibling) 362 } 363 } 364 setCheckStatus(this); 365 } 366 } 367 368 expand() { 369 if (this.open === 'true') return; 370 let uul = this.parentElement!.querySelector('ul'); 371 this.expandSection(uul); 372 this.arrowElement!.style.transform = 'translateX(-50%) rotateZ(0deg)'; 373 } 374 375 collapse() { 376 if (this.open === 'false') return; 377 let uul = this.parentElement!.querySelector('ul'); 378 this.collapseSection(uul); 379 this.arrowElement!.style.transform = 'translateX(-50%) rotateZ(-90deg)'; 380 } 381 382 autoExpand() { 383 let uul = this.parentElement!.querySelector('ul'); 384 if (this.open === 'true') { 385 this.collapseSection(uul); 386 this.arrowElement!.style.transform = 'translateX(-50%) rotateZ(-90deg)'; 387 } else { 388 this.expandSection(uul); 389 this.arrowElement!.style.transform = 'translateX(-50%) rotateZ(0deg)'; 390 } 391 } 392 393 //收起 394 collapseSection(element: any) { 395 if (!element) return; 396 let sectionHeight = element.scrollHeight; 397 let elementTransition = element.style.transition; 398 element.style.transition = ''; 399 requestAnimationFrame(function () { 400 element.style.height = sectionHeight + 'px'; 401 element.style.transition = elementTransition; 402 requestAnimationFrame(function () { 403 element.style.height = 0 + 'px'; 404 }); 405 }); 406 this.open = 'false'; 407 } 408 409 //展开 410 expandSection(element: any) { 411 if (!element) return; 412 let sectionHeight = element.scrollHeight; 413 element.style.height = sectionHeight + 'px'; 414 element.ontransitionend = (e: any) => { 415 element.ontransitionend = null; 416 element.style.height = null; 417 this.open = 'true'; 418 }; 419 } 420 421 //当 custom element从文档DOM中删除时,被调用。 422 disconnectedCallback() { 423 424 } 425 426 //当 custom element被移动到新的文档时,被调用。 427 adoptedCallback() { 428 429 } 430 431 //当 custom element增加、删除、修改自身属性时,被调用。 432 attributeChangedCallback(name: string, oldValue: any, newValue: any) { 433 if (name === 'title') { 434 this.shadowRoot!.querySelector('#title')!.textContent = newValue; 435 } else if (name === 'icon-name') { 436 if (this.iconElement) { 437 if (newValue !== null && newValue !== '' && newValue !== 'null') { 438 this.iconElement!.setAttribute('name', newValue); 439 this.iconElement!.style.display = 'flex'; 440 } else { 441 this.iconElement!.style.display = 'none'; 442 } 443 } 444 } else if (name === 'checkable') { 445 if (this.checkboxElement) { 446 if (newValue === 'true') { 447 this.checkboxElement!.style.display = 'inline-block'; 448 } else { 449 this.checkboxElement!.style.display = 'none'; 450 } 451 } 452 } else if (name === 'checked') { 453 if (this.checkboxElement) { 454 this.checkboxElement.checked = this.hasAttribute('checked'); 455 } 456 } 457 } 458 459 //在node top top-right bottom bottom-right 画线条 460 drawLine(direction: string/*string[top|bottom|top-right|bottom-right]*/) { 461 let item = this.shadowRoot!.querySelector('#item'); 462 if (!item) return; 463 item.removeAttribute('line-top'); 464 item.removeAttribute('line-top-right'); 465 item.removeAttribute('line-bottom'); 466 item.removeAttribute('line-bottom-right'); 467 switch (direction) { 468 case 'top': 469 item.setAttribute('line-top', '') 470 break; 471 case 'bottom': 472 item.setAttribute('line-bottom', '') 473 break; 474 case 'top-right': 475 item.setAttribute('line-top-right', '') 476 break; 477 case 'bottom-right': 478 item.setAttribute('line-bottom-right', '') 479 break; 480 } 481 } 482} 483 484if (!customElements.get('lit-tree-node')) { 485 customElements.define('lit-tree-node', LitTreeNode); 486}