Files
Draggable-Resizable-Window/window.html
2025-12-25 10:09:55 +01:00

526 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Resizable & Snap Window with Live Snap & Restore on Grab</title>
<style>
body, html {
margin: 0; height: 100%; overflow: hidden; user-select: none;
background: #f0f2f5;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.customWindow {
position: fixed;
left: 100px;
top: 100px;
width: 400px;
height: 300px;
background: rgba(255, 255, 255, 0.85);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
border: 1px solid rgba(0,0,0,0.1);
box-sizing: border-box;
display: flex;
flex-direction: column;
backdrop-filter: saturate(180%) blur(10px);
-webkit-backdrop-filter: saturate(180%) blur(10px);
transition: box-shadow 0.3s ease;
}
.customWindow:hover {
box-shadow: 0 14px 45px rgba(0,0,0,0.18);
}
.customWindow .titlebar {
height: 32px;
background: linear-gradient(90deg, #4b6cb7, #182848);
color: white;
cursor: grab;
user-select: none;
padding: 6px 14px;
font-weight: 600;
font-size: 14px;
line-height: 20px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
display: flex;
align-items: center;
box-shadow: inset 0 -1px 0 rgba(255,255,255,0.15);
}
.customWindow .titlebar:active {
cursor: grabbing;
}
.customWindow .titlebar .buttons {
margin-left: auto;
display: flex;
gap: 6px;
}
.customWindow .titlebar button {
background: transparent;
border: none;
color: white;
font-weight: 700;
font-size: 16px;
width: 28px;
height: 24px;
line-height: 20px;
cursor: pointer;
user-select: none;
border-radius: 4px;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.customWindow .titlebar button:hover {
background: rgba(255,255,255,0.2);
}
.customWindow .titlebar button:active {
background: rgba(255,255,255,0.35);
}
.customWindow .content {
flex-grow: 1;
position: relative;
overflow: hidden;
height: calc(100% - 28px); /* 28px is titlebar height */
}
canvas {
width: 100%;
height: 100%;
display: block;
border-radius: 8px;
background: #fff;
box-shadow: inset 0 0 6px rgba(0,0,0,0.05);
}
/* Resizer handles */
.resizer {
position: absolute;
background: transparent;
z-index: 10;
}
.resizer.n, .resizer.s {
height: 8px;
left: 0;
right: 0;
cursor: ns-resize;
}
.resizer.e, .resizer.w {
width: 8px;
top: 0;
bottom: 0;
cursor: ew-resize;
}
.resizer.nw, .resizer.ne, .resizer.sw, .resizer.se {
width: 12px;
height: 12px;
background: transparent;
cursor: nwse-resize;
z-index: 20;
}
.resizer.n { top: -4px; }
.resizer.s { bottom: -4px; }
.resizer.e { right: -4px; }
.resizer.w { left: -4px; }
.resizer.nw { top: -6px; left: -6px; cursor: nwse-resize; }
.resizer.ne { top: -6px; right: -6px; cursor: nesw-resize; }
.resizer.sw { bottom: -6px; left: -6px; cursor: nesw-resize; }
.resizer.se { bottom: -6px; right: -6px; cursor: nwse-resize; }
</style>
</head>
<body>
<div class="customWindow">
<div class="titlebar">
Drag Me
<div class="buttons">
<button id="btnMaximize" title="Maximize"></button>
<button id="btnClose" title="Close">×</button>
</div>
</div>
<div class="content">
<canvas id="canvas"></canvas>
</div>
<!-- Resizers -->
<div class="resizer n"></div>
<div class="resizer e"></div>
<div class="resizer s"></div>
<div class="resizer w"></div>
<div class="resizer nw"></div>
<div class="resizer ne"></div>
<div class="resizer sw"></div>
<div class="resizer se"></div>
</div>
<script>
const customWindow = document.querySelector(".customWindow");
const canvas = document.getElementById("canvas");
const titlebar = customWindow.querySelector(".titlebar");
// Store old position/size for restore after unsnap or maximize
let isSnapped = false;
let savedPosition = null;
// Maximize state
let isMaximized = false;
let savedBeforeMaximize = null;
// Drag and resize state
let isDragging = false;
let dragType = null; // 'move' or 'resize'
let currentResizer = null;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
let startLeft = 0;
let startTop = 0;
// Save current window state before snapping or maximizing
function saveWindowState() {
savedPosition = {
left: customWindow.offsetLeft,
top: customWindow.offsetTop,
width: customWindow.offsetWidth,
height: customWindow.offsetHeight
};
}
// Restore window state after unsnap or restore from maximize
function restoreWindowState() {
if (savedPosition) {
customWindow.style.left = savedPosition.left + "px";
customWindow.style.top = savedPosition.top + "px";
customWindow.style.width = savedPosition.width + "px";
customWindow.style.height = savedPosition.height + "px";
savedPosition = null;
isSnapped = false;
}
}
// Resize canvas to fill container
function resizeCanvas() {
const style = getComputedStyle(customWindow);
const w = parseInt(style.width, 10);
const h = parseInt(style.height, 10);
canvas.width = w;
canvas.height = h;
}
// Snap window if near edges/corners (called live during drag/resize)
function snapWindow(mouseX, mouseY) {
if (isMaximized) return; // no snap when maximized
const snapMargin = 30;
const vw = window.innerWidth;
const vh = window.innerHeight;
const nearLeft = mouseX <= snapMargin;
const nearRight = mouseX >= vw - snapMargin;
const nearTop = mouseY <= snapMargin;
const nearBottom = mouseY >= vh - snapMargin;
if (!isSnapped && (nearLeft || nearRight || nearTop || nearBottom)) {
saveWindowState();
isSnapped = true;
} else if (!nearLeft && !nearRight && !nearTop && !nearBottom && isSnapped) {
// unsnap if moved out of snap zone
restoreWindowState();
return;
}
if (!isSnapped) return; // nothing to snap
if (nearTop && nearLeft) {
// Top-left quarter
customWindow.style.left = "0px";
customWindow.style.top = "0px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearTop && nearRight) {
// Top-right quarter
customWindow.style.left = Math.floor(vw / 2) + "px";
customWindow.style.top = "0px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearBottom && nearLeft) {
// Bottom-left quarter
customWindow.style.left = "0px";
customWindow.style.top = Math.floor(vh / 2) + "px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearBottom && nearRight) {
// Bottom-right quarter
customWindow.style.left = Math.floor(vw / 2) + "px";
customWindow.style.top = Math.floor(vh / 2) + "px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearTop) {
// Top half
customWindow.style.left = "0px";
customWindow.style.top = "0px";
customWindow.style.width = vw + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearBottom) {
// Bottom half
customWindow.style.left = "0px";
customWindow.style.top = Math.floor(vh / 2) + "px";
customWindow.style.width = vw + "px";
customWindow.style.height = Math.floor(vh / 2) + "px";
} else if (nearLeft) {
// Left half
customWindow.style.left = "0px";
customWindow.style.top = "0px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = vh + "px";
} else if (nearRight) {
// Right half
customWindow.style.left = Math.floor(vw / 2) + "px";
customWindow.style.top = "0px";
customWindow.style.width = Math.floor(vw / 2) + "px";
customWindow.style.height = vh + "px";
}
resizeCanvas();
}
// Drag move handler
function onDragMove(e) {
e.preventDefault();
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (dragType === "move") {
let newLeft = startLeft + dx;
let newTop = startTop + dy;
newLeft = Math.max(0, Math.min(window.innerWidth - customWindow.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - customWindow.offsetHeight, newTop));
customWindow.style.left = newLeft + "px";
customWindow.style.top = newTop + "px";
snapWindow(e.clientX, e.clientY);
} else if (dragType === "resize") {
let newLeft = startLeft;
let newTop = startTop;
let newWidth = startWidth;
let newHeight = startHeight;
switch (currentResizer) {
case "e": newWidth = startWidth + dx; break;
case "w": newWidth = startWidth - dx; newLeft = startLeft + dx; break;
case "s": newHeight = startHeight + dy; break;
case "n": newHeight = startHeight - dy; newTop = startTop + dy; break;
case "ne": newHeight = startHeight - dy; newTop = startTop + dy;
newWidth = startWidth + dx; break;
case "nw": newWidth = startWidth - dx; newLeft = startLeft + dx;
newHeight = startHeight - dy; newTop = startTop + dy; break;
case "se": newWidth = startWidth + dx;
newHeight = startHeight + dy; break;
case "sw": newWidth = startWidth - dx; newLeft = startLeft + dx;
newHeight = startHeight + dy; break;
}
const minWidth = 150;
const minHeight = 100;
if (newWidth < minWidth) {
if (currentResizer.includes("w")) {
newLeft -= (minWidth - newWidth);
}
newWidth = minWidth;
}
if (newHeight < minHeight) {
if (currentResizer.includes("n")) {
newTop -= (minHeight - newHeight);
}
newHeight = minHeight;
}
if (newLeft < 0) {
if (currentResizer.includes("w")) {
newWidth += newLeft;
}
newLeft = 0;
}
if (newTop < 0) {
if (currentResizer.includes("n")) {
newHeight += newTop;
}
newTop = 0;
}
if (newLeft + newWidth > window.innerWidth) {
if (currentResizer.includes("e")) {
newWidth = window.innerWidth - newLeft;
} else {
newLeft = window.innerWidth - newWidth;
}
}
if (newTop + newHeight > window.innerHeight) {
if (currentResizer.includes("s")) {
newHeight = window.innerHeight - newTop;
} else {
newTop = window.innerHeight - newHeight;
}
}
customWindow.style.left = newLeft + "px";
customWindow.style.top = newTop + "px";
customWindow.style.width = newWidth + "px";
customWindow.style.height = newHeight + "px";
resizeCanvas();
snapWindow(e.clientX, e.clientY);
}
}
// Drag end handler
function onDragEnd(e) {
isDragging = false;
dragType = null;
currentResizer = null;
document.removeEventListener("mousemove", onDragMove);
document.removeEventListener("mouseup", onDragEnd);
}
// Titlebar mouse down for dragging
function onTitlebarMouseDown(e) {
e.preventDefault();
dragType = "move";
let offsetX = e.clientX - customWindow.offsetLeft;
let offsetY = e.clientY - customWindow.offsetTop;
if (isSnapped) {
restoreWindowState();
// Recompute start position after restoring to maintain offset
startX = e.clientX;
startY = e.clientY;
startLeft = e.clientX - 100;
startTop = e.clientY - offsetY;
customWindow.style.left = startLeft + "px";
customWindow.style.top = startTop + "px";
} else {
startX = e.clientX;
startY = e.clientY;
startLeft = customWindow.offsetLeft;
startTop = customWindow.offsetTop;
}
isDragging = true;
document.addEventListener("mousemove", onDragMove);
document.addEventListener("mouseup", onDragEnd);
}
// Resizer mouse down for resizing
function onResizerMouseDown(e) {
e.preventDefault();
startX = e.clientX;
startY = e.clientY;
startLeft = customWindow.offsetLeft;
startTop = customWindow.offsetTop;
startWidth = customWindow.offsetWidth;
startHeight = customWindow.offsetHeight;
isDragging = true;
dragType = "resize";
currentResizer = e.target.classList[1]; // e.g. 'e', 'nw', etc.
document.addEventListener("mousemove", onDragMove);
document.addEventListener("mouseup", onDragEnd);
}
// Maximize window function
function maximizeWindow() {
if (isMaximized) return;
savedBeforeMaximize = {
left: customWindow.offsetLeft,
top: customWindow.offsetTop,
width: customWindow.offsetWidth,
height: customWindow.offsetHeight
};
customWindow.style.left = "0px";
customWindow.style.top = "0px";
customWindow.style.width = window.innerWidth + "px";
customWindow.style.height = window.innerHeight + "px";
resizeCanvas();
isMaximized = true;
isSnapped = false;
savedPosition = null;
}
// Restore from maximize
function restoreFromMaximize() {
if (!isMaximized || !savedBeforeMaximize) return;
customWindow.style.left = savedBeforeMaximize.left + "px";
customWindow.style.top = savedBeforeMaximize.top + "px";
customWindow.style.width = savedBeforeMaximize.width + "px";
customWindow.style.height = savedBeforeMaximize.height + "px";
resizeCanvas();
isMaximized = false;
savedBeforeMaximize = null;
}
// Close window function
function closeWindow() {
customWindow.style.display = "none";
}
// Attach event listeners
titlebar.addEventListener("mousedown", onTitlebarMouseDown);
const resizers = customWindow.querySelectorAll(".resizer");
resizers.forEach(function(resizer) {
resizer.addEventListener("mousedown", onResizerMouseDown);
});
const btnClose = document.getElementById("btnClose");
const btnMaximize = document.getElementById("btnMaximize");
btnClose.addEventListener("click", closeWindow);
btnMaximize.addEventListener("click", function() {
if (isMaximized) {
restoreFromMaximize();
} else {
maximizeWindow();
}
});
// Initial canvas sizing
resizeCanvas();
</script>
</body>
</html>