1const devMode = false; 2const baseUrl = devMode ? '' : 'https://documentero.com'; 3const loadingWrapper = '<div class="loading-wrapper"><div class="lds-ring"><div></div><div></div><div></div><div></div></div></div>'; 4 5class TemplateGalleryWidget extends HTMLElement { 6 constructor() { 7 super(); 8 this.attachShadow({ mode: 'open' }); 9 this.mergedTemplates = []; // Store merged templates for selection 10 11 this.currentTemplateId = ''; 12 this.currentTemplateType = ''; 13 this.currentTemplateName = ''; 14 this.currentTemplateHtml = ''; 15 this.currentTemplateDownloadUrl = ''; 16 this.selectedCategoryId = ''; 17 this.searchQuery = ''; 18 this.templatesRenderVersion = 0; 19 this.searchDebounceTimer = null; 20 this.userSelectedCategoryDuringSearch = false; 21 22 this.shadowRoot.innerHTML = ` 23 <style> 24 /* Embedded styles for the widget */ 25 :host { 26 display: block; 27 font-family: Arial, sans-serif; 28 font-size: 14px; 29 color: #4a4a4a; 30 font-weight: 400; 31 line-height: 1.5; 32 } 33 .widget-container { 34 display: flex; 35 flex-direction: column; 36 height: 100%; 37 max-height: calc(100vh - 340px); 38 min-height: 340px; 39 } 40 .columns { 41 display: flex; 42 flex: 1; 43 overflow: hidden; 44 max-height: calc(100vh - 340px); 45 min-height: 340px; 46 } 47 .list-pane { 48 display: flex; 49 flex-direction: column; 50 flex: 0 0 400px; 51 min-width: 400px; 52 max-width: 400px; 53 } 54 .list-columns { 55 display: flex; 56 flex: 1; 57 min-height: 0; 58 overflow-y: auto; 59 overflow-x: hidden; 60 } 61 .search-row { 62 display: flex; 63 gap: 0; 64 padding-bottom: 8px; 65 } 66 .search-input-wrapper { 67 position: relative; 68 min-width: 0; 69 flex: 1; 70 } 71 .search-input-icon { 72 position: absolute; 73 left: 10px; 74 top: 50%; 75 transform: translateY(-50%); 76 color: #656d77; 77 pointer-events: none; 78 } 79 .search-input { 80 width: 100%; 81 height: 38px; 82 border: none; 83 border-bottom: 1px solid #dedede; 84 padding: 0 12px 0 34px; 85 font-size: 12px; 86 font-family: Roboto, Arial, sans-serif; 87 color: #4a4a4a; 88 outline: none; 89 box-sizing: border-box; 90 background: transparent; 91 } 92 .search-input::placeholder { 93 color: #9aa0a6; 94 } 95 .search-input::-webkit-search-cancel-button { 96 cursor: pointer; 97 opacity: 0.7; 98 transition: opacity 0.15s ease; 99 } 100 .search-input::-webkit-search-cancel-button:hover { 101 opacity: 1; 102 } 103 .column { 104 flex: 1; 105 overflow-y: auto; 106 border-right: 1px solid #dedede; 107 box-sizing: border-box; 108 min-width: 175px; 109 max-width: 175px; 110 font-size: 12px; 111 font-family: Roboto, Arial, sans-serif; 112 font-weight: 400; 113 -webkit-user-select: none; /* Safari */ 114 user-select: none; /* Standard syntax */ 115 } 116 .column-templates { 117 min-width: 225px; 118 max-width: 225px; 119 } 120 .column-preview { 121 flex: 1; 122 min-width: 0; 123 max-width: none; 124 } 125 .column:last-child {
126 border-right: none; 127 } 128 .list-columns .column:last-child { 129 border-right: none; 130 } 131 .category-item, .template-item { 132 padding: 5px 8px; 133 cursor: pointer; 134 border-top: 1px solid #f0f0f0; 135 white-space: nowrap; 136 text-overflow: ellipsis; 137 overflow: hidden; 138 } 139 .category-item:hover, .template-item:hover { 140 background-color: #f0f0f0; 141 } 142 .category-item { 143 display: flex; 144 align-items: center; 145 gap: 6px; 146 } 147 .category-item.selected, .template-item.selected { 148 background-color:rgb(32,107,196); 149 color: #fff; 150 } 151 .match-highlight { 152 display: inline; 153 background: #fff1a8; 154 color: #3d2f00; 155 border-radius: 2px; 156 padding: 0; 157 line-height: inherit; 158 vertical-align: baseline; 159 box-shadow: 1px 0 0 #fff1a8, -1px 0 0 #fff1a8; 160 } 161 .category-item.selected .match-highlight, 162 .template-item.selected .match-highlight { 163 background: rgba(255, 255, 255, 0.24); 164 color: inherit; 165 box-shadow: 1px 0 0 rgba(255, 255, 255, 0.24), -1px 0 0 rgba(255, 255, 255, 0.24); 166 } 167 .column-header { 168 padding: 8px; 169 font-size: 13px; 170 cursor: default; 171 text-align: center; 172 border-top: none; 173 } 174 .column-header:hover { 175 background-color: transparent; 176 } 177 .column-header svg { 178 margin-right: 4px; 179 vertical-align: middle; 180 } 181 .actions { 182 display: flex; 183 justify-content: space-between; 184 padding: 10px; 185 border-top: 1px solid #ccc; 186 } 187 button { 188 padding: 10px; 189 cursor: pointer; 190 } 191 .badge { 192 display: inline-block; 193 padding: 1px 4px; 194 margin-right: 6px; 195 margin-left: 0px; 196 line-height: 12px; 197 font-size: 0.75em; 198 text-align: center; 199 min-width: 24px; 200 color: #216bc4; 201 background-color: #f0f0f0; 202 border-radius: 3px; 203 } 204 .badge.badge-xlsx { 205 color: #2fb344; 206 } 207 .template-name, 208 .category-name { 209 vertical-align: middle; 210 } 211 .category-name { 212 flex: 1 1 auto; 213 min-width: 0; 214 overflow: hidden; 215 text-overflow: ellipsis; 216 white-space: nowrap; 217 } 218 .category-count { 219 flex: 0 0 auto; 220 min-width: 18px; 221 height: 18px; 222 margin-left: auto; 223 padding: 0 5px; 224 border-radius: 999px; 225 font-size: 10px; 226 line-height: 18px; 227 text-align: center; 228 color: #656d77; 229 background: #edf1f5; 230 } 231 .category-item.selected .category-count { 232 color: #206bc4; 233 background: #ffffff; 234 } 235 .selected { 236 background-color:#206bc4; 237 color: #fff; 238 } 239 .empty-state { 240 display: flex; 241 flex-direction: column; 242 align-items: center; 243 justify-content: center; 244 gap: 10px; 245 min-height: 180px; 246 padding: 24px 18px; 247 color: #656d77; 248 text-align: center; 249 } 250 .empty-state svg { 251 color: #9aa0a6; 252 } 253 .empty-state-title { 254 font-size: 13px; 255 font-weight: 500; 256 color: #4a4a4a; 257 } 258 .empty-state-copy { 259 font-size: 12px; 260 line-height: 1.5; 261 } 262 .document-template-view { 263 background: white; 264 padding: 30px 40px; 265 margin: 0 10px; 266 border: 1px solid #ccc; 267 max-width: 680px; 268 box-shadow: 0 0 8px 0 rgba(0, 0, 0, .12); 269 font-family: Arial; 270 color: #000; 271 overflow: auto; 272 min-width: 500px; 273 margin-right: 0; 274 } 275 276 .document-template-view * { 277 font-family: Arial; 278 margin: 0; 279 } 280 281 .document-template-view.is-spreadsheet { 282 padding: 22px; 283 max-width: 100%; 284 overflow: auto; 285 background: #F9F9F9; 286 } 287 288 /* Document template view styles */ 289 .document-template-view h1, .document-template-view h2, .document-template-view h3, .document-template-view h4, .document-template-view h5 { 290 font-family: Arial, sans-serif; 291 color: #000; 292 font-weight: bold; 293 margin-top: 10pt; 294 } 295 .document-template-view i { 296 font-style: italic; 297 } 298 .document-template-view p, .document-template-view dl, .document-template-view ol, .document-template-view ul { 299 margin-top: 0; 300 margin-bottom: 1rem; 301 } 302 .document-template-view table, .document-template-view figure {
303 margin: .9em auto; 304 } 305 .document-template-view ul, .document-template-view ol { 306 list-style-type: initial; 307 } 308 .document-template-view h1,.document-template-view h2, .document-template-view h3, .document-template-view h4,.document-template-view h5,.document-template-view h6 { 309 margin-top: 0; 310 margin-bottom: .5rem; 311 font-weight: 600; 312 line-height: 1.2 313 } 314 .document-template-view strong, .document-template-view b{ 315 font-weight: 600; 316 } 317 .document-template-view h1 { 318 font-size:20px; 319 text-align: center; 320 } 321 .document-template-view h2 { 322 font-size:16px; 323 } 324 .document-template-view h3 { 325 font-size:14px; 326 } 327 .document-template-view h4 { 328 font-size:13px; 329 } 330 .document-template-view h5 { 331 font-size:12px; 332 } 333 .document-template-view body { 334 font-family: Arial, sans-serif; 335 margin: 0; 336 padding: 1em; 337 } 338 .document-template-view table { 339 width: 100%; 340 border: 1px solid #bfbfbf; 341 } 342 343 .document-template-view table td, 344 .document-template-view table th { 345 padding: 8px; 346 min-width: 2pt; 347 border-top: none; 348 border-left: none; 349 border-bottom: 1px solid #bfbfbf; 350 border-right: 1px solid #bfbfbf; 351 } 352 353 .document-template-view table td:last-child, 354 .document-template-view table th:last-child { 355 border-right: 0px solid #bfbfbf; 356 } 357 358 .document-template-view table tr:last-child td, 359 .document-template-view table tbody tr:last-child th { 360 border-bottom: 0px solid #bfbfbf; 361 } 362 363 .document-template-view table th { 364 background-color: #ededed; 365 font-weight: bold; 366 color: #656d77; 367 } 368 369 .document-template-view table { 370 overflow: hidden; 371 } 372 373 /* not supported by html to docx */ 374 .document-template-view blockquote { 375 display: block; 376 border-left: 5px solid #cccccc; 377 font-style: italic; 378 margin-left: 0; 379 margin-right: 0; 380 padding-left: 1.5em; 381 padding-right: 1.5em; 382 } 383 /* END: Document template view styles */ 384 385 /* Excel-like table styles */ 386 387 /* General table styling */ 388 table.excel-like-table { 389 border-collapse: collapse; 390 width: 100%; 391 font-family: Arial, sans-serif; 392 font-size: 11px; 393 color: #333; 394 } 395 396 /* Table borders */ 397 table.excel-like-table, table.excel-like-table th, table.excel-like-table td { 398 border: 1px solid #D4D4D4; 399 } 400 401 /* Header row styling */ 402 table.excel-like-table th { 403 background-color: #D4D4D4; 404 font-weight: bold; 405 padding: 3px; 406 } 407 408 /* Cell padding */ 409 table.excel-like-table td { 410 padding: 3px 6px; 411 text-align: left; 412 height: 22px; 413 overflow: hidden; 414 background: white; 415 white-space: nowrap; 416 } 417 418 /* Alternating row colors */ 419 table.excel-like-table tr:nth-child(even) { 420 background-color: #f9f9f9; 421 } 422 423 /* Highlighting selected row */ 424 table.excel-like-table tr:hover { 425 background-color: #e6f7ff; 426 } 427 428 /* Bold text for specific cells */ 429 table.excel-like-table td[style*="font-weight: bold"] { 430 font-weight: bold; 431 } 432 433 /* Center alignment for specific cells */ 434 table.excel-like-table td[style*="text-align: center"] { 435 text-align: center; 436 } 437 438 /* Font size adjustments */ 439 table.excel-like-table td[style*="font-size: 16px"] { 440 font-size: 16px; 441 } 442 table.excel-like-table td[style*="font-size: 14px"] { 443 font-size: 14px; 444 } 445 446 447 @media (max-width: 1280px) { 448 .list-pane { 449 flex-basis: 275px; 450 min-width: 275px; 451 max-width: 275px; 452 } 453 .column-templates { 454 min-width: 150px; 455 max-width: 150px; 456 } 457 .column-categories { 458 min-width: 125px; 459 max-width: 125px; 460 } 461 } 462 463 .loading-wrapper { 464 min-height: 100px; 465 width: 100%; 466 height: 100%; 467 display: flex; 468 justify-content: center; /* horizontal center */ 469 align-items: center; /* vertical center */ 470 } 471 .lds-ring { 472 /* change color here */ 473 color: #1c4c5b 474 } 475 .lds-ring, 476 .lds-ring div { 477 box-sizing: border-box; 478 } 479 .lds-ring { 480 display: inline-block; 481 position: relative; 482 width: 80px; 483 height: 80px; 484 } 485 .lds-ring div { 486 box-sizing: border-box; 487 display: block; 488 position: absolute; 489 width: 64px; 490 height: 64px; 491 margin: 8px; 492 border: 8px solid currentColor; 493 border-radius: 50%; 494 animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 495 border-color: currentColor transparent transparent transparent; 496 } 497 .lds-ring div:nth-child(1) { 498 animation-delay: -0.45s; 499 } 500 .lds-ring div:nth-child(2) { 501 animation-delay: -0.3s; 502 } 503 .lds-ring div:nth-child(3) { 504 animation-delay: -0.15s; 505 } 506 @keyframes lds-ring { 507 0% { 508 transform: rotate(0deg); 509 } 510 100% { 511 transform: rotate(360deg); 512 } 513 } 514 </style> 515 <div class="widget-container"> 516 <div class="columns"> 517 <div class="list-pane"> 518 <div class="search-row"> 519 <div class="search-input-wrapper"> 520 <svg class="search-input-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 11a7 7 0 1 0 14 0a7 7 0 0 0 -14 0" /><path d="M20 20l-3.5 -3.5" /></svg> 521 <input id="search-input" class="search-input" type="search" placeholder="Search templates and categories" aria-label="Search templates and categories"> 522 </div> 523 </div> 524 <div class="list-columns"> 525 <div class="column column-categories" id="categories"></div> 526 <div class="column column-templates" id="templates"></div> 527 </div> 528 </div> 529 <div class="column column-preview" id="preview"></div> 530 </div> 531 </div> 532 `; 533 this.setupSearchInputs(); 534 this.loadCategories(); 535 } 536 537 connectedCallback() { 538 const onTemplateSelectAttr = this.getAttribute('onTemplateSelect'); 539 if (onTemplateSelectAttr && typeof window[onTemplateSelectAttr] === 'function') { 540 this.onTemplateSelect = window[onTemplateSelectAttr]; 541 } 542 } 543 544 // Move cached variables inside the class 545 cachedCategories = null; 546 cachedTemplates = null; 547 548 setupSearchInputs() { 549 const searchInput = this.shadowRoot.getElementById('search-input'); 550 551 const syncSearch = (value) => { 552 const nextQuery = (value || '').trim().toLowerCase(); 553 const queryChanged = nextQuery !== this.searchQuery; 554 555 this.searchQuery = nextQuery; 556 if (queryChanged) { 557 this.userSelectedCategoryDuringSearch = false;
558 } 559 560 this.renderFilteredResults(); 561 }; 562 563 searchInput.addEventListener('input', (event) => { 564 const inputValue = event.target.value; 565 clearTimeout(this.searchDebounceTimer); 566 this.searchDebounceTimer = setTimeout(() => { 567 syncSearch(inputValue); 568 }, 180); 569 }); 570 } 571 572 matchesSearch(value) { 573 return !this.searchQuery || value.toLowerCase().includes(this.searchQuery); 574 } 575 576 appendHighlightedText(container, text) { 577 if (!this.searchQuery || !text) { 578 container.textContent = text || ''; 579 return; 580 } 581 582 const normalizedText = text.toLowerCase(); 583 const matchIndex = normalizedText.indexOf(this.searchQuery); 584 585 if (matchIndex === -1) { 586 container.textContent = text; 587 return; 588 } 589 590 const beforeMatch = text.slice(0, matchIndex); 591 const matchedText = text.slice(matchIndex, matchIndex + this.searchQuery.length); 592 const afterMatch = text.slice(matchIndex + this.searchQuery.length); 593 594 if (beforeMatch) { 595 container.appendChild(document.createTextNode(beforeMatch)); 596 } 597 598 const highlight = document.createElement('span'); 599 highlight.className = 'match-highlight'; 600 highlight.textContent = matchedText; 601 container.appendChild(highlight); 602 603 if (afterMatch) { 604 container.appendChild(document.createTextNode(afterMatch)); 605 } 606 } 607 608 getFilteredCategories() { 609 if (!this.cachedCategories) { 610 return []; 611 } 612 613 return this.cachedCategories.categories.filter((category) => { 614 const categoryText = [category.name, category.shortName, category.description, category.id] 615 .filter(Boolean) 616 .join(' '); 617 618 if (this.matchesSearch(categoryText)) { 619 return true; 620 } 621 622 return this.cachedTemplates?.some((template) => { 623 if (template.categoryId !== category.id) { 624 return false; 625 } 626 627 const templateText = [template.name, template.description, template.id] 628 .filter(Boolean) 629 .join(' '); 630 631 return this.matchesSearch(templateText); 632 }) ?? false; 633 }); 634 } 635 636 getFilteredTemplates(categoryId) { 637 if (!this.cachedTemplates || !categoryId) { 638 return []; 639 } 640 641 return this.cachedTemplates.filter((template) => { 642 if (template.categoryId !== categoryId) { 643 return false; 644 } 645 646 const templateText = [template.name, template.description, template.id] 647 .filter(Boolean) 648 .join(' '); 649 650 return this.matchesSearch(templateText); 651 }); 652 } 653 654 getCategoryMatchCount(categoryId) { 655 return this.getFilteredTemplates(categoryId).length; 656 } 657 658 renderFilteredResults() { 659 if (!this.cachedCategories || !this.cachedTemplates) { 660 return; 661 } 662 663 const visibleCategories = this.getFilteredCategories(); 664 const bestCategory = visibleCategories.reduce((currentBest, category) => { 665 if (!currentBest) { 666 return category; 667 } 668 669 const bestCount = this.getCategoryMatchCount(currentBest.id); 670 const categoryCount = this.getCategoryMatchCount(category.id); 671 672 return categoryCount > bestCount ? category : currentBest; 673 }, null); 674 675 if (this.searchQuery) { 676 const selectedCategoryStillVisible = visibleCategories.some((category) => category.id === this.selectedCategoryId); 677 678 if (!selectedCategoryStillVisible || !this.userSelectedCategoryDuringSearch) { 679 this.selectedCategoryId = bestCategory?.id || ''; 680 } 681 } else { 682 const selectedCategoryStillVisible = visibleCategories.some((category) => category.id === this.selectedCategoryId); 683 this.selectedCategoryId = selectedCategoryStillVisible ? this.selectedCategoryId : (visibleCategories[0]?.id || ''); 684 this.userSelectedCategoryDuringSearch = false;
685 } 686 687 this.renderCategories(visibleCategories); 688 this.renderTemplates(this.selectedCategoryId); 689 } 690 691 renderCategories(categories) { 692 const categoriesContainer = this.shadowRoot.getElementById('categories'); 693 categoriesContainer.innerHTML = '<div class="category-item column-header"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-category"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4h6v6h-6z" /><path d="M14 4h6v6h-6z" /><path d="M4 14h6v6h-6z" /><path d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /></svg> Categories</div>'; 694 695 categories.forEach((category) => { 696 const templateCount = this.getCategoryMatchCount(category.id); 697 const categoryElement = document.createElement('div'); 698 categoryElement.dataset.id = category.id; 699 categoryElement.className = `category-item${category.id === this.selectedCategoryId ? ' selected' : ''}`; 700 categoryElement.title = category.description 701 ? `${category.name}\n\n${category.description}` 702 : category.name; 703 704 const categoryName = document.createElement('span'); 705 categoryName.className = 'category-name'; 706 this.appendHighlightedText(categoryName, category.shortName ? category.shortName : category.name); 707 708 const countBadge = document.createElement('span'); 709 countBadge.className = 'category-count'; 710 countBadge.textContent = templateCount; 711 712 categoryElement.appendChild(categoryName); 713 categoryElement.appendChild(countBadge); 714 categoryElement.addEventListener('click', () => { 715 this.selectedCategoryId = category.id; 716 this.userSelectedCategoryDuringSearch = Boolean(this.searchQuery); 717 this.renderFilteredResults(); 718 }); 719 categoriesContainer.appendChild(categoryElement); 720 }); 721 } 722 723 renderEmptyTemplates(message) { 724 const templatesContainer = this.shadowRoot.getElementById('templates'); 725 const previewContainer = this.shadowRoot.getElementById('preview'); 726 727 templatesContainer.innerHTML = '<div class="template-item column-header"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-template"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 1a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1z" /><path d="M4 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 12l6 0" /><path d="M14 16l6 0" /><path d="M14 20l6 0" /></svg> Templates</div><div class="empty-state"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" /><path d="M9 8h6" /><path d="M9 12h6" /><path d="M9 16h3" /></svg><div class="empty-state-title">No templates found</div><div class="empty-state-copy">' + message + '</div></div>'; 728 previewContainer.innerHTML = ''; 729 previewContainer.className = 'column'; 730 this.mergedTemplates = []; 731 this.currentTemplateId = ''; 732 this.currentTemplateType = ''; 733 this.currentTemplateName = ''; 734 this.currentTemplateHtml = ''; 735 this.currentTemplateDownloadUrl = ''; 736 } 737 738 async renderTemplates(categoryId) { 739 const renderVersion = ++this.templatesRenderVersion; 740 const templatesContainer = this.shadowRoot.getElementById('templates'); 741 const previewContainer = this.shadowRoot.getElementById('preview'); 742 743 templatesContainer.innerHTML = '<div class="template-item column-header"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
743ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-template"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 1a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1z" /><path d="M4 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 12l6 0" /><path d="M14 16l6 0" /><path d="M14 20l6 0" /></svg> Templates</div>'; 744 745 if (!categoryId) { 746 this.renderEmptyTemplates('Try a different keyword or clear the search to see available templates.'); 747 return; 748 } 749 750 const mergedTemplates = this.getFilteredTemplates(categoryId); 751 this.mergedTemplates = mergedTemplates; 752 753 mergedTemplates.forEach((template) => { 754 const templateElement = document.createElement('div'); 755 templateElement.dataset.id = template.id; 756 templateElement.dataset.type = template.type; 757 templateElement.dataset.categoryId = template.categoryId; 758 templateElement.className = 'template-item'; 759 const badge = document.createElement('span'); 760 badge.className = `badge ${template.type === 'xlsx' ? 'badge-xlsx' : 'badge-docx'}`; 761 badge.textContent = template.type === 'docx' ? 'DOCX' : 'XLSX'; 762 templateElement.appendChild(badge); 763 const templateName = document.createElement('span'); 764 templateName.className = 'template-name'; 765 this.appendHighlightedText(templateName, template.name); 766 templateElement.appendChild(templateName); 767 templateElement.title = template.description 768 ? `${template.type === 'docx' ? 'Document' : 'Spreadsheet'} - ${template.name}\n\n${template.description}` 769 : `${template.type === 'docx' ? 'Document' : 'Spreadsheet'} - ${template.name}`; 770 templateElement.addEventListener('click', async () => { 771 await this.loadPreview(categoryId, template.id, template.type); 772 if (renderVersion !== this.templatesRenderVersion || !templateElement.isConnected) { 773 return; 774 } 775 this.highlightSelection('templates', templateElement); 776 }); 777 templatesContainer.appendChild(templateElement); 778 }); 779 780 const hasSelectedTemplate = mergedTemplates.some((template) => template.id === this.currentTemplateId && template.type === this.currentTemplateType); 781 const templateToSelect = hasSelectedTemplate 782 ? mergedTemplates.find((template) => template.id === this.currentTemplateId && template.type === this.currentTemplateType) 783 : mergedTemplates[0]; 784 785 if (!templateToSelect) { 786 this.renderEmptyTemplates('Try a different keyword or clear the search to see more results in this category.'); 787 return; 788 } 789 790 const selectedTemplateElement = Array.from(templatesContainer.children).find((child) => { 791 return child.dataset.id === templateToSelect.id && child.dataset.type === templateToSelect.type; 792 }); 793 794 await this.loadPreview(categoryId, templateToSelect.id, templateToSelect.type); 795 if (renderVersion !== this.templatesRenderVersion) { 796 return; 797 } 798 799 if (selectedTemplateElement?.isConnected) { 800 this.highlightSelection('templates', selectedTemplateElement); 801 } 802 } 803 804 async loadCategories() { 805 const categoriesContainer = this.shadowRoot.getElementById('categories'); 806 categoriesContainer.innerHTML = loadingWrapper; // Show loading indicator 807 808 if (!this.cachedCategories) { 809 const response = await fetch(baseUrl + '/widget/list/categories.json'); 810 this.cachedCategories = await response.json(); 811 } 812 813 if (!this.cachedTemplates) { 814 const docResponse = await fetch(baseUrl + '/widget/list/list.json'); 815 const excelResponse = await fetch(baseUrl + '/widget/list/listExcel.json'); 816 const docData = await docResponse.json(); 817 const excelData = await excelResponse.json(); 818 819 const docTemplates = docData.templates.map(template => ({ ...template, type: 'docx' })); 820 const excelTemplates = excelData.templates.map(template => ({ ...template, type: 'xlsx' })); 821 822 this.cachedTemplates = [...docTemplates, ...excelTemplates]; 823 } 824 825 this.selectedCategoryId = this.cachedCategories.categories[0]?.id || ''; 826 this.renderFilteredResults(); 827 } 828 829 async loadTemplates(categoryId) { 830 const templatesContainer = this.shadowRoot.getElementById('templates'); 831 templatesContainer.innerHTML = loadingWrapper; // Show loading indicator 832 833 if (!this.cachedTemplates) { 834 const docResponse = await fetch(baseUrl + '/widget/list/list.json'); 835 const excelResponse = await fetch(baseUrl + '/widget/list/listExcel.json'); 836 const docData = await docResponse.json(); 837 const excelData = await excelResponse.json(); 838 839 const docTemplates = docData.templates.map(template => ({ ...template, type: 'docx' })); 840 const excelTemplates = excelData.templates.map(template => ({ ...template, type: 'xlsx' })); 841 842 this.cachedTemplates = [...docTemplates, ...excelTemplates]; 843 } 844 this.selectedCategoryId = categoryId; 845 await this.renderTemplates(categoryId); 846 } 847 848 async loadPreview(categoryId, templateId, type) { 849 const previewContainer = this.shadowRoot.getElementById('preview'); 850 previewContainer.innerHTML = loadingWrapper; // Show loading indicator 851 852 const fileType = type === 'docx' ? 'docx' : 'xlsx'; 853 const htmlType = type === 'docx' ? 'html' : 'htmlExcel'; 854 855 const response = await fetch(baseUrl + `/widget/${htmlType}/${categoryId}/${templateId}.html`); 856 let previewHTML = await response.text(); 857 this.currentTemplateHtml = previewHTML; // Store current template HTML 858 859 // Wrap placeholders with curly braces into span tags with red bg color 860 previewHTML = previewHTML.replace(/\{([^}]+)\}/g, '<span style="background-color: #e2ebef;">{$1}</span>'); 861 862 previewContainer.innerHTML = previewHTML; 863 previewContainer.className = `document-template-view ${type === 'xlsx' ? 'is-spreadsheet' : ''}`; 864 865 // Scroll to top when new preview is loaded 866 previewContainer.scrollTop = 0; 867 } 868 869 highlightSelection(type, element) { 870 const container = this.shadowRoot.getElementById(type); 871 if (!container || !element || !element.isConnected) { 872 return; 873 } 874
875 Array.from(container.children).forEach(child => child.classList.remove('selected')); 876 element.classList.add('selected'); 877 878 if(type === 'templates') { 879 const template = this.mergedTemplates.find(t => t.id === element.dataset.id && t.type === element.dataset.type && t.categoryId === element.dataset.categoryId); 880 if (!template) { 881 return; 882 } 883 884 const downloadUrl = baseUrl + `/templates/${template.type === 'docx' ? 'docx' : 'xlsx'}/${template.categoryId}/${template.id}.${template.type}`; 885 886 // Store current template details 887 this.currentTemplateId = template.id; 888 this.currentTemplateType = template.type; 889 this.currentTemplateName = template.name; 890 this.currentTemplateDownloadUrl = downloadUrl; 891 892 // Trigger a callback if defined 893 if (typeof this.onTemplateSelect === 'function') { 894 895 this.onTemplateSelect({ detail: { template, html: this.currentTemplateHtml, downloadUrl } }); 896 } 897 } 898 } 899} 900 901customElements.define('template-gallery-widget', TemplateGalleryWidget);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.