Newer
Older
<script lang="ts">
import type Message from '$lib/types/message';
import { displayTime } from '$lib/utils/date';
import {
ChatBubbleBottomCenter,
ChatBubbleBottomCenterText,
Check,
Icon,
Pencil,
PencilSquare
} from 'svelte-hero-icons';
import { onMount } from 'svelte';
import SpellCheck from '$lib/components/icons/spellCheck.svelte';
import { sanitize } from '$lib/utils/sanitize';
let timer: number;
$: displayedTime = displayTime(message.created_at);
$: {
clearInterval(timer);
timer = setInterval(() => {
displayedTime = displayTime(message.created_at);
}, 1000);
}
let isEdit = false;
let contentDiv: HTMLDivElement;
let historyModal: HTMLDialogElement;
$: messageVersions = message.versions;
function startEdit() {
isEdit = true;
setTimeout(() => {
if (!contentDiv) return;
contentDiv.focus();
}, 0);
}
async function endEdit(validate = true) {
if (!validate) {
contentDiv.innerText = message.content;
isEdit = false;
return;
}
if (contentDiv.innerText.trim() === message.content) {
isEdit = false;
return;
}
const res = await message.update(contentDiv.innerText.trim(), []);
if (res) {
isEdit = false;
}
}
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
let hightlight: HTMLDivElement;
onMount(() => {
document.addEventListener('selectionchange', onTextSelect);
});
function getSelectionCharacterOffsetWithin() {
var start = 0;
var end = 0;
var doc = contentDiv.ownerDocument;
var win = doc.defaultView;
if (!doc || !win) return { start: 0, end: 0 };
var sel;
if (typeof win.getSelection === 'undefined') {
return { start: 0, end: 0 };
}
sel = win.getSelection();
if (!sel) return { start: 0, end: 0 };
if (sel.rangeCount <= 0) return { start: 0, end: 0 };
var range = sel.getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(contentDiv);
preCaretRange.setEnd(range.startContainer, range.startOffset);
start = preCaretRange.toString().length;
preCaretRange.setEnd(range.endContainer, range.endOffset);
end = preCaretRange.toString().length;
return { start: start, end: end };
}
function onTextSelect() {
const selection = window.getSelection();
if (!selection) return;
const range = selection.getRangeAt(0);
const start = range.startOffset;
const end = range.endOffset;
if (range.commonAncestorContainer.parentElement === contentDiv && end - start > 0) {
const rects = range.getClientRects();
if (!rects.length) {
hightlight.style.visibility = 'hidden';
return;
}
const rect = rects[rects.length - 1];
if (!rect) {
hightlight.style.visibility = 'hidden';
return;
}
hightlight.style.top = rect.bottom + 'px';
hightlight.style.left = rect.right + 'px';
hightlight.style.visibility = 'visible';
} else {
hightlight.style.visibility = 'hidden';
}
}
async function onSpellSelect() {
const selection = window.getSelection();
if (!selection) {
hightlight.style.visibility = 'hidden';
return;
}
const range = getSelectionCharacterOffsetWithin();
const start = range.start;
const end = range.end;
console.log(start, end);
const res = await message.addFeedback(start, end);
if (res) {
selection.removeAllRanges();
hightlight.style.visibility = 'hidden';
contentDiv.innerHTML = sanitize(message.content)
.replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">')
.replaceAll('µ¤', '</span>');
}
}
<div class="chat group" class:chat-start={!isSender} class:chat-end={isSender}>
<div class="rounded-full mx-2 chat-image size-12" title={message.user.nickname}>
<Gravatar
email={message.user.email}
size={64}
title={message.user.nickname}
class="rounded-full"
/>
<div
class="chat-bubble whitespace-pre-wrap"
class:bg-blue-700={isSender}
class:bg-gray-300={!isSender}
class:text-black={!isSender}
class:text-white={isSender}
>
<div contenteditable={isEdit} bind:this={contentDiv} class:bg-blue-900={isEdit}>
{@html sanitize(message.content)
.replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">')
.replaceAll('µ¤', '</span>')}
</div>
{#if isEdit}
<button
class="float-end border rounded-full px-4 py-2 mt-2 bg-white text-blue-700"
on:click={() => endEdit()}
>
{$t('button.save')}
</button>
<button
class="float-end border rounded-full px-4 py-2 mt-2 mr-2"
on:click={() => endEdit(false)}
>
{$t('button.cancel')}
</button>
{/if}
{#if isSender}
<button
class="absolute left-[-1.5rem] mt-2 mr-2 invisible group-hover:visible"
on:click={() => (isEdit ? endEdit() : startEdit())}
>
<Icon src={Pencil} class="w-4 h-4 text-gray-800" />
</button>
{/if}
<button class="italic cursor-help" on:click={historyModal.showModal()}>
</button>
<dialog bind:this={historyModal} class="modal">
<div class="modal-box">
<h3 class="text-xl">{$t('chatbox.history')}</h3>
<div>
{#each $messageVersions as version}
<div class="flex justify-between items-center border-b border-gray-300 py-1">
<div>
{@html sanitize(version.content)
.replaceAll(
'¤µ',
'<span class="decoration-wavy decoration-orange-500 underline">'
)
.replaceAll('µ¤', '</span>')}
</div>
<div class="whitespace-nowrap">{displayTime(version.date)}</div>
</div>
{/each}
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-primary">{$t('button.close')}</button>
</form>
</div>
</div>
</dialog>
<div class="absolute invisible rounded-full border-black border bg-white" bind:this={hightlight}>
<button
on:click={onSpellSelect}
class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pl-4 rounded-l-full"
>
<SpellCheck />
</button><!---
--><button
class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pr-4 rounded-r-full hover:cursor-not-allowed"
>
<Icon src={ChatBubbleBottomCenterText} size="20" />
</button>
</div>