mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-07-11 00:05:33 +00:00
feat: basic rich text input & format
This commit is contained in:
parent
e386fe2583
commit
ca380e9ce6
8 changed files with 219 additions and 24 deletions
|
@ -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-[''];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
19
src/lib/components/RichTextInput.svelte
Normal file
19
src/lib/components/RichTextInput.svelte
Normal 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}
|
24
src/lib/components/RichTextInput/ClientInput.svelte
Normal file
24
src/lib/components/RichTextInput/ClientInput.svelte
Normal 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
70
src/lib/richText.ts
Normal 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;
|
||||
}
|
30
src/routes/(internal)/_rich-text/+page.svelte
Normal file
30
src/routes/(internal)/_rich-text/+page.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue