1class UTColorPicker { 2 constructor() { 3 // Data from PHP 4 this.colorPickerData = window.colorPickerData; 5 6 // Colors 7 this.colorSystem = null; 8 this.colors = []; 9 this.selectedColor = null; 10 11 // Fetch options 12 this.search = ""; 13 this.pagination = { 14 currentPage: 1, 15 nextPage: 2, 16 previousPage: 0, 17 colorsPerPage: 90, 18 totalColors: 0, 19 totalPages: 0, 20 }; 21 22 // Initialize elements as null 23 this.elements = { 24 dialog: null, 25 colorGrid: null, 26 pagination: null, 27 searchInput: null, 28 selectedColor: null, 29 }; 30 31 // Events 32 this.handleEvents(); 33 } 34 35 setColorSystem = (colorSystem) => { 36 this.colorSystem = colorSystem; 37 this.fetchColorSystems(); 38 }; 39 40 // Add method to initialize elements 41 initializeElements = () => { 42 this.elements = { 43 dialog: document.getElementById("utcp-dialog"), 44 colorGrid: document.getElementById("utcp-color-grid"), 45 pagination: document.getElementById("utcp-color-pagination"), 46 searchInput: document.getElementById("utcp-color-search-input"), 47 selectedColor: document.getElementById("utcp-selected-color"), 48 }; 49 }; 50 51 handleEvents = () => { 52 // Use event delegation instead of multiple listeners 53 document.addEventListener("click", this.handleGlobalClick); 54 document.addEventListener("change", this.handleGlobalChange); 55 document.addEventListener("keypress", this.handleSearchKeypress); 56 }; 57 58 handleGlobalClick = (event) => { 59 const target = event.target; 60 61 // Use object literal for cleaner event routing 62 const clickHandlers = { 63 "#utcp-close-dialog-btn": () => this.elements.dialog.close(), 64 "#utcp-open-dialog-btn": () => this.handleOpenDialog(target), 65 ".utcp-color-box": () => this.handleColorBoxClick(target), 66 ".utcp-pagination-btn": () => this.handlePaginationClick(target), 67 ".utcp-thumbnail": () => this.handleThumbnailClick(target), 68 "#utcp-color-search-btn": () => this.performSearch(), 69 "#utcp-submit-btn": () => this.elements.dialog.close(), 70 }; 71 72 for (const [selector, handler] of Object.entries(clickHandlers)) { 73 if (target.closest(selector)) { 74 handler(event); 75 break; 76 } 77 } 78 }; 79 80 handleGlobalChange = (event) => { 81 const target = event.target; 82 83 if (target.matches("#utcp-main-color-system-select")) { 84 this.handleColorSystemChange({ target, isMain: true }); 85 } else if (target.matches(".utcp-sub-color-system-select")) { 86 this.handleColorSystemChange({ target, isMain: false }); 87 } 88 }; 89 90 handleColorSystemChange = ({ target, isMain }) => { 91 const colorSystem = target.value; 92 93 if (isMain) { 94 document 95 .querySelectorAll(".utcp-sub-color-system-select") 96 .forEach((select) => { 97 const isMatching = select.dataset.subColorSystem === colorSystem; 98 select.classList.toggle("utcp-hidden", !isMatching); 99 this.colorSystem = isMatching ? select.value : colorSystem; 100 }); 101 } else { 102 this.colorSystem = colorSystem; 103 } 104 105 this.resetSearchAndPagination(); 106 this.fetchColorData(); 107 }; 108 109 handleColorBoxClick = (target) => { 110 const element = target.closest(".utcp-color-box"); 111 if (!element) return; 112 113 // Remove selected class from previously selected color 114 const previousSelected = document.querySelector( 115 ".utcp-color-box.utcp-selected" 116 ); 117 118 if (previousSelected) { 119 previousSelected.classList.remove("utcp-selected"); 120 } 121 122 // Add selected class to clicked color 123 element.classList.add("utcp-selected"); 124 125 // Get the color code 126 const colorCode = element.dataset.colorCode; 127 128 const headerSelection = document.getElementById( 129 "utcp-dialog-header-selection" 130 ); 131 132 if (headerSelection) { 133 headerSelection.textContent = `${colorCode}`; 134 } 135 136 // Update hidden input and color code display 137 const hex = element.dataset.hex; 138 const input = document.getElementById("utcp-selected-color"); 139 const colorCodeDisplay = document.querySelector(".utcp-color-code"); 140 const colorSwatch = document.querySelector(".utcp-color-swatch"); 141 142 input.value = colorCode; 143 if (colorCodeDisplay) { 144 colorCodeDisplay.textContent = colorCode; 145 colorCodeDisplay.classList.remove("utcp-color-code-placeholder"); 146 } 147 148 // Update color swatch 149 if (colorSwatch) { 150 colorSwatch.style.backgroundColor = hex; 151 colorSwatch.classList.add("utcp-color-swatch-has-color"); 152 } 153 154 // Update preview colors 155 const previewImgs = document.querySelectorAll(".utcp-preview-image");
156 const thumbnails = document.querySelectorAll(".utcp-thumbnail"); 157 158 for (const img of previewImgs) { 159 img.style.backgroundColor = hex; 160 } 161 162 for (const thumb of thumbnails) { 163 thumb.style.backgroundColor = hex; 164 } 165 }; 166 167 handlePaginationClick = async (target) => { 168 const element = target.closest(".utcp-pagination-btn"); 169 if (!element || !element.dataset.page) return; 170 171 this.pagination.currentPage = parseInt(element.dataset.page); 172 await this.fetchColorData(); 173 }; 174 175 handleThumbnailClick = (target) => { 176 const element = target.closest(".utcp-thumbnail"); 177 if (!element) return; 178 179 const container = element.closest("#utcp-dialog-content-right"); 180 const index = parseInt(element.dataset.index); 181 182 container 183 .querySelectorAll(".utcp-thumbnail") 184 .forEach((thumb) => (thumb.dataset.active = "false")); 185 element.dataset.active = "true"; 186 187 container 188 .querySelectorAll(".utcp-preview-image") 189 .forEach((img) => (img.dataset.active = "false")); 190 container.querySelectorAll(".utcp-preview-image")[index].dataset.active = 191 "true"; 192 }; 193 194 resetSearchAndPagination = () => { 195 this.search = ""; 196 this.pagination.currentPage = 1; 197 198 // Reset search input field value 199 const searchInput = document.getElementById("utcp-color-search-input"); 200 if (searchInput) { 201 searchInput.value = ""; 202 } 203 }; 204 205 handleOpenDialog = async (target) => { 206 const element = target.closest("#utcp-open-dialog-btn"); 207 if (!element) return; 208 209 // Don't open dialog if there's only one color (already auto-selected) 210 if (this.pagination.totalColors === 1) return; 211 212 // Initialize elements when dialog is opened 213 this.initializeElements(); 214 215 this.elements.dialog.showModal(); 216 await this.fetchColorData(); 217 218 // Restore selected color after colors are loaded 219 const selectedColorInput = document.getElementById("utcp-selected-color"); 220 if (selectedColorInput && selectedColorInput.value) { 221 const selectedColorBox = document.querySelector( 222 `.utcp-color-box[data-color-code="${selectedColorInput.value}"]` 223 ); 224 225 if (selectedColorBox) { 226 selectedColorBox.classList.add("utcp-selected"); 227 } 228 } 229 }; 230 231 fetchColorSystems = async () => { 232 try { 233 const formData = new FormData(); 234 formData.append("action", "get_color_systems"); 235 formData.append("color_system", this.colorSystem); 236 formData.append("nonce", this.colorPickerData.nonce); 237 238 const response = await fetch(this.colorPickerData.ajaxUrl, { 239 method: "POST", 240 body: formData, 241 }); 242 243 if (!response.ok) { 244 throw new Error(`HTTP error! status: ${response.status}`); 245 } 246 247 const { data, success } = await response.json(); 248 249 if (!success) { 250 throw new Error("API returned unsuccessful response"); 251 } 252 253 const container = document.getElementById("utcp-color-systems-container"); 254 container.innerHTML = data.html; 255 256 this.colorSystem = data.initial_color_system; 257 258 // Fetch colors early to enable auto-selection before dialog opens 259 await this.prefetchColors(); 260 } catch (error) { 261 console.error("Error fetching color data:", error); 262 // Optionally show user-friendly error message 263 } 264 }; 265 266 prefetchColors = async () => { 267 try { 268 const formData = new FormData(); 269 formData.append("action", "get_colors"); 270 formData.append("color_system", this.colorSystem); 271 formData.append("nonce", this.colorPickerData.nonce); 272 formData.append("color_page", 1); 273 formData.append("colors_per_page", this.pagination.colorsPerPage); 274 formData.append("color_search", ""); 275 276 const response = await fetch(this.colorPickerData.ajaxUrl, { 277 method: "POST", 278 body: formData, 279 }); 280 281 if (!response.ok) { 282 throw new Error(`HTTP error! status: ${response.status}`); 283 } 284 285 const { data, success } = await response.json(); 286 if (!success) { 287 throw new Error("API returned unsuccessful response"); 288 } 289 290 this.colors = data.colors; 291 this.pagination = data.pagination; 292 293 // Auto-select if only one color in the system (before dialog opens) 294 if (this.colors.length === 1 && this.pagination.totalColors === 1) { 295 this.autoSelectColor(this.colors[0]); 296 } 297 } catch (error) { 298 console.error("Error prefetching colors:", error); 299 } 300 }; 301 302 autoSelectColor = ({ color, hex }) => { 303 // Update hidden input 304 const input = document.getElementById("utcp-selected-color"); 305 if (input) { 306 input.value = color; 307 } 308 309 // Update color code display 310 const colorCodeDisplay = document.querySelector(".utcp-color-code"); 311 if (colorCodeDisplay) { 312 colorCodeDisplay.textContent = color; 313 colorCodeDisplay.classList.remove("utcp-color-code-placeholder"); 314 } 315 316 // Update color swatch 317 const colorSwatch = document.querySelector(".utcp-color-swatch"); 318 if (colorSwatch) { 319 colorSwatch.style.backgroundColor = hex; 320 colorSwatch.classList.add("utcp-color-swatch-has-color"); 321 } 322 323 // Hide helper text since there's only one color 324 const helperText = document.querySelector(".utcp-color-code-helper"); 325 if (helperText) { 326 helperText.style.display = "none"; 327 } 328 329 // Store selection for when dialog opens 330 this.selectedColor = { color, hex }; 331 }; 332 333 fetchColorData = async () => { 334 try { 335 const formData = new FormData(); 336 formData.append("action", "get_colors"); 337 formData.append("color_system", this.colorSystem); 338 formData.append("nonce", this.colorPickerData.nonce); 339 formData.append("color_page", this.pagination.currentPage); 340 formData.append("colors_per_page", this.pagination.colorsPerPage); 341 formData.append("color_search", this.search); 342 343 const response = await fetch(this.colorPickerData.ajaxUrl, { 344 method: "POST", 345 body: formData, 346 }); 347 348 if (!response.ok) { 349 throw new Error(`HTTP error! status: ${response.status}`); 350 } 351 352 const { data, success } = await response.json(); 353 if (!success) { 354 throw new Error("API returned unsuccessful response"); 355 } 356 357 this.colors = data.colors; 358 this.pagination = data.pagination; 359 360 this.renderColorGrid(); 361 this.renderPagination(); 362 363 // Auto-select if only one color in the system 364 if (this.colors.length === 1 && this.pagination.totalColors === 1) { 365 const singleColorBox = document.querySelector(".utcp-color-box"); 366 if (singleColorBox) { 367 this.handleColorBoxClick(singleColorBox); 368 } 369 } 370 } catch (error) { 371 console.error("Error fetching color data:", error); 372 // Optionally show user-friendly error message 373 } 374 }; 375 376 renderColorGrid = () => { 377 const container = document.getElementById("utcp-color-grid"); 378 container.innerHTML = ""; 379 container.scrollTop = 0; 380 381 if (this.colors.length === 0) { 382 const noResults = document.createElement("div"); 383 noResults.classList.add("utcp-color-grid-msg"); 384 noResults.textContent = 385 "Inga kulörkoder matchade din sökning.\nFörsök igen!"; 386 container.appendChild(noResults); 387 return; 388 } 389 390 for (const { color, hex, text } of this.colors) { 391 const colorBox = document.createElement("div"); 392 393 // Style 394 colorBox.classList.add("utcp-color-box"); 395 colorBox.style.backgroundColor = hex; 396 397 // Data 398 colorBox.dataset.colorCode = color; 399 colorBox.dataset.hex = hex; 400 colorBox.dataset.text = text; 401 402 // Content 403 const span = document.createElement("span"); 404 span.style.color = text; 405 span.textContent = color; 406 colorBox.appendChild(span); 407 408 // Append 409 container.appendChild(colorBox); 410 } 411 }; 412 413 renderPagination = () => { 414 const container = document.getElementById("utcp-color-pagination"); 415 container.innerHTML = ""; 416 417 if (this.pagination.totalPages <= 1) { 418 container.classList.add("utcp-hidden"); 419 return; 420 } else { 421 container.classList.remove("utcp-hidden"); 422 } 423 424 // Create previous button 425 const prevButton = document.createElement("button"); 426 prevButton.type = "button"; 427 prevButton.classList.add("utcp-pagination-btn", "utcp-btn"); 428 prevButton.innerHTML = "â"; 429 prevButton.dataset.page = this.pagination.previousPage || ""; 430 prevButton.disabled = !this.pagination.previousPage; 431 container.appendChild(prevButton); 432
433 // Calculate which page numbers to show 434 const currentPage = this.pagination.currentPage; 435 const totalPages = this.pagination.totalPages; 436 const range = []; 437 const rangeWithDots = []; 438 let l; 439 440 // Show all pages if total pages is 7 or less 441 if (totalPages <= 7) { 442 for (let i = 1; i <= totalPages; i++) { 443 range.push(i); 444 } 445 } else { 446 // Always add page 1 447 range.push(1); 448 449 // Calculate the range around current page 450 if (currentPage <= 4) { 451 // Near the start 452 for (let i = 2; i <= 5; i++) { 453 range.push(i); 454 } 455 } else if (currentPage >= totalPages - 3) { 456 // Near the end 457 for (let i = totalPages - 4; i < totalPages; i++) { 458 range.push(i); 459 } 460 } else { 461 // In the middle 462 for (let i = currentPage - 1; i <= currentPage + 1; i++) { 463 range.push(i); 464 } 465 } 466 467 // Always add last page 468 if (!range.includes(totalPages)) { 469 range.push(totalPages); 470 } 471 } 472 473 // Add dots where needed 474 for (let i of range) { 475 if (l) { 476 if (i - l === 2) { 477 rangeWithDots.push(l + 1); 478 } else if (i - l !== 1) { 479 rangeWithDots.push("..."); 480 } 481 } 482 rangeWithDots.push(i); 483 l = i; 484 } 485 486 // Create page number buttons 487 for (const i of rangeWithDots) { 488 const button = document.createElement("button"); 489 button.type = "button"; 490 button.classList.add("utcp-pagination-btn", "utcp-btn"); 491 492 if (i === "...") { 493 button.textContent = i; 494 button.disabled = true; 495 button.classList.add("utcp-pagination-dots"); 496 } else { 497 button.textContent = i; 498 button.dataset.page = i; 499 if (i === currentPage) { 500 button.classList.add("utcp-pagination-current"); 501 } 502 } 503 504 container.appendChild(button); 505 } 506 507 // Create next button 508 const nextButton = document.createElement("button"); 509 nextButton.type = "button"; 510 nextButton.classList.add("utcp-pagination-btn", "utcp-btn"); 511 nextButton.innerHTML = "â"; 512 nextButton.dataset.page = this.pagination.nextPage || ""; 513 nextButton.disabled = !this.pagination.nextPage; 514 container.appendChild(nextButton); 515 }; 516 517 handleSearchKeypress = async (event) => { 518 const element = event.target.closest("#utcp-color-search-input"); 519 if (!element) return; 520 521 if (event.key === "Enter") { 522 event.preventDefault(); 523 await this.performSearch(); 524 } 525 }; 526 527 performSearch = async () => { 528 const searchInput = document.getElementById("utcp-color-search-input"); 529 this.search = searchInput.value.trim(); 530 this.pagination.currentPage = 1; // Reset to first page when searching 531 await this.fetchColorData(); 532 }; 533} 534 535window.UTColorPicker = UTColorPicker;
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.