1function observeValue(options) { 2 const { value_to_observe, interval = 500, timeout = 50000, callback, timeoutCallback } = options; 3 let elapsedTime = 0; 4 5 const intervalId = setInterval(() => { 6 elapsedTime += interval; 7 if (typeof window[value_to_observe] !== 'undefined') { 8 clearInterval(intervalId); 9 callback(window[value_to_observe]); 10 } else if (elapsedTime >= timeout) { 11 clearInterval(intervalId); 12 if (timeoutCallback) { 13 timeoutCallback(); 14 } 15 } 16 }, interval); 17} 18window.observeValue = observeValue; 19 20const debounce = (function () { 21 let timer = 0; 22 return function (callback, delay) { 23 clearTimeout(timer); 24 timer = setTimeout(callback, delay); 25 }; 26})(); 27window.debounce = debounce; 28 29function toggleSlide(options) { 30 const { container, closeContainer = false, onContainerOpen } = options; 31 if (container) { 32 if (!container.classList.contains('active') && !closeContainer) { 33 container.classList.add('active'); 34 container.style.height = 'auto'; 35 36 let height = container.clientHeight + 'px'; 37 container.style.height = '0px'; 38 setTimeout(function () { 39 container.style.height = height; 40 if (onContainerOpen) { 41 onContainerOpen(); 42 } 43 }, 0); 44 setTimeout(function () { 45 container.style.overflow = 'visible'; 46 }, 500); 47 } else { 48 container.style.overflow = 'hidden'; 49 if (closeContainer) { 50 container.classList.remove('active'); 51 return; 52 } 53 container.style.height = '0px'; 54 container.addEventListener( 55 'transitionend', 56 function () { 57 container.classList.remove('active'); 58 }, 59 { 60 once: true, 61 } 62 ); 63 } 64 } 65} 66window.toggleSlide = toggleSlide; 67 68function toggleFormDropdown(event, elementId) { 69 if (event.type === 'click' || (event.type === 'keydown' && (event.key === 'Enter' || event.key === ' '))) { 70 const dropdown = document.getElementById(elementId); 71 const options = { 72 container: dropdown, 73 }; 74 toggleSlide(options); 75 } 76} 77window.toggleFormDropdown = toggleFormDropdown; 78 79function selectOption(option, dropdownId = 'selectedOption') { 80 const selectedOption = document.getElementById(dropdownId); 81 selectedOption.textContent = option; 82 const dropdown = document.getElementById('form-dropdown'); 83 const options = { 84 container: dropdown, 85 }; 86 toggleSlide(options); 87} 88window.selectOption = selectOption; 89 90function formatDate(date, format = 'MM/DD/YYYY') { 91 if (date === null || date === '' || date === undefined) return ''; 92 const selectedDate = new Date(date);
93 const month = (selectedDate.getMonth() + 1).toString().padStart(2, '0'); 94 const day = selectedDate.getDate().toString().padStart(2, '0'); 95 const year = selectedDate.getFullYear(); 96 97 if (format === 'MM-DD-YYYY') { 98 return `${month}-${day}-${year}`; 99 } 100 else if (format === 'YYYY-MM-DD') { 101 return `${year}-${month}-${day}`; 102 } 103 else if (format === 'MM/DD/YYYY') { 104 return `${month}/${day}/${year}`; 105 } 106 else if (format === 'MM/DD/YY') { 107 const yearShort = selectedDate.getFullYear().toString().slice(-2); // Last 2 digits 108 return `${month}/${day}/${yearShort}`; 109 } 110 else if (format === 'MM/DD/YY h:mmAM/PM') { 111 const monthCompact = (selectedDate.getMonth() + 1).toString(); // No leading zero 112 const dayCompact = selectedDate.getDate().toString(); // No leading zero 113 const yearShort = selectedDate.getFullYear().toString().slice(-2); // Last 2 digits 114 let hours = selectedDate.getHours(); 115 const minutes = selectedDate.getMinutes().toString().padStart(2, '0'); 116 const ampm = hours >= 12 ? 'PM' : 'AM'; 117 hours = hours % 12 || 12; // Convert 0 (midnight) to 12 118 return `${monthCompact}/${dayCompact}/${yearShort} ${hours}:${minutes}${ampm}`; 119 } 120 else { 121 console.error(`Unsupported format: ${format}`); 122 return date; 123 } 124} 125window.formatDate = formatDate; 126export { formatDate }; 127 128window.showCustomDialog = function showCustomDialog(id) { 129 const dialog = document.getElementById(id); 130 if (dialog) { 131 dialog.setAttribute('show-dialog', 'true'); 132 } 133}; 134window.hideCustomDialog = function hideCustomDialog(id) { 135 const dialog = document.getElementById(id); 136 if (dialog) { 137 dialog.setAttribute('show-dialog', 'false'); 138 } 139}; 140 141// it will fix AG Grid header, when its reaches top of the page 142window.fixAgGridHeader = function fixAgGridHeader() { 143 if (Boolean(window.scrollHeader) == true || Boolean(window.fixedAgGridHeader) == true) { 144 const pageHeader = document.getElementById('main-header-v2'); 145 if (pageHeader.classList.contains('fixed')) { 146 pageHeader.classList.remove('fixed'); 147 pageHeader.classList.add('relative'); 148 } 149 const agHeader = document.querySelector('.ag-header'); 150 if (window.scrollY >= 319) { 151 agHeader.style.position = 'fixed'; 152 agHeader.style.top = '0'; 153 agHeader.style.zIndex = '99'; 154 agHeader.style.width = 'calc(100% - 84px)'; 155 } else { 156 agHeader.style.position = 'static'; 157 agHeader.style.zIndex = '0'; 158 agHeader.style.width = '100%'; 159 } 160 } 161}; 162 163// CUSTOM THEME FOR HL 164function primeVueConfiguration(Theme, definePreset) { 165 function setPreset() { 166 return definePreset(Theme, { 167 components: { 168 chip: { 169 colorScheme: { 170 light: { 171 remove: { 172 icon: { 173 // color: '#000', 174 // background: '#ffff', 175 }, 176 }, 177 background: '#000', 178 color: '#ffff', 179 }, 180 }, 181 }, 182 }, 183 semantic: { 184 primary: { 185 50: '{zinc.50}', 186 100: '{zinc.100}', 187 200: '{zinc.200}', 188 300: '{zinc.300}', 189 400: '{zinc.400}', 190 500: '{zinc.500}', 191 600: '{zinc.600}', 192 700: '{zinc.700}', 193 800: '{zinc.800}', 194 900: '{zinc.900}', 195 950: '{zinc.950}', 196 }, 197 colorScheme: { 198 light: { 199 primary: { 200 color: '{zinc.950}', 201 inverseColor: '#ffffff', 202 hoverColor: '{zinc.900}', 203 activeColor: '{zinc.800}', 204 }, 205 highlight: { 206 background: '{zinc.950}',
207 focusBackground: '{zinc.700}', 208 color: '#ffffff', 209 focusColor: '#ffffff', 210 }, 211 }, 212 }, 213 }, 214 }); 215 } 216 return { 217 theme: { 218 preset: setPreset(), 219 options: { 220 darkModeSelector: '.my-app-dark', 221 }, 222 }, 223 zIndex: { 224 menu: 1030, 225 overlay: 1040, 226 modal: 1050, 227 //toast: 1060 228 tooltip: 1070, 229 }, 230 }; 231} 232window.primeVueConfiguration = primeVueConfiguration; 233export { primeVueConfiguration }; 234 235function createCancelableRequest() { 236 let abortController = null; 237 238 return { 239 getSignal() { 240 // Cancel the previous request if it exists 241 if (abortController) { 242 abortController.abort(); 243 } 244 abortController = new AbortController(); 245 return abortController.signal; 246 }, 247 //if you want to manually cancel the API request then invoke this method 248 cancelRequest() { 249 if (abortController) { 250 abortController.abort(); 251 } 252 }, 253 }; 254} 255export { createCancelableRequest }; 256 257// LAZY LOAD IMAGES THAT ADDED AS BACKGROUND-IMAGE 258window.addEventListener('load', function () { 259 const lazyBackgrounds = document.querySelectorAll('.lazy-bg'); 260 261 lazyBackgrounds.forEach((element) => { 262 const bgUrl = element.getAttribute('data-bg-url'); 263 element.style.backgroundImage = `url(${bgUrl})`; 264 }); 265}); 266 267// TO HIDE DROPDOWNS WHEN CLICK OUTSIDE 268function hideDropdowns(selectors) { 269 selectors.forEach((selector) => { 270 let idElement = document.getElementById(selector); 271 let classElements = document.getElementsByClassName(selector); 272 if (idElement) { 273 idElement.style.display = 'none'; 274 } else if (classElements && classElements.length) {
275 [...classElements].forEach((ele) => (ele.style.display = 'none')); 276 } 277 }); 278} 279export { hideDropdowns }; 280 281// TO DYNAMICALLY REPLACE HTML WITH PLACEHODER 282function replaceHTML(config) { 283 const { placeholderId, loaderId, content, callback } = config; 284 const placeholderElement = document.getElementById(placeholderId); 285 const loader = document.getElementById(loaderId); 286 if (typeof callback === 'function') callback(); 287 if (placeholderElement) { 288 if (loader) loader.style.display = 'none'; 289 placeholderElement.innerHTML = content; 290 // Extract and execute script tags 291 const scriptTags = placeholderElement.getElementsByTagName('script'); 292 for (let i = 0; i < scriptTags.length; i++) { 293 const scriptTag = document.createElement('script'); 294 if (scriptTags[i].src) { 295 // If script has a src attribute, re-insert it into the DOM to load it 296 scriptTag.src = scriptTags[i].src; 297 document.body.appendChild(scriptTag); 298 } else { 299 // For inline scripts, execute the script content 300 scriptTag.text = scriptTags[i].innerText; 301 document.body.appendChild(scriptTag); 302 } 303 } 304 } 305} 306export { replaceHTML }; 307 308function isMobile() { 309 return window.matchMedia('(max-width: 768px)').matches; 310} 311window.isMobile = isMobile; 312export { isMobile }; 313 314function getAgGridRangeSelection(gridApi) { 315 const ranges = gridApi.getCellRanges(); 316 if (!ranges || ranges.length === 0) { 317 console.log('No ranges selected'); 318 return []; 319 } 320 const selectedData = []; 321 ranges.forEach((range) => { 322 const startRow = range.startRow.rowIndex; 323 const endRow = range.endRow.rowIndex; 324 325 for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) { 326 const rowNode = gridApi.getDisplayedRowAtIndex(rowIndex); 327 'data' in rowNode && selectedData.push(rowNode.data); 328 } 329 }); 330 return selectedData; 331} 332export { getAgGridRangeSelection }; 333 334// THIS METHOD SET DJANGO TEMPLATE DATA FOR JAVASCRIPT FILES 335function setDjangoData(data = {}) { 336 if (window.djangoData === undefined) { 337 window.djangoData = {}; 338 } 339 window.djangoData = { ...window.djangoData, ...data } 340} 341window.setDjangoData = setDjangoData; 342export { setDjangoData }; 343 344function createDOMElement(config) { 345 const { 346 type = 'div', 347 attributes = [], 348 innerHTML = '', 349 eventListeners = [], 350 styles = {} 351 } = config; 352 if (!type) { 353 throw new Error('The "type" property is required to create a DOM element.'); 354 } 355 const element = document.createElement(type); 356 // Set attributes 357 if (Array.isArray(attributes)) { 358 attributes.forEach(({ name, value }) => { 359 element.setAttribute(name, value); 360 }); 361 } 362 if (innerHTML) { 363 element.innerHTML = innerHTML; 364 } 365 // Attach event listeners 366 if (Array.isArray(eventListeners)) { 367 eventListeners.forEach(({ event, handler }) => { 368 if (event && typeof handler === 'function') { 369 element.addEventListener(event, handler); 370 } 371 }); 372 } 373 // Apply styles 374 if (styles && typeof styles === 'object') { 375 Object.assign(element.style, styles); 376 } 377 return element; 378} 379 380window.createDOMElement = createDOMElement; 381export { createDOMElement }; 382// Add padding and highlight the element 383function highlightContainer({ timeout = 1000, element } = config) { 384 removeHighlight(element) 385 element.classList.add('highlighted-container', 'higlighted-element-BG', 'higlighted-element-transition'); 386 setTimeout(() => removeHighlight(element), timeout); 387} 388 389function removeHighlight(element) { 390 element.classList.remove('higlighted-element-transition'); 391} 392export { highlightContainer, removeHighlight } 393 394function currencyFormatter(value) { 395 if (value === null || value === '' || value === undefined) return ''; 396 return '$' + formatNumber(value); 397} 398export { currencyFormatter } 399 400// normalise figure with comma 401function formatNumber(number) { 402 return Math.floor(number) 403 .toString() 404 .replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); 405} 406export { formatNumber } 407 408function googleMap() { 409 // prettier-ignore 410 (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "
410importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) }) 411 ({ key: window.google_api_key, v: "weekly" }); 412} 413export { googleMap } 414 415function scrollToTop() { 416 window.scrollTo({ 417 top: 0, 418 behavior: 'smooth' 419 }); 420} 421export { scrollToTop } 422 423function handleImageError(event, defaultImage) { 424 if (defaultImage) { 425 event.target.src = defaultImage; 426 } else { 427 event.target.style.display = 'none'; 428 } 429} 430export { handleImageError } 431 432// Sticky AG Grid Horizontal Scrollbar 433// how to use: add row-data-updated event listener to the ag-grid and call this function after the row data is updated 434// window.setTimeout(() => { 435// this.stickyAgGridHorizontalScrollbar(); 436// }, 200); 437function stickyAgGridHorizontalScrollbar(config = {}) { 438 const { theme = 'ag-theme-alpine', horizontalScrollbarOffset = 0 } = config; 439 const scrollbar = document.querySelector('.ag-body-horizontal-scroll'); 440 const gridElement = document.querySelector(`.${theme}`); 441 if (!scrollbar || !gridElement) return; 442 443 const updateScrollbarPosition = () => { 444 const gridRect = gridElement.getBoundingClientRect(); 445 const windowHeight = window.innerHeight; 446 const gridBottom = gridRect.bottom; 447 // If the grid's bottom is above the viewport bottom, adjust the scrollbar position 448 if (gridBottom > windowHeight) { 449 const bottomOffset = gridBottom - windowHeight - horizontalScrollbarOffset; 450 scrollbar.style.bottom = `${bottomOffset}px`; 451 } else { 452 scrollbar.style.bottom = '0px'; 453 } 454 }; 455 // Update on scroll and resize 456 window.addEventListener('scroll', updateScrollbarPosition); 457 window.addEventListener('resize', updateScrollbarPosition); 458 // Initial update 459 updateScrollbarPosition(); 460} 461window.stickyAgGridHorizontalScrollbar = stickyAgGridHorizontalScrollbar; 462export { stickyAgGridHorizontalScrollbar } 463 464// SHOW ALL THOSE HORIZONTAL FILTERS WHICH CAN BE ACCOMODATE IN SINGLE ROW, HIDE ALL OTHER FILTERS 465function adjustInputVisibility() { 466 const container = document.getElementById('horizontal-filters'); 467 const inputs = container.querySelectorAll('.horizontal-filter-field'); 468 if (window.innerWidth <= 768) { 469 inputs.forEach((input) => { 470 input.style.visibility = 'visible'; 471 }); 472 return; 473 } 474 const containerRect = container.getBoundingClientRect(); 475 inputs.forEach((input) => { 476 const inputRect = input.getBoundingClientRect(); 477 // console.log('inputRect', inputRect.right, 'containerRect', containerRect.right, 'inputRect.left', inputRect.left, 'containerRect.left', containerRect.left) 478 // Check if the input is fully visible in the container 479 if (inputRect.right > containerRect.right || inputRect.left < containerRect.left) { 480 input.style.visibility = 'hidden'; 481 } else { 482 input.style.visibility = 'visible'; 483 } 484 }); 485} 486export { adjustInputVisibility } 487 488// add extra space to target element when cards container has extra space during wrap 489// container is the element that has total width 490// card has some width 360px + 20px gap 491// target is the element that will be resized to fit the available width 492function createResponsiveLayout(config = {}) { 493 const { containerSelector, cardSelector, targetSelector, cardDefaultWidth = 360, cardGap = 20 } = config; 494 // Get container width 495 const containerWidth = document.querySelector(containerSelector)?.clientWidth || 0; 496 497 // Get item width 498 const cardEl = document.querySelector(cardSelector); 499 const cardWidth = (cardEl?.clientWidth || cardDefaultWidth) + cardGap; 500 501 // Get target element
502 const targetEl = document.querySelector(targetSelector); 503 const targetWidth = targetEl?.clientWidth || 0; 504 505 // Calculate available width and items per row 506 const widthAvailable = containerWidth - targetWidth; 507 const cardsPerRow = Math.floor(widthAvailable / cardWidth); 508 509 // Calculate width available for target element 510 const widthAvailableForTarget = containerWidth - (cardsPerRow * cardWidth); 511 512 // Apply width to target element 513 if (targetEl) { 514 targetEl.style.width = `${widthAvailableForTarget}px`; 515 } 516 517 return { 518 containerWidth, 519 cardWidth, 520 cardsPerRow, 521 targetWidth: widthAvailableForTarget 522 }; 523} 524export { createResponsiveLayout } 525 526// LOAD CSS AND JS ASSETS IF NOT ALREADY LOADED 527function loadAssets({ css, js, callback }) { 528 // Load CSS if provided and not already loaded 529 if (css && !document.querySelector(`link[href="${css}"]`)) { 530 const cssLink = document.createElement('link'); 531 cssLink.href = css; 532 cssLink.rel = 'stylesheet'; 533 document.head.appendChild(cssLink); 534 } 535 536 // Load JS if provided and not already loaded 537 if (js) { 538 const jsIdentifier = js.split('/').slice(-1)[0]; // last part of URL to check uniqueness 539 if (!document.querySelector(`script[src*="${jsIdentifier}"]`)) { 540 const script = document.createElement('script'); 541 script.src = js; 542 script.onload = () => { 543 if (typeof callback === 'function') callback(); 544 }; 545 document.head.appendChild(script); 546 } else if (typeof callback === 'function') { 547 callback(); // Already loaded, just call callback 548 } 549 } else if (typeof callback === 'function') { 550 callback(); // No JS to load, still run callback 551 } 552} 553window.loadAssets = loadAssets; 554export { loadAssets } 555 556// COOKIE MANAGEMENT UTILITIES FOR LEAD SELECTION 557// Based on vanilla JS functionality from lead_list.html 558 559/** 560 * Core function that manages the lead_list cookie for bulk operations 561 * @param {string|number} leadId - The ID of the lead to add/remove 562 * @param {boolean} isSelected - Whether the lead is being selected or deselected 563 * @param {Object} options - Configuration options 564 * @param {string} options.cookieName - Name of the cookie (default: 'lead_list') 565 * @param {number} options.maxItems - Maximum number of items allowed (default: 100) 566 * @param {Function} options.onError - Error callback function 567 * @returns {boolean} - Whether the operation was successful 568 */ 569function compute_cookies(leadId, isSelected, options = {}) { 570 const { 571 cookieName = 'lead_list', 572 maxItems = 100, 573 onError = null 574 } = options; 575 576 let cookie_val_leads = getCookieItems(cookieName); 577 578 if (isSelected) { 579 // Lead is selected 580 if (cookie_val_leads.indexOf(leadId.toString()) === -1) { 581 // Add the value 582 if (cookie_val_leads.length < maxItems) { 583 if (leadId !== 0 && leadId !== '') { 584 cookie_val_leads.push(leadId.toString()); 585 setCookie(cookieName, cookie_val_leads.join(','), { path: '/' }); 586 } 587 } else { 588 // Error handling for max items limit 589 const errorMessage = `You cannot select more than ${maxItems} items.`; 590 if (onError && typeof onError === 'function') { 591 onError(errorMessage); 592 } else { 593 console.warn(errorMessage); 594 } 595 return false; // Indicate selection should be prevented 596 } 597 } 598 } else { 599 // Lead is deselected 600 const index = cookie_val_leads.indexOf(leadId.toString()); 601 if (index > -1) { 602 cookie_val_leads.splice(index, 1); 603 setCookie(cookieName, cookie_val_leads.join(','), { path: '/' }); 604 } 605 } 606 return true; 607} 608 609/** 610 * Retrieves selected lead IDs from cookies 611 * @param {string} cookieName - Name of the cookie (default: 'lead_list') 612 * @returns {Array<string>} - Array of lead IDs 613 */ 614function getCookieLeads(cookieName = 'lead_list') { 615 return getCookieItems(cookieName); 616} 617 618/** 619 * Generic function to get items from a comma-separated cookie 620 * @param {string} cookieName - Name of the cookie 621 * @returns {Array<string>} - Array of items 622 */ 623function getCookieItems(cookieName) { 624 const items = getCookie(cookieName) || ''; 625 return items ? items.split(',').filter(id => id !== '') : []; 626} 627 628/** 629 * Sets a cookie with the given name, value, and options 630 * @param {string} name - Cookie name 631 * @param {string} value - Cookie value 632 * @param {Object} options - Cookie options (path, expires, domain, etc.) 633 */ 634function setCookie(name, value, options = {}) {
635 let cookieString = `${name}=${encodeURIComponent(value)}`; 636 637 if (options.path) { 638 cookieString += `; path=${options.path}`; 639 } 640 if (options.expires) { 641 cookieString += `; expires=${options.expires}`; 642 } 643 if (options.domain) { 644 cookieString += `; domain=${options.domain}`; 645 } 646 if (options.secure) { 647 cookieString += `; secure`; 648 } 649 if (options.sameSite) { 650 cookieString += `; samesite=${options.sameSite}`; 651 } 652 653 document.cookie = cookieString; 654} 655 656/** 657 * Gets a cookie value by name 658 * @param {string} name - Cookie name 659 * @returns {string|null} - Cookie value or null if not found 660 */ 661function getCookie(name) { 662 const value = `; ${document.cookie}`; 663 const parts = value.split(`; ${name}=`); 664 if (parts.length === 2) { 665 return decodeURIComponent(parts.pop().split(';').shift()); 666 } 667 return null; 668} 669 670/** 671 * Deletes a cookie by setting it to expire in the past 672 * @param {string} name - Cookie name 673 * @param {string} path - Cookie path (default: '/') 674 */ 675function deleteCookie(name, path = '/') { 676 setCookie(name, '', { expires: 'Thu, 01 Jan 1970 00:00:00 GMT', path: path }); 677} 678 679/** 680 * Initializes selected items from cookies for a given data array 681 * @param {Array} dataArray - Array of objects with 'id' property 682 * @param {string} cookieName - Name of the cookie (default: 'lead_list') 683 * @returns {Array} - Array of selected items from dataArray 684 */ 685function initializeSelectedFromCookies(dataArray, cookieName = 'lead_list') { 686 const cookieItems = getCookieItems(cookieName); 687 if (cookieItems.length > 0) { 688 return dataArray.filter(item => 689 cookieItems.includes(item.id.toString()) 690 ); 691 } 692 return []; 693} 694 695/** 696 * Updates cookies based on selection changes (for ag-grid or similar components) 697 * @param {Array} previousSelection - Previous selected items 698 * @param {Array} newSelection - New selected items 699 * @param {Object} options - Configuration options 700 * @param {string} options.cookieName - Name of the cookie (default: 'lead_list') 701 * @param {Function} options.onError - Error callback function 702 * @param {Function} options.onMaxReached - Callback when max items reached 703 * @returns {Object} - Object with newly selected and deselected items 704 */ 705function updateSelectionCookies(previousSelection, newSelection, options = {}) { 706 const { cookieName = 'lead_list', onError = null, onMaxReached = null } = options; 707 708 const previousIds = previousSelection.map(item => item.id.toString()); 709 const newIds = newSelection.map(item => item.id.toString()); 710 711 // Find newly selected items 712 const newlySelected = newIds.filter(id => !previousIds.includes(id)); 713 // Find newly deselected items 714 const newlyDeselected = previousIds.filter(id => !newIds.includes(id)); 715 716 const failedSelections = []; 717 718 // Update cookies for newly selected items 719 newlySelected.forEach(leadId => { 720 const canSelect = compute_cookies(leadId, true, { 721 cookieName, 722 onError: onError || ((msg) => console.warn(msg)) 723 }); 724 if (!canSelect) { 725 failedSelections.push(leadId); 726 if (onMaxReached && typeof onMaxReached === 'function') { 727 onMaxReached(leadId); 728 } 729 } 730 }); 731 732 // Update cookies for newly deselected items 733 newlyDeselected.forEach(leadId => { 734 compute_cookies(leadId, false, { cookieName }); 735 }); 736 737 return { 738 newlySelected, 739 newlyDeselected, 740 failedSelections 741 }; 742} 743 744// Export all cookie utility functions 745export { 746 compute_cookies, 747 getCookieLeads, 748 getCookieItems, 749 setCookie, 750 getCookie, 751 deleteCookie, 752 initializeSelectedFromCookies, 753 updateSelectionCookies 754}; 755 756// Make functions available on window object for compatibility 757window.compute_cookies = compute_cookies; 758window.getCookieLeads = getCookieLeads; 759window.getCookieItems = getCookieItems; 760window.setCookie = setCookie; 761window.getCookie = getCookie; 762window.deleteCookie = deleteCookie; 763window.initializeSelectedFromCookies = initializeSelectedFromCookies; 764window.updateSelectionCookies = updateSelectionCookies; 765 766 767 768// THIS FUNCTION IS USED TO SPLIT TAGS AND RETURN A ARRAY OF OBJECTS WITH TAGS AND THEIR COLORS 769// Example usage: 770// tagsColor(["python", "django", "vue"]) 771function tagsColor(tags) { 772 if (!tags || tags.length === 0) return null; 773 774 // HSV â RGB â HEX 775 function hsvToRgb(h, s, v) { 776 let r, g, b; 777 778 let i = Math.floor(h * 6); 779 let f = h * 6 - i; 780 let p = v * (1 - s); 781 let q = v * (1 - f * s); 782 let t = v * (1 - (1 - f) * s); 783 784 switch (i % 6) { 785 case 0: r = v, g = t, b = p; break; 786 case 1: r = q, g = v, b = p; break; 787 case 2: r = p, g = v, b = t; break; 788 case 3: r = p, g = q, b = v; break; 789 case 4: r = t, g = p, b = v; break; 790 case 5: r = v, g = p, b = q; break; 791 } 792 793 return "#" + [r, g, b].map(x => { 794 let hex = Math.round(x * 255).toString(16); 795 return hex.length === 1 ? "0" + hex : hex; 796 }).join(""); 797 } 798 799 function simpleHash(str) { 800 let hash = 0; 801 for (let i = 0; i < str.length; i++) { 802 hash = (hash << 5) - hash + str.charCodeAt(i); 803 hash |= 0; // Convert to 32bit integer 804 } 805 return Math.abs(hash); 806 } 807 808 const tagList = []; 809 for (let tag of tags) { 810 const hash = simpleHash(tag); 811 812 const firstNum = hash & 0xff; 813 const secondNum = (hash >> 8) & 0xff;
814 815 const mappedNum = Math.floor((firstNum / 255) * 360); 816 817 const hue = (mappedNum % 360) / 360; 818 const variation = secondNum / 510 - 0.25; 819 const saturation = Math.min(0.8 + variation, 1.0); 820 const brightness = 0.7 + variation; 821 822 const color = hsvToRgb(hue, saturation, brightness); 823 tagList.push({ tag: tag, color: color }); 824 } 825 826 return tagList; 827} 828 829export { tagsColor }; 830 831// THIS FUNCTION BUILDS THE SUGGESTION LIST FOR THE "EDIT TAGS" AUTOCOMPLETE. 832// Tags are normalised the same way the LeadTags model does (force_lowercase = True). 833// If the typed query matches no existing tag (and is not already selected) it is 834// appended to the suggestions so the user can create it. It is appended, not 835// prepended, so that Enter still picks the best existing match when there is one. 836// Returns { suggestions, newTag } where newTag is the pending tag name or null. 837// Example usage: 838// buildTagSuggestions("NewTag", ["vip", "hot"], ["hot"]) -> { suggestions: ["vip", "hot", "newtag"], newTag: "newtag" } 839function buildTagSuggestions(query, results, selectedTags = []) { 840 const suggestions = [...(results || [])]; 841 const newTag = String(query || '').replace(/,/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase(); 842 if (!newTag) return { suggestions, newTag: null }; 843 844 const taken = new Set([...suggestions, ...(selectedTags || [])].map(t => String(t).trim().toLowerCase())); 845 if (taken.has(newTag)) return { suggestions, newTag: null }; 846 847 return { suggestions: [...suggestions, newTag], newTag }; 848} 849 850export { buildTagSuggestions }; 851
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.