Add basic draggable item that saves to the server

This commit is contained in:
2025-07-08 17:39:09 +12:00
parent 5c35e68ab2
commit deca1b6c37
8 changed files with 376 additions and 427 deletions

View File

@@ -0,0 +1,143 @@
import { getWebSocket } from "./websocket.js";
export class LightComponent extends HTMLElement {
constructor() {
super();
// Create a shadow DOM for encapsulation
const shadow = this.attachShadow({ mode: "open" });
// Create the content for the component
const style = document.createElement("style");
style.textContent = `
:host {
display: block;
width: 100px;
height: 100px;
background-color: #4caf50;
color: white;
text-align: center;
line-height: 100px;
cursor: grab;
position: absolute;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
:host:active {
cursor: grabbing;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
.color-picker {
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
cursor: pointer;
}
`;
// Create the main content (draggable area)
const content = document.createElement("div");
content.textContent = this.textContent || "Light Me Up!";
content.style.position = "absolute";
content.style.top = "0";
content.style.left = "0";
content.style.width = "100%";
content.style.height = "100%";
content.style.display = "flex";
content.style.justifyContent = "center";
content.style.alignItems = "center";
// Create the color picker
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.classList.add("color-picker");
colorPicker.value = "#4caf50"; // Default color
colorPicker.addEventListener("input", () => {
this.style.backgroundColor = colorPicker.value;
this.dispatchEvent(
new CustomEvent("color-change", {
detail: { lightId: this.lightId, color: colorPicker.value },
}),
);
});
// Append the style, content, and color picker to the shadow DOM
shadow.appendChild(style);
shadow.appendChild(content);
shadow.appendChild(colorPicker);
// Add event listeners for drag-and-drop
content.addEventListener("mousedown", this.handleMouseDown.bind(this));
document.addEventListener("mousemove", this.handleMouseMove.bind(this));
document.addEventListener("mouseup", this.handleMouseUp.bind(this));
}
// Track the initial mouse position and component position
handleMouseDown(event) {
event.preventDefault();
// Get the initial mouse position relative to the component
this.initialMouseX = event.clientX;
this.initialMouseY = event.clientY;
// Get the initial position of the component
const rect = this.getBoundingClientRect();
this.initialComponentX = rect.left;
this.initialComponentY = rect.top;
// Add a class to indicate dragging
this.classList.add("dragging");
}
// Update the component's position as the mouse moves
handleMouseMove(event) {
if (!this.classList.contains("dragging")) return;
// Calculate the new position of the component
const newX = this.initialComponentX + (event.clientX - this.initialMouseX);
const newY = this.initialComponentY + (event.clientY - this.initialMouseY);
// Update the component's position
this.style.left = `${newX}px`;
this.style.top = `${newY}px`;
}
// Stop dragging when the mouse is released
handleMouseUp() {
// Check if the component is being dragged
if (!this.classList.contains("dragging")) {
return; // Do nothing if not dragging
}
// Remove the dragging class
this.classList.remove("dragging");
// Get the current position of the component
const rect = this.getBoundingClientRect();
const newX = rect.left;
const newY = rect.top;
// Dispatch an event to notify the parent about the updated position
this.dispatchEvent(
new CustomEvent("position-change", {
detail: { lightId: this.lightId, x: newX, y: newY },
}),
);
}
// Add a property to hold the lightId
set lightId(id) {
this._lightId = id;
}
get lightId() {
return this._lightId;
}
}
// Define the custom element
customElements.define("light-component", LightComponent);

View File

@@ -1,214 +1,105 @@
let delayTimeout;
let brightnessTimeout;
let colorTimeout;
let color2Timeout;
let ws; // Variable to hold the WebSocket connection
let connectionStatusElement; // Variable to hold the connection status element
// Import the LightComponent from light-component.js
import { LightComponent } from "./light-component.js";
import { getWebSocket } from "./websocket.js";
// Function to update the connection status indicator
function updateConnectionStatus(status) {
if (!connectionStatusElement) {
connectionStatusElement = document.getElementById("connection-status");
}
if (connectionStatusElement) {
connectionStatusElement.className = ""; // Clear existing classes
connectionStatusElement.classList.add(status);
// Optionally, you could also update text content based on status
// connectionStatusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
}
// Wait for the DOM to be fully loaded
document.addEventListener("DOMContentLoaded", async () => {
// Select the container where the light-components will be added
const appContainer = document.getElementById("app");
// Function to establish WebSocket connection
function connectWebSocket() {
// Determine the WebSocket URL based on the current location
const wsUrl = `ws://${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
updateConnectionStatus("connecting"); // Indicate connecting state
ws.onopen = function (event) {
console.log("WebSocket connection opened:", event);
updateConnectionStatus("open"); // Indicate open state
// Optionally, you could send an initial message here
};
ws.onmessage = function (event) {
console.log("WebSocket message received:", event.data);
};
ws.onerror = function (event) {
console.error("WebSocket error:", event);
updateConnectionStatus("closed"); // Indicate error state (treat as closed)
};
ws.onclose = function (event) {
if (event.wasClean) {
console.log(
`WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`,
);
updateConnectionStatus("closed"); // Indicate closed state
} else {
console.error("WebSocket connection died");
updateConnectionStatus("closed"); // Indicate closed state
}
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 1000);
};
}
// Function to send data over WebSocket
function sendWebSocketData(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("Sending data over WebSocket:", data);
ws.send(JSON.stringify(data));
} else {
console.error("WebSocket is not connected. Cannot send data:", data);
// You might want to queue messages or handle this in a different way
}
}
// Keep the post and get functions for now, they might still be useful
async function post(path, data) {
console.log(`POST to ${path}`, data);
// Fetch the JSON data from the /light endpoint
try {
const response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const response = await fetch("/light");
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error("Error during POST request:", error);
}
}
const lightData = await response.json();
async function get(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
// Map to store backend IDs and their corresponding components
const componentMap = new Map();
// Function to create and configure a light component
function createLightComponent(data, key) {
const lightComponent = document.createElement("light-component");
lightComponent.style.left = `${data.x}px`; // Set the x position
lightComponent.style.top = `${data.y}px`; // Set the y position
lightComponent.style.backgroundColor = data.settings?.color || "#4caf50"; // Set the background color
lightComponent.textContent = data.name || "Light Me Up!"; // Set the text content
// Set the lightId property
lightComponent.lightId = key; // Use the backend ID as the lightId
// Store the component in the map
componentMap.set(key, lightComponent);
// Append the light component to the container
appContainer.appendChild(lightComponent);
// Handle position change
lightComponent.addEventListener("position-change", (event) => {
const { lightId, x, y } = event.detail;
updatePositionOnServer(lightId, x, y);
});
// Handle color change
lightComponent.addEventListener("color-change", (event) => {
const { lightId, color } = event.detail;
sendColorToServer(lightId, color);
});
// Example: Add a click event listener to the light-component
lightComponent.addEventListener("click", () => {
console.log(`Light component clicked! ID: ${lightComponent.lightId}`);
});
}
// Iterate over the JSON data and create light components
for (const key in lightData) {
if (lightData.hasOwnProperty(key)) {
const light = lightData[key];
createLightComponent(light, key); // Pass the backend ID
}
}
// Function to send the updated position to the server via a PATCH request
async function updatePositionOnServer(componentId, x, y) {
try {
const response = await fetch(`/light/${componentId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ x, y }),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
console.log(
`Updated position for component ${componentId}: x=${x}, y=${y}`,
);
} catch (error) {
console.error("Error updating position on server:", error);
}
}
// Function to send the selected color to the server via WebSocket
function sendColorToServer(componentId, color) {
const websocket = getWebSocket();
const message = JSON.stringify({
componentId,
color: color,
});
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
console.log("Sent color to server:", message);
} else {
console.warn("WebSocket is not open. Unable to send color.");
}
}
return await response.json();
} catch (error) {
console.error("Error during GET request:", error);
console.error("Error fetching light data:", error);
}
}
async function updateColor(event) {
event.preventDefault();
clearTimeout(colorTimeout);
colorTimeout = setTimeout(function () {
const color = document.getElementById("color").value;
sendWebSocketData({ color1: color });
}, 500);
}
async function updateColor2(event) {
event.preventDefault();
clearTimeout(color2Timeout);
color2Timeout = setTimeout(function () {
const color = document.getElementById("color2").value;
sendWebSocketData({ color2: color });
}, 500);
}
async function updatePattern(pattern) {
sendWebSocketData({ pattern: pattern });
}
async function updateBrightness(event) {
event.preventDefault();
clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(function () {
const brightness = document.getElementById("brightness").value;
sendWebSocketData({ brightness: brightness });
}, 500);
}
async function updateDelay(event) {
event.preventDefault();
clearTimeout(delayTimeout);
delayTimeout = setTimeout(function () {
const delay = document.getElementById("delay").value;
sendWebSocketData({ delay: delay });
}, 500);
}
async function updateNumLeds(event) {
event.preventDefault();
const numLeds = document.getElementById("num_leds").value;
sendWebSocketData({ num_leds: parseInt(numLeds) });
}
async function updateName(event) {
event.preventDefault();
const name = document.getElementById("name").value;
sendWebSocketData({ name: name });
}
function createPatternButtons(patterns) {
const container = document.getElementById("pattern_buttons");
container.innerHTML = ""; // Clear previous buttons
patterns.forEach((pattern) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = pattern;
button.value = pattern;
button.addEventListener("click", async function (event) {
event.preventDefault();
await updatePattern(pattern);
});
container.appendChild(button);
});
}
document.addEventListener("DOMContentLoaded", async function () {
// Get the connection status element once the DOM is ready
connectionStatusElement = document.getElementById("connection-status");
// Establish WebSocket connection on page load
connectWebSocket();
document.getElementById("color").addEventListener("input", updateColor);
document.getElementById("color2").addEventListener("input", updateColor2);
document.getElementById("delay").addEventListener("input", updateDelay);
document
.getElementById("brightness")
.addEventListener("input", updateBrightness);
document
.getElementById("num_leds_form")
.addEventListener("submit", updateNumLeds);
document.getElementById("name_form").addEventListener("submit", updateName);
document.getElementById("delay").addEventListener("touchend", updateDelay);
document
.getElementById("brightness")
.addEventListener("touchend", updateBrightness);
document.querySelectorAll(".pattern_button").forEach((button) => {
console.log(button.value);
button.addEventListener("click", async (event) => {
event.preventDefault();
await updatePattern(button.value);
});
});
});
// Function to toggle the display of the settings menu
function selectSettings() {
const settingsMenu = document.getElementById("settings_menu");
controls = document.getElementById("controls");
settingsMenu.style.display = "block";
controls.style.display = "none";
}
function selectControls() {
const settingsMenu = document.getElementById("settings_menu");
controls = document.getElementById("controls");
settingsMenu.style.display = "none";
controls.style.display = "block";
}

20
src/static/styles.css Normal file
View File

@@ -0,0 +1,20 @@
/* Default styles for the light component */
light-component {
display: block;
width: 100px;
height: 100px;
background-color: #4caf50;
color: white;
text-align: center;
line-height: 100px;
cursor: grab;
position: absolute;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* Styles when the component is being dragged */
light-component:active {
cursor: grabbing;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}

26
src/static/websocket.js Normal file
View File

@@ -0,0 +1,26 @@
// websocket.js
let websocket = null;
export function getWebSocket() {
if (!websocket) {
// Replace 'ws://your-server-url' with your WebSocket server URL
websocket = new WebSocket(`ws://${window.location.host}/ws`);
// Handle WebSocket connection open
websocket.onopen = () => {
console.log("WebSocket connection established");
};
// Handle WebSocket connection close
websocket.onclose = () => {
console.log("WebSocket connection closed");
};
// Handle WebSocket errors
websocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
return websocket;
}