Update.
This commit is contained in:
parent
c0b4ba715c
commit
48c3daf398
src/snek
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user