Dup Ver Goto 📝

GeminiExample_01

PT2/lang/js/windowing does not exist
To
358 lines, 1003 words, 9749 chars Page 'GeminiExample_01' does not exist.

This is a simple floating window class made by Gemini.

Prompts

In HTML and Javascript, some attractive looking openable, draggable, possibly
resizeable, closeable windows. Overflow options can be hidden, scroll. Also a
resize to contents button. With an X in the corner to close. As a Javascript
class that we subclass to define the contents and configuration for. And
necessary CSS for styling.

yes please. and an example html file with clickable elements that open various
windows.

Snapping to the siees works. But could the windows remember their unsnapped
size to return to when unsnapping from an an edge.

I don't like the snapping animation.

CSS

:root {
  --win-bg: #ffffff;
  --win-border: #bdc3c7;
  --win-header: #ecf0f1;
  --win-shadow: 0 10px 30px rgba(0,0,0,0.3);
  --taskbar-bg: #2c3e50;
  --accent: #3498db;
}

body {
  margin: 0;
  height: 100vh;
  overflow: hidden; /* Prevent body scroll while dragging */
}

/* The Window Container */
.win-instance {
  position: absolute;
  min-width: 200px;
  min-height: 150px;
  background: var(--win-bg);
  border: 1px solid var(--win-border);
  box-shadow: var(--win-shadow);
  display: flex;
  flex-direction: column;
  border-radius: 8px;
  overflow: hidden;
  z-index: 10;
  xxtransition: width 0.15s ease-out, height 0.15s ease-out; /* Smooth snapping */
}

/* Header/Title Bar */
.win-header {
  padding: 10px 15px;
  background: var(--win-header);
  cursor: move;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--win-border);
  user-select: none;
}

.win-title {
  font-weight: bold;
  font-size: 14px;
  color: #333;
}

/* Window Buttons */
.win-controls button {
  margin-left: 4px;
  cursor: pointer;
  border: none;
  background: #d5dbdb;
  border-radius: 4px;
  padding: 4px 10px;
  font-size: 12px;
  transition: background 0.2s;
}

.win-controls button:hover { background: #bdc3c7; }
.win-close:hover { background: #e74c3c !important; color: white; }

/* Content Area */
.win-content {
  flex-grow: 1;
  padding: 15px;
  position: relative;
  background: white;
}

/* Resize Handle */
.resizer {
  width: 16px;
  height: 16px;
  position: absolute;
  right: 0;
  bottom: 0;
  cursor: nwse-resize;
  background: linear-gradient(135deg, transparent 50%, #bdc3c7 50%);
}

/* Taskbar */
#taskbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  height: 45px;
  background: var(--taskbar-bg);
  display: flex;
  padding: 0 10px;
  gap: 8px;
  align-items: center;
  z-index: 10000;
}

.task-item {
  background: #34495e;
  color: white;
  padding: 6px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
  border-bottom: 2px solid transparent;
}

.task-item:hover { background: #456789; }
.task-item.active { border-bottom: 2px solid var(--accent); }

/* States */
.win-minimized { display: none !important; }

Javascript

class FloatingWindow {
  constructor(title, options = {}) {
    this.title = title;
    this.width = options.width || '400px';
    this.height = options.height || '300px';
    this.overflow = options.overflow || 'auto';
    this.id = 'win-' + Math.random().toString(36).substr(2, 9);

    // State Tracking
    this.isSnapped = false;
    this.previousState = { width: this.width, height: this.height, top: '100px', left: '100px' };

    this.init();
  }

  init() {
    this.el = document.createElement('div');
    this.el.className = 'win-instance';
    this.el.id = this.id;
    this.el.style.width = this.width;
    this.el.style.height = this.height;
    this.el.style.left = '100px';
    this.el.style.top = '100px';

    this.el.innerHTML = `
      <div class="win-header">
        <span class="win-title">${this.title}</span>
        <div class="win-controls">
          <button class="win-fit" title="Fit to content">Fit</button>
          <button class="win-min" title="Minimize">_</button>
          <button class="win-close" title="Close">✕</button>
        </div>
      </div>
      <div class="win-content" style="overflow: ${this.overflow}"></div>
      <div class="resizer"></div>
    `;

    this.contentArea = this.el.querySelector('.win-content');
    document.body.appendChild(this.el);
    this.createTaskbarItem();
    this.setupEvents();
    this.bringToFront();
    this.render(); // Subclass content
  }

  createTaskbarItem() {
    const bar = document.getElementById('taskbar');
    if (!bar) return;
    const btn = document.createElement('div');
    btn.className = 'task-item';
    btn.id = 'task-' + this.id;
    btn.innerText = this.title;
    btn.onclick = () => this.toggleMinimize();
    bar.appendChild(btn);
  }

  bringToFront() {
    document.querySelectorAll('.win-instance').forEach(w => w.style.zIndex = 10);
    document.querySelectorAll('.task-item').forEach(t => t.classList.remove('active'));
    this.el.style.zIndex = 100;
    const tItem = document.getElementById('task-' + this.id);
    if (tItem) tItem.classList.add('active');
  }

  toggleMinimize() {
    if (this.el.classList.contains('win-minimized')) {
      this.el.classList.remove('win-minimized');
      this.bringToFront();
    } else {
      this.el.classList.add('win-minimized');
    }
  }

  setupEvents() {
    const header = this.el.querySelector('.win-header');

    // Header Buttons
    this.el.querySelector('.win-close').onclick = () => {
      this.el.remove();
      const tItem = document.getElementById('task-' + this.id);
      if (tItem) tItem.remove();
    };

    this.el.querySelector('.win-min').onclick = () => this.toggleMinimize();

    this.el.querySelector('.win-fit').onclick = () => {
      this.isSnapped = false;
      this.el.style.width = 'auto';
      this.el.style.height = 'auto';
    };

    // Mouse Interaction
    this.el.onmousedown = () => this.bringToFront();
    header.onmousedown = (e) => this.startDrag(e);
    this.el.querySelector('.resizer').onmousedown = (e) => this.startResize(e);
  }

  startDrag(e) {
    let pos1 = 0, pos2 = 0, pos3 = e.clientX, pos4 = e.clientY;

    document.onmousemove = (moveEvent) => {
      // 1. If snapped, restore before moving further
      if (this.isSnapped) {
        this.restoreFromSnap(moveEvent.clientX);
        // Recalculate positions after restore to avoid jumping
        pos3 = moveEvent.clientX;
        pos4 = moveEvent.clientY;
      }

      pos1 = pos3 - moveEvent.clientX;
      pos2 = pos4 - moveEvent.clientY;
      pos3 = moveEvent.clientX;
      pos4 = moveEvent.clientY;

      let newTop = this.el.offsetTop - pos2;
      let newLeft = this.el.offsetLeft - pos1;

      // 2. Check for Snap Zones
      if (moveEvent.clientX < 10) { 
        this.applySnap(0, 0, '50%', 'calc(100% - 45px)'); 
      } else if (moveEvent.clientX > window.innerWidth - 10) { 
        this.applySnap('50%', 0, '50%', 'calc(100% - 45px)');
      } else if (moveEvent.clientY < 10) {
        this.applySnap(0, 0, '100%', 'calc(100% - 45px)');
      } else {
        this.el.style.top = newTop + "px";
        this.el.style.left = newLeft + "px";
      }
    };

    document.onmouseup = () => document.onmousemove = null;
  }

  applySnap(left, top, w, h) {
    if (!this.isSnapped) {
      this.previousState = {
        width: this.el.style.width,
        height: this.el.style.height,
        top: this.el.style.top,
        left: this.el.style.left
      };
      this.isSnapped = true;
    }
    this.el.style.left = left;
    this.el.style.top = top;
    this.el.style.width = w;
    this.el.style.height = h;
  }

  restoreFromSnap(mouseX) {
    this.isSnapped = false;
    this.el.style.width = this.previousState.width;
    this.el.style.height = this.previousState.height;

    // Center window under cursor horizontally during pull-away
    const w = parseInt(this.el.style.width) || 400;
    this.el.style.left = (mouseX - (w / 2)) + 'px';
  }

  startResize(e) {
    e.preventDefault();
    e.stopPropagation();
    this.isSnapped = false; // Resizing breaks snap
    document.onmousemove = (ev) => {
      this.el.style.width = (ev.clientX - this.el.offsetLeft) + 'px';
      this.el.style.height = (ev.clientY - this.el.offsetTop) + 'px';
    };
    document.onmouseup = () => document.onmousemove = null;
  }

  render() {} // To be overridden
}

HTML Example Usage

Assuming CSS is in win1.css and Javascript in win1.js. See the example here.

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf8'/>
    <title>JS Window System</title>
<link rel="stylesheet" href="win1.css"/>
<script src="win1.js"></script>
    </head>
<body style="background: #2c3e50; font-family: sans-serif;">

    <div style="padding: 20px;">
        <h2 style="color: white;">Dashboard</h2>
        <button onclick="new ProfileWindow('My Profile')">Open Profile</button>
        <button onclick="new LogWindow('System Logs')">Open Logs</button>
    </div>

    <div id="taskbar"></div>

    <script>
        // --- SUBCLASSES ---

        class ProfileWindow extends FloatingWindow {
            render() {
                this.contentArea.innerHTML = `
                    <h3>User Info</h3>
                    <p>Name: Jane Doe</p>
                    <button onclick="alert('Saved!')">Save Profile</button>
                `;
            }
        }

        class LogWindow extends FloatingWindow {
            render() {
                this.contentArea.innerHTML = `
                    <div style="background: #000; color: #0f0; padding: 10px; height: 100%;">
                        <code>> Initializing system...<br>> Connection stable.<br>> Cache active.</code>
                    </div>
                `;
            }
        }
    </script>
</body>
</html>