Files
risuai/src/lib/UI/GUI/TextAreaInput.svelte
2024-10-23 02:31:37 +09:00

321 lines
12 KiB
Svelte

<div
class={"border border-darkborderc relative n-scroll focus-within:border-borderc rounded-md shadow-sm text-textcolor bg-transparent focus-within:ring-borderc focus-within:ring-2 focus-within:outline-none transition-colors duration-200 z-20 focus-within:z-40" + ((className) ? (' ' + className) : '')}
class:text-sm={size === 'sm' || (size === 'default' && $textAreaTextSize === 1)}
class:text-md={size === 'md' || (size === 'default' && $textAreaTextSize === 2)}
class:text-lg={size === 'lg' || (size === 'default' && $textAreaTextSize === 3)}
class:text-xl={size === 'xl'}
class:text-xs={size === 'xs' || (size === 'default' && $textAreaTextSize === 0)}
class:w-full={fullwidth}
class:h-20={height === '20' || (height === 'default' && $textAreaSize === -5)}
class:h-24={height === '24' || (height === 'default' && $textAreaSize === -4)}
class:h-28={height === '28' || (height === 'default' && $textAreaSize === -3)}
class:h-32={height === '32' || (height === 'default' && $textAreaSize === -2)}
class:h-36={height === '36' || (height === 'default' && $textAreaSize === -1)}
class:h-40={height === 'default' && $textAreaSize === 0}
class:h-44={height === 'default' && $textAreaSize === 1}
class:h-48={height === 'default' && $textAreaSize === 2}
class:h-52={height === 'default' && $textAreaSize === 3}
class:h-56={height === 'default' && $textAreaSize === 4}
class:h-60={height === 'default' && $textAreaSize === 5}
class:h-full={height === 'full'}
class:min-h-20={height === '20' || (height === 'default' && $textAreaSize === -5)}
class:min-h-24={height === '24' || (height === 'default' && $textAreaSize === -4)}
class:min-h-28={height === '28' || (height === 'default' && $textAreaSize === -3)}
class:min-h-32={height === '32' || (height === 'default' && $textAreaSize === -2)}
class:min-h-36={height === '36' || (height === 'default' && $textAreaSize === -1)}
class:min-h-40={height === 'default' && $textAreaSize === 0}
class:min-h-48={height === 'default' && $textAreaSize === 1}
class:min-h-56={height === 'default' && $textAreaSize === 2}
class:min-h-64={height === 'default' && $textAreaSize === 3}
class:min-h-72={height === 'default' && $textAreaSize === 4}
class:min-h-80={height === 'default' && $textAreaSize === 5}
class:mb-4={margin === 'bottom'}
class:mb-2={margin === 'both'}
class:mt-4={margin === 'top'}
class:mt-2={margin === 'both'}
bind:this={highlightDom}
onfocusout={() => {
hideAutoComplete()
}}
>
{#if !highlight}
<textarea
class="w-full h-full bg-transparent focus-within:outline-none resize-none absolute top-0 left-0 z-50 overflow-y-auto"
class:px-4={padding}
class:py-2={padding}
{autocomplete}
{placeholder}
id={id}
bind:value={value}
oninput={(e) => {
if(optimaizedInput){
if(inpa++ > 10){
value = e.currentTarget.value
inpa = 0
onInput()
}
}
else{
value = e.currentTarget.value
onInput()
}
}}
onchange={(e) => {
if(optimaizedInput){
value = e.currentTarget.value
onInput()
}
}}
></textarea>
{:else if isFirefox}
<div
class="w-full h-full bg-transparent focus-within:outline-none resize-none absolute top-0 left-0 z-50 overflow-y-auto px-4 py-2 break-words whitespace-pre-wrap"
contenteditable="true"
bind:textContent={value}
onkeydown={(e) => {
handleKeyDown(e)
onInput()
}}
oninput={(e) => {
autoComplete()
}}
onpaste={(e) => {
e.preventDefault()
const text = e.clipboardData.getData('text/plain')
if(text){
insertTextAtSelection(text)
}
}}
bind:this={inputDom}
translate="no"
>{value ?? ''}</div>
{:else}
<div
class="w-full h-full bg-transparent focus-within:outline-none resize-none absolute top-0 left-0 z-50 overflow-y-auto px-4 py-2 break-words whitespace-pre-wrap"
contenteditable="plaintext-only"
bind:innerText={value}
onkeydown={(e) => {
handleKeyDown(e)
onInput()
}}
oninput={(e) => {
autoComplete()
}}
bind:this={inputDom}
translate="no"
>{value ?? ''}</div>
{/if}
<div class="hidden absolute z-100 bg-bgcolor border border-darkborderc p-2 flex-col" bind:this={autoCompleteDom}>
{#each autocompleteContents as content, i}
<button class="w-full text-left py-1 px-2 bg-bgcolor" class:text-blue-500={selectingAutoComplete === i} onclick={() => {
insertContent(content)
}}>{content}</button>
{/each}
</div>
</div>
<script lang="ts">
import { run } from 'svelte/legacy';
import { textAreaSize, textAreaTextSize } from 'src/ts/gui/guisize'
import { highlighter, getNewHighlightId, removeHighlight, AllCBS } from 'src/ts/gui/highlight'
import { isMobile } from 'src/ts/storage/globalApi';
import { isFirefox, sleep } from 'src/ts/util';
import { onDestroy, onMount } from 'svelte';
interface Props {
size?: 'xs'|'sm'|'md'|'lg'|'xl'|'default';
autocomplete?: 'on'|'off';
placeholder?: string;
value: string;
id?: string;
padding?: boolean;
margin?: "none"|"top"|"bottom"|"both";
onInput?: any;
fullwidth?: boolean;
height?: '20'|'24'|'28'|'32'|'36'|'full'|'default';
className?: string;
optimaizedInput?: boolean;
highlight?: boolean;
}
let {
size = 'default',
autocomplete = 'off',
placeholder = '',
value = $bindable(),
id = undefined,
padding = true,
margin = "none",
onInput = () => {},
fullwidth = false,
height = 'default',
className = '',
optimaizedInput = true,
highlight = false
}: Props = $props();
let selectingAutoComplete = $state(0)
let highlightId = highlight ? getNewHighlightId() : 0
let inpa = $state(0)
let highlightDom: HTMLDivElement = $state()
let optiValue = $state(value)
let autoCompleteDom: HTMLDivElement = $state()
let autocompleteContents:string[] = $state([])
let inputDom: HTMLDivElement = $state()
const autoComplete = () => {
if(isMobile){
return
}
//autocomplete
selectingAutoComplete = 0
const sel = window.getSelection()
if(!sel){
return
}
const range = sel.getRangeAt(0)
if(range){
const qValue = (range.startContainer).textContent
const splited = qValue.substring(0, range.startOffset).split('{{')
if(splited.length === 1){
hideAutoComplete()
return
}
const qText = splited.pop()
let filtered = AllCBS.filter((cb) => cb.startsWith(qText))
if(filtered.length === 0){
hideAutoComplete()
return
}
filtered = filtered.slice(0, 10)
autocompleteContents = filtered
}
const hlRect = highlightDom.getBoundingClientRect()
const rect = range.getBoundingClientRect()
if(rect.top === 0 && rect.left === 0){
hideAutoComplete()
return
}
const top = rect.top - hlRect.top + 15
const left = rect.left - hlRect.left
autoCompleteDom.style.top = top + 'px'
autoCompleteDom.style.left = left + 'px'
autoCompleteDom.style.display = 'flex'
}
const insertContent = (insertContent:string, type:'autoComplete'|'paste' = 'autoComplete') => {
console.log(insertContent)
const sel = window.getSelection()
if(sel){
const range = sel.getRangeAt(0)
let content = (range.startContainer).textContent
let contentStart = content.substring(0, range.startOffset)
let contentEnd = content.substring(range.startOffset)
if(type === 'autoComplete'){
contentStart = contentStart.substring(0, contentStart.lastIndexOf('{{'))
if(insertContent.endsWith(':')){
insertContent = `{{${insertContent}:`
}
else if(insertContent.startsWith('#')){
insertContent = `{{${insertContent} `
}
else{
insertContent = `{{${insertContent}}}`
}
}
const cons = contentStart + insertContent + contentEnd
range.startContainer.textContent = cons
hideAutoComplete()
try {
sel.collapse(range.startContainer, contentStart.length + insertContent.length)
} catch (error) {}
//invoke onInput
try {
inputDom.dispatchEvent(new Event('input'))
inputDom.dispatchEvent(new Event('change'))
} catch (error) {}
}
}
const hideAutoComplete = () => {
autoCompleteDom.style.display = 'none'
selectingAutoComplete = 0
autocompleteContents = []
}
onMount(() => {
highlighter(highlightDom, highlightId)
})
onDestroy(() => {
removeHighlight(highlightId)
})
const highlightChange = async (value:string, highlightId:number) => {
await sleep(1)
highlighter(highlightDom, highlightId)
}
const handleKeyDown = (e:KeyboardEvent) => {
if(autocompleteContents.length >= 1){
switch(e.key){
case 'ArrowDown':
selectingAutoComplete = Math.min(selectingAutoComplete + 1, autocompleteContents.length - 1)
e.preventDefault()
return
case 'ArrowUp':
selectingAutoComplete = Math.max(selectingAutoComplete - 1, 0)
e.preventDefault()
return
case 'Enter':
case 'Tab':
e.preventDefault()
insertContent(autocompleteContents[selectingAutoComplete])
return
case 'Escape':
hideAutoComplete()
return
}
}
if(e.key === 'Enter' && isFirefox){
e.stopPropagation()
e.preventDefault()
insertTextAtSelection('\n')
}
}
function insertTextAtSelection(txt:string) {
txt = txt.replace(/\r/g, '')
let div = inputDom;
let sel = window.getSelection();
let text = div.textContent;
let before = Math.min(sel.focusOffset, sel.anchorOffset);
let after = Math.max(sel.focusOffset, sel.anchorOffset);
let afterStr = text.substring(after);
if (afterStr == "") afterStr = "\n";
div.textContent = text.substring(0, before) + txt + afterStr;
sel.removeAllRanges();
let range = document.createRange();
range.setStart(div.childNodes[0], before + txt.length);
range.setEnd(div.childNodes[0], before + txt.length);
sel.addRange(range);
try {
inputDom.dispatchEvent(new Event('input'))
inputDom.dispatchEvent(new Event('change'))
} catch (error) {}
}
run(() => {
optiValue = value
});
run(() => {
highlightChange(value, highlightId)
});
</script>