1import {VirtualizerController} from '@tanstack/lit-virtual'; 2import {html, LitElement} from 'lit-element'; 3import {repeat} from 'lit/directives/repeat.js'; 4import {when} from 'lit/directives/when.js'; 5import {createRef, ref} from 'lit/directives/ref.js'; 6import {unsafeHTML} from 'lit/directives/unsafe-html.js'; 7 8window.process = { 9 env: { 10 NODE_ENV: 'production', 11 }, 12}; 13 14/** 15 * @typedef {{ 16 * identifier: string, 17 * preview: string 18 * }} Icon 19 */ 20 21/** 22 * @typedef {{ 23 * name: string, 24 * identifier: string, 25 * iconCount: number, 26 * previews: Icon[], 27 * meta: { 28 * version: string, 29 * license: { 30 * title: string, 31 * } 32 * } 33 * }} IconSet 34 */ 35 36/** 37 * @typedef {{ 38 * link: string, 39 * previews: string[], 40 * set: IconSet 41 * }} IconSetRow 42 */ 43 44customElements.define( 45 'c3icons-set-list', 46 /** 47 * @property {{sets: IconSetRow[], selectedIconCountBySet: Record<string, number>}} data 48 */ 49 class extends LitElement { 50 static properties = { 51 data: { 52 type: Object, 53 }, 54 }; 55 56 container = createRef(); 57 searchInput = createRef(); 58 /** 59 * @type {IconSetRow[]} 60 */ 61 filteredSets = []; 62 /** 63 * @type {number|null} 64 */ 65 resizeTimeout = null; 66 67 connectedCallback() { 68 super.connectedCallback(); 69 this.filteredSets = this.data.sets; 70 if (Array.isArray(this.data.selectedIconCountBySet)) { 71 this.data.selectedIconCountBySet = {}; 72 } 73 this.#attachVirtualListController(); 74 window.addEventListener('resize', this.#onResize); 75 } 76 77 disconnectedCallback() { 78 super.disconnectedCallback(); 79 window.removeEventListener('resize', this.#onResize); 80 } 81 82 createRenderRoot() { 83 return this; 84 } 85 86 render() { 87 const virtualizer = this.virtualizerController.getVirtualizer(); 88 const virtualRows = virtualizer.getVirtualItems(); 89 return html` 90 <div class="input-group mb-4"> 91 <span class="input-group-text"> 92 <typo3-backend-icon identifier="actions-search" size="small"></typo3-backend-icon> 93 </span> 94 <input 95 type="search" 96 class="form-control" 97 ${ref(this.searchInput)} 98 @input="${this.#updateSearch}" 99 placeholder="${TYPO3.lang['content.search.placeholder']}" 100 /> 101 </div> 102 <div 103 class="card-container virtual-container" 104 ${ref(this.container)} 105 style="height: ${virtualizer.getTotalSize()}px;" 106 > 107 ${repeat( 108 virtualRows, 109 virtualRow => virtualRow.key, 110 virtualRow => { 111 const row = this.filteredSets[virtualRow.index]; 112 const set = row.set; 113 return html` 114 <a 115 href="${row.link}" 116 class="card card-size-fixed-small virtual-item" 117 style=" 118 left: ${virtualRow.lane * 300 + virtualRow.lane * 16}px; 119 width: 300px; 120 height: ${virtualRow.size}px; 121 transform: translateY(${virtualRow.start}px) 122 " 123 > 124 <div class="card-header"> 125 <div class="card-header-body"> 126 <h2 class="card-title">${set.name}</h2>
127 <span class="card-subtitle">${set.identifier}</span> 128 </div> 129 </div> 130 <div class="card-body"> 131 <ul class="list-unstyled"> 132 ${when( 133 this.data.selectedIconCountBySet[set.identifier], 134 () => html` 135 <li> 136 <b 137 >${this.data.selectedIconCountBySet[set.identifier]} 138 ${TYPO3.lang['content.iconsSelected']}</b 139 > 140 </li> 141 `, 142 )} 143 <li>${set.iconCount} Icons</li> 144 ${when( 145 set.meta.version, 146 () => html` 147 <li>${TYPO3.lang['content.version']}: ${set.meta.version}</li> 148 `, 149 )} 150 ${when( 151 set.meta.license, 152 () => html` 153 <li>${TYPO3.lang['content.license']}: ${set.meta.license.title}</li> 154 `, 155 )} 156 </ul> 157 <div class="row row-cols-6 gx-2"> 158 ${repeat( 159 set.previews, 160 preview => html` 161 <div class="col"> 162 <div class="p-1 bg-white rounded-3"> 163 <div class="ratio ratio-1x1"> 164 <img 165 src="${preview.preview}" 166 alt="${preview.identifier}" 167 loading="lazy" 168 /> 169 </div> 170 </div> 171 </div> 172 `, 173 )} 174 </div> 175 </div> 176 </a> 177 `; 178 }, 179 )} 180 </div> 181 `; 182 } 183 184 #onResize = () => { 185 clearTimeout(this.resizeTimeout); 186 this.resizeTimeout = setTimeout(() => { 187 this.#attachVirtualListController(); 188 }, 250); 189 }; 190 191 #updateSearch() { 192 this.filteredSets = this.data.sets.filter(item => 193 item.set.name.toLowerCase().includes(this.searchInput.value?.value.toLowerCase() ?? ''), 194 ); 195 this.#attachVirtualListController(); 196 } 197 198 #attachVirtualListController() { 199 const rect = this.getBoundingClientRect(); 200 const lanes = Math.floor(rect.width / 316); 201 202 this.virtualizerController?.cleanup(); 203 this.virtualizerController = new VirtualizerController(this, { 204 getScrollElement: () => document.querySelector('.module-body'), 205 count: this.filteredSets.length, 206 estimateSize: () => 200, 207 overscan: 5, 208 lanes, 209 gap: 16, 210 }); 211 212 this.requestUpdate(); 213 } 214 }, 215); 216 217customElements.define( 218 'c3icons-icon-list', 219 /** 220 * 221 * @property {{icons: Icon[], selectedIcons: string[]}} data 222 */ 223 class extends LitElement { 224 static properties = { 225 data: { 226 type: Object, 227 }, 228 }; 229 230 container = createRef(); 231 searchInput = createRef(); 232 /** 233 * @type {Icon[]} 234 */ 235 filteredIcons = []; 236 /** 237 * @type {number|null} 238 */ 239 resizeTimeout = null; 240 /** 241 * @type {Set<string>} 242 */ 243 selectedIcons; 244 /** 245 * @type {HTMLInputElement} 246 */ 247 selectionInput; 248 249 connectedCallback() { 250 super.connectedCallback(); 251 this.selectionInput = document.querySelector('#c3icons-icon-list-selected'); 252 this.filteredIcons = this.data.icons; 253 this.selectedIcons = new Set(this.data.selectedIcons); 254 this.#attachVirtualListController(); 255 window.addEventListener('resize', this.#onResize); 256 } 257 258 disconnectedCallback() { 259 super.disconnectedCallback(); 260 window.removeEventListener('resize', this.#onResize); 261 } 262 263 createRenderRoot() { 264 return this; 265 } 266 267 render() { 268 const virtualizer = this.virtualizerController.getVirtualizer(); 269 const virtualRows = virtualizer.getVirtualItems(); 270 return html` 271 <h1 class="mb-4">${this.data.set.name}</h1> 272 <div class="input-group mb-4">
273 <span class="input-group-text"> 274 <typo3-backend-icon identifier="actions-search" size="small"></typo3-backend-icon> 275 </span> 276 <input 277 type="search" 278 class="form-control" 279 ${ref(this.searchInput)} 280 @input="${this.#updateSearch}" 281 placeholder="${TYPO3.lang['content.search.placeholder']}" 282 /> 283 </div> 284 <div class="mb-4"> 285 <button class="btn btn-default" @click="${this.#onUseAll}" type="button"> 286 ${TYPO3.lang['content.iconSet.toggleAll']} 287 </button> 288 </div> 289 <div ${ref(this.container)} class="virtual-container" style="height: ${virtualizer.getTotalSize()}px;"> 290 ${repeat( 291 virtualRows, 292 virtualRow => virtualRow.key, 293 virtualRow => { 294 const icon = this.filteredIcons[virtualRow.index]; 295 return html` 296 <div 297 class="col virtual-item" 298 style=" 299 left: ${virtualRow.lane * 66 + virtualRow.lane * 8}px; 300 width: 66px; 301 height: ${virtualRow.size}px; 302 transform: translateY(${virtualRow.start}px) 303 " 304 > 305 <label class="c3icons-icon mb-0" title="${icon.identifier}"> 306 <img 307 src="${icon.preview}" 308 class="rounded" 309 loading="lazy" 310 alt="${icon.identifier}" 311 width="32" 312 height="32" 313 /> 314 <input 315 type="checkbox" 316 class="form-check-input mt-1 me-1 position-absolute top-0 end-0 shadow-none" 317 ?checked="${this.selectedIcons.has(icon.identifier)}" 318 @change="${this.#toggleIcon}" 319 value="${icon.identifier}" 320 /> 321 </label> 322 </div> 323 `; 324 }, 325 )} 326 </div> 327 `; 328 } 329 330 #onUseAll() { 331 if (this.selectedIcons.size) { 332 this.selectedIcons.clear(); 333 } else { 334 for (const icon of this.data.icons) { 335 this.selectedIcons.add(icon.identifier); 336 } 337 } 338 this.#updateSelection(); 339 this.requestUpdate(); 340 } 341 342 /** 343 * @param {Event} event 344 */ 345 #toggleIcon(event) { 346 /** @type {HTMLInputElement} */ 347 const target = event.target; 348 349 if (this.selectedIcons.has(target.value)) { 350 this.selectedIcons.delete(target.value); 351 } else { 352 this.selectedIcons.add(target.value); 353 } 354 this.#updateSelection(); 355 } 356 357 #updateSelection() { 358 this.selectionInput.value = [...this.selectedIcons].join(','); 359 } 360 361 #onResize = () => { 362 clearTimeout(this.resizeTimeout); 363 this.resizeTimeout = setTimeout(() => { 364 this.#attachVirtualListController(); 365 }, 250); 366 }; 367 368 #updateSearch() { 369 this.filteredIcons = this.data.icons.filter(icon => 370 icon.identifier.toLowerCase().includes(this.searchInput.value?.value.toLowerCase() ?? ''), 371 ); 372 this.#attachVirtualListController(); 373 } 374 375 #attachVirtualListController() { 376 const rect = this.getBoundingClientRect(); 377 const lanes = Math.floor(rect.width / 74); 378 379 this.virtualizerController?.cleanup(); 380 this.virtualizerController = new VirtualizerController(this, { 381 getScrollElement: () => document.querySelector('.module-body'), 382 count: this.filteredIcons.length, 383 estimateSize: () => 66, 384 overscan: lanes, 385 lanes, 386 gap: 8, 387 }); 388 389 this.requestUpdate(); 390 } 391 }, 392);
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.