PageSourceSearch

https://documentero.com/widget/script.js?06102026

js documentero.com collected 2026-09-28 09:12:29 UTC 37,286 bytes, 901 lines download raw bytes

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.