refac/fix: rich text input in ff

This commit is contained in:
Timothy Jaeryang Baek 2025-07-06 17:32:03 +04:00
parent be7166d6fc
commit e84c521177
3 changed files with 91 additions and 84 deletions

View file

@ -31,6 +31,7 @@
let content = '';
let files = [];
let chatInputElement;
let filesInputElement;
let inputFiles;
@ -287,8 +288,9 @@
await tick();
const chatInputElement = document.getElementById(`chat-input-${id}`);
chatInputElement?.focus();
if (chatInputElement) {
chatInputElement.focus();
}
};
$: if (content) {
@ -297,9 +299,10 @@
onMount(async () => {
window.setTimeout(() => {
const chatInput = document.getElementById(`chat-input-${id}`);
chatInput?.focus();
}, 0);
if (chatInputElement) {
chatInputElement.focus();
}
}, 100);
window.addEventListener('keydown', handleKeyDown);
await tick();
@ -402,7 +405,10 @@
recording = false;
await tick();
document.getElementById(`chat-input-${id}`)?.focus();
if (chatInputElement) {
chatInputElement.focus();
}
}}
onConfirm={async (data) => {
const { text, filename } = data;
@ -410,7 +416,10 @@
recording = false;
await tick();
document.getElementById(`chat-input-${id}`)?.focus();
if (chatInputElement) {
chatInputElement.focus();
}
}}
/>
{:else}
@ -485,17 +494,21 @@
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
>
<RichTextInput
bind:value={content}
id={`chat-input-${id}`}
bind:this={chatInputElement}
json={true}
messageInput={true}
shiftEnter={!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)}
{placeholder}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
largeTextAsFile={$settings?.largeTextAsFile ?? false}
onChange={(e) => {
const { md } = e;
content = md;
}}
on:keydown={async (e) => {
e = e.detail.event;
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac

View file

@ -385,7 +385,6 @@
};
onMount(async () => {
console.log('codeblock', lang, code);
if (token) {
onUpdate(token);
}

View file

@ -176,35 +176,34 @@
if (!editor) return;
text = text.replaceAll('\n\n', '\n');
const { state, view } = editor;
const { schema, tr } = state;
if (text.includes('\n')) {
// Multiple lines: make paragraphs
const { schema, tr } = state;
const lines = text.split('\n');
// Map each line to a paragraph node (empty lines -> empty paragraph)
const nodes = lines.map((line) =>
schema.nodes.paragraph.create({}, line ? schema.text(line) : undefined)
);
// Create a document fragment containing all parsed paragraphs
const fragment = Fragment.fromArray(nodes);
// Replace current selection with these paragraphs
tr.replaceSelectionWith(fragment, false /* don't select new */);
// You probably want to move the cursor after the inserted content
// tr.setSelection(Selection.near(tr.doc.resolve(tr.selection.to)));
view.dispatch(tr);
} else if (text === '') {
// Empty: delete selection or paragraph
editor.commands.clearContent();
// Empty: replace with empty paragraph using tr
const emptyParagraph = schema.nodes.paragraph.create();
tr.replaceSelectionWith(emptyParagraph, false);
view.dispatch(tr);
} else {
editor.commands.setContent(editor.state.schema.text(text));
// Single line: create paragraph with text
const paragraph = schema.nodes.paragraph.create({}, schema.text(text));
tr.replaceSelectionWith(paragraph, false);
view.dispatch(tr);
}
selectNextTemplate(editor.view.state, editor.view.dispatch);
focus();
};
export const replaceVariables = (variables) => {
@ -253,7 +252,7 @@
export const focus = () => {
if (editor) {
editor.view.focus();
// Scroll to the top of the editor
// Scroll to the current selection
editor.view.dispatch(editor.view.state.tr.scrollIntoView());
}
};
@ -326,11 +325,7 @@
setTimeout(() => {
const templateFound = selectNextTemplate(editor.view.state, editor.view.dispatch);
if (!templateFound) {
// If no template found, set cursor at the end
const endPos = editor.view.state.doc.content.size;
editor.view.dispatch(
editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, endPos))
);
editor.commands.focus('end');
}
}, 0);
}
@ -339,7 +334,11 @@
onMount(async () => {
let content = value;
if (!json) {
if (json) {
if (!content) {
content = html ? html : null;
}
} else {
if (preserveBreaks) {
turndownService.addRule('preserveBreaks', {
filter: 'br', // Target <br> elements
@ -370,10 +369,6 @@
// Usage example
content = await tryParse(value);
}
} else {
if (html && !content) {
content = html;
}
}
console.log('content', content);
@ -417,40 +412,34 @@
// force re-render so `editor.isActive` works as expected
editor = editor;
html = editor.getHTML();
const htmlValue = editor.getHTML();
const jsonValue = editor.getJSON();
let mdValue = turndownService
.turndown(
htmlValue
.replace(/<p><\/p>/g, '<br/>')
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
)
.replace(/\u00a0/g, ' ');
onChange({
html: editor.getHTML(),
json: editor.getJSON(),
md: turndownService
.turndown(
editor
.getHTML()
.replace(/<p><\/p>/g, '<br/>')
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
)
.replace(/\u00a0/g, ' ')
html: htmlValue,
json: jsonValue,
md: mdValue
});
if (json) {
value = editor.getJSON();
value = jsonValue;
} else {
if (!raw) {
let newValue = turndownService
.turndown(
editor
.getHTML()
.replace(/<p><\/p>/g, '<br/>')
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
)
.replace(/\u00a0/g, ' ');
if (raw) {
value = htmlValue;
} else {
if (!preserveBreaks) {
newValue = newValue.replace(/<br\/>/g, '');
mdValue = mdValue.replace(/<br\/>/g, '');
}
if (value !== newValue) {
value = newValue;
if (value !== mdValue) {
value = mdValue;
// check if the node is paragraph as well
if (editor.isActive('paragraph')) {
@ -459,8 +448,6 @@
}
}
}
} else {
value = editor.getHTML();
}
}
},
@ -609,36 +596,44 @@
const onValueChange = () => {
if (!editor) return;
const jsonValue = editor.getJSON();
const htmlValue = editor.getHTML();
let mdValue = turndownService
.turndown(
(preserveBreaks ? htmlValue.replace(/<p><\/p>/g, '<br/>') : htmlValue).replace(
/ {2,}/g,
(m) => m.replace(/ /g, '\u00a0')
)
)
.replace(/\u00a0/g, ' ');
if (value === '') {
editor.commands.clearContent(); // Clear content if value is empty
selectTemplate();
return;
}
if (json) {
if (JSON.stringify(value) !== JSON.stringify(editor.getJSON())) {
if (JSON.stringify(value) !== JSON.stringify(jsonValue)) {
editor.commands.setContent(value);
selectTemplate();
}
} else {
if (raw) {
if (value !== editor.getHTML()) {
if (value !== htmlValue) {
editor.commands.setContent(value);
selectTemplate();
}
} else {
if (
value !==
turndownService
.turndown(
(preserveBreaks
? editor.getHTML().replace(/<p><\/p>/g, '<br/>')
: editor.getHTML()
).replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
)
.replace(/\u00a0/g, ' ')
) {
preserveBreaks
? editor.commands.setContent(value)
: editor.commands.setContent(
marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
if (value !== mdValue) {
editor.commands.setContent(
preserveBreaks
? value
: marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
breaks: false
})
); // Update editor content
);
selectTemplate();
}