This commit is contained in:
retoor 2025-05-17 00:54:15 +02:00
parent c0b4ba715c
commit 48c3daf398
3 changed files with 245 additions and 189 deletions
src/snek

View File

@ -1,69 +1,234 @@
// Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element for a chat input widget, featuring a text area and an upload button. It handles user input and triggers events for input changes and message submission.
import { app } from '../app.js';
// Includes standard DOM manipulation methods; no external imports used.
class ChatInputComponent extends HTMLElement {
autoCompletions = {
'example 1': () => {
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License.
},
'example 2': () => {
class ChatInputElement extends HTMLElement {
_chatWindow = null
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
}
set chatWindow(value){
this._chatWindow = value
}
}
}
get chatWindow(){
return this._chatWindow
}
get channelUid() {
return this.chatWindow.channel.uid
}
connectedCallback() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
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;
}
this.container = document.createElement('div');
this.container.classList.add('chat-input');
this.container.innerHTML = `
<textarea placeholder="Type a message..." rows="2"></textarea>
<upload-button></upload-button>
`;
this.textBox = this.container.querySelector('textarea');
this.uploadButton = this.container.querySelector('upload-button');
this.uploadButton.chatInput = this
this.textBox.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));
const message = e.target.value;
const button = this.container.querySelector('button');
button.disabled = !message;
});
get value() {
return this._value;
}
this.textBox.addEventListener('change', (e) => {
e.preventDefault();
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
console.error(e.target.value);
});
set value(value) {
this._value = value || "";
this.textarea.value = this._value;
}
this.textBox.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const message = e.target.value.trim();
if (!message) return;
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
e.target.value = '';
}
});
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;
}
this.component.appendChild(this.container);
}
isActive() {
return document.activeElement === this.textarea;
}
focus() {
this.textarea.focus();
}
connectedCallback() {
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();
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);
});
}
trackSecondsBetweenEvents(event1Time, event2Time) {
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
return millisecondsDifference / 1000;
}
newMessage() {
if (!this.messageUid) {
this.messageUid = '?';
}
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);
}
}
}
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);
}
}
customElements.define('chat-input', ChatInputElement);
customElements.define('chat-input', ChatInputComponent);

View File

@ -18,6 +18,7 @@
<script src="/file-manager.js" type="module"></script>
<script src="/user-list.js"></script>
<script src="/message-list.js" type="module"></script>
<script src="/chat-input.js" type="module"></script>
<link rel="stylesheet" href="/user-list.css">
<link rel="stylesheet" href="/base.css">

View File

@ -4,10 +4,6 @@
{% block main %}
<section class="chat-area">
<message-list class="chat-messages">
{% for message in messages %}
@ -16,10 +12,7 @@
{% endautoescape %}
{% endfor %}
</message-list>
<div class="chat-input">
<textarea list="chat-input-autocomplete-items" placeholder="Type a message..." rows="2" autocomplete="on"></textarea>
<upload-button channel="{{ channel.uid.value }}"></upload-button>
</div>
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input>
</section>
{% include "dialog_help.html" %}
{% include "dialog_online.html" %}
@ -27,11 +20,8 @@
import { app } from "/app.js";
import { Schedule } from "/schedule.js";
const channelUid = "{{ channel.uid.value }}";
function getInputField(){
return document.querySelector("textarea")
}
getInputField().autoComplete = {
const chatInputField = document.querySelector("chat-input");
chatInputField.autoCompletions = {
"/online": () =>{
showOnline();
},
@ -39,117 +29,15 @@
document.querySelector(".chat-messages").innerHTML = '';
},
"/live": () =>{
getInputField().liveType = !getInputField().liveType
chatInputField.liveType = !chatInputField.liveType
},
"/help": () => {
showHelp();
}
}
function initInputField(textBox) {
if(textBox.liveType == undefined){
textBox.liveType = false
}
let typeTimeout = null;
textBox.addEventListener('keydown',async (e) => {
if(typeTimeout){
clearTimeout(typeTimeout)
typeTimeout = null
}
if(e.target.liveType){
typeTimeout = setTimeout(()=>{
e.target.lastMessageUid = null
e.target.value = ''
},3000)
}
if(e.key === "ArrowUp"){
const value = findDivAboveText(e.target.value).querySelector('.text')
e.target.value = value.textContent
console.info("HIERR")
return
}
if (e.key === "Tab") {
const message = e.target.value.trim();
if (!message) {
return
}
let autoCompleteHandler = null;
Object.keys(e.target.autoComplete).forEach((key)=>{
if(key.startsWith(message)){
if(autoCompleteHandler){
return
}
autoCompleteHandler = key
}
})
if(autoCompleteHandler){
e.preventDefault();
e.target.value = autoCompleteHandler;
return
}
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const message = e.target.value.trim();
if (!message) {
return
}
let autoCompleteHandler = e.target.autoComplete[message]
if(autoCompleteHandler){
const value = message;
e.target.value = '';
autoCompleteHandler(value)
return
}
e.target.value = '';
if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){
app.rpc.updateMessageText(textBox.lastMessageUid, message)
textBox.lastMessageUid = null
return
}
const messageResponse = await app.rpc.sendMessage(channelUid, message);
}else{
if(textBox.liveType){
if(e.target.value.endsWith("\n") || e.target.value.endsWith(" ")){
return
}
if(e.target.value[0] == "/"){
return
}
if(!textBox.lastMessageUid){
textBox.lastMessageUid = '?'
app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{
textBox.lastMessageUid = messageResponse
})
}
if(textBox.lastMessageUid == '?'){
return;
}
app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)
}else{
app.rpc.set_typing(channelUid)
}
}
});
document.querySelector("upload-button").addEventListener("upload",function(e){
getInputField().focus();
})
document.querySelector("upload-button").addEventListener("uploaded",function(e){
let message = ""
e.detail.files.forEach((file)=>{
message += `[${file.name}](/channel/attachment/${file.relative_url})`
})
app.rpc.sendMessage(channelUid,message)
})
}
const textBox = document.querySelector("chat-input").textarea
textBox.addEventListener("paste", async (e) => {
try {
const clipboardItems = await navigator.clipboard.read();
@ -168,7 +56,7 @@
}
if (dt.items.length > 0) {
const uploadButton = document.querySelector("upload-button");
const uploadButton = chatInputField.uploadButton
const input = uploadButton.shadowRoot.querySelector('.file-input')
input.files = dt.files;
@ -187,7 +75,7 @@
const dt = e.dataTransfer;
if (dt.items.length > 0) {
const uploadButton = document.querySelector("upload-button");
const uploadButton = chatInputField.uploadButton
const input = uploadButton.shadowRoot.querySelector('.file-input')
input.files = dt.files;
@ -197,13 +85,16 @@
chatInput.addEventListener("dragover", async (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "link";
})
textBox.focus();
}
chatInputField.textarea.focus();
function replyMessage(message) {
const field = getInputField()
const field = chatInputField
field.value = "```markdown\n> " + (message || '') + "\n```\n";
field.focus();
}
@ -294,8 +185,8 @@
lastMessage = messagesContainer.querySelector(".message:last-child");
if (doScrollDown) {
lastMessage?.scrollIntoView({ block: "end", inline: "nearest" });
const inputBox = document.querySelector(".chat-input");
inputBox.scrollIntoView({ block: "end", inline: "nearest" });
chatInputField.scrollIntoView({ block: "end", inline: "nearest" });
}
}
@ -378,17 +269,17 @@
messagesContainer.querySelector(".message:last-child").scrollIntoView({ block: "end", inline: "nearest" });
setTimeout(() => {
getInputField().focus();
chatInputField.focus();
},500)
}
}
if (event.shiftKey && event.key === 'G') {
if(document.activeElement != getInputField()){
if(chatInputField.isActive()){
updateLayout(true);
setTimeout(() => {
getInputField().focus();
chatInputField.focus();
},500)
}
@ -432,7 +323,6 @@
document.body.removeChild(overlay);
});
});
initInputField(getInputField());
updateLayout(true);
</script>
{% endblock %}