# Source: 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 pageContains(node) {
            // Needed because document.body.contains(node) returns false if node
            // is contained in a ShadowRoot.

            let foundDoc = false;
            while (node) {
                if (node === document) {
                    foundDoc = true;
                    break;
                }

                const parent = node.parentNode;
                if (parent instanceof DocumentFragment) {
                    // A ShadowRoot is a DocumentFragment having a host property.
                    node = parent.host;
                } else {
                    node = parent;
                }
            }

            return foundDoc;
        }

        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
        // -----------------------------------------------------------------------

        /**
         * Return a number, the value of property having specified name
         * if found in the computed style of specified element.
         * Return <code>NaN</code> if property is not found or
         * if its value is not a <code>px</code> length.
         */
        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));
            }
        }

        /**
         * Return an array of numbers, values of the properties
         * having specified names if found in the computed style of
         * specified element.
         *
         * @param {Element} element - the element.
         * @param {...(string|number)} args - property name, number pairs.
         * if property is not found or if its value is not a
         * <code>px</code> length, the number, used as fallback value,
         * is appended to the returned array.
         * @return {array} an array having same size as the number of
         * specified property names.
         */
        static getPxProperties(element, ...args) {
            let values = [];

            const props = window.getComputedStyle(element);
            for (let i = 0; i < args.length; i += 2) {
                const propName = args[i];
                const fallback = args[i+1];

                let value = NaN;
                let propValue = props.getPropertyValue(propName);
                if (propValue !== null && propValue.endsWith("px")) {
                    value = parseFloat(propValue.substring(0, propValue.length-2));
                }
                if (isNaN(value)) {
                    value = fallback;
                }
                values.push(value);
            }

            return values;
        }

        // -----------------------------------------------------------------------
        // 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;
            }
        }

        // -----------------------------------------------------------------------
        // Miscellaneous
        // -----------------------------------------------------------------------

        static intersects(r1, r2) {
            return (r1.right > r2.left) && (r1.left < r2.right) &&
                (r1.bottom > r2.top) && (r1.top < r2.bottom);
        }
    }

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