File size: 4,237 Bytes
b2ecf7d
76d4920
b2ecf7d
49cc62d
b2ecf7d
76d4920
 
b2ecf7d
 
 
 
 
 
 
 
 
da081e4
76d4920
b2ecf7d
 
 
 
76d4920
 
da081e4
b2ecf7d
da081e4
b2ecf7d
 
 
 
 
 
 
 
 
 
da081e4
 
 
 
 
 
 
 
 
 
b2ecf7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da081e4
 
 
 
 
b2ecf7d
 
 
 
 
 
76d4920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2ecf7d
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<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>