window.js

import { Component } from "./component.js";
import { Label } from "./label.js";
import { Style } from "./style.js";

/**
 * Creates a draggable, collapsible window to be used as a parent for other components.
 * <div><img src="https://www.minicomps.org/images/window.png"/></div>
 * @example
 * const win = new Window(document.body, 20, 20, 200, 200);
 * new Button(win, 20, 20, "Click");
 * @extends Component
 */
export class Window extends Component {
  /**
   * Constructor
   * @param {HTMLElement} parent - The element to add this window to.
   * @param {number} x - The x position of the window. Default 0.
   * @param {number} y - The y position of the window. Default 0.
   * @param {number} w - The width of the window. Default 400.
   * @param {number} h - The height of the window. Default 400.
   * @param {number} label - The label to put in the title bar. Default 0.
   */
  constructor(parent, x, y, w, h, label) {
    super(parent, x, y);
    w = w || 400;
    h = h || 400;
    this._label = label;
    this._minimized = false;

    this._createChildren();
    this._createStyle();
    this._createListeners();

    this.setDraggable(Window.draggable);
    this.setMinimizable(Window.minimizable);
    this.setSize(w, h);
    this._addToParent();
  }

  //////////////////////////////////
  // Core
  //////////////////////////////////

  _createChildren() {
    this._setWrapperClass("MinimalWindow");
    this._titleBar = this._createDiv(this._wrapper, "MinimalWindowTitleBar");
    this._textLabel = new Label(this._titleBar, 5, 0, this._label);
    this._textLabel.height = 30;
    this._button = this._createDiv(this._titleBar, "MinimalWindowButton");
    this._content = this._createDiv(this._wrapper, "MinimalWindowContent");
    this._content.appendChild(document.createElement("slot"));
  }

  _createStyle() {
    const style = document.createElement("style");
    style.textContent = Style.window;
    this.shadowRoot.append(style);
  }

  _createWrapper() {
    this._wrapper = this._createDiv(null, "MinimalWrapper");
    this.shadowRoot.appendChild(this._wrapper);
  }

  _createListeners() {
    this._onMouseDown = this._onMouseDown.bind(this);
    this._onMouseUp = this._onMouseUp.bind(this);
    this._onMouseMove = this._onMouseMove.bind(this);
    this._onMinimize = this._onMinimize.bind(this);
    this._titleBar.addEventListener("mousedown", this._onMouseDown);
    this._titleBar.addEventListener("touchstart", this._onMouseDown);
    this._button.addEventListener("click", this._onMinimize);
  }

  //////////////////////////////////
  // Handlers
  //////////////////////////////////

  _onMouseDown(event) {
    this.style.zIndex = Style.windowIndex++;
    let mouseX;
    let mouseY;
    if (event.changedTouches) {
      event.preventDefault();
      mouseX = event.changedTouches[0].clientX;
      mouseY = event.changedTouches[0].clientY;
    } else {
      mouseX = event.clientX;
      mouseY = event.clientY;
    }
    this._offsetX = mouseX - this.getBoundingClientRect().left;
    this._offsetY = mouseY - this.getBoundingClientRect().top;
    document.addEventListener("mousemove", this._onMouseMove);
    document.addEventListener("touchmove", this._onMouseMove);
    document.addEventListener("mouseup", this._onMouseUp);
    document.addEventListener("touchend", this._onMouseUp);
  }

  _onMouseMove(event) {
    let mouseX;
    let mouseY;
    if (event.changedTouches) {
      mouseX = event.changedTouches[0].clientX;
      mouseY = event.changedTouches[0].clientY;
    } else {
      mouseX = event.clientX;
      mouseY = event.clientY;
    }
    const x = mouseX - this.offsetParent.getBoundingClientRect().left - this._offsetX;
    const y = mouseY - this.offsetParent.getBoundingClientRect().top - this._offsetY;
    this.move(x, y);
  }

  _onMouseUp() {
    document.removeEventListener("mousemove", this._onMouseMove);
    document.removeEventListener("touchmove", this._onMouseMove);
    document.removeEventListener("mouseup", this._onMouseUp);
    document.removeEventListener("touchend", this._onMouseUp);
  }

  _onMinimize() {
    this._minimized = !this._minimized;
    if (this._minimized) {
      super.setHeight(30);
    } else {
      super.setHeight(this._openHeight);
    }
  }

  //////////////////////////////////
  // Public
  //////////////////////////////////

  /**
   * @returns Whether or not this window can be moved by dragging its title bar.
   */
  getDraggable() {
    return this._draggable;
  }

  /**
   * @returns Whether this window has a minimize button.
   */
  getMinimizable() {
    return this._minimizable;
  }

  /**
   * @returns This window's label.
   */
  getLabel() {
    return this._label;
  }

  /**
   * Sets whether or not this window can be dragged by its title bar.
   * @param {boolean} draggable - Whether this window can be dragged.
   * @returns This instance, suitable for chaining.
   */
  setDraggable(draggable) {
    if (this._draggable !== draggable) {
      this._draggable = draggable;
      if (draggable) {
        this._titleBar.style.cursor = "pointer";
        this._titleBar.addEventListener("mousedown", this._onMouseDown);
        this._titleBar.addEventListener("touchstart", this._onMouseDown);
      } else {
        this._titleBar.style.cursor = "default";
        this._titleBar.removeEventListener("mousedown", this._onMouseDown);
        this._titleBar.removeEventListener("touchstart", this._onMouseDown);
      }
    }
    return this;
  }

  setEnabled(enabled) {
    if (this._enabled === enabled) {
      return this;
    }
    super.setEnabled(enabled);
    if (this._enabled) {
      this._minimized = true;
      this._onMinimize();
      this.setMinimizable(this._enabledMinimizable);
      this.setDraggable(this._enabledDraggable);
      this._wrapper.setAttribute("class", "MinimalWindow");
    } else {
      this._minimized = false;
      this._onMinimize();
      this._enabledMinimizable = this._minimizable;
      this._enabledDraggable = this._draggable;
      this.setMinimizable(false);
      this.setDraggable(false);
      this._wrapper.setAttribute("class", "MinimalWindowDisabled");
    }
    return this;
  }

  setHeight(height) {
    super.setHeight(height);
    this._openHeight = height;
    this._content.style.height = (height - 30) + "px";
    return this;
  }

  /**
   * Sets the test shown in this window's title bar.
   * @param {string} label - The label in the title bar.
   * @returns This instance, suitable for chaining.
   */
  setLabel(label) {
    this._label = label;
    this._textLabel.text = label;
    return this;
  }

  /**
   * Sets whether or not this window can be minimized.
   * @param {boolean} minimizable - Whether this window can be minimized.
   * @returns This instance, suitable for chaining.
   */
  setMinimizable(minimizable) {
    this._minimizable = minimizable;

    if (minimizable) {
      this._button.style.visibility = "visible";
    } else {
      this._button.style.visibility = "hidden";
    }
    return this;
  }

  setWidth(width) {
    super.setWidth(width);
    this._titleBar.style.width = width + "px";
    this._content.style.width = width + "px";
    return this;
  }

  //////////////////////////////////
  // Getters and Setters
  //////////////////////////////////

  /**
   * Gets and sets whether the window can be dragged by its title bar.
   */
  get draggable() {
    return this.getDraggable();
  }
  set draggable(draggable) {
    this.setDraggable(draggable);
  }

  /**
   * Sets and gets the label shown in the window's title bar.
   */
  get label() {
    return this.getLabel();
  }
  set label(label) {
    this.setLabel(label);
  }

  /**
   * Gets and sets whether the window has a minimize button.
   */
  get minimizable() {
    return this.getMinimizable();
  }
  set minimizable(minimizable) {
    this.setMinimizable(minimizable);
  }
}

//////////////////////////////////
// Defaults
//////////////////////////////////

/**
 * Default draggable state for all Windows.
 */
Window.draggable = true;
/**
 * Default minimizable state for all Windows.
 */
Window.minimizable = true;

customElements.define("minimal-window", Window);