Source: xui/Util.js

/**
 * Utility functions (static methods) used to implement the XUI module.
 * <p>Part of the XUI module which, for now, has an undocumented API.
 */
export class Util {
    // -----------------------------------------------------------------------
    // Text
    // -----------------------------------------------------------------------
    
    static escapeHTML(text) {
        return text.replaceAll('&', "&amp;")
            .replaceAll('<', "&lt;")
            .replaceAll('>', "&gt;")
            .replaceAll('"', "&quot;")
            .replaceAll("'", "&#039;"); // &apos, only as of HTML 5.0.
    }

    static shortenText(text, maxLength) {
        if (maxLength < 4) {
            maxLength = 4;
        }
        if (text.length > maxLength) {
            const half = maxLength / 2;
            text = text.substring(0, half - 1) + "\u2026" + // Ellipsis
                text.substring(text.length - half);
        }
        return text;
    }
    
    static wordWrap(text, lineMaxLength, maxLines=-1) {
        let wrapped = "";

        let lineCount = 0;
        let lines = text.split('\n', /*limit*/ maxLines); // -1 means: no limit.
        for (let i = 0; i < lines.length; ++i) {
            let line = lines[i];

            if (i > 0) {
                wrapped += '\n';
            }

            if (line.length <= lineMaxLength || line.indexOf(' ') < 0) {
                wrapped += line;
                ++lineCount;
            } else {
                let lineLength = 0;

                let words = line.split(' ');
                for (let word of words) {
                    let wordLength = word.length;
                    if (lineLength + wordLength > lineMaxLength) {
                        if (lineLength > 0) {
                            wrapped += '\n';
                            lineLength = 0;
                            ++lineCount;

                            if (maxLines > 0 && lineCount >= maxLines) {
                                break;
                            }
                        }
                    } else {
                        if (lineLength > 0) {
                            wrapped += ' ';
                            ++lineLength;
                        }
                    }

                    wrapped += word;
                    lineLength += wordLength;
                }

                if (lineLength > 0) {
                    ++lineCount;
                }
            }

            if (maxLines > 0 && lineCount >= maxLines) {
                break;
            }
        }

        return wrapped;
    }
    
    // -----------------------------------------------------------------------
    // DOM
    // -----------------------------------------------------------------------
    
    static removeAllChildren(parent) {
        let child = parent.firstChild;
        while (child !== null) {
            let next = child.nextSibling;
            parent.removeChild(child);
            child = next;
        }
    }

    static addStylesheetLink(tree, css=null) {
        let link = document.createElement("link");
        link.setAttribute("rel", "stylesheet");
        link.setAttribute("type", "text/css");
        if (!css) {
            css = "xui.css";
        }
        let url = new URL(css, import.meta.url);
        link.setAttribute("href", url.toString());
        tree.appendChild(link);

        return link;
    }
    
    // -----------------------------------------------------------------------
    // CSSOM
    // -----------------------------------------------------------------------
    
    static getPxProperty(element, propName) {
        let propValue =
            window.getComputedStyle(element).getPropertyValue(propName);
        if (!propValue || !propValue.endsWith("px")) {
            return NaN;
        } else {
            // Note that parseFloat would have ignored the trailing "px"!
            return parseFloat(propValue.substring(0, propValue.length-2));
        }
    }
    
    // -----------------------------------------------------------------------
    // Events
    // -----------------------------------------------------------------------
    
    static consumeEvent(event) {
        // Prevent default behavior if any.
        event.preventDefault();
        
        // Prevent capturing and bubbling
        // (but, unlike, stopImmediatePropagation, NOT other listeners are
        // attached to the same element for the same event type).
        event.stopPropagation(); 
    }
    
    static modKey(event) {
        return ((Util.PLATFORM_IS_MAC_OS && event.metaKey) ||
                (!Util.PLATFORM_IS_MAC_OS && event.ctrlKey));
    }
    
    // -----------------------------------------------------------------------
    // Dialogs
    // -----------------------------------------------------------------------
    
    static badTextField(textField) {
        textField.select();
        textField.focus();
    }
    
    static rememberDatalistItem(localStorageKey, value) {
        let valueList = [];
        let values = window.localStorage.getItem(localStorageKey);
        if (values !== null) {
            valueList = values.split('\n');
        }

        if (value) {
            while (valueList.length >= 10) {
                valueList.pop();
            }

            let index = valueList.indexOf(value);
            if (index < 0) {
                valueList.unshift(value);
            } else if (index > 0) {
                valueList.splice(index, 1);
                valueList.unshift(value);
            }
            // Otherwise, already first item. Nothing to do.
        }

        if (valueList.length > 0) {
            window.localStorage.setItem(localStorageKey, valueList.join('\n'));
        } else {
            window.localStorage.removeItem(localStorageKey);
        }
        
        return valueList;
    }
    
    static attachDatalist(textField, valueList, form) {
        if (textField.hasAttribute("list") ||
            !Array.isArray(valueList) || valueList.length === 0) {
            // Nothing to do.
            return;
        }
        
        let datalistId = Util.uid();
        Util.appendDatalist(datalistId, valueList, form);
        textField.setAttribute("list", datalistId);
    }
    
    static uid() {
        return Date.now().toString(36) +
            Math.random().toString(36).substring(2); /*Skip leading "0."*/
    }
    
    static appendDatalist(id, items, parent) {
        let list = document.createElement("datalist");
        list.id = id;

        for (let item of items) {
            let option = document.createElement("option");
            option.value = item;
            list.appendChild(option);
        }
        
        parent.appendChild(list);
        
        return list;
    }
    
    static prependDatalistItem(list, item, maxItems=20) {
        if (item === null || item.length === 0) {
            return;
        }

        let option = list.firstElementChild;
        if (option !== null) {
            if (option.value === item) {
                // Nothing to do.
                return;
            }
            
            while (option !== null) {
                if (option.value === item) {
                    list.removeChild(option);
                    break;
                }
                option = option.nextElementSibling;
            }
        }

        // ---
        
        option = document.createElement("option");
        option.value = item;
        list.insertBefore(option, list.firstElementChild);

        if (maxItems < 2) {
            maxItems = 2;
        }
        
        let itemCount = list.childElementCount;
        while (itemCount > maxItems) {
            list.removeChild(list.lastElementChild);
            --itemCount;
        }
    }
}

Util.PLATFORM_IS_MAC_OS = window.navigator.platform.match(/mac/i);