view mrjunejune/src/public/mjj-composer.js @ 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 41a49c29a28f
children
line wrap: on
line source

/**
 * Layer 2 composer composite.
 *
 * Dependencies flow downward only: Zenbu Layer 1 primitives and Layer 0
 * themes/i18n/API mapping. Application routes and state remain outside.
 */

/**
 * Coordinates a native composer form built from Zenbu controls.
 *
 * @extends {HTMLElement}
 */
export class MjjComposer extends HTMLElement {
  connectedCallback() {
    this._form = this.querySelector(":scope > form");
    this._textarea = this._form?.querySelector("textarea");
    this._submit = this._form?.querySelector('button[type="submit"]');
    this._cancel = this._form?.querySelector("[data-composer-cancel]");
    this._cancelOwner = this._form?.querySelector("[data-composer-cancel-owner]");
    this._onSubmit = event => {
      event.preventDefault();
      const message = this._textarea?.value.trim();
      if (!message || this._submit?.disabled) return;
      this.dispatchEvent(new CustomEvent("mjj-composer-submit", {
        bubbles: true,
        detail: { message },
      }));
      this._form.reset();
    };
    this._onKeydown = event => {
      if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
      event.preventDefault();
      this._form?.requestSubmit();
    };
    this._onCancel = () => {
      this.dispatchEvent(new CustomEvent("mjj-composer-cancel", {
        bubbles: true,
      }));
    };
    this._form?.addEventListener("submit", this._onSubmit);
    this._textarea?.addEventListener("keydown", this._onKeydown);
    this._cancel?.addEventListener("click", this._onCancel);
  }

  disconnectedCallback() {
    this._form?.removeEventListener("submit", this._onSubmit);
    this._textarea?.removeEventListener("keydown", this._onKeydown);
    this._cancel?.removeEventListener("click", this._onCancel);
  }

  /**
   * Sets the active request state.
   *
   * @param {boolean} busy
   */
  setBusy(busy) {
    if (this._textarea) this._textarea.disabled = busy;
    if (this._submit) {
      this._submit.disabled = busy;
      this._submit.setAttribute("aria-busy", String(busy));
    }
    if (this._cancelOwner) this._cancelOwner.hidden = !busy;
  }

  /**
   * Shows or hides the cancellation action.
   *
   * @param {boolean} cancellable
   */
  setCancellable(cancellable) {
    if (this._cancelOwner) this._cancelOwner.hidden = !cancellable;
  }

  /**
   * Enables or disables composer input.
   *
   * @param {boolean} disabled
   */
  setDisabled(disabled) {
    if (this._textarea) this._textarea.disabled = disabled;
    if (this._submit) this._submit.disabled = disabled;
  }
}

if (!customElements.get("mjj-composer")) {
  customElements.define("mjj-composer", MjjComposer);
}