feat: basic rich text input & format

This commit is contained in:
Shibo Lyu 2024-08-30 03:21:51 +08:00
parent e386fe2583
commit ca380e9ce6
8 changed files with 219 additions and 24 deletions

View file

@ -43,3 +43,9 @@
}
}
}
@layer utilities {
.rich-text {
@apply prose prose-slate dark:prose-invert prose-p:my-0 prose-p:leading-tight prose-code:before:content-[''] prose-code:after:content-[''];
}
}

View file

@ -11,7 +11,7 @@
</head>
<body
data-sveltekit-preload-data="hover"
class="relative h-[100dvh] max-w-[100vw] touch-pan-x touch-pan-y select-none overflow-hidden bg-sb-secondary text-sf-primary"
class="relative flex h-[100dvh] max-w-[100vw] touch-pan-x touch-pan-y select-none flex-col overflow-hidden bg-sb-secondary text-sf-primary"
>
<div style="display: contents">%sveltekit.body%</div>
</body>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { browser } from '$app/environment';
import type { Delta } from 'typewriter-editor';
export let delta: Delta;
let className = '';
export { className as class };
const loadClientComponent = async () => {
if (!browser) return;
const { default: ClientInput } = await import('./RichTextInput/ClientInput.svelte');
return ClientInput;
};
</script>
{#await loadClientComponent() then Input}
<svelte:component this={Input} bind:delta class={className}><slot /></svelte:component>
{/await}

View file

@ -0,0 +1,24 @@
<script lang="ts">
import InputFrame from '$lib/components/InputFrame.svelte';
import { tw } from '$lib/tw';
import { Delta, Editor, asRoot } from 'typewriter-editor';
let className = '';
export { className as class };
export let delta: Delta;
const editor = new Editor();
delta = editor.getDelta();
editor.on('change', () => {
delta = editor.getDelta();
});
$: editor.setDelta(delta);
</script>
<InputFrame class={tw('overflow-y-auto', className)}>
<div class="rich-text w-full outline-none" use:asRoot={editor}>
<slot />
</div>
</InputFrame>

70
src/lib/richText.ts Normal file
View file

@ -0,0 +1,70 @@
import type { AttributeMap, Delta } from 'typewriter-editor';
import { z } from 'zod';
export const blahRichTextSpanAttributesSchema = z.object({
b: z.boolean().default(false),
i: z.boolean().default(false),
u: z.boolean().default(false),
s: z.boolean().default(false),
m: z.boolean().default(false),
hashtag: z.boolean().default(false),
link: z.string().url().optional()
});
export type BlahRichTextSpanAttributes = z.input<typeof blahRichTextSpanAttributesSchema>;
export const blahRichTextSpanSchema = z.union([
z.tuple([z.string()]),
z.tuple([z.string(), blahRichTextSpanAttributesSchema])
]);
export type BlahRichTextSpan = z.input<typeof blahRichTextSpanSchema>;
export const blahRichTextBlockSchema = z.array(blahRichTextSpanSchema);
export type BlahRichTextBlock = z.input<typeof blahRichTextBlockSchema>;
export const blahRichTextSchema = z.array(blahRichTextBlockSchema);
export type BlahRichText = z.input<typeof blahRichTextSchema>;
function isObjectEmpty(obj: object) {
for (const _ in obj) return false;
return true;
}
function deltaAttributesToBlahRichTextSpanAttributes(
attributes?: AttributeMap
): BlahRichTextSpanAttributes | null {
if (!attributes) return null;
const blahRichTextSpanAttributes: BlahRichTextSpanAttributes = {};
if (attributes.bold) blahRichTextSpanAttributes.b = true;
if (attributes.italic) blahRichTextSpanAttributes.i = true;
if (attributes.code) blahRichTextSpanAttributes.m = true;
if (attributes.link) blahRichTextSpanAttributes.link = attributes.link;
if (attributes.underline) blahRichTextSpanAttributes.u = true;
if (attributes.strike) blahRichTextSpanAttributes.s = true;
return isObjectEmpty(blahRichTextSpanAttributes) ? null : blahRichTextSpanAttributes;
}
export function deltaToBlahRichText(delta: Delta): BlahRichText {
const blocks: BlahRichText = [];
let block: BlahRichTextBlock = [];
for (const op of delta.ops) {
const lines = op.insert?.split('\n');
if (!lines) continue;
const attributes = deltaAttributesToBlahRichTextSpanAttributes(op.attributes);
const line = lines.shift();
block.push(attributes ? [line, attributes] : [line]);
for (const line of lines) {
blocks.push(block);
block = [];
block.push(attributes ? [line, attributes] : [line]);
}
}
return blocks;
}

View file

@ -0,0 +1,30 @@
<script lang="ts">
import RichTextInput from '$lib/components/RichTextInput.svelte';
import { deltaToBlahRichText } from '$lib/richText';
import type { Delta } from 'typewriter-editor';
let delta: Delta;
$: brt = delta ? deltaToBlahRichText(delta) : null;
</script>
<RichTextInput bind:delta class="m-4 max-h-32">
<p>A <strong>quick</strong> brown <em>fox</em> jumps over the lazy dog.</p>
<p>A test engineer <a href="https://example.com">tests</a> the <code>RichTextInput</code>.</p>
</RichTextInput>
<div class="flex min-h-0 flex-1 gap-4 p-4">
<div class="flex min-h-0 flex-1 flex-col">
<h2 class="text-lg">Delta (Editor's internal representation)</h2>
<div class="min-h-0 flex-1 select-text overflow-auto">
<pre><code>{JSON.stringify(delta, null, 2)}</code></pre>
</div>
</div>
<div class="flex min-h-0 flex-1 flex-col">
<h2 class="text-lg">Blah Rich Text</h2>
<div class="min-h-0 flex-1 select-text overflow-auto">
<pre><code>{JSON.stringify(brt, null, 2)}</code></pre>
</div>
</div>
</div>