Format.
This commit is contained in:
parent
b2a4887e23
commit
f0545cbf02
src/snek
static
app.jschat-input.jschat-window.jsdumb-term.jsevent-handler.jsfancy-button.jsfile-manager.jsgeneric-form.jshtml-frame.jsmarkdown-frame.jsmedia-upload.jsmessage-list-manager.jsmessage-list.jsmodels.jsonline-users.jspush.jsschedule.jsservice-worker.jssocket.jsupload-button.jsuser-list.js
templates
@ -1,250 +1,259 @@
|
||||
// Written by retoor@molodetz.nl
|
||||
|
||||
// This project implements a client-server communication system using WebSockets and REST APIs.
|
||||
// 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
|
||||
|
||||
import { Schedule } from './schedule.js';
|
||||
import { Schedule } from "./schedule.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
export class RESTClient {
|
||||
debug = false;
|
||||
debug = false;
|
||||
|
||||
async get(url, params = {}) {
|
||||
const encodedParams = new URLSearchParams(params);
|
||||
if (encodedParams) url += '?' + encodedParams;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, params, result });
|
||||
}
|
||||
return result;
|
||||
async get(url, params = {}) {
|
||||
const encodedParams = new URLSearchParams(params);
|
||||
if (encodedParams) url += "?" + encodedParams;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, params, result });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async post(url, data) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
async post(url, data) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, data, result });
|
||||
}
|
||||
return result;
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, data, result });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export 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._waitConnect = null;
|
||||
this._promises = {};
|
||||
constructor() {
|
||||
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._waitConnect) {
|
||||
return this._waitConnect;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._waitConnect = resolve;
|
||||
console.debug("Connecting..");
|
||||
|
||||
connect() {
|
||||
if (this._waitConnect) {
|
||||
return this._waitConnect;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._waitConnect = resolve;
|
||||
console.debug("Connecting..");
|
||||
try {
|
||||
this._socket = new WebSocket(this._url);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setTimeout(() => {
|
||||
this.ensureConnection();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
try {
|
||||
this._socket = new WebSocket(this._url);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setTimeout(() => {
|
||||
this.ensureConnection();
|
||||
}, 1000);
|
||||
}
|
||||
this._socket.onconnect = () => {
|
||||
this._connected();
|
||||
this._waitSocket();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this._socket.onconnect = () => {
|
||||
this._connected();
|
||||
this._waitSocket();
|
||||
};
|
||||
});
|
||||
}
|
||||
generateUniqueId() {
|
||||
return "id-" + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
_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");
|
||||
};
|
||||
}
|
||||
|
||||
_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,
|
||||
text,
|
||||
});
|
||||
}
|
||||
async privmsg(room, text) {
|
||||
await rest.post("/api/privmsg", {
|
||||
room,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationAudio {
|
||||
constructor(timeout = 500) {
|
||||
this.schedule = new Schedule(timeout);
|
||||
}
|
||||
constructor(timeout = 500) {
|
||||
this.schedule = new Schedule(timeout);
|
||||
}
|
||||
|
||||
sounds = {
|
||||
"message": "/audio/soundfx.d_beep3.mp3",
|
||||
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
}
|
||||
sounds = {
|
||||
message: "/audio/soundfx.d_beep3.mp3",
|
||||
mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
messageOtherChannel:
|
||||
"/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
};
|
||||
|
||||
play(soundIndex = 0) {
|
||||
this.schedule.delay(() => {
|
||||
new Audio(this.sounds[soundIndex]).play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
play(soundIndex = 0) {
|
||||
this.schedule.delay(() => {
|
||||
new Audio(this.sounds[soundIndex])
|
||||
.play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class App extends EventHandler {
|
||||
rest = new RESTClient();
|
||||
ws = null;
|
||||
rpc = null;
|
||||
audio = null;
|
||||
user = {};
|
||||
typeLock = null;
|
||||
typeListener = null
|
||||
typeEventChannelUid = null
|
||||
async set_typing(channel_uid){
|
||||
this.typeEventChannel_uid = channel_uid
|
||||
rest = new RESTClient();
|
||||
ws = null;
|
||||
rpc = null;
|
||||
audio = null;
|
||||
user = {};
|
||||
typeLock = null;
|
||||
typeListener = null;
|
||||
typeEventChannelUid = null;
|
||||
async set_typing(channel_uid) {
|
||||
this.typeEventChannel_uid = channel_uid;
|
||||
}
|
||||
|
||||
async ping(...args) {
|
||||
if (this.is_pinging) return false;
|
||||
this.is_pinging = true;
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false;
|
||||
}
|
||||
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.is_pinging = false;
|
||||
this.ping_interval = setInterval(() => {
|
||||
this.ping("active");
|
||||
}, 15000);
|
||||
this.typeEventChannelUid = null;
|
||||
this.typeListener = setInterval(() => {
|
||||
if (this.typeEventChannelUid) {
|
||||
this.rpc.set_typing(this.typeEventChannelUid);
|
||||
this.typeEventChannelUid = null;
|
||||
}
|
||||
});
|
||||
|
||||
const me = this;
|
||||
this.ws.addEventListener("connected", (data) => {
|
||||
this.ping("online");
|
||||
});
|
||||
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
me.emit("channel-message", data);
|
||||
});
|
||||
this.ws.addEventListener("event", (data) => {
|
||||
console.info("aaaa");
|
||||
});
|
||||
this.rpc.getUser(null).then((user) => {
|
||||
me.user = user;
|
||||
});
|
||||
}
|
||||
|
||||
playSound(index) {
|
||||
this.audio.play(index);
|
||||
}
|
||||
|
||||
timeDescription(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}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
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) {
|
||||
return `${days} ${days > 1 ? "days" : "day"} ago`;
|
||||
}
|
||||
|
||||
async ping(...args) {
|
||||
if (this.is_pinging) return false
|
||||
this.is_pinging = true
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
|
||||
}
|
||||
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
|
||||
}
|
||||
return "just now";
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.is_pinging = false
|
||||
this.ping_interval = setInterval(() => {
|
||||
this.ping("active")
|
||||
}, 15000)
|
||||
this.typeEventChannelUid = null
|
||||
this.typeListener = setInterval(()=>{
|
||||
if(this.typeEventChannelUid){
|
||||
this.rpc.set_typing(this.typeEventChannelUid)
|
||||
this.typeEventChannelUid = null
|
||||
}
|
||||
})
|
||||
|
||||
const me = this
|
||||
this.ws.addEventListener("connected", (data) => {
|
||||
this.ping("online")
|
||||
})
|
||||
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
me.emit("channel-message", data);
|
||||
});
|
||||
this.ws.addEventListener("event",(data)=>{
|
||||
console.info("aaaa")
|
||||
})
|
||||
this.rpc.getUser(null).then(user => {
|
||||
me.user = user;
|
||||
});
|
||||
}
|
||||
|
||||
playSound(index) {
|
||||
this.audio.play(index);
|
||||
}
|
||||
|
||||
timeDescription(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}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
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) {
|
||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||
}
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
async benchMark(times = 100, message = "Benchmark Message") {
|
||||
const promises = [];
|
||||
const me = this;
|
||||
for (let i = 0; i < times; i++) {
|
||||
promises.push(this.rpc.getChannels().then(channels => {
|
||||
channels.forEach(channel => {
|
||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
async benchMark(times = 100, message = "Benchmark Message") {
|
||||
const promises = [];
|
||||
const me = this;
|
||||
for (let i = 0; i < times; i++) {
|
||||
promises.push(
|
||||
this.rpc.getChannels().then((channels) => {
|
||||
channels.forEach((channel) => {
|
||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const app = new App();
|
||||
|
@ -1,235 +1,243 @@
|
||||
|
||||
import { app } from '../app.js';
|
||||
import { app } from "../app.js";
|
||||
|
||||
class ChatInputComponent extends HTMLElement {
|
||||
autoCompletions = {
|
||||
'example 1': () => {
|
||||
autoCompletions = {
|
||||
"example 1": () => {},
|
||||
"example 2": () => {},
|
||||
};
|
||||
|
||||
},
|
||||
'example 2': () => {
|
||||
constructor() {
|
||||
super();
|
||||
this.lastUpdateEvent = new Date();
|
||||
this.textarea = document.createElement("textarea");
|
||||
this._value = "";
|
||||
this.value = this.getAttribute("value") || "";
|
||||
this.previousValue = this.value;
|
||||
this.lastChange = new Date();
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._value = value || "";
|
||||
this.textarea.value = this._value;
|
||||
}
|
||||
|
||||
resolveAutoComplete() {
|
||||
let count = 0;
|
||||
let value = null;
|
||||
Object.keys(this.autoCompletions).forEach((key) => {
|
||||
if (key.startsWith(this.value)) {
|
||||
count++;
|
||||
value = key;
|
||||
}
|
||||
});
|
||||
if (count == 1) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return document.activeElement === this.textarea;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.user = await app.rpc.getUser(null);
|
||||
this.liveType = this.getAttribute("live-type") === "true";
|
||||
this.liveTypeInterval =
|
||||
parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
this.messageUid = null;
|
||||
|
||||
this.classList.add("chat-input");
|
||||
|
||||
this.textarea.setAttribute("placeholder", "Type a message...");
|
||||
this.textarea.setAttribute("rows", "2");
|
||||
|
||||
this.appendChild(this.textarea);
|
||||
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||
this.uploadButton.addEventListener("upload", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("upload", e));
|
||||
});
|
||||
this.uploadButton.addEventListener("uploaded", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("uploaded", e));
|
||||
});
|
||||
|
||||
this.appendChild(this.uploadButton);
|
||||
|
||||
this.textarea.addEventListener("keyup", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
this.value = "";
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
this.value = e.target.value;
|
||||
this.changed = true;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.textarea.addEventListener("keydown", (e) => {
|
||||
this.value = e.target.value;
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
let autoCompletion = this.resolveAutoComplete();
|
||||
if (autoCompletion) {
|
||||
e.target.value = autoCompletion;
|
||||
this.value = autoCompletion;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.lastUpdateEvent = new Date();
|
||||
this.textarea = document.createElement("textarea");
|
||||
this._value = "";
|
||||
this.value = this.getAttribute("value") || "";
|
||||
this.previousValue = this.value;
|
||||
this.lastChange = new Date();
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._value = value || "";
|
||||
this.textarea.value = this._value;
|
||||
}
|
||||
|
||||
resolveAutoComplete() {
|
||||
let count = 0;
|
||||
let value = null;
|
||||
Object.keys(this.autoCompletions).forEach((key) => {
|
||||
if (key.startsWith(this.value)) {
|
||||
count++;
|
||||
value = key;
|
||||
}
|
||||
});
|
||||
if (count == 1)
|
||||
return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return document.activeElement === this.textarea;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.user = await app.rpc.getUser(null);
|
||||
this.liveType = this.getAttribute("live-type") === "true";
|
||||
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
const message = e.target.value;
|
||||
this.messageUid = null;
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
|
||||
this.classList.add("chat-input");
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.textarea.setAttribute("placeholder", "Type a message...");
|
||||
this.textarea.setAttribute("rows", "2");
|
||||
let autoCompletion = this.autoCompletions[message];
|
||||
if (autoCompletion) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
e.target.value = "";
|
||||
autoCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
this.appendChild(this.textarea);
|
||||
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||
this.uploadButton.addEventListener("upload", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("upload", e));
|
||||
});
|
||||
this.uploadButton.addEventListener("uploaded", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("uploaded", e));
|
||||
e.target.value = "";
|
||||
this.value = "";
|
||||
this.messageUid = null;
|
||||
this.sendMessage(this.channelUid, message).then((uid) => {
|
||||
this.messageUid = uid;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.appendChild(this.uploadButton);
|
||||
this.changeInterval = setInterval(() => {
|
||||
if (!this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.value !== this.previousValue) {
|
||||
if (
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval
|
||||
) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
}
|
||||
this.lastChange = new Date();
|
||||
}
|
||||
this.update();
|
||||
}, 300);
|
||||
|
||||
this.textarea.addEventListener("keyup", (e) => {
|
||||
if(e.key === 'Enter' && !e.shiftKey) {
|
||||
this.value = ''
|
||||
e.target.value = '';
|
||||
return
|
||||
}
|
||||
this.value = e.target.value;
|
||||
this.changed = true;
|
||||
this.update();
|
||||
});
|
||||
this.addEventListener("upload", (e) => {
|
||||
this.focus();
|
||||
});
|
||||
this.addEventListener("uploaded", function (e) {
|
||||
let message = "";
|
||||
e.detail.files.forEach((file) => {
|
||||
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||
});
|
||||
app.rpc.sendMessage(this.channelUid, message);
|
||||
});
|
||||
}
|
||||
|
||||
this.textarea.addEventListener("keydown", (e) => {
|
||||
this.value = e.target.value;
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
let autoCompletion = this.resolveAutoComplete();
|
||||
if (autoCompletion) {
|
||||
e.target.value = autoCompletion;
|
||||
this.value = autoCompletion;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||
return millisecondsDifference / 1000;
|
||||
}
|
||||
|
||||
const message = e.target.value;
|
||||
this.messageUid = null;
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
let autoCompletion = this.autoCompletions[message];
|
||||
if (autoCompletion) {
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
e.target.value = '';
|
||||
autoCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.value = '';
|
||||
this.value = '';
|
||||
this.messageUid = null;
|
||||
this.sendMessage(this.channelUid, message).then((uid) => {
|
||||
this.messageUid = uid;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.changeInterval = setInterval(() => {
|
||||
if (!this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.value !== this.previousValue) {
|
||||
if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) {
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
}
|
||||
this.lastChange = new Date();
|
||||
}
|
||||
this.update();
|
||||
}, 300);
|
||||
|
||||
this.addEventListener("upload", (e) => {
|
||||
this.focus();
|
||||
});
|
||||
this.addEventListener("uploaded", function (e) {
|
||||
let message = "";
|
||||
e.detail.files.forEach((file) => {
|
||||
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||
});
|
||||
app.rpc.sendMessage(this.channelUid, message);
|
||||
});
|
||||
newMessage() {
|
||||
if (!this.messageUid) {
|
||||
this.messageUid = "?";
|
||||
}
|
||||
|
||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||
return millisecondsDifference / 1000;
|
||||
this.sendMessage(this.channelUid, this.value).then((uid) => {
|
||||
this.messageUid = uid;
|
||||
});
|
||||
}
|
||||
|
||||
updateMessage() {
|
||||
if (this.value[0] == "/") {
|
||||
return false;
|
||||
}
|
||||
if (!this.messageUid) {
|
||||
this.newMessage();
|
||||
return false;
|
||||
}
|
||||
if (this.messageUid === "?") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.updateMessageText === "function"
|
||||
) {
|
||||
app.rpc.updateMessageText(this.messageUid, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
if (this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||
this.lastUpdateEvent = new Date();
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.set_typing === "function"
|
||||
) {
|
||||
app.rpc.set_typing(this.channelUid, this.user.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const expired =
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval;
|
||||
const changed = this.value !== this.previousValue;
|
||||
|
||||
if (changed || expired) {
|
||||
this.lastChange = new Date();
|
||||
this.updateStatus();
|
||||
}
|
||||
|
||||
newMessage() {
|
||||
if (!this.messageUid) {
|
||||
this.messageUid = '?';
|
||||
}
|
||||
this.previousValue = this.value;
|
||||
|
||||
this.sendMessage(this.channelUid, this.value).then((uid) => {
|
||||
this.messageUid = uid;
|
||||
});
|
||||
if (this.liveType && expired) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
this.messageUid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
updateMessage() {
|
||||
if (this.value[0] == "/") {
|
||||
return false;
|
||||
}
|
||||
if (!this.messageUid) {
|
||||
this.newMessage();
|
||||
return false;
|
||||
}
|
||||
if (this.messageUid === '?') {
|
||||
return false;
|
||||
}
|
||||
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.updateMessageText === "function") {
|
||||
app.rpc.updateMessageText(this.messageUid, this.value);
|
||||
}
|
||||
if (changed) {
|
||||
if (this.liveType) {
|
||||
this.updateMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
if (this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||
this.lastUpdateEvent = new Date();
|
||||
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.set_typing === "function") {
|
||||
app.rpc.set_typing(this.channelUid,this.user.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval;
|
||||
const changed = (this.value !== this.previousValue);
|
||||
|
||||
if (changed || expired) {
|
||||
this.lastChange = new Date();
|
||||
this.updateStatus();
|
||||
}
|
||||
|
||||
this.previousValue = this.value;
|
||||
|
||||
if (this.liveType && expired) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
this.messageUid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (this.liveType) {
|
||||
this.updateMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(channelUid, value) {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
return await app.rpc.sendMessage(channelUid, value);
|
||||
async sendMessage(channelUid, value) {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
return await app.rpc.sendMessage(channelUid, value);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-input', ChatInputComponent);
|
||||
customElements.define("chat-input", ChatInputComponent);
|
||||
|
@ -6,77 +6,77 @@
|
||||
|
||||
// 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;
|
||||
channel = null
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('section');
|
||||
this.app = app;
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
receivedHistory = false;
|
||||
channel = null;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("section");
|
||||
this.app = app;
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
get user() {
|
||||
return this.app.user;
|
||||
}
|
||||
get 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", "chat-window");
|
||||
async connectedCallback() {
|
||||
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 chatTitle = document.createElement('h2');
|
||||
chatTitle.classList.add("chat-title");
|
||||
chatTitle.classList.add("no-select");
|
||||
chatTitle.innerText = "Loading...";
|
||||
chatHeader.appendChild(chatTitle);
|
||||
this.container.appendChild(chatHeader);
|
||||
const chatHeader = document.createElement("div");
|
||||
chatHeader.classList.add("chat-header");
|
||||
|
||||
const channels = await app.rpc.getChannels();
|
||||
const channel = channels[0];
|
||||
this.channel = channel;
|
||||
chatTitle.innerText = channel.name;
|
||||
const chatTitle = document.createElement("h2");
|
||||
chatTitle.classList.add("chat-title");
|
||||
chatTitle.classList.add("no-select");
|
||||
chatTitle.innerText = "Loading...";
|
||||
chatHeader.appendChild(chatTitle);
|
||||
this.container.appendChild(chatHeader);
|
||||
|
||||
const channelElement = document.createElement('message-list');
|
||||
channelElement.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(channelElement);
|
||||
const channels = await app.rpc.getChannels();
|
||||
const channel = channels[0];
|
||||
this.channel = channel;
|
||||
chatTitle.innerText = channel.name;
|
||||
|
||||
const chatInput = document.createElement('chat-input');
|
||||
chatInput.chatWindow = this;
|
||||
chatInput.addEventListener("submit", (e) => {
|
||||
app.rpc.sendMessage(channel.uid, e.detail);
|
||||
});
|
||||
this.container.appendChild(chatInput);
|
||||
const channelElement = document.createElement("message-list");
|
||||
channelElement.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(channelElement);
|
||||
|
||||
this.component.appendChild(this.container);
|
||||
const chatInput = document.createElement("chat-input");
|
||||
chatInput.chatWindow = this;
|
||||
chatInput.addEventListener("submit", (e) => {
|
||||
app.rpc.sendMessage(channel.uid, e.detail);
|
||||
});
|
||||
this.container.appendChild(chatInput);
|
||||
|
||||
const messages = await app.rpc.getMessages(channel.uid);
|
||||
messages.forEach(message => {
|
||||
if (!message['user_nick']) return;
|
||||
channelElement.addMessage(message);
|
||||
});
|
||||
this.component.appendChild(this.container);
|
||||
|
||||
const me = this;
|
||||
channelElement.addEventListener("message", (message) => {
|
||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||
|
||||
message.detail.element.scrollIntoView({"block": "end"});
|
||||
});
|
||||
}
|
||||
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({ block: "end" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-window', ChatWindowElement);
|
||||
customElements.define("chat-window", ChatWindowElement);
|
||||
|
150
src/snek/static/dumb-term.js
Normal file
150
src/snek/static/dumb-term.js
Normal file
@ -0,0 +1,150 @@
|
||||
class DumbTerminal extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
--terminal-bg: #111;
|
||||
--terminal-fg: #0f0;
|
||||
--terminal-accent: #0ff;
|
||||
--terminal-font: monospace;
|
||||
|
||||
display: block;
|
||||
background: var(--terminal-bg);
|
||||
color: var(--terminal-fg);
|
||||
font-family: var(--terminal-font);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.output {
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--terminal-accent);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--terminal-fg);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
dialog {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialog-backdrop {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="output" id="output"></div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">></span>
|
||||
<input type="text" id="input" autocomplete="off" autofocus />
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.outputEl = this.shadowRoot.getElementById("output");
|
||||
this.inputEl = this.shadowRoot.getElementById("input");
|
||||
|
||||
this.history = [];
|
||||
this.historyIndex = -1;
|
||||
|
||||
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
const value = this.inputEl.value;
|
||||
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
this.executeCommand(value);
|
||||
this.history.push(value);
|
||||
this.historyIndex = this.history.length;
|
||||
this.inputEl.value = "";
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
this.inputEl.value = this.history[this.historyIndex];
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
if (this.historyIndex < this.history.length - 1) {
|
||||
this.historyIndex++;
|
||||
this.inputEl.value = this.history[this.historyIndex];
|
||||
} else {
|
||||
this.historyIndex = this.history.length;
|
||||
this.inputEl.value = "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
executeCommand(command) {
|
||||
const outputLine = document.createElement("div");
|
||||
outputLine.textContent = `> ${command}`;
|
||||
this.outputEl.appendChild(outputLine);
|
||||
|
||||
const resultLine = document.createElement("div");
|
||||
resultLine.textContent = this.mockExecute(command);
|
||||
this.outputEl.appendChild(resultLine);
|
||||
|
||||
this.outputEl.scrollTop = this.outputEl.scrollHeight;
|
||||
}
|
||||
|
||||
mockExecute(command) {
|
||||
switch (command.trim()) {
|
||||
case "help":
|
||||
return "Available commands: help, clear, date";
|
||||
case "date":
|
||||
return new Date().toString();
|
||||
case "clear":
|
||||
this.outputEl.innerHTML = "";
|
||||
return "";
|
||||
default:
|
||||
return `Unknown command: ${command}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a modal dialog with the terminal
|
||||
* @returns {HTMLDialogElement}
|
||||
*/
|
||||
static createModal() {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.innerHTML = `
|
||||
<div class="dialog-backdrop">
|
||||
<web-terminal></web-terminal>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("web-terminal", WebTerminal);
|
@ -1,16 +1,15 @@
|
||||
|
||||
|
||||
export class EventHandler {
|
||||
constructor() {
|
||||
this.subscribers = {};
|
||||
}
|
||||
constructor() {
|
||||
this.subscribers = {};
|
||||
}
|
||||
|
||||
addEventListener(type, handler) {
|
||||
if (!this.subscribers[type]) this.subscribers[type] = [];
|
||||
this.subscribers[type].push(handler);
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
emit(type, ...data) {
|
||||
if (this.subscribers[type])
|
||||
this.subscribers[type].forEach((handler) => handler(...data));
|
||||
}
|
||||
}
|
||||
|
@ -2,28 +2,25 @@
|
||||
|
||||
// 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 {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.url = null;
|
||||
this.type = "button";
|
||||
this.value = null;
|
||||
}
|
||||
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%';
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("span");
|
||||
let size = this.getAttribute("size");
|
||||
console.info({ GG: size });
|
||||
size = size === "auto" ? "1%" : "33%";
|
||||
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
:root {
|
||||
width: 100%;
|
||||
--width: 100%;
|
||||
@ -49,29 +46,30 @@ class FancyButton extends HTMLElement {
|
||||
}
|
||||
`;
|
||||
|
||||
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.url = this.getAttribute("url");
|
||||
|
||||
this.value = this.getAttribute('value');
|
||||
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
if(this.url == 'submit'){
|
||||
this.closest('form').submit()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.url === "/back" || this.url === "/back/") {
|
||||
window.history.back();
|
||||
} else if (this.url) {
|
||||
window.location = this.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.value = this.getAttribute("value");
|
||||
this.buttonElement.appendChild(
|
||||
document.createTextNode(this.getAttribute("text")),
|
||||
);
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
if (this.url == "submit") {
|
||||
this.closest("form").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.url === "/back" || this.url === "/back/") {
|
||||
window.history.back();
|
||||
} else if (this.url) {
|
||||
window.location = this.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("fancy-button", FancyButton);
|
||||
|
@ -3,13 +3,13 @@ class FileBrowser extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.path = ""; // current virtual path ("" = ROOT)
|
||||
this.offset = 0; // pagination offset
|
||||
this.limit = 40; // items per request
|
||||
this.path = ""; // current virtual path ("" = ROOT)
|
||||
this.offset = 0; // pagination offset
|
||||
this.limit = 40; // items per request
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.path = this.getAttribute("path") || "";
|
||||
this.path = this.getAttribute("path") || "";
|
||||
this.renderShell();
|
||||
this.load();
|
||||
}
|
||||
@ -40,19 +40,30 @@ class FileBrowser extends HTMLElement {
|
||||
<button id="next">Next</button>
|
||||
</nav>
|
||||
`;
|
||||
this.shadowRoot.getElementById("up").addEventListener("click", () => this.goUp());
|
||||
this.shadowRoot
|
||||
.getElementById("up")
|
||||
.addEventListener("click", () => this.goUp());
|
||||
this.shadowRoot.getElementById("prev").addEventListener("click", () => {
|
||||
if (this.offset > 0) { this.offset -= this.limit; this.load(); }
|
||||
if (this.offset > 0) {
|
||||
this.offset -= this.limit;
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
this.shadowRoot.getElementById("next").addEventListener("click", () => {
|
||||
this.offset += this.limit; this.load();
|
||||
this.offset += this.limit;
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Networking ----------------------------------------------
|
||||
async load() {
|
||||
const r = await fetch(`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`);
|
||||
if (!r.ok) { console.error(await r.text()); return; }
|
||||
const r = await fetch(
|
||||
`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
|
||||
);
|
||||
if (!r.ok) {
|
||||
console.error(await r.text());
|
||||
return;
|
||||
}
|
||||
const data = await r.json();
|
||||
this.renderTiles(data.items);
|
||||
this.updateNav(data.pagination);
|
||||
@ -62,13 +73,17 @@ class FileBrowser extends HTMLElement {
|
||||
renderTiles(items) {
|
||||
const grid = this.shadowRoot.getElementById("grid");
|
||||
grid.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
items.forEach((item) => {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
|
||||
if (item.type === "directory") {
|
||||
tile.innerHTML = `<div class="icon">📂</div><div>${item.name}</div>`;
|
||||
tile.addEventListener("click", () => { this.path = item.path; this.offset = 0; this.load(); });
|
||||
tile.addEventListener("click", () => {
|
||||
this.path = item.path;
|
||||
this.offset = 0;
|
||||
this.load();
|
||||
});
|
||||
} else {
|
||||
if (item.mimetype?.startsWith("image/")) {
|
||||
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;
|
||||
@ -87,7 +102,7 @@ class FileBrowser extends HTMLElement {
|
||||
this.shadowRoot.getElementById("crumb").textContent = `/${this.path}`;
|
||||
this.shadowRoot.getElementById("prev").disabled = offset === 0;
|
||||
this.shadowRoot.getElementById("next").disabled = offset + limit >= total;
|
||||
this.shadowRoot.getElementById("up").disabled = this.path === "";
|
||||
this.shadowRoot.getElementById("up").disabled = this.path === "";
|
||||
}
|
||||
|
||||
goUp() {
|
||||
|
@ -40,7 +40,7 @@ class GenericField extends HTMLElement {
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
val = val ?? '';
|
||||
val = val ?? "";
|
||||
this.inputElement.value = val;
|
||||
this.inputElement.setAttribute("value", val);
|
||||
}
|
||||
@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.container = document.createElement('div');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
h1 {
|
||||
@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
|
||||
|
||||
if (this.inputElement == null && this.field) {
|
||||
this.inputElement = document.createElement(this.field.tag);
|
||||
if (this.field.tag === 'button' && this.field.value === "submit") {
|
||||
if (this.field.tag === "button" && this.field.value === "submit") {
|
||||
this.action = this.field.value;
|
||||
}
|
||||
this.inputElement.name = this.field.name;
|
||||
@ -182,26 +182,39 @@ class GenericField extends HTMLElement {
|
||||
|
||||
const me = this;
|
||||
this.inputElement.addEventListener("keyup", (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
if (e.key === "Enter") {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
|
||||
me.dispatchEvent(new Event("submit"));
|
||||
} else if (me.field.value !== e.target.value) {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("click", (e) => {
|
||||
const event = new CustomEvent("click", {detail: me, bubbles: true});
|
||||
const event = new CustomEvent("click", { detail: me, bubbles: true });
|
||||
me.dispatchEvent(event);
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("blur", (e) => {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
me.dispatchEvent(event);
|
||||
}, true);
|
||||
this.inputElement.addEventListener(
|
||||
"blur",
|
||||
(e) => {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
this.container.appendChild(this.inputElement);
|
||||
}
|
||||
@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inputElement.setAttribute("type", this.field.type ?? 'input');
|
||||
this.inputElement.setAttribute("name", 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;
|
||||
@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
|
||||
this.inputElement.removeAttribute("required");
|
||||
}
|
||||
if (!this.footerElement) {
|
||||
this.footerElement = document.createElement('div');
|
||||
this.footerElement.style.clear = 'both';
|
||||
this.footerElement = document.createElement("div");
|
||||
this.footerElement.style.clear = "both";
|
||||
this.container.appendChild(this.footerElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-field', GenericField);
|
||||
customElements.define("generic-field", GenericField);
|
||||
|
||||
class GenericForm extends HTMLElement {
|
||||
fields = {};
|
||||
@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
@ -281,27 +294,29 @@ class GenericForm extends HTMLElement {
|
||||
}
|
||||
}`;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container = document.createElement("div");
|
||||
this.container.appendChild(this.styleElement);
|
||||
this.container.classList.add("generic-form-container");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const preloadedForm = this.getAttribute('preloaded-structure');
|
||||
const preloadedForm = this.getAttribute("preloaded-structure");
|
||||
if (preloadedForm) {
|
||||
try {
|
||||
const form = JSON.parse(preloadedForm);
|
||||
this.constructForm(form)
|
||||
this.constructForm(form);
|
||||
} catch (error) {
|
||||
console.error(error, preloadedForm);
|
||||
}
|
||||
}
|
||||
const url = this.getAttribute('url');
|
||||
const url = this.getAttribute("url");
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) {
|
||||
fullUrl.searchParams.set('url', url);
|
||||
fullUrl.searchParams.set("url", url);
|
||||
}
|
||||
this.loadForm(fullUrl.toString());
|
||||
} else {
|
||||
@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
|
||||
let hasAutoFocus = Object.keys(this.fields).length !== 0;
|
||||
|
||||
fields.sort((a, b) => a.index - b.index);
|
||||
fields.forEach(field => {
|
||||
const updatingField = field.name in this.fields
|
||||
fields.forEach((field) => {
|
||||
const updatingField = field.name in this.fields;
|
||||
|
||||
this.fields[field.name] ??= document.createElement('generic-field');
|
||||
this.fields[field.name] ??= document.createElement("generic-field");
|
||||
|
||||
const fieldElement = this.fields[field.name];
|
||||
|
||||
@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
|
||||
window.location.pathname = saveResult.redirect_url;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
||||
throw new Error(
|
||||
`Failed to fetch: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.constructForm(await response.json());
|
||||
@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
|
||||
const url = this.getAttribute("url");
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({"action": "validate", "form": this.form})
|
||||
body: JSON.stringify({ action: "validate", form: this.form }),
|
||||
});
|
||||
|
||||
const form = await response.json();
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
if (!this.form.fields[field.name]) {
|
||||
return;
|
||||
}
|
||||
@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
|
||||
this.fields[field.name].setAttribute("field", field);
|
||||
this.fields[field.name].updateAttributes();
|
||||
});
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
this.fields[field.name].setErrors(field.errors);
|
||||
});
|
||||
return form['is_valid'];
|
||||
return form["is_valid"];
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const url = this.getAttribute("url");
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({"action": "submit", "form": this.form})
|
||||
body: JSON.stringify({ action: "submit", form: this.form }),
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-form', GenericForm);
|
||||
customElements.define("generic-form", GenericForm);
|
||||
|
@ -7,48 +7,50 @@
|
||||
// MIT License
|
||||
|
||||
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");
|
||||
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!";
|
||||
}
|
||||
connectedCallback() {
|
||||
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}`);
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
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();
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('html-frame', HTMLFrame);
|
||||
customElements.define("html-frame", HTMLFrame);
|
||||
|
@ -12,22 +12,22 @@
|
||||
class HTMLFrame extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
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');
|
||||
this.container.classList.add("html_frame");
|
||||
const url = this.getAttribute("url");
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith('/')
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + '/http-get');
|
||||
if (!url.startsWith('/')) fullUrl.searchParams.set('url', 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!";
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('markdown-frame', HTMLFrame);
|
||||
customElements.define("markdown-frame", HTMLFrame);
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
// 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:
|
||||
@ -13,20 +12,19 @@
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.gridId = this.getAttribute("grid");
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.textContent = `
|
||||
connectedCallback() {
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.grid {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@ -47,53 +45,53 @@ class TileGridElement extends HTMLElement {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
this.component.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('gallery');
|
||||
this.component.appendChild(this.container);
|
||||
}
|
||||
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;
|
||||
item.classList.add('tile');
|
||||
item.style.width = '100px';
|
||||
item.style.height = '100px';
|
||||
this.container.appendChild(item);
|
||||
}
|
||||
addImage(src) {
|
||||
const item = document.createElement("img");
|
||||
item.src = src;
|
||||
item.classList.add("tile");
|
||||
item.style.width = "100px";
|
||||
item.style.height = "100px";
|
||||
this.container.appendChild(item);
|
||||
}
|
||||
|
||||
addImages(srcs) {
|
||||
srcs.forEach(src => this.addImage(src));
|
||||
}
|
||||
addImages(srcs) {
|
||||
srcs.forEach((src) => this.addImage(src));
|
||||
}
|
||||
|
||||
addElement(element) {
|
||||
element.classList.add('tile');
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
addElement(element) {
|
||||
element.classList.add("tile");
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
window.u = this;
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
window.u = this;
|
||||
}
|
||||
|
||||
get gridSelector() {
|
||||
return this.getAttribute('grid');
|
||||
}
|
||||
grid = null;
|
||||
get gridSelector() {
|
||||
return this.getAttribute("grid");
|
||||
}
|
||||
grid = null;
|
||||
|
||||
addImages(urls) {
|
||||
this.grid.addImages(urls);
|
||||
}
|
||||
addImages(urls) {
|
||||
this.grid.addImages(urls);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.textContent = `
|
||||
connectedCallback() {
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -115,61 +113,61 @@ class UploadButton extends HTMLElement {
|
||||
background-color: #999;
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('upload-button');
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.multiple = true;
|
||||
input.addEventListener('change', (e) => {
|
||||
const files = e.target.files;
|
||||
const urls = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
urls.push(e.target.result);
|
||||
if (urls.length === files.length) {
|
||||
this.addImages(urls);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
});
|
||||
const label = document.createElement('label');
|
||||
label.textContent = 'Upload Images';
|
||||
label.appendChild(input);
|
||||
this.container.appendChild(label);
|
||||
}
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("upload-button");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.multiple = true;
|
||||
input.addEventListener("change", (e) => {
|
||||
const files = e.target.files;
|
||||
const urls = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
urls.push(e.target.result);
|
||||
if (urls.length === files.length) {
|
||||
this.addImages(urls);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
});
|
||||
const label = document.createElement("label");
|
||||
label.textContent = "Upload Images";
|
||||
label.appendChild(input);
|
||||
this.container.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('upload-button', UploadButton);
|
||||
customElements.define('tile-grid', TileGridElement);
|
||||
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);
|
||||
}
|
||||
constructor() {
|
||||
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);
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("div");
|
||||
this.component.style.height = "100%";
|
||||
this.component.style.backgroundColor = "blue";
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
|
||||
this.tileElement = document.createElement("tile-grid");
|
||||
this.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);
|
||||
}
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.component.appendChild(this.uploadButton);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('menia-upload', MeniaUploadElement);
|
||||
customElements.define("menia-upload", MeniaUploadElement);
|
||||
|
@ -3,7 +3,7 @@
|
||||
// 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
|
||||
@ -22,23 +22,22 @@
|
||||
// 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);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const channels = await app.rpc.getChannels();
|
||||
channels.forEach(channel => {
|
||||
const messageList = document.createElement("message-list");
|
||||
messageList.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(messageList);
|
||||
});
|
||||
}
|
||||
async connectedCallback() {
|
||||
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);
|
||||
|
@ -5,226 +5,57 @@
|
||||
// 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.
|
||||
import {app} from '../app.js'
|
||||
class MessageList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text",(data)=>{
|
||||
this.updateMessageText(data.data.uid,data.data)
|
||||
})
|
||||
app.ws.addEventListener("set_typing",(data)=>{
|
||||
this.triggerGlow(data.data.user_uid)
|
||||
import { app } from "../app.js";
|
||||
class MessageList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text", (data) => {
|
||||
this.updateMessageText(data.uid, data);
|
||||
});
|
||||
app.ws.addEventListener("set_typing", (data) => {
|
||||
this.triggerGlow(data.user_uid);
|
||||
});
|
||||
|
||||
})
|
||||
this.items = [];
|
||||
}
|
||||
updateMessageText(uid, message) {
|
||||
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
||||
|
||||
this.items = [];
|
||||
if (!messageDiv) {
|
||||
return;
|
||||
}
|
||||
const receivedHtml = document.createElement("div");
|
||||
receivedHtml.innerHTML = message.html;
|
||||
const html = receivedHtml.querySelector(".text").innerHTML;
|
||||
const textElement = messageDiv.querySelector(".text");
|
||||
textElement.innerHTML = html;
|
||||
textElement.style.display = message.text == "" ? "none" : "block";
|
||||
}
|
||||
triggerGlow(uid) {
|
||||
let lastElement = null;
|
||||
this.querySelectorAll(".avatar").forEach((el) => {
|
||||
const div = el.closest("a");
|
||||
if (el.href.indexOf(uid) != -1) {
|
||||
lastElement = el;
|
||||
}
|
||||
updateMessageText(uid,message){
|
||||
const messageDiv = this.querySelector("div[data-uid=\""+uid+"\"]")
|
||||
|
||||
if(!messageDiv){
|
||||
return
|
||||
}
|
||||
const receivedHtml = document.createElement("div")
|
||||
receivedHtml.innerHTML = message.html
|
||||
const html = receivedHtml.querySelector(".text").innerHTML
|
||||
const textElement = messageDiv.querySelector(".text")
|
||||
textElement.innerHTML = html
|
||||
textElement.style.display = message.text == '' ? 'none' : 'block'
|
||||
|
||||
}
|
||||
triggerGlow(uid) {
|
||||
let lastElement = null;
|
||||
this.querySelectorAll(".avatar").forEach((el)=>{
|
||||
const div = el.closest('a');
|
||||
if(el.href.indexOf(uid)!=-1){
|
||||
lastElement = el
|
||||
}
|
||||
|
||||
})
|
||||
if(lastElement){
|
||||
lastElement.classList.add("glow")
|
||||
setTimeout(()=>{
|
||||
lastElement.classList.remove("glow")
|
||||
},1000)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
set data(items) {
|
||||
this.items = items;
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
|
||||
//this.insertAdjacentHTML("beforeend", html);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
if (lastElement) {
|
||||
lastElement.classList.add("glow");
|
||||
setTimeout(() => {
|
||||
lastElement.classList.remove("glow");
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageList);
|
||||
set data(items) {
|
||||
this.items = items;
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = "";
|
||||
|
||||
class MessageListElementOLD extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["messages"];
|
||||
}
|
||||
|
||||
messages = [];
|
||||
room = null;
|
||||
url = null;
|
||||
container = null;
|
||||
messageEventSchedule = null;
|
||||
observer = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
linkifyText(text) {
|
||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
||||
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) {
|
||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||
}
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
timeDescription(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}, ${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;
|
||||
|
||||
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.classList.add("no-select");
|
||||
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;
|
||||
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,
|
||||
message.user_uid,
|
||||
message.user_nick,
|
||||
message.color,
|
||||
message.message,
|
||||
message.html,
|
||||
message.created_at,
|
||||
message.updated_at
|
||||
);
|
||||
|
||||
const element = this.createElement(obj);
|
||||
this.messages.push(obj);
|
||||
this.container.appendChild(element);
|
||||
|
||||
this.messageEventSchedule.delay(() => {
|
||||
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
|
||||
});
|
||||
|
||||
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.component.appendChild(this.container);
|
||||
|
||||
this.messageEventSchedule = new Schedule(500);
|
||||
this.messages = [];
|
||||
this.channel_uid = this.getAttribute("channel");
|
||||
|
||||
app.addEventListener(this.channel_uid, (data) => {
|
||||
this.addMessage(data);
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
|
||||
|
||||
this.timeUpdateInterval = setInterval(() => {
|
||||
this.messages.forEach((message) => {
|
||||
const newText = this.timeDescription(message.created_at);
|
||||
if (newText != message.element.innerText) {
|
||||
message.element.querySelector(".time").innerText = newText;
|
||||
}
|
||||
});
|
||||
}, 30000);
|
||||
}
|
||||
//this.insertAdjacentHTML("beforeend", html);
|
||||
}
|
||||
}
|
||||
|
||||
//customElements.define('message-list', MessageListElement);
|
||||
customElements.define("message-list", MessageList);
|
||||
|
@ -7,20 +7,30 @@
|
||||
// MIT License
|
||||
|
||||
class MessageModel {
|
||||
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.created_at = created_at
|
||||
this.updated_at = updated_at
|
||||
this.element = 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;
|
||||
this.user_nick = user_nick;
|
||||
this.color = color;
|
||||
this.channel_uid = channel_uid;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
|
||||
const models = {
|
||||
Message: MessageModel
|
||||
}
|
||||
Message: MessageModel,
|
||||
};
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -1,30 +1,34 @@
|
||||
this.onpush = (event) => {
|
||||
console.log(event.data);
|
||||
// From here we can write the data to IndexedDB, send it to any open
|
||||
// windows, display a notification, etc.
|
||||
};
|
||||
console.log(event.data);
|
||||
// From here we can write the data to IndexedDB, send it to any open
|
||||
// windows, display a notification, etc.
|
||||
};
|
||||
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((serviceWorkerRegistration) => {
|
||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||
(pushSubscription) => {
|
||||
const subscriptionObject = {
|
||||
endpoint: pushSubscription.endpoint,
|
||||
keys: {
|
||||
p256dh: pushSubscription.getKey('p256dh'),
|
||||
auth: pushSubscription.getKey('auth'),
|
||||
},
|
||||
encoding: PushManager.supportedContentEncodings,
|
||||
/* other app-specific data, such as user identity */
|
||||
};
|
||||
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject);
|
||||
// The push subscription details needed by the application
|
||||
// server are now available, and can be sent to it using,
|
||||
// for example, the fetch() API.
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((serviceWorkerRegistration) => {
|
||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||
(pushSubscription) => {
|
||||
const subscriptionObject = {
|
||||
endpoint: pushSubscription.endpoint,
|
||||
keys: {
|
||||
p256dh: pushSubscription.getKey("p256dh"),
|
||||
auth: pushSubscription.getKey("auth"),
|
||||
},
|
||||
encoding: PushManager.supportedContentEncodings,
|
||||
/* other app-specific data, such as user identity */
|
||||
};
|
||||
console.log(
|
||||
pushSubscription.endpoint,
|
||||
pushSubscription,
|
||||
subscriptionObject,
|
||||
);
|
||||
// The push subscription details needed by the application
|
||||
// server are now available, and can be sent to it using,
|
||||
// for example, the fetch() API.
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -51,4 +51,4 @@ export class Schedule {
|
||||
me.timeOutCount = 0;
|
||||
}, this.msDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,34 @@
|
||||
async function requestNotificationPermission() {
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === 'granted';
|
||||
return permission === "granted";
|
||||
}
|
||||
|
||||
// Subscribe to Push Notifications
|
||||
async function subscribeUser() {
|
||||
const registration = await navigator.serviceWorker.register('/service-worker.js');
|
||||
|
||||
const registration =
|
||||
await navigator.serviceWorker.register("/service-worker.js");
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
|
||||
});
|
||||
|
||||
// Send subscription to your backend
|
||||
await fetch('/subscribe', {
|
||||
method: 'POST',
|
||||
await fetch("/subscribe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(subscription),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Service Worker (service-worker.js)
|
||||
self.addEventListener('push', event => {
|
||||
self.addEventListener("push", (event) => {
|
||||
const data = event.data.json();
|
||||
self.registration.showNotification(data.title, {
|
||||
body: data.message,
|
||||
icon: data.icon
|
||||
icon: data.icon,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,142 +1,143 @@
|
||||
import {EventHandler} from "./event-handler.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
|
||||
export class Socket extends EventHandler {
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
url
|
||||
/**
|
||||
* @type {WebSocket|null}
|
||||
*/
|
||||
ws = null
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
url;
|
||||
/**
|
||||
* @type {WebSocket|null}
|
||||
*/
|
||||
ws = null;
|
||||
|
||||
/**
|
||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||
*/
|
||||
connection = null
|
||||
/**
|
||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||
*/
|
||||
connection = null;
|
||||
|
||||
shouldReconnect = true;
|
||||
shouldReconnect = true;
|
||||
|
||||
get isConnected() {
|
||||
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
get isConnected() {
|
||||
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.url = new URL("/rpc.ws", window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace("http", "ws");
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws) {
|
||||
return this.connection.promise;
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
||||
if (!this.connection || this.connection.resolved) {
|
||||
this.connection = Promise.withResolvers();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.connection.resolved = true;
|
||||
this.connection.resolve(this);
|
||||
this.emit("connected");
|
||||
});
|
||||
|
||||
this.url = new URL('/rpc.ws', window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace('http', 'ws');
|
||||
|
||||
this.connect()
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws) {
|
||||
return this.connection.promise;
|
||||
this.ws.addEventListener("close", () => {
|
||||
console.log("Connection closed");
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("Connection error", e);
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||
console.error("Binary data not supported");
|
||||
} else {
|
||||
try {
|
||||
this.onData(JSON.parse(e.data));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.connection || this.connection.resolved) {
|
||||
this.connection = Promise.withResolvers()
|
||||
}
|
||||
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.connection.resolved = true;
|
||||
this.connection.resolve(this);
|
||||
this.emit("connected");
|
||||
});
|
||||
|
||||
this.ws.addEventListener("close", () => {
|
||||
console.log("Connection closed");
|
||||
this.disconnect()
|
||||
})
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("Connection error", e);
|
||||
this.disconnect()
|
||||
})
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||
console.error("Binary data not supported");
|
||||
} else {
|
||||
try {
|
||||
this.onData(JSON.parse(e.data));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
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);
|
||||
}
|
||||
if (data.channel_uid) {
|
||||
this.emit(data.channel_uid, data.data);
|
||||
if(!data['event'])
|
||||
this.emit("channel-message", data);
|
||||
}
|
||||
this.emit("data", data.data)
|
||||
if(data['event']){
|
||||
this.emit(data.event, data)
|
||||
}
|
||||
if (data.callId) {
|
||||
this.emit(data.callId, data.data);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
|
||||
if (this.shouldReconnect) setTimeout(() => {
|
||||
console.log("Reconnecting");
|
||||
return this.connect();
|
||||
}, 0);
|
||||
if (data.channel_uid) {
|
||||
this.emit(data.channel_uid, data.data);
|
||||
if (!data["event"]) this.emit("channel-message", data);
|
||||
}
|
||||
|
||||
|
||||
_camelToSnake(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
this.emit("data", data.data);
|
||||
if (data["event"]) {
|
||||
this.emit(data.event, data.data);
|
||||
}
|
||||
}
|
||||
|
||||
get client() {
|
||||
const me = this;
|
||||
return new Proxy({}, {
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
|
||||
generateCallId() {
|
||||
return self.crypto.randomUUID();
|
||||
}
|
||||
if (this.shouldReconnect)
|
||||
setTimeout(() => {
|
||||
console.log("Reconnecting");
|
||||
return this.connect();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async sendJson(data) {
|
||||
await this.connect().then(api => {
|
||||
api.ws.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
_camelToSnake(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
async call(method, ...args) {
|
||||
const call = {
|
||||
callId: this.generateCallId(),
|
||||
method,
|
||||
args,
|
||||
};
|
||||
const me = this
|
||||
return new Promise((resolve) => {
|
||||
me.addEventListener(call.callId, data => resolve(data));
|
||||
me.sendJson(call);
|
||||
});
|
||||
}
|
||||
get client() {
|
||||
const me = this;
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
generateCallId() {
|
||||
return self.crypto.randomUUID();
|
||||
}
|
||||
|
||||
async sendJson(data) {
|
||||
await this.connect().then((api) => {
|
||||
api.ws.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
async call(method, ...args) {
|
||||
const call = {
|
||||
callId: this.generateCallId(),
|
||||
method,
|
||||
args,
|
||||
};
|
||||
const me = this;
|
||||
return new Promise((resolve) => {
|
||||
me.addEventListener(call.callId, (data) => resolve(data));
|
||||
me.sendJson(call);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,61 +2,62 @@
|
||||
|
||||
// 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' });
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
chatInput = null;
|
||||
async uploadFiles() {
|
||||
const fileInput = this.container.querySelector(".file-input");
|
||||
const uploadButton = this.container.querySelector(".upload-button");
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
chatInput = null
|
||||
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.responseType = 'json';
|
||||
request.open('POST', `/channel/${this.channelUid}/attachment.bin`, true);
|
||||
|
||||
request.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
const me = this
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
me.dispatchEvent(new CustomEvent('uploaded', { detail: request.response }));
|
||||
uploadButton.innerHTML = '📤';
|
||||
} else {
|
||||
alert('Upload failed');
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert('Error while uploading.');
|
||||
};
|
||||
|
||||
request.send(formData);
|
||||
const uploadEvent = new Event('upload',{});
|
||||
this.dispatchEvent(uploadEvent);
|
||||
const files = fileInput.files;
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files[]", files[i]);
|
||||
}
|
||||
channelUid = null
|
||||
connectedCallback() {
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.innerHTML = `
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.responseType = "json";
|
||||
request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
|
||||
|
||||
request.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
const me = this;
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
me.dispatchEvent(
|
||||
new CustomEvent("uploaded", { detail: request.response }),
|
||||
);
|
||||
uploadButton.innerHTML = "📤";
|
||||
} else {
|
||||
alert("Upload failed");
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert("Error while uploading.");
|
||||
};
|
||||
|
||||
request.send(formData);
|
||||
const uploadEvent = new Event("upload", {});
|
||||
this.dispatchEvent(uploadEvent);
|
||||
}
|
||||
channelUid = null;
|
||||
connectedCallback() {
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
@ -97,9 +98,9 @@ class UploadButtonElement extends HTMLElement {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.innerHTML = `
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement("div");
|
||||
this.container.innerHTML = `
|
||||
<div class="upload-container">
|
||||
<button class="upload-button">
|
||||
📤
|
||||
@ -107,17 +108,17 @@ class UploadButtonElement extends HTMLElement {
|
||||
<input class="hidden-input file-input" type="file" multiple />
|
||||
</div>
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.channelUid = this.getAttribute('channel');
|
||||
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();
|
||||
});
|
||||
}
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
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);
|
||||
customElements.define("upload-button", UploadButtonElement);
|
||||
|
@ -1,36 +1,36 @@
|
||||
class UserList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.users = [];
|
||||
}
|
||||
class UserList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.users = [];
|
||||
}
|
||||
|
||||
set data(userArray) {
|
||||
this.users = userArray;
|
||||
this.render();
|
||||
}
|
||||
set data(userArray) {
|
||||
this.users = userArray;
|
||||
this.render();
|
||||
}
|
||||
|
||||
formatRelativeTime(timestamp) {
|
||||
const now = new Date();
|
||||
const msgTime = new Date(timestamp);
|
||||
const diffMs = now - msgTime;
|
||||
const minutes = Math.floor(diffMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
formatRelativeTime(timestamp) {
|
||||
const now = new Date();
|
||||
const msgTime = new Date(timestamp);
|
||||
const diffMs = now - msgTime;
|
||||
const minutes = Math.floor(diffMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${days} day${days > 1 ? 's' : ''} ago`;
|
||||
} else if (hours > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${minutes} min ago`;
|
||||
}
|
||||
}
|
||||
if (days > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${days} day${days > 1 ? "s" : ""} ago`;
|
||||
} else if (hours > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${hours} hour${hours > 1 ? "s" : ""} ago`;
|
||||
} else {
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${minutes} min ago`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
render() {
|
||||
this.innerHTML = "";
|
||||
|
||||
this.users.forEach(user => {
|
||||
const html = `
|
||||
this.users.forEach((user) => {
|
||||
const html = `
|
||||
<div class="user-list__item"
|
||||
data-uid="${user.uid}"
|
||||
data-color="${user.color}"
|
||||
@ -51,9 +51,9 @@
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('user-list', UserList);
|
||||
customElements.define("user-list", UserList);
|
||||
|
@ -6,11 +6,12 @@
|
||||
|
||||
<section class="chat-area">
|
||||
<message-list class="chat-messages">
|
||||
{% for message in messages %}
|
||||
{% autoescape false %}
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
{% for message in messages %}
|
||||
{% autoescape false %}
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
<div class class="message-list-bottom"></div>
|
||||
</message-list>
|
||||
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input>
|
||||
</section>
|
||||
|
Loading…
Reference in New Issue
Block a user