Compare commits

..

No commits in common. "f395d1617394045cac7c41af0cd5ce9d6ef55ed8" and "f4a5536dcf1e27a7e8319488f8f39a8acfb818a2" have entirely different histories.

16 changed files with 1006 additions and 1153 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,309 +1,347 @@
// 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) url += '?' + encodedParams; if (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, params, result }); console.debug({ url: url, params: params, result: 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, data, result }); console.debug({ url: url, params: params, result: result })
} }
return result; return result
} }
} }
const rest = new RESTClient()
class EventHandler { class EventHandler {
constructor() { constructor() {
this.subscribers = {}; this.subscribers = {}
} }
addEventListener(type, handler) { addEventListener(type, handler) {
if (!this.subscribers[type]) this.subscribers[type] = []; if (!this.subscribers[type])
this.subscribers[type].push(handler); this.subscribers[type] = []
this.subscribers[type].push(handler)
}
emit(type, ...data) {
if (this.subscribers[type])
this.subscribers[type].forEach(handler => handler(...data))
} }
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._waitConnect = null; this._wait_connect = null
this._promises = {}; this._promises = {}
} }
connect() { connect() {
if (this._waitConnect) { if (this._wait_connect)
return this._waitConnect; return this._wait_connect
}
return new Promise((resolve) => { const me = this
this._waitConnect = resolve; return new Promise(async (resolve, reject) => {
console.debug("Connecting.."); me._wait_connect = resolve
console.debug("Connecting..")
try { try {
this._socket = new WebSocket(this._url); me._socket = new WebSocket(me._url)
} catch (e) { }catch(e){
console.warn(e); console.warning(e)
setTimeout(() => { setTimeout(()=>{
this.ensureConnection(); me.ensureConnection()
}, 1000); },1000)
} }
this._socket.onconnect = () => { me._socket.onconnect = () => {
this._connected(); me._connected()
this._waitSocket(); me._wait_socket(me)
};
});
} }
})
}
generateUniqueId() { generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9); return 'id-' + Math.random().toString(36).substr(2, 9); // Example: id-k5f9zq7
} }
call(method, ...args) { call(method, ...args) {
return new Promise((resolve, reject) => { const me = this
return new Promise(async (resolve, reject) => {
try { try {
const command = { method, args, message_id: this.generateUniqueId() }; const command = { method: method, args: args, message_id: me.generateUniqueId() }
this._promises[command.message_id] = resolve; me._promises[command.message_id] = resolve
this._socket.send(JSON.stringify(command)); await me._socket.send(JSON.stringify(command))
} catch (e) {
reject(e);
}
});
}
_connected() { } catch (e) {
this._socket.onmessage = (event) => { reject(e)
const message = JSON.parse(event.data); }
if (message.message_id && this._promises[message.message_id]) { })
this._promises[message.message_id](message); }
delete this._promises[message.message_id]; _connected() {
} else { const me = this
this.emit("message", message); this._socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.message_id && me._promises[message.message_id]) {
me._promises[message.message_id](message)
delete me._promises[message.message_id]
} else {
me.emit("message", me, 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.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); return str
.replace(/([a-z])([A-Z])/g, '$1_$2')
.toLowerCase();
} }
get client() { get client() {
const me = this; const me = this
return new Proxy({}, { const proxy = new Proxy(
get(_, prop) { {},
{
get(target, prop) {
return (...args) => { return (...args) => {
const functionName = me._camelToSnake(prop); let 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() {
if (this.isConnected || this.isConnecting) { const me = this
return new Promise((resolve) => { if (!this.isConnected && !this.isConnecting) {
this.connectPromises.push(resolve); this.isConnecting = true
if (!this.isConnected) resolve(this); } else if (this.isConnecting) {
}); return new Promise((resolve, reject) => {
me.connectPromises.push(resolve)
})
} else if (this.isConnected) {
return new Promise((resolve, reject) => {
resolve(me)
})
} }
this.isConnecting = true; return new Promise((resolve, reject) => {
return new Promise((resolve) => { me.connectPromises.push(resolve)
this.connectPromises.push(resolve); console.debug("Connecting..")
console.debug("Connecting..");
const ws = new WebSocket(this.url); const ws = new WebSocket(this.url)
ws.onopen = () => {
this.ws = ws; ws.onopen = (event) => {
this.isConnected = true; me.ws = ws
this.isConnecting = false; me.isConnected = true
me.isConnecting = false
ws.onmessage = (event) => { ws.onmessage = (event) => {
this.onData(JSON.parse(event.data)); me.onData(JSON.parse(event.data))
}; }
ws.onclose = () => { ws.onclose = (event) => {
this.onClose(); me.onClose()
};
ws.onerror = () => { }
this.onClose(); ws.onerror = (event)=>{
}; me.onClose()
this.connectPromises.forEach(resolver => resolver(this)); }
}; me.connectPromises.forEach(resolve => {
}); resolve(me)
})
}
})
}
onData(data) {
if(data.success != undefined && !data.success){
console.error(data)
} }
onData(data) {
if (data.success !== undefined && !data.success) {
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) {
await this.connect().then(api => { return 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);
});
} }
onClose() { const me = this
console.info("Connection lost. Reconnecting."); return new Promise(async (resolve, reject) => {
this.isConnected = false; me.addEventListener(call.callId, (data) => {
this.isConnecting = false; resolve(data)
this.ensureConnection().then(() => { })
console.info("Reconnected."); await me.sendJson(call)
});
})
} }
onClose() {
console.info("Connection lost. Reconnecting.")
this.isConnected = false
this.isConnecting = false
this.ensureConnection().then(() => {
console.info("Reconnected.")
})
}
} }
class NotificationAudio { class NotificationAudio {
constructor(timeout = 500) { constructor(timeout){
this.schedule = new Schedule(timeout); if(!timeout)
timeout = 500
this.schedule = new Schedule(timeout)
} }
sounds = ["/audio/soundfx.d_beep3.mp3"]
sounds = ["/audio/soundfx.d_beep3.mp3"]; play(soundIndex) {
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 = new RESTClient(); rest = rest
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
this.audio = new NotificationAudio(500); const me = this
this.audio = new NotificationAudio(500)
this.ws.addEventListener("channel-message", (data) => { this.ws.addEventListener("channel-message", (data) => {
this.emit(data.channel_uid, data); me.emit(data.channel_uid, data)
}); })
this.rpc.getUser(null).then(user => { this.rpc.getUser(null).then(user=>{
this.user = user; me.user = user
}); })
} }
playSound(index){
playSound(index) { this.audio.play(index)
this.audio.play(index);
} }
async benchMark(times, message) {
async benchMark(times = 100, message = "Benchmark Message") { if (!times)
const promises = []; times = 100
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 => {
this.rpc.sendMessage(channel.uid, `${message} ${i}`); me.rpc.sendMessage(channel.uid, `${message} ${i}`).then(data => {
});
})); })
})
}))
} }
//return await Promise.all(promises)
} }
} }
const app = new App(); const app = new App()

View File

@ -180,6 +180,7 @@ message-list {
.chat-messages .message .message-content .text { .chat-messages .message .message-content .text {
margin-bottom: 5px; margin-bottom: 5px;
color: #e6e6e6; color: #e6e6e6;
white-space: pre-wrap;
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -208,7 +209,7 @@ message-list {
resize: none; resize: none;
} }
.chat-input upload-button { .chat-input button {
background-color: #f05a28; background-color: #f05a28;
color: white; color: white;
border: none; border: none;
@ -244,13 +245,12 @@ message-list {
max-width: 90%; max-width: 90%;
border-radius: 20px; border-radius: 20px;
} }
{
padding: 0;
margin: 0;
}
} }
.avatar { .avatar {
opacity: 0; opacity: 0;
height: 0;
padding: 0;
margin: 0;
} }
.author { .author {

View File

@ -1,59 +1,63 @@
// 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 link = document.createElement('link'); const me = this
link.rel = 'stylesheet'; const link = document.createElement("link")
link.href = '/base.css'; link.rel = 'stylesheet'
this.component.appendChild(link); link.href = '/base.css'
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>
<upload-button></upload-button> <button>Send</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) {
e.preventDefault();
const message = e.target.value.trim();
if (!message) return;
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
e.target.value = '';
}
});
this.component.appendChild(this.container); if (e.key == 'Enter') {
if(!e.shiftKey){
e.preventDefault()
const message = e.target.value.trim();
if(!message)
return
this.dispatchEvent(new CustomEvent("submit", { detail: message, bubbles: true }))
e.target.value = ''
}
}
})
this.container.querySelector('button').addEventListener('click', (e) => {
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")
this.container = document.createElement("section"); const chatHeader = document.createElement("div")
this.container.classList.add("chat-area", "chat-window"); chatHeader.classList.add("chat-header")
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 channelElement = document.createElement('message-list'); const chatTitle = document.createElement('h2')
channelElement.setAttribute("channel", channel.uid); chatTitle.classList.add("chat-title")
this.container.appendChild(channelElement); 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 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)
messages.forEach(message=>{
if(!message['user_nick'])
return
channelElement.addMessage(message)
})
const me = this
channelElement.addEventListener("message",(message)=>{
if(me.user.uid != message.detail.user_uid)
app.playSound(0)
message.detail.element.scrollIntoView()
})
const messages = await app.rpc.getMessages(channel.uid);
messages.forEach(message => {
if (!message['user_nick']) return;
channelElement.addMessage(message);
});
const me = this;
channelElement.addEventListener("message", (message) => {
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
message.detail.element.scrollIntoView();
});
} }
} }
customElements.define('chat-window', ChatWindowElement); customElements.define('chat-window', ChatWindowElement);

View File

@ -1,31 +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 {
constructor() { url = null
super(); type="button"
this.attachShadow({ mode: 'open' }); value = null
this.url = null; constructor(){
this.type = "button"; super()
this.value = null; this.attachShadow({mode:'open'})
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement('span');
let size = this.getAttribute('size');
console.info({ GG: size });
size = size === 'auto' ? '1%' : '33%';
this.styleElement = document.createElement("style"); this.container = document.createElement('span')
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%;
--width: 100%; --width: 100%;
} }
button { button {
@ -40,6 +37,7 @@ 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 {
@ -47,24 +45,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.container.appendChild(this.styleElement); this.buttonElement = document.createElement('button')
this.buttonElement = document.createElement('button'); this.container.appendChild(this.buttonElement)
this.container.appendChild(this.buttonElement); this.shadowRoot.appendChild(this.container)
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')
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text"))); const me = this
this.buttonElement.addEventListener("click", () => { this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")))
if (this.url === "/back" || this.url === "/back/") { this.buttonElement.addEventListener("click",()=>{
window.history.back(); if(me.url == "/back" || me.url == "/back/"){
} else if (this.url) { window.history.back()
window.location = this.url; }else if(me.url){
window.location = me.url
} }
}); })
} }
} }
customElements.define("fancy-button", FancyButton); customElements.define("fancy-button",FancyButton)

View File

@ -1,70 +1,44 @@
// 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() { get value() {
return this.inputElement.value; return this.inputElement.value
} }
get type() { get type() {
return this.field.tag;
}
return this.field.tag
}
set value(val) { set value(val) {
val = val ?? ''; val = val == null ? '' : 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 {
@ -119,101 +93,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)
connectedCallback() { this.shadowRoot.appendChild(this.container)
this.updateAttributes();
} }
connectedCallback(){
setAttribute(name, value) { this.updateAttributes()
this[name] = value;
}
updateAttributes() {
if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && this.field.value === "submit") {
this.action = this.field.value;
} }
this.inputElement.name = this.field.name; setAttribute(name,value){
this.name = this.inputElement.name; this[name] = value
}
updateAttributes(){
if(this.inputElement == null && this.field){
this.inputElement = document.createElement(this.field.tag)
if(this.field.tag == 'button'){
if(this.field.value == "submit"){
const me = this;
this.inputElement.addEventListener("keyup", (e) => {
if (e.key === 'Enter') {
me.dispatchEvent(new Event("submit"));
} else if (me.field.value !== e.target.value) {
const event = new CustomEvent("change", { detail: me, bubbles: true });
me.dispatchEvent(event);
}
});
this.inputElement.addEventListener("click", (e) => { }
const event = new CustomEvent("click", { detail: me, bubbles: true }); this.action = this.field.value
me.dispatchEvent(event); }
}); this.inputElement.name = this.field.name
this.name = this.inputElement.name
const me = this
this.inputElement.addEventListener("keyup",(e)=>{
if(e.key == 'Enter'){
me.dispatchEvent(new Event("submit"))
}else if(me.field.value != e.target.value)
{
const event = new CustomEvent("change", {detail:me,bubbles:true})
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.container.appendChild(this.inputElement); }
if(!this.field){
return
} }
this.inputElement.setAttribute("type",this.field.type == null ? 'input' : this.field.type)
this.inputElement.setAttribute("name",this.field.name == null ? '' : this.field.name)
if (!this.field) { if(this.field.text != null){
return; this.inputElement.innerText = this.field.text
} }
if(this.field.html != null){
this.inputElement.setAttribute("type", this.field.type ?? 'input'); this.inputElement.innerHTML = this.field.html
this.inputElement.setAttribute("name", this.field.name ?? '');
if (this.field.text != null) {
this.inputElement.innerText = this.field.text;
} }
if (this.field.html != null) { if(this.field.class_name){
this.inputElement.innerHTML = this.field.html; this.inputElement.classList.add(this.field.class_name)
} }
if (this.field.class_name) { this.inputElement.setAttribute("tabindex", this.field.index)
this.inputElement.classList.add(this.field.class_name); this.inputElement.classList.add(this.field.name)
this.value = this.field.value
let place_holder = null
if(this.field.place_holder)
place_holder = this.field.place_holder
if(this.field.required && place_holder){
place_holder = place_holder
} }
this.inputElement.setAttribute("tabindex", this.field.index); if(place_holder)
this.inputElement.classList.add(this.field.name); this.field.place_holder = "* " + place_holder
this.value = this.field.value; this.inputElement.setAttribute("placeholder",place_holder)
if(this.field.required)
let place_holder = this.field.place_holder ?? null; this.inputElement.setAttribute("required","required")
if (this.field.required && place_holder) { else
place_holder = "* " + place_holder; this.inputElement.removeAttribute("required")
} if(!this.footerElement){
this.inputElement.setAttribute("placeholder", place_holder); this.footerElement = document.createElement('div')
if (this.field.required) { this.footerElement.style.clear = 'both'
this.inputElement.setAttribute("required", "required"); this.container.appendChild(this.footerElement)
} else {
this.inputElement.removeAttribute("required");
}
if (!this.footerElement) {
this.footerElement = document.createElement('div');
this.footerElement.style.clear = 'both';
this.container.appendChild(this.footerElement);
} }
} }
} }
@ -221,52 +195,56 @@ 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!";
@ -274,89 +252,95 @@ 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}`);
} }
this.form = await response.json(); me.form = await response.json();
let fields = Object.values(this.form.fields); let fields = Object.values(me.form.fields)
fields.sort((a, b) => a.index - b.index); fields = fields.sort((a,b)=>{
fields.forEach(field => { console.info(a.index,b.index)
const fieldElement = document.createElement('generic-field'); return a.index - b.index
this.fields[field.name] = fieldElement; })
fieldElement.setAttribute("form", this); fields.forEach(field=>{
fieldElement.setAttribute("field", field); const fieldElement = document.createElement('generic-field')
this.container.appendChild(fieldElement); me.fields[field.name] = fieldElement
fieldElement.updateAttributes(); fieldElement.setAttribute("form", me)
fieldElement.setAttribute("field", field)
fieldElement.addEventListener("change", (e) => { me.container.appendChild(fieldElement)
this.form.fields[e.detail.name].value = e.detail.value; 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" && e.detail.value === "submit") { fieldElement.addEventListener("click",async (e)=>{
const isValid = await this.validate(); if(e.detail.type == "button"){
if (isValid) { if(e.detail.value == "submit")
const saveResult = await this.submit(); {
if (saveResult.redirect_url) { const isValid = await me.validate()
window.location.pathname = saveResult.redirect_url; if(isValid){
const saveResult = await me.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": this.form }) body: JSON.stringify({"action":"validate", "form":me.form})
}); });
const form = await response.json();
Object.values(form.fields).forEach(field => {
if (!this.form.fields[field.name]) {
return;
}
this.form.fields[field.name].is_valid = field.is_valid;
if (!field.is_valid) {
this.fields[field.name].setInvalid();
this.fields[field.name].setErrors(field.errors);
} else {
this.fields[field.name].setValid();
}
this.fields[field.name].setAttribute("field", field);
this.fields[field.name].updateAttributes();
});
Object.values(form.fields).forEach(field => {
this.fields[field.name].setErrors(field.errors);
});
return form['is_valid'];
}
async submit() { const form = await response.json()
const url = this.getAttribute("url"); Object.values(form.fields).forEach(field=>{
const response = await fetch(url, { if(!me.form.fields[field.name])
return
me.form.fields[field.name].is_valid = field.is_valid
if(!field.is_valid){
me.fields[field.name].setInvalid()
me.fields[field.name].setErrors(field.errors)
}else{
me.fields[field.name].setValid()
}
me.fields[field.name].setAttribute("field",field)
me.fields[field.name].updateAttributes()
})
Object.values(form.fields).forEach(field=>{
me.fields[field.name].setErrors(field.errors)
})
return form['is_valid']
}
async submit(){
const me = this
const url = me.getAttribute("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": this.form }) body: JSON.stringify({"action":"submit", "form":me.form})
}); });
return await response.json(); return await response.json()
}
}
customElements.define('generic-form', GenericForm); }
}
customElements.define('generic-form', GenericForm);

View File

@ -1,11 +1,3 @@
// 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();
@ -15,16 +7,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) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); let fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url); 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!";
@ -38,17 +30,18 @@ class HTMLFrame extends HTMLElement {
throw new Error(`Error: ${response.status} ${response.statusText}`); throw new Error(`Error: ${response.status} ${response.statusText}`);
} }
const html = await response.text(); const html = await response.text();
if (url.endsWith(".md")) { if(url.endsWith(".md")){
const markdownElement = document.createElement('div'); const parent = this
markdownElement.innerHTML = html; const markdownElement = document.createElement('div')
this.outerHTML = html; markdownElement.innerHTML = html
} else { this.outerHTML = html
}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,13 +1,5 @@
// 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() {
@ -18,16 +10,15 @@ 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('/') const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
? window.location.origin + url if(!url.startsWith("/"))
: new URL(window.location.origin + '/http-get'); fullUrl.searchParams.set('url', url)
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!";
} }
} }
@ -39,10 +30,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,32 +1,18 @@
// 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 { 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.textContent = ` this.styleElement.innerText = `
.grid { .grid {
padding: 10px; padding: 10px;
display: flex; display: flex;
@ -46,13 +32,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;
@ -61,39 +47,38 @@ 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.classList.add('tile'); element.cclassList.add('tile');
this.container.appendChild(element); this.container.appendChild(element);
} }
} }
class UploadButton extends HTMLElement { class UploadButton 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);
window.u = this;
}
get gridSelector() { this.shadowRoot.appendChild(this.component)
window.u = this
}
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.textContent = ` this.styleElement.innerHTML = `
.upload-button { .upload-button {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -127,6 +112,7 @@ 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);
@ -134,7 +120,7 @@ class UploadButton extends HTMLElement {
this.addImages(urls); this.addImages(urls);
} }
}; };
reader.readAsDataURL(files[i]); reader.readAsDataURL(file);
} }
}); });
const label = document.createElement('label'); const label = document.createElement('label');
@ -145,31 +131,37 @@ 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.component.style.height = '100%';
this.component.style.backgroundColor = 'blue';
this.shadowRoot.appendChild(this.container);
this.tileElement = document.createElement("tile-grid"); this.container = document.createElement("div")
this.tileElement.style.backgroundColor = 'red'; this.component.style.height = '100%'
this.tileElement.style.height = '100%'; this.component.style.backgroundColor ='blue';
this.component.appendChild(this.tileElement); this.shadowRoot.appendChild(this.container)
this.uploadButton = document.createElement('upload-button'); this.tileElement = document.createElement("tile-grid")
this.component.appendChild(this.uploadButton); this.tileElement.style.backgroundColor = 'red'
this.tileElement.style.height = '100%'
this.component.appendChild(this.tileElement)
this.uploadButton = document.createElement('upload-button')
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,44 +1,23 @@
// 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() {
const channels = await app.rpc.getChannels(); let channels = await app.rpc.getChannels()
channels.forEach(channel => { const me = this
const messageList = document.createElement("message-list"); channels.forEach(channel=>{
messageList.setAttribute("channel", channel.uid); const messageList = document.createElement("message-list")
this.container.appendChild(messageList); messageList.setAttribute("channel",channel.uid)
}); me.container.appendChild(messageList)
})
} }
} }
customElements.define("message-list-manager", MessageListManagerElement); customElements.define("message-list-manager",MessageListManagerElement)

View File

@ -1,113 +1,115 @@
// 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 = []
messages = []; room = null
room = null; url = null
url = null; container = null
container = null; messageEventSchedule = null
messageEventSchedule = null; observer = 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) {
return `${days} ${days > 1 ? 'days' : 'day'} ago`; if (days > 1)
return `${days} days ago`
else
return `${days} day ago`
} }
if (hours) { if (hours) {
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`; if (hours > 1)
} return `${hours} hours ago`
if (minutes) { else
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`; return `${hours} hour 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}, ${this.timeAgo(new Date(isoDate), Date.now())}`; let timeStr = `${hours}:${minutes}`
return timeStr; timeStr += ", " + this.timeAgo(new Date(isoDate), Date.now())
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 || this.messages[this.messages.length - 1].user_uid != message.user_uid) { if (!this.messages.length) {
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;
const text = document.createElement("div"); message.element = element
text.classList.add("text");
if (message.html) text.innerHTML = message.html;
const time = document.createElement("div"); return element
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,
@ -118,51 +120,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)
const element = this.createElement(obj); this.messages.push(obj)
this.messages.push(obj); this.container.appendChild(element)
this.container.appendChild(element); const me = this
this.messageEventSchedule.delay(() => { this.messageEventSchedule.delay(() => {
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true })); me.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 = document.createElement('div'); //this.container.classList.add("chat-messages")
this.component.appendChild(this.container); this.component.appendChild(this.container)
this.messageEventSchedule = new Schedule(500)
this.messageEventSchedule = new Schedule(500); this.messages = []
this.messages = []; this.channel_uid = this.getAttribute("channel")
this.channel_uid = this.getAttribute("channel"); const me = this
app.addEventListener(this.channel_uid, (data) => { app.addEventListener(this.channel_uid, (data) => {
this.addMessage(data); me.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(() => {
this.messages.forEach((message) => { me.messages.forEach((message) => {
const newText = this.timeDescription(message.created_at); const newText = me.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,13 +1,13 @@
// 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 {
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) { 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){
this.uid = uid this.uid = uid
this.message = message this.message = message
this.html = html this.html = html
@ -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,54 +1,47 @@
// 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) {
this.msDelay = msDelay; constructor(msDelay) {
this._once = false; if(!msDelay){
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.timeOutCount = 0; me.cancelDelay()
}, this.msDelay); me.timeOutCount = 0
}, this.msDelay)
} }
} }

View File

@ -1,121 +0,0 @@
// 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="/upload-button.js"></script> <script src="/media-upload.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>