PageSourceSearch

https://www.hudsonvieweast.com/assets/v2/js/utils/index.js?v=2026-09-25_17-04

js hudsonvieweast.com collected 2026-09-29 15:14:58 UTC 30,141 bytes, 851 lines download raw bytes

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.