Non working upload button.
This commit is contained in:
parent
fe707dca4e
commit
b48a901e33
@ -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" }
|
||||
|
@ -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)
|
||||
}catch(e){
|
||||
console.warning(e)
|
||||
setTimeout(()=>{
|
||||
me.ensureConnection()
|
||||
},1000)
|
||||
}
|
||||
|
||||
me._socket.onconnect = () => {
|
||||
me._connected()
|
||||
me._wait_socket(me)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
generateUniqueId() {
|
||||
return 'id-' + Math.random().toString(36).substr(2, 9); // Example: id-k5f9zq7
|
||||
}
|
||||
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))
|
||||
|
||||
this._socket = new WebSocket(this._url);
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
console.warn(e);
|
||||
setTimeout(() => {
|
||||
this.ensureConnection();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
|
||||
this._socket.onconnect = () => {
|
||||
this._connected();
|
||||
this._waitSocket();
|
||||
};
|
||||
});
|
||||
}
|
||||
_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]
|
||||
} else {
|
||||
me.emit("message", me, message)
|
||||
|
||||
generateUniqueId() {
|
||||
return 'id-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
//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)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_connected() {
|
||||
this._socket.onmessage = (event) => {
|
||||
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 {
|
||||
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) {
|
||||
return (...args) => {
|
||||
let functionName = me._camelToSnake(prop)
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
return proxy
|
||||
const me = this;
|
||||
return new Proxy({}, {
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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..")
|
||||
|
||||
const ws = new WebSocket(this.url)
|
||||
|
||||
ws.onopen = (event) => {
|
||||
me.ws = ws
|
||||
me.isConnected = true
|
||||
me.isConnecting = false
|
||||
this.isConnecting = true;
|
||||
return new Promise((resolve) => {
|
||||
this.connectPromises.push(resolve);
|
||||
console.debug("Connecting..");
|
||||
|
||||
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))
|
||||
}
|
||||
ws.onclose = (event) => {
|
||||
me.onClose()
|
||||
|
||||
}
|
||||
ws.onerror = (event)=>{
|
||||
me.onClose()
|
||||
}
|
||||
me.connectPromises.forEach(resolve => {
|
||||
resolve(me)
|
||||
})
|
||||
}
|
||||
})
|
||||
this.onData(JSON.parse(event.data));
|
||||
};
|
||||
ws.onclose = () => {
|
||||
this.onClose();
|
||||
};
|
||||
ws.onerror = () => {
|
||||
this.onClose();
|
||||
};
|
||||
this.connectPromises.forEach(resolver => resolver(this));
|
||||
};
|
||||
});
|
||||
}
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
const me = this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
me.addEventListener(call.callId, (data) => {
|
||||
resolve(data)
|
||||
})
|
||||
await me.sendJson(call)
|
||||
|
||||
|
||||
})
|
||||
method,
|
||||
args,
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
this.addEventListener(call.callId, data => resolve(data));
|
||||
this.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()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification")
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
})
|
||||
new Audio(this.sounds[soundIndex]).play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class App extends EventHandler {
|
||||
rest = rest
|
||||
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)
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
me.emit(data.channel_uid, data)
|
||||
})
|
||||
rest = new RESTClient();
|
||||
ws = null;
|
||||
rpc = null;
|
||||
audio = null;
|
||||
user = {};
|
||||
|
||||
this.rpc.getUser(null).then(user=>{
|
||||
me.user = user
|
||||
})
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
this.emit(data.channel_uid, data);
|
||||
});
|
||||
|
||||
this.rpc.getUser(null).then(user => {
|
||||
this.user = user;
|
||||
});
|
||||
}
|
||||
playSound(index){
|
||||
this.audio.play(index)
|
||||
|
||||
playSound(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();
|
@ -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;
|
||||
|
@ -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")
|
||||
this.container.innerHTML = `
|
||||
<textarea placeholder="Type a message..." rows="2"></textarea>
|
||||
<button>Send</button>
|
||||
`;
|
||||
this.textBox = this.container.querySelector('textarea')
|
||||
this.textBox.addEventListener('input', (e) => {
|
||||
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)
|
||||
})
|
||||
this.textBox.addEventListener('keydown', (e) => {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
this.component.appendChild(link);
|
||||
|
||||
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)
|
||||
}
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('chat-input');
|
||||
this.container.innerHTML = `
|
||||
<textarea placeholder="Type a message..." rows="2"></textarea>
|
||||
<upload-button></upload-button>
|
||||
`;
|
||||
this.textBox = this.container.querySelector('textarea');
|
||||
|
||||
this.textBox.addEventListener('input', (e) => {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
customElements.define('chat-input', ChatInputElement);
|
||||
|
||||
customElements.define('chat-input', ChatInputElement);
|
@ -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 chatHeader = document.createElement("div")
|
||||
chatHeader.classList.add("chat-header")
|
||||
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", "chat-window");
|
||||
|
||||
|
||||
|
||||
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')
|
||||
|
||||
chatInput.addEventListener("submit",(e)=>{
|
||||
app.rpc.sendMessage(channel.uid,e.detail)
|
||||
})
|
||||
this.container.appendChild(chatInput)
|
||||
const chatHeader = document.createElement("div");
|
||||
chatHeader.classList.add("chat-header");
|
||||
|
||||
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 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);
|
||||
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);
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
customElements.define('chat-window', ChatWindowElement);
|
||||
customElements.define('chat-window', ChatWindowElement);
|
@ -1,30 +1,33 @@
|
||||
// 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'})
|
||||
constructor() {
|
||||
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%;
|
||||
width: 100%;
|
||||
--width: 100%;
|
||||
}
|
||||
}
|
||||
button {
|
||||
width: var(--width);
|
||||
min-width: ${size};
|
||||
@ -37,32 +40,31 @@ class FancyButton extends HTMLElement {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
border: 1px solid #f05a28;
|
||||
}
|
||||
border: 1px solid #f05a28;
|
||||
}
|
||||
button:hover {
|
||||
color: #EFEFEF;
|
||||
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)
|
||||
|
||||
color: #EFEFEF;
|
||||
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.url = this.getAttribute('url');
|
||||
this.value = this.getAttribute('value')
|
||||
const me = this
|
||||
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
|
||||
this.value = this.getAttribute('value');
|
||||
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
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);
|
@ -1,346 +1,362 @@
|
||||
// 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
|
||||
}
|
||||
set value(val) {
|
||||
val = val == null ? '' : val
|
||||
this.inputElement.value = val
|
||||
this.inputElement.setAttribute("value", val)
|
||||
}
|
||||
setInvalid(){
|
||||
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","")
|
||||
}
|
||||
setValid(){
|
||||
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')
|
||||
this.styleElement.innerHTML = `
|
||||
get value() {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #f05a28;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
get type() {
|
||||
return this.field.tag;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6e6e6;
|
||||
font-size: 1em;
|
||||
}
|
||||
set value(val) {
|
||||
val = val ?? '';
|
||||
this.inputElement.value = val;
|
||||
this.inputElement.setAttribute("value", val);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 50%;
|
||||
padding: 10px;
|
||||
background-color: #f05a28;
|
||||
border: none;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
clear: both;
|
||||
}
|
||||
setInvalid() {
|
||||
this.inputElement.classList.add("error");
|
||||
this.inputElement.classList.remove("valid");
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #e04924;
|
||||
}
|
||||
setErrors(errors) {
|
||||
const errorText = errors.length ? errors[0] : "";
|
||||
this.inputElement.setAttribute("title", errorText);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #f05a28;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
font-size: 0.9em;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
setValid() {
|
||||
this.inputElement.classList.remove("error");
|
||||
this.inputElement.classList.add("valid");
|
||||
}
|
||||
|
||||
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.shadowRoot.appendChild(this.container)
|
||||
}
|
||||
connectedCallback(){
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
this.updateAttributes()
|
||||
|
||||
}
|
||||
setAttribute(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.action = this.field.value
|
||||
}
|
||||
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)
|
||||
|
||||
}
|
||||
if(!this.field){
|
||||
return
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #f05a28;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
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.text != null){
|
||||
this.inputElement.innerText = this.field.text
|
||||
}
|
||||
if(this.field.html != null){
|
||||
this.inputElement.innerHTML = this.field.html
|
||||
}
|
||||
if(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
|
||||
if(this.field.required && place_holder){
|
||||
place_holder = place_holder
|
||||
}
|
||||
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)
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6e6e6;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 50%;
|
||||
padding: 10px;
|
||||
background-color: #f05a28;
|
||||
border: none;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #e04924;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #f05a28;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
font-size: 0.9em;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
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.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateAttributes();
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (this.field.html != null) {
|
||||
this.inputElement.innerHTML = this.field.html;
|
||||
}
|
||||
if (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 = this.field.place_holder ?? null;
|
||||
if (this.field.required && 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 (!this.footerElement) {
|
||||
this.footerElement = document.createElement('div');
|
||||
this.footerElement.style.clear = 'both';
|
||||
this.container.appendChild(this.footerElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.innerHTML = `
|
||||
|
||||
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.styleElement = document.createElement("style")
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
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.shadowRoot.appendChild(this.container);
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
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)
|
||||
this.loadForm(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No URL provided!";
|
||||
}
|
||||
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: 80%;
|
||||
}
|
||||
}`;
|
||||
|
||||
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();
|
||||
|
||||
let fields = Object.values(me.form.fields)
|
||||
|
||||
fields = fields.sort((a,b)=>{
|
||||
console.info(a.index,b.index)
|
||||
return 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
|
||||
}
|
||||
}
|
||||
this.container = document.createElement('div');
|
||||
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);
|
||||
}
|
||||
this.loadForm(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No URL provided!";
|
||||
}
|
||||
}
|
||||
|
||||
async loadForm(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
this.form = await response.json();
|
||||
|
||||
let fields = Object.values(this.form.fields);
|
||||
|
||||
fields.sort((a, b) => a.index - b.index);
|
||||
fields.forEach(field => {
|
||||
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
|
||||
let response = await fetch(url,{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({"action":"validate", "form":me.form})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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(!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']
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
async submit(){
|
||||
const me = this
|
||||
const url = me.getAttribute("url")
|
||||
const response = await fetch(url,{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({"action":"submit", "form":me.form})
|
||||
});
|
||||
return await response.json()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
customElements.define('generic-form', GenericForm);
|
||||
|
||||
async validate() {
|
||||
const url = this.getAttribute("url");
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ "action": "validate", "form": this.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 url = this.getAttribute("url");
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ "action": "submit", "form": this.form })
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-form', GenericForm);
|
@ -1,47 +1,54 @@
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container.classList.add("html_frame")
|
||||
let url = this.getAttribute('url');
|
||||
if(!url.startsWith("https")){
|
||||
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)
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
this.container.classList.add("html_frame");
|
||||
let url = this.getAttribute('url');
|
||||
if (!url.startsWith("https")) {
|
||||
url = "https://" + 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);
|
||||
}
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
}
|
||||
|
||||
async loadAndRender(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
if (url.endsWith(".md")) {
|
||||
const markdownElement = document.createElement('div');
|
||||
markdownElement.innerHTML = html;
|
||||
this.outerHTML = html;
|
||||
} else {
|
||||
this.container.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
const html = await response.text();
|
||||
if(url.endsWith(".md")){
|
||||
const parent = this
|
||||
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);
|
||||
}
|
||||
|
||||
customElements.define('html-frame', HTMLFrame);
|
@ -1,39 +1,48 @@
|
||||
// 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() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
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)
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
}
|
||||
|
||||
async loadAndRender(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
this.container.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
connectedCallback() {
|
||||
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);
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = 'No source URL!';
|
||||
}
|
||||
}
|
||||
customElements.define('markdown-frame', HTMLFrame);
|
||||
|
||||
async loadAndRender(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
this.container.innerHTML = html;
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('markdown-frame', HTMLFrame);
|
@ -1,20 +1,34 @@
|
||||
// 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.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;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
@ -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,38 +61,39 @@ 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 {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
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(){
|
||||
|
||||
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');
|
||||
@ -130,38 +144,32 @@ class UploadButton extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('upload-button', UploadButton);
|
||||
|
||||
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.component.style.backgroundColor ='blue';
|
||||
this.shadowRoot.appendChild(this.container)
|
||||
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.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.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('menia-upload', MeniaUploadElement)
|
||||
customElements.define('menia-upload', MeniaUploadElement);
|
@ -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
|
||||
channels.forEach(channel=>{
|
||||
const messageList = document.createElement("message-list")
|
||||
messageList.setAttribute("channel",channel.uid)
|
||||
me.container.appendChild(messageList)
|
||||
})
|
||||
const channels = await app.rpc.getChannels();
|
||||
channels.forEach(channel => {
|
||||
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);
|
@ -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);
|
||||
|
||||
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)
|
||||
if (minutes > 1)
|
||||
return `${minutes} minutes ago`
|
||||
else
|
||||
return `${minutes} minute ago`
|
||||
|
||||
return `just now`
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? '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) {
|
||||
|
||||
addMessage(message) {
|
||||
const obj = new models.Message(
|
||||
message.uid,
|
||||
message.channel_uid,
|
||||
@ -120,52 +118,52 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageListElement);
|
||||
customElements.define('message-list', MessageListElement);
|
@ -1,26 +1,26 @@
|
||||
// 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
|
||||
this.html = html
|
||||
this.user_uid = user_uid
|
||||
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) {
|
||||
this.uid = uid
|
||||
this.message = message
|
||||
this.html = html
|
||||
this.user_uid = user_uid
|
||||
this.user_nick = user_nick
|
||||
this.color = color
|
||||
this.channel_uid = channel_uid
|
||||
this.channel_uid = channel_uid
|
||||
this.created_at = created_at
|
||||
this.updated_at = updated_at
|
||||
this.element = null
|
||||
}
|
||||
}
|
||||
|
||||
const models = {
|
||||
Message: MessageModel
|
||||
|
||||
}
|
@ -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 = 100) {
|
||||
this.msDelay = msDelay;
|
||||
this._once = false;
|
||||
this.timeOutCount = 0;
|
||||
this.timeOut = null;
|
||||
this.interval = null;
|
||||
}
|
||||
|
||||
constructor(msDelay) {
|
||||
if(!msDelay){
|
||||
msDelay = 100
|
||||
}
|
||||
this.msDelay = msDelay
|
||||
this._once = false
|
||||
this.timeOutCount = 0;
|
||||
this.timeOut = null
|
||||
this.interval = null
|
||||
}
|
||||
cancelRepeat() {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
cancelDelay() {
|
||||
clearTimeout(this.timeOut)
|
||||
this.timeOut = null
|
||||
}
|
||||
repeat(func){
|
||||
if(this.interval){
|
||||
return false
|
||||
}
|
||||
this.interval = setInterval(()=>{
|
||||
func()
|
||||
}, this.msDelay)
|
||||
}
|
||||
delay(func) {
|
||||
this.timeOutCount++
|
||||
if(this.timeOut){
|
||||
this.cancelDelay()
|
||||
}
|
||||
const me = this
|
||||
this.timeOut = setTimeout(()=>{
|
||||
func(me.timeOutCount)
|
||||
clearTimeout(me.timeOut)
|
||||
me.timeOut = null
|
||||
|
||||
me.cancelDelay()
|
||||
me.timeOutCount = 0
|
||||
}, this.msDelay)
|
||||
}
|
||||
cancelRepeat() {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
|
||||
cancelDelay() {
|
||||
clearTimeout(this.timeOut);
|
||||
this.timeOut = null;
|
||||
}
|
||||
|
||||
repeat(func) {
|
||||
if (this.interval) {
|
||||
return false;
|
||||
}
|
||||
this.interval = setInterval(() => {
|
||||
func();
|
||||
}, this.msDelay);
|
||||
}
|
||||
|
||||
delay(func) {
|
||||
this.timeOutCount++;
|
||||
if (this.timeOut) {
|
||||
this.cancelDelay();
|
||||
}
|
||||
const me = this;
|
||||
this.timeOut = setTimeout(() => {
|
||||
func(me.timeOutCount);
|
||||
clearTimeout(me.timeOut);
|
||||
me.timeOut = null;
|
||||
me.cancelDelay();
|
||||
me.timeOutCount = 0;
|
||||
}, this.msDelay);
|
||||
}
|
||||
}
|
121
src/snek/static/upload-button.js
Normal file
121
src/snek/static/upload-button.js
Normal 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);
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user