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
|
|
||||||
|
|
||||||
}
|
constructor() {
|
||||||
get chatWindow(){
|
super();
|
||||||
return this._chatWindow
|
this.lastUpdateEvent = new Date();
|
||||||
}
|
this.textarea = document.createElement("textarea");
|
||||||
get channelUid() {
|
this._value = "";
|
||||||
return this.chatWindow.channel.uid
|
this.value = this.getAttribute("value") || "";
|
||||||
}
|
this.previousValue = this.value;
|
||||||
connectedCallback() {
|
this.lastChange = new Date();
|
||||||
const link = document.createElement('link');
|
this.changed = false;
|
||||||
link.rel = 'stylesheet';
|
}
|
||||||
link.href = '/base.css';
|
|
||||||
this.component.appendChild(link);
|
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
get value() {
|
||||||
this.container.classList.add('chat-input');
|
return this._value;
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.textBox.addEventListener('change', (e) => {
|
set value(value) {
|
||||||
e.preventDefault();
|
this._value = value || "";
|
||||||
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
this.textarea.value = this._value;
|
||||||
console.error(e.target.value);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.textBox.addEventListener('keydown', (e) => {
|
resolveAutoComplete() {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
let count = 0;
|
||||||
e.preventDefault();
|
let value = null;
|
||||||
const message = e.target.value.trim();
|
Object.keys(this.autoCompletions).forEach((key) => {
|
||||||
if (!message) return;
|
if (key.startsWith(this.value)) {
|
||||||
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
|
count++;
|
||||||
e.target.value = '';
|
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="/file-manager.js" type="module"></script>
|
||||||
<script src="/user-list.js"></script>
|
<script src="/user-list.js"></script>
|
||||||
<script src="/message-list.js" type="module"></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="/user-list.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/base.css">
|
||||||
|
@ -4,10 +4,6 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<section class="chat-area">
|
<section class="chat-area">
|
||||||
<message-list class="chat-messages">
|
<message-list class="chat-messages">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
@ -16,10 +12,7 @@
|
|||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</message-list>
|
</message-list>
|
||||||
<div class="chat-input">
|
<chat-input live-type="false" channel="{{ channel.uid.value }}"></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>
|
|
||||||
</section>
|
</section>
|
||||||
{% include "dialog_help.html" %}
|
{% include "dialog_help.html" %}
|
||||||
{% include "dialog_online.html" %}
|
{% include "dialog_online.html" %}
|
||||||
@ -27,11 +20,8 @@
|
|||||||
import { app } from "/app.js";
|
import { app } from "/app.js";
|
||||||
import { Schedule } from "/schedule.js";
|
import { Schedule } from "/schedule.js";
|
||||||
const channelUid = "{{ channel.uid.value }}";
|
const channelUid = "{{ channel.uid.value }}";
|
||||||
|
const chatInputField = document.querySelector("chat-input");
|
||||||
function getInputField(){
|
chatInputField.autoCompletions = {
|
||||||
return document.querySelector("textarea")
|
|
||||||
}
|
|
||||||
getInputField().autoComplete = {
|
|
||||||
"/online": () =>{
|
"/online": () =>{
|
||||||
showOnline();
|
showOnline();
|
||||||
},
|
},
|
||||||
@ -39,117 +29,15 @@
|
|||||||
document.querySelector(".chat-messages").innerHTML = '';
|
document.querySelector(".chat-messages").innerHTML = '';
|
||||||
},
|
},
|
||||||
"/live": () =>{
|
"/live": () =>{
|
||||||
getInputField().liveType = !getInputField().liveType
|
|
||||||
|
chatInputField.liveType = !chatInputField.liveType
|
||||||
},
|
},
|
||||||
"/help": () => {
|
"/help": () => {
|
||||||
showHelp();
|
showHelp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textBox = document.querySelector("chat-input").textarea
|
||||||
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)
|
|
||||||
})
|
|
||||||
textBox.addEventListener("paste", async (e) => {
|
textBox.addEventListener("paste", async (e) => {
|
||||||
try {
|
try {
|
||||||
const clipboardItems = await navigator.clipboard.read();
|
const clipboardItems = await navigator.clipboard.read();
|
||||||
@ -168,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dt.items.length > 0) {
|
if (dt.items.length > 0) {
|
||||||
const uploadButton = document.querySelector("upload-button");
|
const uploadButton = chatInputField.uploadButton
|
||||||
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
||||||
input.files = dt.files;
|
input.files = dt.files;
|
||||||
|
|
||||||
@ -187,7 +75,7 @@
|
|||||||
|
|
||||||
const dt = e.dataTransfer;
|
const dt = e.dataTransfer;
|
||||||
if (dt.items.length > 0) {
|
if (dt.items.length > 0) {
|
||||||
const uploadButton = document.querySelector("upload-button");
|
const uploadButton = chatInputField.uploadButton
|
||||||
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
||||||
input.files = dt.files;
|
input.files = dt.files;
|
||||||
|
|
||||||
@ -197,13 +85,16 @@
|
|||||||
chatInput.addEventListener("dragover", async (e) => {
|
chatInput.addEventListener("dragover", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = "link";
|
e.dataTransfer.dropEffect = "link";
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
textBox.focus();
|
chatInputField.textarea.focus();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function replyMessage(message) {
|
function replyMessage(message) {
|
||||||
const field = getInputField()
|
const field = chatInputField
|
||||||
field.value = "```markdown\n> " + (message || '') + "\n```\n";
|
field.value = "```markdown\n> " + (message || '') + "\n```\n";
|
||||||
field.focus();
|
field.focus();
|
||||||
}
|
}
|
||||||
@ -294,8 +185,8 @@
|
|||||||
lastMessage = messagesContainer.querySelector(".message:last-child");
|
lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||||
if (doScrollDown) {
|
if (doScrollDown) {
|
||||||
lastMessage?.scrollIntoView({ block: "end", inline: "nearest" });
|
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" });
|
messagesContainer.querySelector(".message:last-child").scrollIntoView({ block: "end", inline: "nearest" });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
||||||
getInputField().focus();
|
chatInputField.focus();
|
||||||
},500)
|
},500)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.shiftKey && event.key === 'G') {
|
if (event.shiftKey && event.key === 'G') {
|
||||||
if(document.activeElement != getInputField()){
|
if(chatInputField.isActive()){
|
||||||
|
|
||||||
updateLayout(true);
|
updateLayout(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getInputField().focus();
|
chatInputField.focus();
|
||||||
},500)
|
},500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,7 +323,6 @@
|
|||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
initInputField(getInputField());
|
|
||||||
updateLayout(true);
|
updateLayout(true);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user