- Add textarea for input

- Remove channel switch button
- Integrate channel switch selection into channel name
- Fix state() warnings
This commit is contained in:
Infi
2025-09-24 22:57:22 +02:00
parent e2df709003
commit 11311316fd
10 changed files with 194 additions and 90 deletions
@@ -0,0 +1,79 @@
<script lang="ts">
import {isChannelLocked, channelOptions} from "$lib/shared.svelte";
let selectElement: HTMLSelectElement;
async function requestChannelSwitch(event: Event) {
if (!event.currentTarget)
return;
let element = (event.currentTarget as HTMLSelectElement);
let requestedChannel = element.value;
console.log(element.value)
element.value = '0';
const rawResponse = await fetch('/channel', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ channel: requestedChannel })
});
// const content = await rawResponse.json();
// TODO: use the response
}
let canvas: HTMLCanvasElement | null = null;
function getTextWidth(text: string): number {
// re-use canvas object for better performance
if (canvas === null)
canvas = document.createElement("canvas");
const context: CanvasRenderingContext2D | null = canvas.getContext("2d");
if (!context)
return 0;
context.font = getCanvasFont(selectElement);
const metrics = context.measureText(text);
return metrics.width;
}
function getCssStyle(element: Element, prop: string): string {
return window.getComputedStyle(element, null).getPropertyValue(prop);
}
function getCanvasFont(el = document.body) {
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
const fontSize = getCssStyle(el, 'font-size') || '16px';
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
return `${fontWeight} ${fontSize} ${fontFamily}`;
}
</script>
<select
bind:this={selectElement}
id="channel-select"
style="pointer-events: {isChannelLocked.locked ? 'none' : 'inherit'}; width: {(channelOptions.length > 1 ? getTextWidth(channelOptions[0].text) : 1) + 40}px"
onchange={(e) => requestChannelSwitch(e)}>
{#each channelOptions as channelOption}
{#if channelOption.preview }
<option selected disabled hidden value={channelOption.value}>
{channelOption.text}
</option>
{:else}
<option value={channelOption.value}>
{channelOption.text}
</option>
{/if}
{/each}
</select>
<style>
select {
border: none;
background-color: transparent;
}
</style>
@@ -0,0 +1,57 @@
<script lang="ts">
let textarea: HTMLTextAreaElement;
let content: string = $state('');
function preventNewlines(e: KeyboardEvent) {
if (e.key === 'Enter') {
// Prevent key from creating a newline
e.preventDefault();
// submit the data
const newEvent = new Event('submit', {bubbles: true, cancelable: true});
if (e.currentTarget !== null) {
(e.currentTarget as HTMLTextAreaElement).closest('form')?.dispatchEvent(newEvent);
}
}
}
function resize() {
if (!textarea)
return;
textarea.style.height = '1px';
textarea.style.height = `${textarea.scrollHeight}px`;
}
</script>
<textarea
bind:this={textarea}
bind:value={content}
oninput={() => resize()}
onkeydown={(e) => preventNewlines(e)}
id="chat-input"
autocomplete="off"
placeholder="Message"
enterkeyhint="send"
maxlength="500">
</textarea>
<style>
textarea {
flex-grow: 0;
font-size: 1rem;
border: 3px solid transparent;
border-radius: 20px;
background-color: var(--bg-input);
&:focus {
outline: 2px solid var(--focus-color);
}
width: 100%;
padding: 5px 20px;
}
</style>
@@ -1,9 +1,7 @@
import {type Source, source} from "sveltekit-sse";
import {channelOptions, isChannelLocked} from "$lib/shared.svelte";
interface ChatElements {
channelHint: HTMLElement | null,
channelSelect: HTMLElement | null,
messagesContainer: Element | null,
messagesList: HTMLElement | null,
@@ -35,6 +33,7 @@ interface Template {
// ref `DataStructure.SwitchChannel`
interface SwitchChannel {
channelName: Template[];
channelValue: number;
channelLocked: boolean;
}
@@ -58,7 +57,6 @@ export class ChatTwoWeb {
elements!: ChatElements;
maxTimestampWidth: number = 0;
scrolledToBottom: boolean = true;
channelLocked: boolean = false;
sse!: EventSource;
connection!: Source;
@@ -70,8 +68,8 @@ export class ChatTwoWeb {
setupDOMElements() {
this.elements = {
channelHint: document.getElementById('channel-hint'),
channelSelect: document.getElementById('channel-select'),
// channelHint: document.getElementById('channel-hint'),
// channelSelect: document.getElementById('channel-select'),
messagesContainer: document.querySelector('#messages > .scroll-container')!,
messagesList: document.getElementById('messages-list'),
@@ -82,23 +80,6 @@ export class ChatTwoWeb {
chatInput: document.getElementById('chat-input')
};
// channel selector
this.elements.channelSelect?.addEventListener('change', async (event) => {
if (event.currentTarget === null)
return;
const rawResponse = await fetch('/channel', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ channel: (event.currentTarget as HTMLSelectElement).value })
});
// const content = await rawResponse.json();
// TODO: use the response
});
// add indicator signaling more messages below
this.elements.messagesContainer?.addEventListener('scroll', (event) => {
if (event.currentTarget === null)
@@ -167,39 +148,25 @@ export class ChatTwoWeb {
}
updateChannelHint(channel: SwitchChannel) {
if (this.elements.channelHint === null || this.elements.channelSelect === null)
return;
this.elements.channelHint.innerHTML = '';
// Set storage to the current lock state
isChannelLocked.locked = channel.channelLocked;
const channelElement = this.processTemplate(channel.channelName);
if (!channelElement.firstChild)
return;
// Makes the channel selector unclickable if the channel is fixed
this.channelLocked = channel.channelLocked;
if (this.channelLocked) {
if (channelElement.firstChild === null)
return;
let channelName = (channelElement.firstChild as HTMLSpanElement).innerText;
if (channel.channelLocked)
channelName = `(Locked) ${channelName}`;
// @ts-ignore
channelElement.firstChild.innerText = `(Locked) ${channelElement.firstChild.innerText}`;
this.elements.channelSelect.style.pointerEvents = 'none';
} else {
this.elements.channelSelect.style.removeProperty('pointer-events');
}
this.elements.channelHint.appendChild(channelElement);
channelOptions[0] = {text: channelName, value: 0, preview: true }
}
updateChannels(channelList: ChannelList) {
if (this.elements.channelSelect === null)
return;
channelOptions.length = 1;
this.elements.channelSelect.innerHTML = '';
for (const [ label, channel ] of Object.entries(channelList.channels)) {
const option = document.createElement('option');
option.value = channel.toString();
option.innerText = label;
this.elements.channelSelect.appendChild(option);
channelOptions.push( { text: label, value: channel, preview: false } )
}
}
@@ -0,0 +1,8 @@
export const isChannelLocked: { locked: boolean } = $state({ locked: false });
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
export interface ChannelOption {
text: string;
value: number;
preview: boolean;
}
@@ -2,7 +2,7 @@
import { page } from '$app/state'
import { Alert } from '@sveltestrap/sveltestrap';
let data: App.Warning | null = null;
let data: App.Warning = $state({ hasWarning: false, content: '' });
$effect.pre(() => {
if (page.url.searchParams.has('message')) {
data = {
@@ -2,10 +2,12 @@
import { page } from '$app/state'
import {Alert} from "@sveltestrap/sveltestrap";
import { onMount } from 'svelte';
import { ChatTwoWeb } from '$lib/chat'
import {ChatTwoWeb} from '$lib/chat.svelte'
import {addGfdStylesheet} from "$lib/gfd";
import DynamicTextArea from "../../components/DynamicTextArea.svelte";
import ChannelSelector from "../../components/ChannelSelector.svelte";
let data: App.Warning | null = null;
let data: App.Warning = $state({ hasWarning: false, content: '' });
$effect.pre(() => {
if (page.url.searchParams.has('message')) {
data = {
@@ -51,13 +53,9 @@
<section id="input">
<form>
<div class="select-container">
<select id="channel-select"></select>
</div>
<div class="input-container">
<input type="text" id="chat-input" autocomplete="off" placeholder="Message" enterkeyhint="send" maxlength="500">
<div id="channel-hint"></div>
<DynamicTextArea/>
<ChannelSelector/>
</div>
<button type="submit">Send</button>
+24 -30
View File
@@ -183,7 +183,7 @@ section#input {
gap: 10px;
}
input, button, select {
input, button {
font-size: 1rem;
border: 3px solid transparent;
border-radius: 20px;
@@ -194,7 +194,7 @@ section#input {
}
}
button, select {
button {
padding: 5px 15px;
border: 3px solid var(--bg-input);
background-image: var(--gradient-clickable);
@@ -207,29 +207,12 @@ section#input {
}
}
.select-container, button {
button {
position: relative;
flex-grow: 0;
flex-shrink: 0;
}
.select-container {
flex-basis: 0;
&::before {
/* "message-circle" icon from https://github.com/feathericons/feather, under MIT license */
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>');
}
select {
width: 100%;
padding-left: 1.5rem;
padding-right: 1.5rem;
appearance: none;
color: transparent;
}
}
button {
padding-left: calc(20px + 1.5rem);
@@ -239,7 +222,7 @@ section#input {
}
}
.select-container::before, button::before {
button::before {
content: '';
position: absolute;
left: 15px;
@@ -252,11 +235,6 @@ section#input {
pointer-events: none;
}
.select-container::before {
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
.input-container {
flex: 1;
position: relative;
@@ -266,9 +244,9 @@ section#input {
padding: 5px 20px;
}
#channel-hint {
#channel-select {
position: absolute;
top: -1.2em;
top: -1.5em;
left: 23px;
max-width: 100%;
overflow: hidden;
@@ -276,7 +254,23 @@ section#input {
font-weight: 550;
white-space: nowrap;
text-overflow: ellipsis;
pointer-events: none;
border: 0;
border-radius: 0;
background-color: transparent;
padding: 5px 15px;
cursor: pointer;
&:hover {
border-color: var(--bg-input-hover);
background-color: var(--bg-input-hover);
background-image: var(--gradient-clickable-hover);
}
&:focus {
outline: 2px solid var(--focus-color);
}
}
}
@@ -360,7 +354,7 @@ section#input {
transform: translateX(-50%) translateY(-50%);
}
.input-container #channel-hint {
.input-container #channel-select {
font-size: 0.9rem;
}
}