Non working upload button.

This commit is contained in:
retoor 2025-02-03 20:45:29 +01:00
parent fe707dca4e
commit b48a901e33
16 changed files with 1167 additions and 1015 deletions

View File

@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "Snek" name = "Snek"
version = "1.0.0" version = "1.0.0"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE", content-type="text/markdown" } #license = { file = "LICENSE", content-type="text/markdown" }
description = "Snek Chat Application by Molodetz" description = "Snek Chat Application by Molodetz"
authors = [ authors = [
{ name = "retoor", email = "retoor@molodetz.nl" } { name = "retoor", email = "retoor@molodetz.nl" }

View File

@ -1,347 +1,309 @@
// Written by retoor@molodetz.nl
// This project implements a client-server communication system using WebSockets and REST APIs.
// It features a chat system, a notification sound system, and interaction with server endpoints.
// No additional imports were used beyond standard JavaScript objects and constructors.
// MIT License
class RESTClient { class RESTClient {
debug = false debug = false;
async get(url, params) { async get(url, params = {}) {
params = params ? params : {}
const encodedParams = new URLSearchParams(params); const encodedParams = new URLSearchParams(params);
if (encodedParams) if (encodedParams) url += '?' + encodedParams;
url += '?' + encodedParams
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}); });
const result = await response.json() const result = await response.json();
if (this.debug) { if (this.debug) {
console.debug({ url: url, params: params, result: result }) console.debug({ url, params, result });
} }
return result return result;
} }
async post(url, data) { async post(url, data) {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
}); });
const result = await response.json() const result = await response.json();
if (this.debug) { if (this.debug) {
console.debug({ url: url, params: params, result: result }) console.debug({ url, data, result });
} }
return result return result;
} }
} }
const rest = new RESTClient()
class EventHandler { class EventHandler {
constructor() { constructor() {
this.subscribers = {} this.subscribers = {};
}
addEventListener(type, handler) {
if (!this.subscribers[type])
this.subscribers[type] = []
this.subscribers[type].push(handler)
}
emit(type, ...data) {
if (this.subscribers[type])
this.subscribers[type].forEach(handler => handler(...data))
} }
addEventListener(type, handler) {
if (!this.subscribers[type]) this.subscribers[type] = [];
this.subscribers[type].push(handler);
}
emit(type, ...data) {
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
}
} }
class Chat extends EventHandler { class Chat extends EventHandler {
constructor() { constructor() {
super() super();
this._url = window.location.hostname == 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws' this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
this._socket = null this._socket = null;
this._wait_connect = null this._waitConnect = null;
this._promises = {} this._promises = {};
} }
connect() { connect() {
if (this._wait_connect) if (this._waitConnect) {
return this._wait_connect return this._waitConnect;
}
const me = this return new Promise((resolve) => {
return new Promise(async (resolve, reject) => { this._waitConnect = resolve;
me._wait_connect = resolve console.debug("Connecting..");
console.debug("Connecting..")
try { try {
me._socket = new WebSocket(me._url) this._socket = new WebSocket(this._url);
} catch (e) { } catch (e) {
console.warning(e) console.warn(e);
setTimeout(() => { setTimeout(() => {
me.ensureConnection() this.ensureConnection();
},1000) }, 1000);
} }
me._socket.onconnect = () => { this._socket.onconnect = () => {
me._connected() this._connected();
me._wait_socket(me) this._waitSocket();
};
});
} }
})
}
generateUniqueId() { generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9); // Example: id-k5f9zq7 return 'id-' + Math.random().toString(36).substr(2, 9);
} }
call(method, ...args) {
const me = this
return new Promise(async (resolve, reject) => {
try {
const command = { method: method, args: args, message_id: me.generateUniqueId() }
me._promises[command.message_id] = resolve
await me._socket.send(JSON.stringify(command))
call(method, ...args) {
return new Promise((resolve, reject) => {
try {
const command = { method, args, message_id: this.generateUniqueId() };
this._promises[command.message_id] = resolve;
this._socket.send(JSON.stringify(command));
} catch (e) { } catch (e) {
reject(e) reject(e);
} }
}) });
} }
_connected() { _connected() {
const me = this
this._socket.onmessage = (event) => { this._socket.onmessage = (event) => {
const message = JSON.parse(event.data) const message = JSON.parse(event.data);
if (message.message_id && me._promises[message.message_id]) { if (message.message_id && this._promises[message.message_id]) {
me._promises[message.message_id](message) this._promises[message.message_id](message);
delete me._promises[message.message_id] delete this._promises[message.message_id];
} else { } else {
me.emit("message", me, message) this.emit("message", message);
}
//const room = this.rooms.find(room=>room.name == message.room)
//if(!room){
// this.rooms.push(new Room(message.room))
}
this._socket.onclose = (event) => {
me._wait_socket = null
me._socket = null
me.emit('close', me)
} }
};
this._socket.onclose = () => {
this._waitSocket = null;
this._socket = null;
this.emit('close');
};
} }
async privmsg(room, text) { async privmsg(room, text) {
await rest.post("/api/privmsg", { await rest.post("/api/privmsg", {
room: room, room,
text: text text,
}) });
} }
} }
class Socket extends EventHandler { class Socket extends EventHandler {
ws = null ws = null;
isConnected = null isConnected = null;
isConnecting = null isConnecting = null;
url = null url = null;
connectPromises = [] connectPromises = [];
ensureTimer = null ensureTimer = null;
constructor() { constructor() {
super() super();
this.url = window.location.hostname == 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws' this.url = window.location.hostname === 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws';
this.ensureConnection() this.ensureConnection();
} }
_camelToSnake(str) { _camelToSnake(str) {
return str return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
.replace(/([a-z])([A-Z])/g, '$1_$2')
.toLowerCase();
} }
get client() { get client() {
const me = this const me = this;
const proxy = new Proxy( return new Proxy({}, {
{}, get(_, prop) {
{
get(target, prop) {
return (...args) => { return (...args) => {
let functionName = me._camelToSnake(prop) const functionName = me._camelToSnake(prop);
return me.call(functionName, ...args); return me.call(functionName, ...args);
}; };
}, },
});
} }
);
return proxy
}
ensureConnection() { ensureConnection() {
if(this.ensureTimer) if (this.ensureTimer) {
return this.connect() return this.connect();
const me = this
this.ensureTimer = setInterval(()=>{
if (me.isConnecting)
me.isConnecting = false
me.connect()
},5000)
return this.connect()
} }
this.ensureTimer = setInterval(() => {
if (this.isConnecting) this.isConnecting = false;
this.connect();
}, 5000);
return this.connect();
}
generateUniqueId() { generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9); return 'id-' + Math.random().toString(36).substr(2, 9);
} }
connect() { connect() {
const me = this if (this.isConnected || this.isConnecting) {
if (!this.isConnected && !this.isConnecting) { return new Promise((resolve) => {
this.isConnecting = true this.connectPromises.push(resolve);
} else if (this.isConnecting) { if (!this.isConnected) resolve(this);
return new Promise((resolve, reject) => { });
me.connectPromises.push(resolve)
})
} else if (this.isConnected) {
return new Promise((resolve, reject) => {
resolve(me)
})
} }
return new Promise((resolve, reject) => { this.isConnecting = true;
me.connectPromises.push(resolve) return new Promise((resolve) => {
console.debug("Connecting..") this.connectPromises.push(resolve);
console.debug("Connecting..");
const ws = new WebSocket(this.url) const ws = new WebSocket(this.url);
ws.onopen = () => {
ws.onopen = (event) => { this.ws = ws;
me.ws = ws this.isConnected = true;
me.isConnected = true this.isConnecting = false;
me.isConnecting = false
ws.onmessage = (event) => { ws.onmessage = (event) => {
me.onData(JSON.parse(event.data)) this.onData(JSON.parse(event.data));
};
ws.onclose = () => {
this.onClose();
};
ws.onerror = () => {
this.onClose();
};
this.connectPromises.forEach(resolver => resolver(this));
};
});
} }
ws.onclose = (event) => {
me.onClose()
}
ws.onerror = (event)=>{
me.onClose()
}
me.connectPromises.forEach(resolve => {
resolve(me)
})
}
})
}
onData(data) { onData(data) {
if(data.success != undefined && !data.success){ if (data.success !== undefined && !data.success) {
console.error(data) console.error(data);
} }
if (data.callId) { if (data.callId) {
this.emit(data.callId, data.data) this.emit(data.callId, data.data);
} }
if (data.channel_uid) { if (data.channel_uid) {
this.emit(data.channel_uid, data.data) this.emit(data.channel_uid, data.data);
this.emit("channel-message", data) this.emit("channel-message", data);
}
} }
}
async sendJson(data) { async sendJson(data) {
return await this.connect().then((api) => { await this.connect().then(api => {
api.ws.send(JSON.stringify(data)) api.ws.send(JSON.stringify(data));
}) });
} }
async call(method, ...args) { async call(method, ...args) {
const call = { const call = {
callId: this.generateUniqueId(), callId: this.generateUniqueId(),
method: method, method,
args: args args,
};
return new Promise((resolve) => {
this.addEventListener(call.callId, data => resolve(data));
this.sendJson(call);
});
} }
const me = this
return new Promise(async (resolve, reject) => {
me.addEventListener(call.callId, (data) => {
resolve(data)
})
await me.sendJson(call)
})
}
onClose() { onClose() {
console.info("Connection lost. Reconnecting.") console.info("Connection lost. Reconnecting.");
this.isConnected = false this.isConnected = false;
this.isConnecting = false this.isConnecting = false;
this.ensureConnection().then(() => { this.ensureConnection().then(() => {
console.info("Reconnected.") console.info("Reconnected.");
}) });
} }
} }
class NotificationAudio { class NotificationAudio {
constructor(timeout){ constructor(timeout = 500) {
if(!timeout) this.schedule = new Schedule(timeout);
timeout = 500
this.schedule = new Schedule(timeout)
} }
sounds = ["/audio/soundfx.d_beep3.mp3"]
play(soundIndex) { sounds = ["/audio/soundfx.d_beep3.mp3"];
play(soundIndex = 0) {
this.schedule.delay(() => { this.schedule.delay(() => {
new Audio(this.sounds[soundIndex]).play()
if (!soundIndex)
soundIndex = 0
const player = new Audio(this.sounds[soundIndex]);
player.play()
.then(() => { .then(() => {
console.debug("Gave sound notification") console.debug("Gave sound notification");
}) })
.catch((error) => { .catch(error => {
console.error("Notification failed:", error); console.error("Notification failed:", error);
}); });
}) });
} }
} }
class App extends EventHandler { class App extends EventHandler {
rest = rest rest = new RESTClient();
ws = null ws = null;
rpc = null rpc = null;
audio = null audio = null;
user = {} user = {};
constructor() { constructor() {
super() super();
this.ws = new Socket() this.ws = new Socket();
this.rpc = this.ws.client this.rpc = this.ws.client;
const me = this this.audio = new NotificationAudio(500);
this.audio = new NotificationAudio(500)
this.ws.addEventListener("channel-message", (data) => { this.ws.addEventListener("channel-message", (data) => {
me.emit(data.channel_uid, data) this.emit(data.channel_uid, data);
}) });
this.rpc.getUser(null).then(user => { this.rpc.getUser(null).then(user => {
me.user = user this.user = user;
}) });
} }
playSound(index) { playSound(index) {
this.audio.play(index) this.audio.play(index);
} }
async benchMark(times, message) {
if (!times) async benchMark(times = 100, message = "Benchmark Message") {
times = 100 const promises = [];
if (!message)
message = "Benchmark Message"
let promises = []
const me = this
for (let i = 0; i < times; i++) { for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => { promises.push(this.rpc.getChannels().then(channels => {
channels.forEach(channel => { channels.forEach(channel => {
me.rpc.sendMessage(channel.uid, `${message} ${i}`).then(data => { this.rpc.sendMessage(channel.uid, `${message} ${i}`);
});
}) }));
}) }
}))
} }
//return await Promise.all(promises)
} }
const app = new App();
}
const app = new App()

View File

@ -209,7 +209,7 @@ message-list {
resize: none; resize: none;
} }
.chat-input button { .chat-input upload-button {
background-color: #f05a28; background-color: #f05a28;
color: white; color: white;
border: none; border: none;
@ -240,11 +240,16 @@ message-list {
max-width: 100%; max-width: 100%;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre-wrap;
hyphens: auto; hyphens: auto;
img { img {
max-width: 90%; max-width: 90%;
border-radius: 20px; border-radius: 20px;
} }
{
padding: 0;
margin: 0;
}
} }
.avatar { .avatar {
opacity: 0; opacity: 0;

View File

@ -1,63 +1,59 @@
// Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element for a chat input widget, featuring a text area and an upload button. It handles user input and triggers events for input changes and message submission.
// Includes standard DOM manipulation methods; no external imports used.
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License.
class ChatInputElement extends HTMLElement { class ChatInputElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.component = document.createElement('div'); this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
const me = this const link = document.createElement('link');
const link = document.createElement("link") link.rel = 'stylesheet';
link.rel = 'stylesheet' link.href = '/base.css';
link.href = '/base.css' this.component.appendChild(link);
this.component.appendChild(link)
this.container = document.createElement('div') this.container = document.createElement('div');
this.container.classList.add("chat-input") this.container.classList.add('chat-input');
this.container.innerHTML = ` this.container.innerHTML = `
<textarea placeholder="Type a message..." rows="2"></textarea> <textarea placeholder="Type a message..." rows="2"></textarea>
<button>Send</button> <upload-button></upload-button>
`; `;
this.textBox = this.container.querySelector('textarea') this.textBox = this.container.querySelector('textarea');
this.textBox.addEventListener('input', (e) => { this.textBox.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent("input", { detail: e.target.value, bubbles: true })) this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));
const message = e.target.value; const message = e.target.value;
const button = this.container.querySelector('button'); const button = this.container.querySelector('button');
button.disabled = !message; button.disabled = !message;
}) });
this.textBox.addEventListener('change', (e) => { this.textBox.addEventListener('change', (e) => {
e.preventDefault() e.preventDefault();
this.dispatchEvent(new CustomEvent("change", { detail: e.target.value, bubbles: true })) this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
console.error(e.target.value) console.error(e.target.value);
}) });
this.textBox.addEventListener('keydown', (e) => { this.textBox.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key == 'Enter') { e.preventDefault();
if(!e.shiftKey){
e.preventDefault()
const message = e.target.value.trim(); const message = e.target.value.trim();
if(!message) if (!message) return;
return this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
this.dispatchEvent(new CustomEvent("submit", { detail: message, bubbles: true })) e.target.value = '';
e.target.value = ''
} }
} });
})
this.container.querySelector('button').addEventListener('click', (e) => { this.component.appendChild(this.container);
}
}
const message = me.textBox.value.trim();
if(!message){
return
}
this.dispatchEvent(new CustomEvent("submit", { detail: me.textBox.value, bubbles: true }))
setTimeout(()=>{
me.textBox.value = ''
me.textBox.focus()
},200)
})
this.component.appendChild(this.container)
}
}
customElements.define('chat-input', ChatInputElement); customElements.define('chat-input', ChatInputElement);

View File

@ -1,78 +1,78 @@
// Written by retoor@molodetz.nl
// This code defines a custom HTML element called ChatWindowElement that provides a chat interface within a shadow DOM, handling connection callbacks, displaying messages, and user interactions.
// No external imports or includes other than standard DOM and HTML elements.
// The MIT License (MIT)
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class ChatWindowElement extends HTMLElement { class ChatWindowElement extends HTMLElement {
receivedHistory = false receivedHistory = false;
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.component = document.createElement('section'); this.component = document.createElement('section');
this.app = app this.app = app;
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
get user() { get user() {
return this.app.user return this.app.user;
} }
async connectedCallback() { async connectedCallback() {
const link = document.createElement('link') const link = document.createElement('link');
link.rel = 'stylesheet' link.rel = 'stylesheet';
link.href = '/base.css' link.href = '/base.css';
this.component.appendChild(link) this.component.appendChild(link);
this.component.classList.add("chat-area") this.component.classList.add("chat-area");
this.container = document.createElement("section")
this.container.classList.add("chat-area")
this.container.classList.add("chat-window")
const chatHeader = document.createElement("div") this.container = document.createElement("section");
chatHeader.classList.add("chat-header") this.container.classList.add("chat-area", "chat-window");
const chatHeader = document.createElement("div");
chatHeader.classList.add("chat-header");
const chatTitle = document.createElement('h2');
chatTitle.classList.add("chat-title");
chatTitle.innerText = "Loading...";
chatHeader.appendChild(chatTitle);
this.container.appendChild(chatHeader);
const channels = await app.rpc.getChannels();
const channel = channels[0];
chatTitle.innerText = channel.name;
const chatTitle = document.createElement('h2') const channelElement = document.createElement('message-list');
chatTitle.classList.add("chat-title") channelElement.setAttribute("channel", channel.uid);
chatTitle.innerText = "Loading..." this.container.appendChild(channelElement);
chatHeader.appendChild(chatTitle)
this.container.appendChild(chatHeader)
const channels = await app.rpc.getChannels()
const channel = channels[0]
chatTitle.innerText = channel.name
const channelElement = document.createElement('message-list')
channelElement.setAttribute("channel", channel.uid)
//channelElement.classList.add("chat-messages")
this.container.appendChild(channelElement)
//const tileElement = document.createElement('tile-grid')
//tileElement.classList.add("message-list")
//this.container.appendChild(tileElement)
//const uploadButton = document.createElement('upload-button')
//uploadButton.grid = tileElement
//uploadButton.setAttribute('grid', "#grid")
//this.container.appendChild(uploadButton)
const chatInput = document.createElement('chat-input')
const chatInput = document.createElement('chat-input');
chatInput.addEventListener("submit", (e) => { chatInput.addEventListener("submit", (e) => {
app.rpc.sendMessage(channel.uid,e.detail) app.rpc.sendMessage(channel.uid, e.detail);
}) });
this.container.appendChild(chatInput) this.container.appendChild(chatInput);
this.component.appendChild(this.container) this.component.appendChild(this.container);
const messages = await app.rpc.getMessages(channel.uid)
const messages = await app.rpc.getMessages(channel.uid);
messages.forEach(message => { messages.forEach(message => {
if(!message['user_nick']) if (!message['user_nick']) return;
return channelElement.addMessage(message);
channelElement.addMessage(message) });
})
const me = this const me = this;
channelElement.addEventListener("message", (message) => { channelElement.addEventListener("message", (message) => {
if(me.user.uid != message.detail.user_uid) if (me.user.uid !== message.detail.user_uid) app.playSound(0);
app.playSound(0) message.detail.element.scrollIntoView();
message.detail.element.scrollIntoView() });
})
} }
} }
customElements.define('chat-window', ChatWindowElement); customElements.define('chat-window', ChatWindowElement);

View File

@ -1,25 +1,28 @@
// Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
// MIT License
class FancyButton extends HTMLElement { class FancyButton extends HTMLElement {
url = null
type="button"
value = null
constructor() { constructor() {
super() super();
this.attachShadow({mode:'open'}) this.attachShadow({ mode: 'open' });
this.url = null;
this.type = "button";
this.value = null;
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement('span');
let size = this.getAttribute('size');
console.info({ GG: size });
size = size === 'auto' ? '1%' : '33%';
this.container = document.createElement('span') this.styleElement = document.createElement("style");
let size = this.getAttribute('size')
console.info({GG:size})
if(size == 'auto'){
size = '1%'
}else{
size = '33%'
}
this.styleElement = document.createElement("style")
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
:root { :root {
width: 100%; width: 100%;
@ -37,7 +40,6 @@ class FancyButton extends HTMLElement {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
border: 1px solid #f05a28; border: 1px solid #f05a28;
} }
button:hover { button:hover {
@ -45,24 +47,24 @@ class FancyButton extends HTMLElement {
background-color: #e04924; background-color: #e04924;
border: 1px solid #efefef; border: 1px solid #efefef;
} }
` `;
this.container.appendChild(this.styleElement)
this.buttonElement = document.createElement('button') this.container.appendChild(this.styleElement);
this.container.appendChild(this.buttonElement) this.buttonElement = document.createElement('button');
this.shadowRoot.appendChild(this.container) this.container.appendChild(this.buttonElement);
this.shadowRoot.appendChild(this.container);
this.url = this.getAttribute('url'); this.url = this.getAttribute('url');
this.value = this.getAttribute('value') this.value = this.getAttribute('value');
const me = this this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")))
this.buttonElement.addEventListener("click", () => { this.buttonElement.addEventListener("click", () => {
if(me.url == "/back" || me.url == "/back/"){ if (this.url === "/back" || this.url === "/back/") {
window.history.back() window.history.back();
}else if(me.url){ } else if (this.url) {
window.location = me.url window.location = this.url;
} }
}) });
} }
} }
customElements.define("fancy-button",FancyButton) customElements.define("fancy-button", FancyButton);

View File

@ -1,44 +1,70 @@
// Written by retoor@molodetz.nl
// This code defines two custom HTML elements, `GenericField` and `GenericForm`. The `GenericField` element represents a form field with validation and styling functionalities, and the `GenericForm` fetches and manages form data, handling field validation and submission.
// No external imports are present; all utilized functionality is native to JavaScript and web APIs.
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
class GenericField extends HTMLElement { class GenericField extends HTMLElement {
form = null form = null;
field = null field = null;
inputElement = null inputElement = null;
footerElement = null footerElement = null;
action = null action = null;
container = null container = null;
styleElement = null styleElement = null;
name = null name = null;
get value() {
return this.inputElement.value
}
get type() {
return this.field.tag get value() {
return this.inputElement.value;
} }
get type() {
return this.field.tag;
}
set value(val) { set value(val) {
val = val == null ? '' : val val = val ?? '';
this.inputElement.value = val this.inputElement.value = val;
this.inputElement.setAttribute("value", val) this.inputElement.setAttribute("value", val);
} }
setInvalid() { setInvalid() {
this.inputElement.classList.add("error") this.inputElement.classList.add("error");
this.inputElement.classList.remove("valid") this.inputElement.classList.remove("valid");
} }
setErrors(errors) { setErrors(errors) {
if(errors.length) const errorText = errors.length ? errors[0] : "";
this.inputElement.setAttribute("title", errors[0]) this.inputElement.setAttribute("title", errorText);
else
this.inputElement.setAttribute("title","")
} }
setValid() { setValid() {
this.inputElement.classList.remove("error") this.inputElement.classList.remove("error");
this.inputElement.classList.add("valid") this.inputElement.classList.add("valid");
} }
constructor() { constructor() {
super() super();
this.attachShadow({mode:'open'}) this.attachShadow({ mode: 'open' });
this.container = document.createElement('div') this.container = document.createElement('div');
this.styleElement = document.createElement('style') this.styleElement = document.createElement('style');
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
h1 { h1 {
@ -93,101 +119,101 @@ class GenericField extends HTMLElement {
a:hover { a:hover {
color: #e04924; color: #e04924;
} }
.valid { .valid {
border: 1px solid green; border: 1px solid green;
color: green; color: green;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
.error { .error {
border: 3px solid red; border: 3px solid red;
color: #d8000c; color: #d8000c;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
@media (max-width: 500px) { @media (max-width: 500px) {
input { input {
width: 90%; width: 90%;
} }
} }
`;
this.container.appendChild(this.styleElement);
` this.shadowRoot.appendChild(this.container);
this.container.appendChild(this.styleElement)
this.shadowRoot.appendChild(this.container)
} }
connectedCallback() { connectedCallback() {
this.updateAttributes();
this.updateAttributes()
} }
setAttribute(name, value) { setAttribute(name, value) {
this[name] = value this[name] = value;
} }
updateAttributes() { updateAttributes() {
if (this.inputElement == null && this.field) { if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag) this.inputElement = document.createElement(this.field.tag);
if(this.field.tag == 'button'){ if (this.field.tag === 'button' && this.field.value === "submit") {
if(this.field.value == "submit"){ this.action = this.field.value;
} }
this.action = this.field.value this.inputElement.name = this.field.name;
} this.name = this.inputElement.name;
this.inputElement.name = this.field.name
this.name = this.inputElement.name const me = this;
const me = this
this.inputElement.addEventListener("keyup", (e) => { this.inputElement.addEventListener("keyup", (e) => {
if(e.key == 'Enter'){ if (e.key === 'Enter') {
me.dispatchEvent(new Event("submit")) me.dispatchEvent(new Event("submit"));
}else if(me.field.value != e.target.value) } else if (me.field.value !== e.target.value) {
{ const event = new CustomEvent("change", { detail: me, bubbles: true });
const event = new CustomEvent("change", {detail:me,bubbles:true}) me.dispatchEvent(event);
me.dispatchEvent(event)
} }
}) });
this.inputElement.addEventListener("click",(e)=>{
const event = new CustomEvent("click",{detail:me,bubbles:true})
me.dispatchEvent(event)
})
this.container.appendChild(this.inputElement)
this.inputElement.addEventListener("click", (e) => {
const event = new CustomEvent("click", { detail: me, bubbles: true });
me.dispatchEvent(event);
});
this.container.appendChild(this.inputElement);
} }
if (!this.field) { if (!this.field) {
return return;
} }
this.inputElement.setAttribute("type",this.field.type == null ? 'input' : this.field.type)
this.inputElement.setAttribute("name",this.field.name == null ? '' : this.field.name) this.inputElement.setAttribute("type", this.field.type ?? 'input');
this.inputElement.setAttribute("name", this.field.name ?? '');
if (this.field.text != null) { if (this.field.text != null) {
this.inputElement.innerText = this.field.text this.inputElement.innerText = this.field.text;
} }
if (this.field.html != null) { if (this.field.html != null) {
this.inputElement.innerHTML = this.field.html this.inputElement.innerHTML = this.field.html;
} }
if (this.field.class_name) { if (this.field.class_name) {
this.inputElement.classList.add(this.field.class_name) this.inputElement.classList.add(this.field.class_name);
} }
this.inputElement.setAttribute("tabindex", this.field.index) this.inputElement.setAttribute("tabindex", this.field.index);
this.inputElement.classList.add(this.field.name) this.inputElement.classList.add(this.field.name);
this.value = this.field.value this.value = this.field.value;
let place_holder = null
if(this.field.place_holder) let place_holder = this.field.place_holder ?? null;
place_holder = this.field.place_holder
if (this.field.required && place_holder) { if (this.field.required && place_holder) {
place_holder = place_holder place_holder = "* " + place_holder;
}
this.inputElement.setAttribute("placeholder", place_holder);
if (this.field.required) {
this.inputElement.setAttribute("required", "required");
} else {
this.inputElement.removeAttribute("required");
} }
if(place_holder)
this.field.place_holder = "* " + place_holder
this.inputElement.setAttribute("placeholder",place_holder)
if(this.field.required)
this.inputElement.setAttribute("required","required")
else
this.inputElement.removeAttribute("required")
if (!this.footerElement) { if (!this.footerElement) {
this.footerElement = document.createElement('div') this.footerElement = document.createElement('div');
this.footerElement.style.clear = 'both' this.footerElement.style.clear = 'both';
this.container.appendChild(this.footerElement) this.container.appendChild(this.footerElement);
} }
} }
} }
@ -195,56 +221,52 @@ class GenericField extends HTMLElement {
customElements.define('generic-field', GenericField); customElements.define('generic-field', GenericField);
class GenericForm extends HTMLElement { class GenericForm extends HTMLElement {
fields = {} fields = {};
form = {} form = {};
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.styleElement = document.createElement("style") this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
width:90% width: 90%;
} }
div { div {
background-color: #0f0f0f; background-color: #0f0f0f;
border-radius: 10px; border-radius: 10px;
padding: 30px; padding: 30px;
width: 400px; width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center; text-align: center;
} }
@media (max-width: 500px) { @media (max-width: 500px) {
width: 100%; width: 100%;
height: 100%; height: 100%;
form { form {
height: 100%; height: 100%;
width: 100%;
width: 80%; width: 80%;
} }
}` }`;
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.appendChild(this.styleElement) this.container.appendChild(this.styleElement);
this.container.classList.add("generic-form-container") this.container.classList.add("generic-form-container");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
const url = this.getAttribute('url'); const url = this.getAttribute('url');
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get") const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if(!url.startsWith("/")) if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url) fullUrl.searchParams.set('url', url);
}
this.loadForm(fullUrl.toString()); this.loadForm(fullUrl.toString());
} else { } else {
this.container.textContent = "No URL provided!"; this.container.textContent = "No URL provided!";
@ -252,95 +274,89 @@ class GenericForm extends HTMLElement {
} }
async loadForm(url) { async loadForm(url) {
const me = this
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
} }
me.form = await response.json(); this.form = await response.json();
let fields = Object.values(me.form.fields) let fields = Object.values(this.form.fields);
fields = fields.sort((a,b)=>{ fields.sort((a, b) => a.index - b.index);
console.info(a.index,b.index)
return a.index - b.index
})
fields.forEach(field => { fields.forEach(field => {
const fieldElement = document.createElement('generic-field') const fieldElement = document.createElement('generic-field');
me.fields[field.name] = fieldElement this.fields[field.name] = fieldElement;
fieldElement.setAttribute("form", me) fieldElement.setAttribute("form", this);
fieldElement.setAttribute("field", field) fieldElement.setAttribute("field", field);
me.container.appendChild(fieldElement) this.container.appendChild(fieldElement);
fieldElement.updateAttributes() fieldElement.updateAttributes();
fieldElement.addEventListener("change",(e)=>{
me.form.fields[e.detail.name].value = e.detail.value
})
fieldElement.addEventListener("click",async (e)=>{
if(e.detail.type == "button"){
if(e.detail.value == "submit")
{
const isValid = await me.validate()
if(isValid){
const saveResult = await me.submit()
if(saveResult.redirect_url){
window.location.pathname = saveResult.redirect_url
}
}
}
}
}) fieldElement.addEventListener("change", (e) => {
}) this.form.fields[e.detail.name].value = e.detail.value;
});
fieldElement.addEventListener("click", async (e) => {
if (e.detail.type === "button" && e.detail.value === "submit") {
const isValid = await this.validate();
if (isValid) {
const saveResult = await this.submit();
if (saveResult.redirect_url) {
window.location.pathname = saveResult.redirect_url;
}
}
}
});
});
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
} }
} }
async validate() { async validate() {
const url = this.getAttribute("url") const url = this.getAttribute("url");
const me = this
let response = await fetch(url, { let response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({"action":"validate", "form":me.form}) body: JSON.stringify({ "action": "validate", "form": this.form })
}); });
const form = await response.json();
const form = await response.json()
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach(field => {
if(!me.form.fields[field.name]) if (!this.form.fields[field.name]) {
return return;
me.form.fields[field.name].is_valid = field.is_valid }
this.form.fields[field.name].is_valid = field.is_valid;
if (!field.is_valid) { if (!field.is_valid) {
me.fields[field.name].setInvalid() this.fields[field.name].setInvalid();
me.fields[field.name].setErrors(field.errors) this.fields[field.name].setErrors(field.errors);
} else { } else {
me.fields[field.name].setValid() this.fields[field.name].setValid();
} }
me.fields[field.name].setAttribute("field",field) this.fields[field.name].setAttribute("field", field);
me.fields[field.name].updateAttributes() this.fields[field.name].updateAttributes();
}) });
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach(field => {
me.fields[field.name].setErrors(field.errors) this.fields[field.name].setErrors(field.errors);
}) });
return form['is_valid'] return form['is_valid'];
} }
async submit() { async submit() {
const me = this const url = this.getAttribute("url");
const url = me.getAttribute("url")
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({"action":"submit", "form":me.form}) body: JSON.stringify({ "action": "submit", "form": this.form })
}); });
return await response.json() return await response.json();
}
} }
}
customElements.define('generic-form', GenericForm); customElements.define('generic-form', GenericForm);

View File

@ -1,3 +1,11 @@
// Written by retoor@molodetz.nl
// The following JavaScript code defines a custom HTML element `<html-frame>` that loads and displays HTML content from a specified URL. If the URL is provided as a markdown file, it attempts to render it as HTML.
// Uses the `HTMLElement` class and the methods `attachShadow`, `createElement`, `fetch`, and `define` to extend and manipulate HTML elements.
// MIT License
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
super(); super();
@ -7,16 +15,16 @@ class HTMLFrame extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this.container.classList.add("html_frame") this.container.classList.add("html_frame");
let url = this.getAttribute('url'); let url = this.getAttribute('url');
if (!url.startsWith("https")) { if (!url.startsWith("https")) {
url = "https://" + url url = "https://" + url;
} }
if (url) { if (url) {
let fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get") const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) {
if(!url.startsWith("/")) fullUrl.searchParams.set('url', url);
fullUrl.searchParams.set('url', url) }
this.loadAndRender(fullUrl.toString()); this.loadAndRender(fullUrl.toString());
} else { } else {
this.container.textContent = "No source URL!"; this.container.textContent = "No source URL!";
@ -31,17 +39,16 @@ class HTMLFrame extends HTMLElement {
} }
const html = await response.text(); const html = await response.text();
if (url.endsWith(".md")) { if (url.endsWith(".md")) {
const parent = this const markdownElement = document.createElement('div');
const markdownElement = document.createElement('div') markdownElement.innerHTML = html;
markdownElement.innerHTML = html this.outerHTML = html;
this.outerHTML = html
} else { } else {
this.container.innerHTML = html; this.container.innerHTML = html;
} }
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
} }
} }
} }
customElements.define('html-frame', HTMLFrame); customElements.define('html-frame', HTMLFrame);

View File

@ -1,5 +1,13 @@
// Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element <markdown-frame> that fetches and loads content from a specified URL into a shadow DOM.
// Utilizes built-in JavaScript functionalities and fetching APIs. No external libraries or imports are used.
// The MIT License (MIT)
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
@ -10,15 +18,16 @@ class HTMLFrame extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this.container.classList.add("html_frame") this.container.classList.add('html_frame');
const url = this.getAttribute('url'); const url = this.getAttribute('url');
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get") const fullUrl = url.startsWith('/')
if(!url.startsWith("/")) ? window.location.origin + url
fullUrl.searchParams.set('url', url) : new URL(window.location.origin + '/http-get');
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url);
this.loadAndRender(fullUrl.toString()); this.loadAndRender(fullUrl.toString());
} else { } else {
this.container.textContent = "No source URL!"; this.container.textContent = 'No source URL!';
} }
} }
@ -30,10 +39,10 @@ class HTMLFrame extends HTMLElement {
} }
const html = await response.text(); const html = await response.text();
this.container.innerHTML = html; this.container.innerHTML = html;
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
} }
} }
} }
customElements.define('markdown-frame', HTMLFrame); customElements.define('markdown-frame', HTMLFrame);

View File

@ -1,18 +1,32 @@
class TileGridElement extends HTMLElement { // Written by retoor@molodetz.nl
// This code defines custom web components to create and interact with a tile grid system for displaying images, along with an upload button to facilitate image additions.
// No external libraries or dependencies are used other than standard web components.
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class TileGridElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.gridId = this.getAttribute('grid'); this.gridId = this.getAttribute('grid');
this.component = document.createElement('div'); this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component) this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
console.log('connected'); console.log('connected');
this.styleElement = document.createElement('style'); this.styleElement = document.createElement('style');
this.styleElement.innerText = ` this.styleElement.textContent = `
.grid { .grid {
padding: 10px; padding: 10px;
display: flex; display: flex;
@ -32,13 +46,13 @@ class TileGridElement extends HTMLElement {
.grid .tile:hover { .grid .tile:hover {
transform: scale(1.1); transform: scale(1.1);
} }
`; `;
this.component.appendChild(this.styleElement); this.component.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('gallery'); this.container.classList.add('gallery');
this.component.appendChild(this.container); this.component.appendChild(this.container);
} }
addImage(src) { addImage(src) {
const item = document.createElement('img'); const item = document.createElement('img');
item.src = src; item.src = src;
@ -47,14 +61,15 @@ class TileGridElement extends HTMLElement {
item.style.height = '100px'; item.style.height = '100px';
this.container.appendChild(item); this.container.appendChild(item);
} }
addImages(srcs) { addImages(srcs) {
srcs.forEach(src => this.addImage(src)); srcs.forEach(src => this.addImage(src));
} }
addElement(element) { addElement(element) {
element.cclassList.add('tile'); element.classList.add('tile');
this.container.appendChild(element); this.container.appendChild(element);
} }
} }
class UploadButton extends HTMLElement { class UploadButton extends HTMLElement {
@ -62,23 +77,23 @@ class UploadButton extends HTMLElement {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.component = document.createElement('div'); this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
this.shadowRoot.appendChild(this.component) window.u = this;
window.u = this
} }
get gridSelector() { get gridSelector() {
return this.getAttribute('grid'); return this.getAttribute('grid');
} }
grid = null grid = null;
addImages(urls) { addImages(urls) {
this.grid.addImages(urls); this.grid.addImages(urls);
} }
connectedCallback()
{ connectedCallback() {
console.log('connected'); console.log('connected');
this.styleElement = document.createElement('style'); this.styleElement = document.createElement('style');
this.styleElement.innerHTML = ` this.styleElement.textContent = `
.upload-button { .upload-button {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -112,7 +127,6 @@ class UploadButton extends HTMLElement {
const files = e.target.files; const files = e.target.files;
const urls = []; const urls = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
urls.push(e.target.result); urls.push(e.target.result);
@ -120,7 +134,7 @@ class UploadButton extends HTMLElement {
this.addImages(urls); this.addImages(urls);
} }
}; };
reader.readAsDataURL(file); reader.readAsDataURL(files[i]);
} }
}); });
const label = document.createElement('label'); const label = document.createElement('label');
@ -131,37 +145,31 @@ class UploadButton extends HTMLElement {
} }
customElements.define('upload-button', UploadButton); customElements.define('upload-button', UploadButton);
customElements.define('tile-grid', TileGridElement); customElements.define('tile-grid', TileGridElement);
class MeniaUploadElement extends HTMLElement { class MeniaUploadElement extends HTMLElement {
constructor(){ constructor(){
super() super();
this.attachShadow({mode:'open'}) this.attachShadow({ mode: 'open' });
this.component = document.createElement("div") this.component = document.createElement("div");
alert('aaaa') alert('aaaa');
this.shadowRoot.appendChild(this.component) this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement("div");
this.container = document.createElement("div") this.component.style.height = '100%';
this.component.style.height = '100%'
this.component.style.backgroundColor = 'blue'; this.component.style.backgroundColor = 'blue';
this.shadowRoot.appendChild(this.container) this.shadowRoot.appendChild(this.container);
this.tileElement = document.createElement("tile-grid") this.tileElement = document.createElement("tile-grid");
this.tileElement.style.backgroundColor = 'red' this.tileElement.style.backgroundColor = 'red';
this.tileElement.style.height = '100%' this.tileElement.style.height = '100%';
this.component.appendChild(this.tileElement) this.component.appendChild(this.tileElement);
this.uploadButton = document.createElement('upload-button') this.uploadButton = document.createElement('upload-button');
this.component.appendChild(this.uploadButton) this.component.appendChild(this.uploadButton);
}
// const mediaUpload = document.createElement('media-upload')
//this.component.appendChild(mediaUpload)
} }
} customElements.define('menia-upload', MeniaUploadElement);
customElements.define('menia-upload', MeniaUploadElement)

View File

@ -1,23 +1,44 @@
// Written by retoor@molodetz.nl
// This JavaScript source code defines a custom HTML element named "message-list-manager" to manage a list of message lists for different channels obtained asynchronously.
//
//
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
class MessageListManagerElement extends HTMLElement { class MessageListManagerElement extends HTMLElement {
constructor() { constructor() {
super() super();
this.attachShadow({mode:'open'}) this.attachShadow({ mode: 'open' });
this.container = document.createElement("div") this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container) this.shadowRoot.appendChild(this.container);
} }
async connectedCallback() { async connectedCallback() {
let channels = await app.rpc.getChannels() const channels = await app.rpc.getChannels();
const me = this
channels.forEach(channel => { channels.forEach(channel => {
const messageList = document.createElement("message-list") const messageList = document.createElement("message-list");
messageList.setAttribute("channel",channel.uid) messageList.setAttribute("channel", channel.uid);
me.container.appendChild(messageList) this.container.appendChild(messageList);
}) });
}
} }
} customElements.define("message-list-manager", MessageListManagerElement);
customElements.define("message-list-manager",MessageListManagerElement)

View File

@ -1,115 +1,113 @@
// Written by retoor@molodetz.nl
// This class defines a custom HTML element that displays a list of messages with avatars and timestamps. It handles message addition with a delay in event dispatch and ensures the display of messages in the correct format.
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
class MessageListElement extends HTMLElement { class MessageListElement extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ["messages"]; return ["messages"];
} }
messages = []
room = null messages = [];
url = null room = null;
container = null url = null;
messageEventSchedule = null container = null;
observer = null messageEventSchedule = null;
observer = null;
constructor() { constructor() {
super() super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.component = document.createElement('div') this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component) this.shadowRoot.appendChild(this.component);
} }
linkifyText(text) { linkifyText(text) {
const urlRegex = /https?:\/\/[^\s]+/g; const urlRegex = /https?:\/\/[^\s]+/g;
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
return text.replace(urlRegex, (url) => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
} }
timeAgo(date1, date2) { timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1); const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000); const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) { if (days) {
if (days > 1) return `${days} ${days > 1 ? 'days' : 'day'} ago`;
return `${days} days ago`
else
return `${days} day ago`
} }
if (hours) { if (hours) {
if (hours > 1) return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
return `${hours} hours ago` }
else if (minutes) {
return `${hours} hour ago` return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
}
return 'just now';
} }
if (minutes)
if (minutes > 1)
return `${minutes} minutes ago`
else
return `${minutes} minute ago`
return `just now`
}
timeDescription(isoDate) { timeDescription(isoDate) {
const date = new Date(isoDate) const date = new Date(isoDate);
const hours = String(date.getHours()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0");
let timeStr = `${hours}:${minutes}` let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
timeStr += ", " + this.timeAgo(new Date(isoDate), Date.now()) return timeStr;
return timeStr
} }
createElement(message) { createElement(message) {
const element = document.createElement("div") const element = document.createElement("div");
element.dataset.uid = message.uid element.dataset.uid = message.uid;
element.dataset.color = message.color element.dataset.color = message.color;
element.dataset.channel_uid = message.channel_uid element.dataset.channel_uid = message.channel_uid;
element.dataset.user_nick = message.user_nick element.dataset.user_nick = message.user_nick;
element.dataset.created_at = message.created_at element.dataset.created_at = message.created_at;
element.dataset.user_uid = message.user_uid element.dataset.user_uid = message.user_uid;
element.dataset.message = message.message element.dataset.message = message.message;
element.classList.add("message") element.classList.add("message");
if (!this.messages.length) { if (!this.messages.length || this.messages[this.messages.length - 1].user_uid != message.user_uid) {
element.classList.add("switch-user") element.classList.add("switch-user");
} else if (this.messages[this.messages.length - 1].user_uid != message.user_uid) {
element.classList.add("switch-user")
} }
const avatar = document.createElement("div")
avatar.classList.add("avatar")
avatar.style.backgroundColor = message.color
avatar.style.color = "black"
avatar.innerText = message.user_nick[0]
const messageContent = document.createElement("div")
messageContent.classList.add("message-content")
const author = document.createElement("div")
author.classList.add("author")
author.style.color = message.color
author.textContent = message.user_nick
const text = document.createElement("div")
text.classList.add("text")
if (message.html)
text.innerHTML = message.html
const time = document.createElement("div")
time.classList.add("time")
time.dataset.created_at = message.created_at
messageContent.appendChild(author)
time.textContent = this.timeDescription(message.created_at)
messageContent.appendChild(text)
messageContent.appendChild(time)
element.appendChild(avatar)
element.appendChild(messageContent)
const avatar = document.createElement("div");
avatar.classList.add("avatar");
avatar.style.backgroundColor = message.color;
avatar.style.color = "black";
avatar.innerText = message.user_nick[0];
const messageContent = document.createElement("div");
messageContent.classList.add("message-content");
const author = document.createElement("div");
author.classList.add("author");
author.style.color = message.color;
author.textContent = message.user_nick;
message.element = element const text = document.createElement("div");
text.classList.add("text");
if (message.html) text.innerHTML = message.html;
return element const time = document.createElement("div");
time.classList.add("time");
time.dataset.created_at = message.created_at;
time.textContent = this.timeDescription(message.created_at);
messageContent.appendChild(author);
messageContent.appendChild(text);
messageContent.appendChild(time);
element.appendChild(avatar);
element.appendChild(messageContent);
message.element = element;
return element;
} }
addMessage(message) { addMessage(message) {
const obj = new models.Message( const obj = new models.Message(
message.uid, message.uid,
message.channel_uid, message.channel_uid,
@ -120,51 +118,51 @@ class MessageListElement extends HTMLElement {
message.html, message.html,
message.created_at, message.created_at,
message.updated_at message.updated_at
) );
const element = this.createElement(obj)
this.messages.push(obj) const element = this.createElement(obj);
this.container.appendChild(element) this.messages.push(obj);
const me = this this.container.appendChild(element);
this.messageEventSchedule.delay(() => { this.messageEventSchedule.delay(() => {
me.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true })) this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
});
}) return obj;
return obj
} }
scrollBottom() { scrollBottom() {
this.container.scrollTop = this.container.scrollHeight; this.container.scrollTop = this.container.scrollHeight;
} }
connectedCallback() { connectedCallback() {
const link = document.createElement('link') const link = document.createElement('link');
link.rel = 'stylesheet' link.rel = 'stylesheet';
link.href = '/base.css' link.href = '/base.css';
this.component.appendChild(link) this.component.appendChild(link);
this.component.classList.add("chat-messages") this.component.classList.add("chat-messages");
this.container = document.createElement('div')
//this.container.classList.add("chat-messages") this.container = document.createElement('div');
this.component.appendChild(this.container) this.component.appendChild(this.container);
this.messageEventSchedule = new Schedule(500)
this.messages = [] this.messageEventSchedule = new Schedule(500);
this.channel_uid = this.getAttribute("channel") this.messages = [];
const me = this this.channel_uid = this.getAttribute("channel");
app.addEventListener(this.channel_uid, (data) => { app.addEventListener(this.channel_uid, (data) => {
me.addMessage(data) this.addMessage(data);
}) });
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }))
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
this.timeUpdateInterval = setInterval(() => { this.timeUpdateInterval = setInterval(() => {
me.messages.forEach((message) => { this.messages.forEach((message) => {
const newText = me.timeDescription(message.created_at) const newText = this.timeDescription(message.created_at);
if (newText != message.element.innerText) { if (newText != message.element.innerText) {
message.element.querySelector(".time").innerText = newText message.element.querySelector(".time").innerText = newText;
} }
}) });
}, 30000) }, 30000);
} }
} }

View File

@ -1,12 +1,12 @@
// Written by retoor@molodetz.nl
// This code defines a class 'MessageModel' representing a message entity with various properties such as user and channel IDs, message content, and timestamps. It includes a constructor to initialize these properties.
// No external imports or includes beyond standard JavaScript language features are used.
// MIT License
class MessageModel { class MessageModel {
message = null
html = null
user_uid = null
channel_uid = null
created_at = null
updated_at = null
element = null
color = null
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) { constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) {
this.uid = uid this.uid = uid
this.message = message this.message = message
@ -17,10 +17,10 @@ class MessageModel {
this.channel_uid = channel_uid this.channel_uid = channel_uid
this.created_at = created_at this.created_at = created_at
this.updated_at = updated_at this.updated_at = updated_at
this.element = null
} }
} }
const models = { const models = {
Message: MessageModel Message: MessageModel
} }

View File

@ -1,47 +1,54 @@
// Written by retoor@molodetz.nl
// This JavaScript class provides functionality to schedule repeated execution of a function or delay its execution using specified intervals and timeouts.
// No external imports or includes are used in this code.
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class Schedule { class Schedule {
constructor(msDelay = 100) {
constructor(msDelay) { this.msDelay = msDelay;
if(!msDelay){ this._once = false;
msDelay = 100
}
this.msDelay = msDelay
this._once = false
this.timeOutCount = 0; this.timeOutCount = 0;
this.timeOut = null this.timeOut = null;
this.interval = null this.interval = null;
} }
cancelRepeat() { cancelRepeat() {
clearInterval(this.interval) clearInterval(this.interval);
this.interval = null this.interval = null;
} }
cancelDelay() { cancelDelay() {
clearTimeout(this.timeOut) clearTimeout(this.timeOut);
this.timeOut = null this.timeOut = null;
} }
repeat(func) { repeat(func) {
if (this.interval) { if (this.interval) {
return false return false;
} }
this.interval = setInterval(() => { this.interval = setInterval(() => {
func() func();
}, this.msDelay) }, this.msDelay);
} }
delay(func) { delay(func) {
this.timeOutCount++ this.timeOutCount++;
if (this.timeOut) { if (this.timeOut) {
this.cancelDelay() this.cancelDelay();
} }
const me = this const me = this;
this.timeOut = setTimeout(() => { this.timeOut = setTimeout(() => {
func(me.timeOutCount) func(me.timeOutCount);
clearTimeout(me.timeOut) clearTimeout(me.timeOut);
me.timeOut = null me.timeOut = null;
me.cancelDelay();
me.cancelDelay() me.timeOutCount = 0;
me.timeOutCount = 0 }, this.msDelay);
}, this.msDelay)
} }
} }

View File

@ -0,0 +1,121 @@
// Written by retoor@molodetz.nl
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
class UploadButtonElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
async uploadFiles() {
const fileInput = this.container.querySelector('.file-input');
const uploadButton = this.container.querySelector('.upload-button');
if (!fileInput.files.length) {
return;
}
const files = fileInput.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
const request = new XMLHttpRequest();
request.open('POST', '/upload', true);
request.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
uploadButton.innerText = `${Math.round(percentComplete)}%`;
}
};
request.onload = function () {
if (request.status === 200) {
progressBar.style.width = '0%';
uploadButton.innerHTML = '📤';
} else {
alert('Upload failed');
}
};
request.onerror = function () {
alert('Error while uploading.');
};
request.send(formData);
}
connectedCallback() {
this.styleElement = document.createElement('style');
this.styleElement.innerHTML = `
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f4f4f4;
}
.upload-container {
position: relative;
}
.upload-button {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #f05a28;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
position: relative;
overflow: hidden;
}
.upload-button i {
margin-right: 8px;
}
.progress {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: rgba(255, 255, 255, 0.4);
width: 0%;
}
.hidden-input {
display: none;
}
`;
this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div');
this.container.innerHTML = `
<div class="upload-container">
<button class="upload-button">
📤
</button>
<input class="hidden-input file-input" type="file" multiple />
</div>
`;
this.shadowRoot.appendChild(this.container);
this.uploadButton = this.container.querySelector('.upload-button');
this.fileInput = this.container.querySelector('.hidden-input');
this.uploadButton.addEventListener('click', () => {
this.fileInput.click();
});
this.fileInput.addEventListener('change', () => {
this.uploadFiles();
});
}
}
customElements.define('upload-button', UploadButtonElement);

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snek</title> <title>Snek</title>
<style>{{highlight_styles}}</style> <style>{{highlight_styles}}</style>
<script src="/media-upload.js"></script> <script src="/upload-button.js"></script>
<script src="/html-frame.js"></script> <script src="/html-frame.js"></script>
<script src="/schedule.js"></script> <script src="/schedule.js"></script>
<script src="/app.js"></script> <script src="/app.js"></script>