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"
version = "1.0.0"
readme = "README.md"
license = { file = "LICENSE", content-type="text/markdown" }
#license = { file = "LICENSE", content-type="text/markdown" }
description = "Snek Chat Application by Molodetz"
authors = [
{ 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 {
debug = false
debug = false;
async get(url, params) {
params = params ? params : {}
async get(url, params = {}) {
const encodedParams = new URLSearchParams(params);
if (encodedParams)
url += '?' + encodedParams
if (encodedParams) url += '?' + encodedParams;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
});
const result = await response.json()
const result = await response.json();
if (this.debug) {
console.debug({ url: url, params: params, result: result })
console.debug({ url, params, result });
}
return result
return result;
}
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
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) {
console.debug({ url: url, params: params, result: result })
console.debug({ url, data, result });
}
return result
return result;
}
}
const rest = new RESTClient()
class EventHandler {
constructor() {
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))
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));
}
}
class Chat extends EventHandler {
constructor() {
super()
this._url = window.location.hostname == 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws'
this._socket = null
this._wait_connect = null
this._promises = {}
super();
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
this._socket = null;
this._waitConnect = null;
this._promises = {};
}
connect() {
if (this._wait_connect)
return this._wait_connect
const me = this
return new Promise(async (resolve, reject) => {
me._wait_connect = resolve
console.debug("Connecting..")
if (this._waitConnect) {
return this._waitConnect;
}
return new Promise((resolve) => {
this._waitConnect = resolve;
console.debug("Connecting..");
try {
me._socket = new WebSocket(me._url)
this._socket = new WebSocket(this._url);
} catch (e) {
console.warning(e)
console.warn(e);
setTimeout(() => {
me.ensureConnection()
},1000)
this.ensureConnection();
}, 1000);
}
me._socket.onconnect = () => {
me._connected()
me._wait_socket(me)
this._socket.onconnect = () => {
this._connected();
this._waitSocket();
};
});
}
})
}
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) {
reject(e)
reject(e);
}
})
});
}
_connected() {
const me = this
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]
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];
} 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.emit("message", message);
}
};
this._socket.onclose = () => {
this._waitSocket = null;
this._socket = null;
this.emit('close');
};
}
async privmsg(room, text) {
await rest.post("/api/privmsg", {
room: room,
text: text
})
room,
text,
});
}
}
class Socket extends EventHandler {
ws = null
isConnected = null
isConnecting = null
url = null
connectPromises = []
ensureTimer = null
ws = null;
isConnected = null;
isConnecting = null;
url = null;
connectPromises = [];
ensureTimer = null;
constructor() {
super()
this.url = window.location.hostname == 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws'
this.ensureConnection()
super();
this.url = window.location.hostname === 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws';
this.ensureConnection();
}
_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() {
const me = this
const proxy = new Proxy(
{},
{
get(target, prop) {
const me = this;
return new Proxy({}, {
get(_, prop) {
return (...args) => {
let functionName = me._camelToSnake(prop)
const functionName = me._camelToSnake(prop);
return me.call(functionName, ...args);
};
},
});
}
);
return proxy
}
ensureConnection() {
if(this.ensureTimer)
return this.connect()
const me = this
this.ensureTimer = setInterval(()=>{
if (me.isConnecting)
me.isConnecting = false
me.connect()
},5000)
return this.connect()
if (this.ensureTimer) {
return this.connect();
}
this.ensureTimer = setInterval(() => {
if (this.isConnecting) this.isConnecting = false;
this.connect();
}, 5000);
return this.connect();
}
generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9);
}
connect() {
const me = this
if (!this.isConnected && !this.isConnecting) {
this.isConnecting = true
} else if (this.isConnecting) {
return new Promise((resolve, reject) => {
me.connectPromises.push(resolve)
})
} else if (this.isConnected) {
return new Promise((resolve, reject) => {
resolve(me)
})
if (this.isConnected || this.isConnecting) {
return new Promise((resolve) => {
this.connectPromises.push(resolve);
if (!this.isConnected) resolve(this);
});
}
return new Promise((resolve, reject) => {
me.connectPromises.push(resolve)
console.debug("Connecting..")
this.isConnecting = true;
return new Promise((resolve) => {
this.connectPromises.push(resolve);
console.debug("Connecting..");
const ws = new WebSocket(this.url)
ws.onopen = (event) => {
me.ws = ws
me.isConnected = true
me.isConnecting = false
const ws = new WebSocket(this.url);
ws.onopen = () => {
this.ws = ws;
this.isConnected = true;
this.isConnecting = false;
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) {
if(data.success != undefined && !data.success){
console.error(data)
if (data.success !== undefined && !data.success) {
console.error(data);
}
if (data.callId) {
this.emit(data.callId, data.data)
this.emit(data.callId, data.data);
}
if (data.channel_uid) {
this.emit(data.channel_uid, data.data)
this.emit("channel-message", data)
this.emit(data.channel_uid, data.data);
this.emit("channel-message", data);
}
}
}
async sendJson(data) {
return await this.connect().then((api) => {
api.ws.send(JSON.stringify(data))
})
await this.connect().then(api => {
api.ws.send(JSON.stringify(data));
});
}
async call(method, ...args) {
const call = {
callId: this.generateUniqueId(),
method: method,
args: args
method,
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() {
console.info("Connection lost. Reconnecting.")
this.isConnected = false
this.isConnecting = false
console.info("Connection lost. Reconnecting.");
this.isConnected = false;
this.isConnecting = false;
this.ensureConnection().then(() => {
console.info("Reconnected.")
})
console.info("Reconnected.");
});
}
}
class NotificationAudio {
constructor(timeout){
if(!timeout)
timeout = 500
this.schedule = new Schedule(timeout)
constructor(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(() => {
if (!soundIndex)
soundIndex = 0
const player = new Audio(this.sounds[soundIndex]);
player.play()
new Audio(this.sounds[soundIndex]).play()
.then(() => {
console.debug("Gave sound notification")
console.debug("Gave sound notification");
})
.catch((error) => {
.catch(error => {
console.error("Notification failed:", error);
});
})
});
}
}
class App extends EventHandler {
rest = rest
ws = null
rpc = null
audio = null
user = {}
rest = new RESTClient();
ws = null;
rpc = null;
audio = null;
user = {};
constructor() {
super()
this.ws = new Socket()
this.rpc = this.ws.client
const me = this
this.audio = new NotificationAudio(500)
super();
this.ws = new Socket();
this.rpc = this.ws.client;
this.audio = new NotificationAudio(500);
this.ws.addEventListener("channel-message", (data) => {
me.emit(data.channel_uid, data)
})
this.emit(data.channel_uid, data);
});
this.rpc.getUser(null).then(user => {
me.user = user
})
this.user = user;
});
}
playSound(index) {
this.audio.play(index)
this.audio.play(index);
}
async benchMark(times, message) {
if (!times)
times = 100
if (!message)
message = "Benchmark Message"
let promises = []
const me = this
async benchMark(times = 100, message = "Benchmark Message") {
const promises = [];
for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => {
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;
}
.chat-input button {
.chat-input upload-button {
background-color: #f05a28;
color: white;
border: none;
@ -240,11 +240,16 @@ message-list {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
hyphens: auto;
img {
max-width: 90%;
border-radius: 20px;
}
{
padding: 0;
margin: 0;
}
}
.avatar {
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 {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
}
connectedCallback() {
const me = this
const link = document.createElement("link")
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.container = document.createElement('div')
this.container.classList.add("chat-input")
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.container = document.createElement('div');
this.container.classList.add('chat-input');
this.container.innerHTML = `
<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.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 button = this.container.querySelector('button');
button.disabled = !message;
})
});
this.textBox.addEventListener('change', (e) => {
e.preventDefault()
this.dispatchEvent(new CustomEvent("change", { detail: e.target.value, bubbles: true }))
console.error(e.target.value)
})
e.preventDefault();
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
console.error(e.target.value);
});
this.textBox.addEventListener('keydown', (e) => {
if (e.key == 'Enter') {
if(!e.shiftKey){
e.preventDefault()
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 = ''
if (!message) return;
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
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);

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 {
receivedHistory = false
receivedHistory = false;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('section');
this.app = app
this.app = app;
this.shadowRoot.appendChild(this.component);
}
get user() {
return this.app.user
return this.app.user;
}
async connectedCallback() {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.component.classList.add("chat-area")
this.container = document.createElement("section")
this.container.classList.add("chat-area")
this.container.classList.add("chat-window")
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.component.classList.add("chat-area");
const chatHeader = document.createElement("div")
chatHeader.classList.add("chat-header")
this.container = document.createElement("section");
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')
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')
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 channelElement = document.createElement('message-list');
channelElement.setAttribute("channel", channel.uid);
this.container.appendChild(channelElement);
const chatInput = document.createElement('chat-input');
chatInput.addEventListener("submit", (e) => {
app.rpc.sendMessage(channel.uid,e.detail)
})
this.container.appendChild(chatInput)
app.rpc.sendMessage(channel.uid, e.detail);
});
this.container.appendChild(chatInput);
this.component.appendChild(this.container)
const messages = await app.rpc.getMessages(channel.uid)
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
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()
})
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
message.detail.element.scrollIntoView();
});
}
}
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 {
url = null
type="button"
value = null
constructor() {
super()
this.attachShadow({mode:'open'})
super();
this.attachShadow({ mode: 'open' });
this.url = null;
this.type = "button";
this.value = null;
}
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')
let size = this.getAttribute('size')
console.info({GG:size})
if(size == 'auto'){
size = '1%'
}else{
size = '33%'
}
this.styleElement = document.createElement("style")
this.styleElement = document.createElement("style");
this.styleElement.innerHTML = `
:root {
width: 100%;
@ -37,7 +40,6 @@ class FancyButton extends HTMLElement {
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
border: 1px solid #f05a28;
}
button:hover {
@ -45,24 +47,24 @@ class FancyButton extends HTMLElement {
background-color: #e04924;
border: 1px solid #efefef;
}
`
this.container.appendChild(this.styleElement)
this.buttonElement = document.createElement('button')
this.container.appendChild(this.buttonElement)
this.shadowRoot.appendChild(this.container)
`;
this.container.appendChild(this.styleElement);
this.buttonElement = document.createElement('button');
this.container.appendChild(this.buttonElement);
this.shadowRoot.appendChild(this.container);
this.url = this.getAttribute('url');
this.value = this.getAttribute('value')
const me = this
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")))
this.value = this.getAttribute('value');
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
this.buttonElement.addEventListener("click", () => {
if(me.url == "/back" || me.url == "/back/"){
window.history.back()
}else if(me.url){
window.location = me.url
if (this.url === "/back" || this.url === "/back/") {
window.history.back();
} else if (this.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 {
form = null
field = null
inputElement = null
footerElement = null
action = null
container = null
styleElement = null
name = null
get value() {
return this.inputElement.value
}
get type() {
form = null;
field = null;
inputElement = null;
footerElement = null;
action = null;
container = null;
styleElement = null;
name = null;
return this.field.tag
get value() {
return this.inputElement.value;
}
get type() {
return this.field.tag;
}
set value(val) {
val = val == null ? '' : val
this.inputElement.value = val
this.inputElement.setAttribute("value", val)
val = val ?? '';
this.inputElement.value = val;
this.inputElement.setAttribute("value", val);
}
setInvalid() {
this.inputElement.classList.add("error")
this.inputElement.classList.remove("valid")
this.inputElement.classList.add("error");
this.inputElement.classList.remove("valid");
}
setErrors(errors) {
if(errors.length)
this.inputElement.setAttribute("title", errors[0])
else
this.inputElement.setAttribute("title","")
const errorText = errors.length ? errors[0] : "";
this.inputElement.setAttribute("title", errorText);
}
setValid() {
this.inputElement.classList.remove("error")
this.inputElement.classList.add("valid")
this.inputElement.classList.remove("error");
this.inputElement.classList.add("valid");
}
constructor() {
super()
this.attachShadow({mode:'open'})
this.container = document.createElement('div')
this.styleElement = document.createElement('style')
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement('div');
this.styleElement = document.createElement('style');
this.styleElement.innerHTML = `
h1 {
@ -93,101 +119,101 @@ class GenericField extends HTMLElement {
a:hover {
color: #e04924;
}
.valid {
border: 1px solid green;
color: green;
font-size: 0.9em;
margin-top: 5px;
}
.error {
border: 3px solid red;
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
}
@media (max-width: 500px) {
input {
width: 90%;
}
}
`;
this.container.appendChild(this.styleElement);
`
this.container.appendChild(this.styleElement)
this.shadowRoot.appendChild(this.container)
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.updateAttributes()
this.updateAttributes();
}
setAttribute(name, value) {
this[name] = value
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"){
this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && 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
const me = this
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)
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.inputElement.addEventListener("click", (e) => {
const event = new CustomEvent("click", { detail: me, bubbles: true });
me.dispatchEvent(event);
});
this.container.appendChild(this.inputElement);
}
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) {
this.inputElement.innerText = this.field.text
this.inputElement.innerText = this.field.text;
}
if (this.field.html != null) {
this.inputElement.innerHTML = this.field.html
this.inputElement.innerHTML = this.field.html;
}
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.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
this.inputElement.setAttribute("tabindex", this.field.index);
this.inputElement.classList.add(this.field.name);
this.value = this.field.value;
let place_holder = this.field.place_holder ?? null;
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) {
this.footerElement = document.createElement('div')
this.footerElement.style.clear = 'both'
this.container.appendChild(this.footerElement)
this.footerElement = document.createElement('div');
this.footerElement.style.clear = 'both';
this.container.appendChild(this.footerElement);
}
}
}
@ -195,56 +221,52 @@ class GenericField extends HTMLElement {
customElements.define('generic-field', GenericField);
class GenericForm extends HTMLElement {
fields = {}
form = {}
fields = {};
form = {};
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.styleElement = document.createElement("style")
this.styleElement = document.createElement("style");
this.styleElement.innerHTML = `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
width:90%
width: 90%;
}
div {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
}
@media (max-width: 500px) {
width: 100%;
height: 100%;
form {
height: 100%;
width: 100%;
width: 80%;
}
}`
}`;
this.container = document.createElement('div');
this.container.appendChild(this.styleElement)
this.container.classList.add("generic-form-container")
this.container.appendChild(this.styleElement);
this.container.classList.add("generic-form-container");
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url);
}
this.loadForm(fullUrl.toString());
} else {
this.container.textContent = "No URL provided!";
@ -252,95 +274,89 @@ class GenericForm extends HTMLElement {
}
async loadForm(url) {
const me = this
try {
const response = await fetch(url);
if (!response.ok) {
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)=>{
console.info(a.index,b.index)
return a.index - b.index
})
fields.sort((a, b) => a.index - b.index);
fields.forEach(field => {
const fieldElement = document.createElement('generic-field')
me.fields[field.name] = fieldElement
fieldElement.setAttribute("form", me)
fieldElement.setAttribute("field", field)
me.container.appendChild(fieldElement)
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
}
}
}
}
const fieldElement = document.createElement('generic-field');
this.fields[field.name] = fieldElement;
fieldElement.setAttribute("form", this);
fieldElement.setAttribute("field", field);
this.container.appendChild(fieldElement);
fieldElement.updateAttributes();
})
})
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) {
this.container.textContent = `Error: ${error.message}`;
}
}
async validate() {
const url = this.getAttribute("url")
const me = this
const url = this.getAttribute("url");
let response = await fetch(url, {
method: 'POST',
headers: {
'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 => {
if(!me.form.fields[field.name])
return
me.form.fields[field.name].is_valid = field.is_valid
if (!this.form.fields[field.name]) {
return;
}
this.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)
this.fields[field.name].setInvalid();
this.fields[field.name].setErrors(field.errors);
} else {
me.fields[field.name].setValid()
this.fields[field.name].setValid();
}
me.fields[field.name].setAttribute("field",field)
me.fields[field.name].updateAttributes()
})
this.fields[field.name].setAttribute("field", field);
this.fields[field.name].updateAttributes();
});
Object.values(form.fields).forEach(field => {
me.fields[field.name].setErrors(field.errors)
})
return form['is_valid']
this.fields[field.name].setErrors(field.errors);
});
return form['is_valid'];
}
async submit() {
const me = this
const url = me.getAttribute("url")
const url = this.getAttribute("url");
const response = await fetch(url, {
method: 'POST',
headers: {
'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);

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 {
constructor() {
super();
@ -7,16 +15,16 @@ class HTMLFrame extends HTMLElement {
}
connectedCallback() {
this.container.classList.add("html_frame")
this.container.classList.add("html_frame");
let url = this.getAttribute('url');
if (!url.startsWith("https")) {
url = "https://" + url
url = "https://" + url;
}
if (url) {
let fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url);
}
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
@ -31,17 +39,16 @@ class HTMLFrame extends HTMLElement {
}
const html = await response.text();
if (url.endsWith(".md")) {
const parent = this
const markdownElement = document.createElement('div')
markdownElement.innerHTML = html
this.outerHTML = html
const markdownElement = document.createElement('div');
markdownElement.innerHTML = html;
this.outerHTML = html;
} else {
this.container.innerHTML = html;
}
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
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 {
constructor() {
@ -10,15 +18,16 @@ class HTMLFrame extends HTMLElement {
}
connectedCallback() {
this.container.classList.add("html_frame")
this.container.classList.add('html_frame');
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
const fullUrl = url.startsWith('/')
? window.location.origin + url
: new URL(window.location.origin + '/http-get');
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url);
this.loadAndRender(fullUrl.toString());
} 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();
this.container.innerHTML = html;
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
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() {
super();
this.attachShadow({ mode: 'open' });
this.gridId = this.getAttribute('grid');
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component)
this.shadowRoot.appendChild(this.component);
}
connectedCallback() {
console.log('connected');
this.styleElement = document.createElement('style');
this.styleElement.innerText = `
this.styleElement.textContent = `
.grid {
padding: 10px;
display: flex;
@ -32,13 +46,13 @@ class TileGridElement extends HTMLElement {
.grid .tile:hover {
transform: scale(1.1);
}
`;
this.component.appendChild(this.styleElement);
this.container = document.createElement('div');
this.container.classList.add('gallery');
this.component.appendChild(this.container);
}
addImage(src) {
const item = document.createElement('img');
item.src = src;
@ -47,14 +61,15 @@ class TileGridElement extends HTMLElement {
item.style.height = '100px';
this.container.appendChild(item);
}
addImages(srcs) {
srcs.forEach(src => this.addImage(src));
}
addElement(element) {
element.cclassList.add('tile');
element.classList.add('tile');
this.container.appendChild(element);
}
}
class UploadButton extends HTMLElement {
@ -62,23 +77,23 @@ class UploadButton extends HTMLElement {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component)
window.u = this
this.shadowRoot.appendChild(this.component);
window.u = this;
}
get gridSelector() {
return this.getAttribute('grid');
}
grid = null
grid = null;
addImages(urls) {
this.grid.addImages(urls);
}
connectedCallback()
{
connectedCallback() {
console.log('connected');
this.styleElement = document.createElement('style');
this.styleElement.innerHTML = `
this.styleElement.textContent = `
.upload-button {
display: flex;
flex-direction: column;
@ -112,7 +127,6 @@ class UploadButton extends HTMLElement {
const files = e.target.files;
const urls = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e) => {
urls.push(e.target.result);
@ -120,7 +134,7 @@ class UploadButton extends HTMLElement {
this.addImages(urls);
}
};
reader.readAsDataURL(file);
reader.readAsDataURL(files[i]);
}
});
const label = document.createElement('label');
@ -131,37 +145,31 @@ class UploadButton extends HTMLElement {
}
customElements.define('upload-button', UploadButton);
customElements.define('tile-grid', TileGridElement);
class MeniaUploadElement extends HTMLElement {
constructor(){
super()
this.attachShadow({mode:'open'})
this.component = document.createElement("div")
alert('aaaa')
this.shadowRoot.appendChild(this.component)
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement("div");
alert('aaaa');
this.shadowRoot.appendChild(this.component);
}
connectedCallback() {
this.container = document.createElement("div")
this.component.style.height = '100%'
this.container = document.createElement("div");
this.component.style.height = '100%';
this.component.style.backgroundColor = 'blue';
this.shadowRoot.appendChild(this.container)
this.shadowRoot.appendChild(this.container);
this.tileElement = document.createElement("tile-grid")
this.tileElement.style.backgroundColor = 'red'
this.tileElement.style.height = '100%'
this.component.appendChild(this.tileElement)
this.tileElement = document.createElement("tile-grid");
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)
this.uploadButton = document.createElement('upload-button');
this.component.appendChild(this.uploadButton);
}
}
}
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 {
constructor() {
super()
this.attachShadow({mode:'open'})
this.container = document.createElement("div")
this.shadowRoot.appendChild(this.container)
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container);
}
async connectedCallback() {
let channels = await app.rpc.getChannels()
const me = this
const channels = await app.rpc.getChannels();
channels.forEach(channel => {
const messageList = document.createElement("message-list")
messageList.setAttribute("channel",channel.uid)
me.container.appendChild(messageList)
})
const messageList = document.createElement("message-list");
messageList.setAttribute("channel", channel.uid);
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 {
static get observedAttributes() {
return ["messages"];
}
messages = []
room = null
url = null
container = null
messageEventSchedule = null
observer = null
messages = [];
room = null;
url = null;
container = null;
messageEventSchedule = null;
observer = null;
constructor() {
super()
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div')
this.shadowRoot.appendChild(this.component)
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
}
linkifyText(text) {
const urlRegex = /https?:\/\/[^\s]+/g;
return text.replace(urlRegex, (url) => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
}
timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) {
if (days > 1)
return `${days} days ago`
else
return `${days} day ago`
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
}
if (hours) {
if (hours > 1)
return `${hours} hours ago`
else
return `${hours} hour ago`
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
}
if (minutes) {
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) {
const date = new Date(isoDate)
const date = new Date(isoDate);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
let timeStr = `${hours}:${minutes}`
timeStr += ", " + this.timeAgo(new Date(isoDate), Date.now())
return timeStr
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
return timeStr;
}
createElement(message) {
const element = document.createElement("div")
element.dataset.uid = message.uid
element.dataset.color = message.color
element.dataset.channel_uid = message.channel_uid
element.dataset.user_nick = message.user_nick
element.dataset.created_at = message.created_at
element.dataset.user_uid = message.user_uid
element.dataset.message = message.message
const element = document.createElement("div");
element.dataset.uid = message.uid;
element.dataset.color = message.color;
element.dataset.channel_uid = message.channel_uid;
element.dataset.user_nick = message.user_nick;
element.dataset.created_at = message.created_at;
element.dataset.user_uid = message.user_uid;
element.dataset.message = message.message;
element.classList.add("message")
if (!this.messages.length) {
element.classList.add("switch-user")
} else if (this.messages[this.messages.length - 1].user_uid != message.user_uid) {
element.classList.add("switch-user")
element.classList.add("message");
if (!this.messages.length || 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) {
const obj = new models.Message(
message.uid,
message.channel_uid,
@ -120,51 +118,51 @@ class MessageListElement extends HTMLElement {
message.html,
message.created_at,
message.updated_at
)
const element = this.createElement(obj)
this.messages.push(obj)
this.container.appendChild(element)
const me = this
);
const element = this.createElement(obj);
this.messages.push(obj);
this.container.appendChild(element);
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() {
this.container.scrollTop = this.container.scrollHeight;
}
connectedCallback() {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.component.classList.add("chat-messages")
this.container = document.createElement('div')
//this.container.classList.add("chat-messages")
this.component.appendChild(this.container)
this.messageEventSchedule = new Schedule(500)
this.messages = []
this.channel_uid = this.getAttribute("channel")
const me = this
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.component.classList.add("chat-messages");
this.container = document.createElement('div');
this.component.appendChild(this.container);
this.messageEventSchedule = new Schedule(500);
this.messages = [];
this.channel_uid = this.getAttribute("channel");
app.addEventListener(this.channel_uid, (data) => {
me.addMessage(data)
})
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }))
this.addMessage(data);
});
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
this.timeUpdateInterval = setInterval(() => {
me.messages.forEach((message) => {
const newText = me.timeDescription(message.created_at)
this.messages.forEach((message) => {
const newText = this.timeDescription(message.created_at);
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 {
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.message = message
@ -17,10 +17,10 @@ class MessageModel {
this.channel_uid = channel_uid
this.created_at = created_at
this.updated_at = updated_at
this.element = null
}
}
const models = {
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 {
constructor(msDelay) {
if(!msDelay){
msDelay = 100
}
this.msDelay = msDelay
this._once = false
constructor(msDelay = 100) {
this.msDelay = msDelay;
this._once = false;
this.timeOutCount = 0;
this.timeOut = null
this.interval = null
this.timeOut = null;
this.interval = null;
}
cancelRepeat() {
clearInterval(this.interval)
this.interval = null
clearInterval(this.interval);
this.interval = null;
}
cancelDelay() {
clearTimeout(this.timeOut)
this.timeOut = null
clearTimeout(this.timeOut);
this.timeOut = null;
}
repeat(func) {
if (this.interval) {
return false
return false;
}
this.interval = setInterval(() => {
func()
}, this.msDelay)
func();
}, this.msDelay);
}
delay(func) {
this.timeOutCount++
this.timeOutCount++;
if (this.timeOut) {
this.cancelDelay()
this.cancelDelay();
}
const me = this
const me = this;
this.timeOut = setTimeout(() => {
func(me.timeOutCount)
clearTimeout(me.timeOut)
me.timeOut = null
me.cancelDelay()
me.timeOutCount = 0
}, this.msDelay)
func(me.timeOutCount);
clearTimeout(me.timeOut);
me.timeOut = null;
me.cancelDelay();
me.timeOutCount = 0;
}, 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">
<title>Snek</title>
<style>{{highlight_styles}}</style>
<script src="/media-upload.js"></script>
<script src="/upload-button.js"></script>
<script src="/html-frame.js"></script>
<script src="/schedule.js"></script>
<script src="/app.js"></script>