Source: ui/Window.js

import { UiElement } from "./UiElement.js";
import "./Window.css";

export { Window };

/**
 * Basic Window Popup Class for tools
 * 
 * options = {
 *   top: absolute top position in pixel
 *   left: absolute left position in pixel
 *   pointerEvent: Use pointer event for window position
 *   height: in pixel
 *   width: in pixel
 *   title: string
 *   content: HTML-sting or HTML-element
 *   open: boolean, open window direcly after creating,
 *   responsive: boolean, fullscreen on small screens
 *   draggable: boolean, enable drag move,
 *   mode: slim / default
 * }
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui
 */
class Window extends UiElement {

    static openedWindows = [];

    /**
     * @param {HTMLElement | string} target default is document.body
     * @param {object} options 
     */
    constructor(target, options) {
        super(target || document.body, {
            "open": [],
            "close": [],
            "setTitle": []
        });
        this.titleEditMode_ = false;
        this.title_ = document.createElement("span");
        this.content_ = document.createElement("div");
        this.header_ = document.createElement("div");
        this.opened_ = false;
        this.resizeCallback_ = null;

        this.initElement_();
        this.setOptions(options || {});
        this.options_ = options;
        this.initDragMove_();
    }

    /**
     * <div class="vef-window">
     *   <div class="vef-window-header">
     *     <span class="vef-window-title">{TITLE}</span>
     *     <a class="vef-window-close"></a>
     *   </div>
     *   <div class="vef-window-content">{CONTENT}</div>
     * </div>
     */
    initElement_() {
        const element = this.getElement();
        element.classList.add("vef-window");

        const pointerContainer = document.createElement("div");
        pointerContainer.classList.add("pointer-container");

        const header = this.header_;
        header.classList.add("vef-window-header");

        const close = document.createElement("i");
        close.classList.add("fas");
        close.classList.add("fa-times");
        close.onclick = () => this.close();

        this.title_.classList.add("vef-window-title");
        this.content_.classList.add("vef-window-content");

        header.appendChild(this.title_);
        header.appendChild(close);
        element.appendChild(pointerContainer);
        element.appendChild(header);
        element.appendChild(this.content_);
    }

    /**
     * Update the options of the window
     * 
     * @param {object} options 
     */
    setOptions(options) {
        const element = this.getElement();

        // a pointer event overrides top and right options
        if(options.pointerEvent) {
            element.style.left = (options.pointerEvent.clientX - 5) + "px";
            element.style.top = (options.pointerEvent.clientY - 5) + "px";
        } else {
            if (options.left) element.style.left = options.left;
            if (options.top) element.style.top = options.top;
        }

        if (options.height) element.style.height = options.height;
        if (options.width) element.style.width = options.width;
        if (options.title){
            if(!options.editTitle){
                this.setTitle(options.title);  
            }else{
                this.setTitleEditable(options.title);
            }
        }
        if (options.content) this.setContent(options.content);

        // select different modes for the window
        if (options.mode) {
            if (options.mode == "slim") {
                element.classList.add("slim");
            } else if (options.mode == "default") {
                element.classList.remove("slim");
            }
        }

        // boolean to open window
        if (typeof options.open == "boolean") {
            if (options.open) {
                this.open();
            } else {
                this.close();
            }
        }

        // boolean responsive fullscreen on small devices
        if (typeof options.responsive == "boolean") {
            if (options.responsive) {
                element.classList.add("responsive");
            } else {
                element.classList.remove("responsive");
            }
        }

        // boolean to enable dragging the window
        if (typeof options.draggable == "boolean") {
            if (options.draggable) {
                this.enableDragMove();
            } else {
                this.disableDragMove();
            }
        }

        if (options.center) this.center();
    }

    /**
     * Centers the window globally in the browsers frame
     */
    center() {
        const element = this.getElement();

        const viewportHeight = document.documentElement.clientHeight;
        const viewportWidth = document.documentElement.clientWidth;

        element.style.top = (viewportHeight / 2) - (element.clientHeight / 2) + "px";
        element.style.left = (viewportWidth / 2) - (element.clientWidth / 2) + "px";
    }

    /**
     * calculate the position and place it inside the 
     * browser window bounds.
     * 
     * @private
     */
    calculatePosition_() {
        const element = this.getElement();
        const rect = element.getBoundingClientRect();

        const viewportWidth = document.documentElement.clientWidth;
        const viewportHeight = document.documentElement.clientHeight;

        if ((rect.left + rect.width) > viewportWidth) {
            let left = viewportWidth - rect.width;
            element.style.left = ((left > 0) ? left : 0) + "px";
        }

        if ((rect.top + rect.height) > viewportHeight) {
            let top = viewportHeight - rect.height;
            element.style.top = ((top > 0) ? top : 0) + "px";
        }
    }

    /**
     * enable the window resize event.
     * Used for repositioning the Window on resize
     * 
     * @private
     * @override
     */
    enableResizeEvent_() {
        if (!this.resizeCallback_) {
            this.resizeCallback_ = () => this.calculatePosition_();

            document.addEventListener("scroll", this.resizeCallback_, true);
            window.addEventListener("resize", this.resizeCallback_);
        }
    }

    /**
     * disable the window resize event
     * 
     * @private
     */
    disableResizeEvent_() {
        if (this.resizeCallback_) {
            window.removeEventListener("resize", this.resizeCallback_);
            document.removeEventListener("scroll", this.resizeCallback_, true);
            this.resizeCallback_ = null;
        }
    }

    /**
     * Initializes the events for moving the
     * window with the mouse
     * 
     * @private 
     */
    initDragMove_() {
        const element = this.getElement();

        let parent, x, y, rect, viewportWidth, viewportHeight;

        const mouseMove = (e) => {
            let left = e.clientX - x;
            let top = e.clientY - y;

            // keep window in viewport bounds
            if ((left + rect.width) > viewportWidth) left = viewportWidth - rect.width;
            if ((top + rect.height) > viewportHeight) top = viewportHeight - rect.height;

            element.style.top = ((top > 0) ? top : 0) + "px";
            element.style.left = ((left > 0) ? left : 0) + "px";
        };

        const mouseUp = () => {
            parent.removeEventListener("mousemove", mouseMove);
            document.body.removeEventListener("mouseup", mouseUp);
        };

        this.header_.addEventListener("mousedown", (e) => {
            parent = this.getElement().parentElement;
            if (this.draggable_) {
                viewportWidth = document.documentElement.clientWidth;
                viewportHeight = document.documentElement.clientHeight;
                rect = element.getBoundingClientRect();
                x = e.clientX - rect.left;
                y = e.clientY - rect.top;

                parent.addEventListener("mousemove", mouseMove);
                document.body.addEventListener("mouseup", mouseUp);
            }
        });
    }

    /**
     * enable moving the window with the mouse
     */
    enableDragMove() {
        this.header_.classList.add("draggable");
        this.draggable_ = true;
    }

    /**
     * disable moving the window with the mouse
     */
    disableDragMove() {
        this.header_.classList.remove("draggable");
        this.draggable_ = false;
    }

    /**
     * Change the title in the header
     * 
     * @param {string} title 
     */
    setTitle(title) {
        if(title == undefined) return
        this.title_.innerHTML = title;
        this.title_.title = this.title_.innerText;
    }
    setTitleEditable(title){
        this.title_.innerHTML = title + '<i class="vef vef-rename edit-button"></i>';
        if(this.options_.title){
            this.title_.innerText = this.options_.title;
            this.title_.innerHTML += '<i class="vef vef-rename edit-button"></i>';
        } 
        this.title_.parentElement.classList.remove("draggable");
        if(!this.title_.classList.value.includes(["edit-mode","not-edit-mode"])) this.title_.classList.add('not-edit-mode');
        this.title_.lastChild.addEventListener("click", () => {
            if(this.titleEditMode_){
                this.disableTitleEdit();
            }else {
                this.enableTitleEdit(this.title_);
            }
        })
    }

    enableTitleEdit(){
        this.title_.classList.add('edit-mode');
        this.title_.classList.remove('not-edit-mode');
        const a = document.createElement("input");
        a.className = "vef-window-title-field";
        a.value = this.title_.innerText
        this.title_.replaceWith(a);
        a.focus();
        a.addEventListener("blur", () => {
            this.options_.title = a.value;
            this.titleEditMode_ = true;
            this.disableTitleEdit(a);
        });
        a.addEventListener("keypress", (e) => {
            if(e.key == 'Enter'){
                this.options_.title = a.value;
                this.titleEditMode_ = true;
                this.disableTitleEdit(a);
            }
        });
        this.titleEditMode_ = true;
    }
    disableTitleEdit(element){
        element.replaceWith(this.title_);
        
        this.title_.classList.remove('edit-mode');
        this.title_.classList.add('not-edit-mode');
        this.titleEditMode_ = false;
        this.setTitleEditable(this.options_.title);
    }

    /**
     * Change the content of the window
     * 
     * @param {string | HTMLElement} title 
     */
    setContent(content) {
        if (typeof content === "string") {
            this.content_.innerHTML = content;
        } else if (content instanceof HTMLElement) {
            this.content_.innerHTML = "";
            this.content_.appendChild(content);
        }
    }

    /**
     * Hide the window by removing it from the parent
     */
    close() {
        this.getElement().classList.remove("open");
        this.disableResizeEvent_();
        this.opened_ = false;

        const index = Window.openedWindows.indexOf(this)
        if (index >= 0) {
            Window.openedWindows.splice(index, 1)
        }

        this.fire("close", this);
    }

    /**
     * Show the window by adding it to the parent
     */
    open() {
        this.getElement().classList.add("open");
        this.calculatePosition_();
        this.enableResizeEvent_();
        this.opened_ = true;

        if (!Window.openedWindows.includes(this)) {
            Window.openedWindows.push(this);
        }

        this.fire("open", this);
    }

    /**
     * dispose the Window
     * @override
     */
    dispose() {
        this.close()
        super.dispose();
    }

    static closeAll() {
        for (let i = Window.openedWindows.length - 1; i >= 0; --i) {
            Window.openedWindows[i].close();
        }
    }

}