- Add textarea for input
- Remove channel switch button - Integrate channel switch selection into channel name - Fix state() warnings
This commit is contained in:
@@ -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>
|
||||
+14
-47
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ public struct ChatTabList(ChatTab[] tabs)
|
||||
/// <summary>
|
||||
/// Contains the current channel name
|
||||
/// </summary>
|
||||
public struct SwitchChannel((MessageTemplate[] ChannelName, bool Locked) channel)
|
||||
public struct SwitchChannel((MessageTemplate[] Name, bool Locked) channel)
|
||||
{
|
||||
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.ChannelName;
|
||||
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.Name;
|
||||
[JsonProperty("channelLocked")] public bool Locked = channel.Locked;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class Processing
|
||||
Plugin = plugin;
|
||||
}
|
||||
|
||||
internal (MessageTemplate[] ChannelName, bool Locked) ReadChannelName(Chunk[] channelName)
|
||||
internal (MessageTemplate[] Name, bool Locked) ReadChannelName(Chunk[] channelName)
|
||||
{
|
||||
var locked = Plugin.CurrentTab is not { Channel: null };
|
||||
return (channelName.Select(ProcessChunk).ToArray(), locked);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user