machineuser
Sync widgets demo
76d4920
raw
history blame
4.24 kB
<script lang="ts">
import { createEventDispatcher, tick } from "svelte";
import { delay, onCmdEnter } from "../../../../utils/ViewUtils.js";
import WidgetLabel from "../WidgetLabel/WidgetLabel.svelte";
import LogInPopover from "../../../LogInPopover/LogInPopover.svelte";
import { isLoggedIn } from "../../stores.js";
export let label: string = "";
export let placeholder: string = "Your sentence here...";
export let value: string;
export let isLoading = false;
export let isDisabled = false;
export let size: "small" | "big" = "small";
let containerSpanEl: HTMLSpanElement;
let isOnFocus = false;
let popOverOpen = false;
const typingEffectSpeedMs = 12;
const classNamesInput = "whitespace-pre-wrap inline font-normal text-black dark:text-white";
const classNamesOutput = "whitespace-pre-wrap inline text-blue-600 dark:text-blue-400";
const dispatch = createEventDispatcher<{ cmdEnter: void }>();
export async function renderTextOutput(outputTxt: string, typingEffect = true): Promise<void> {
const spanEl = document.createElement("span");
spanEl.contentEditable = isDisabled ? "false" : "true";
spanEl.className = classNamesOutput;
containerSpanEl?.appendChild(spanEl);
await tick();
// fix Chrome bug that adds `<br>` els on contentedtiable el
const brElts = containerSpanEl?.querySelectorAll("br");
for (const brEl of brElts) {
brEl.remove();
}
await tick();
// split on whitespace or any other character to correctly render newlines \n
if (typingEffect) {
for (const char of outputTxt.split(/(\s|.)/g)) {
await delay(typingEffectSpeedMs);
spanEl.textContent += char;
if (isOnFocus) {
moveCaretToEnd();
}
}
} else {
spanEl.textContent = outputTxt;
}
updateInnerTextValue();
}
function moveCaretToEnd() {
if (containerSpanEl) {
const range = document.createRange();
range.selectNodeContents(containerSpanEl);
range.collapse(false);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
}
// handle FireFox contenteditable paste bug
function handlePaste(e: ClipboardEvent) {
if (isLoading) {
return e.preventDefault();
}
const copiedTxt = e.clipboardData?.getData("text/plain");
const selection = window.getSelection();
if (selection?.rangeCount && !!copiedTxt?.length) {
const range = selection.getRangeAt(0);
range.deleteContents();
const spanEl = document.createElement("span");
spanEl.contentEditable = "true";
spanEl.className = classNamesInput;
spanEl.textContent = copiedTxt;
range.insertNode(spanEl);
}
window.getSelection()?.collapseToEnd();
updateInnerTextValue();
}
function updateInnerTextValue() {
value = containerSpanEl?.textContent ?? "";
}
function onFocus() {
isOnFocus = true;
moveCaretToEnd();
}
export function setValue(text: string): void {
containerSpanEl.textContent = text;
updateInnerTextValue();
}
</script>
<LogInPopover bind:open={popOverOpen}>
<WidgetLabel {label}>
<svelte:fragment slot="after">
<!-- `whitespace-pre-wrap inline-block` are needed to get correct newlines from `el.textContent` on Chrome -->
<span
class="{label ? 'mt-1.5' : ''} block w-full resize-y overflow-auto py-2 px-3 {size === 'small'
? 'min-h-[42px]'
: 'min-h-[144px]'} inline-block max-h-[500px] whitespace-pre-wrap rounded-lg border border-gray-200 shadow-inner outline-none focus:shadow-inner focus:ring focus:ring-blue-200 dark:bg-gray-925"
role="textbox"
style="--placeholder: '{isDisabled ? '' : placeholder}'"
spellcheck="false"
dir="auto"
contenteditable
class:pointer-events-none={isLoading || isDisabled}
use:onCmdEnter={{ disabled: isLoading || isDisabled }}
on:cmdEnter={() => {
if (!$isLoggedIn) {
popOverOpen = true;
return;
}
dispatch("cmdEnter");
}}
bind:this={containerSpanEl}
on:paste|preventDefault={handlePaste}
on:input={updateInnerTextValue}
on:focus={onFocus}
on:blur={() => (isOnFocus = false)}
/>
</svelte:fragment>
</WidgetLabel>
</LogInPopover>
<style>
span[contenteditable]:empty::before {
content: var(--placeholder);
color: rgba(156, 163, 175);
}
</style>