Skip to content

Commit

Permalink
feat: long text allow rich text
Browse files Browse the repository at this point in the history
  • Loading branch information
nichenqin committed Aug 21, 2024
1 parent d799064 commit b6ebbcb
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 34 deletions.
3 changes: 3 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
"@internationalized/date": "^3.5.5",
"@svelte-put/clickoutside": "^3.0.2",
"@tanstack/svelte-query": "^5.51.21",
"@tiptap/core": "^2.6.5",
"@tiptap/pm": "^2.6.5",
"@tiptap/starter-kit": "^2.6.5",
"@trpc/client": "^10.45.2",
"@undb/zod": "workspace:*",
"bits-ui": "^0.21.13",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
$selectedAttachment = null
}
}}
portal="body"
>
<Dialog.Content class="sm:max-w-1/2 max-w-1/2 !w-1/2 gap-0 space-y-0 p-0">
<Dialog.Content class="sm:max-w-1/2 max-w-1/2 z-[100] !w-1/2 gap-0 space-y-0 p-0">
<img class="h-full w-full" src={$selectedAttachment?.signedUrl} alt="Attachment" />
<div class="bg-muted flex items-center gap-2 border-t p-4">
<p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<script lang="ts">
import Tiptap from "$lib/components/tiptap/tiptap.svelte"
import Textarea from "$lib/components/ui/textarea/textarea.svelte"
import type { LongTextField } from "@undb/table"
export let field: LongTextField
export let value: string
export let readonly = false
</script>

<Textarea rows={5} bind:value {...$$restProps} on:change disabled={readonly} />
{#if field.allowRichText}
<Tiptap bind:value />
{:else}
<Textarea rows={5} bind:value {...$$restProps} on:change disabled={readonly} />
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
export let constraint: ILongTextFieldConstraint | undefined
export let option: ILongTextFieldOption = {
allowRichText: false,
allowRichText: true,
}
export let defaultValue: string | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
if (!open) {
}
}}
portal="body"
>
<Dialog.Trigger>
<button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
import type { LongTextField, StringField } from "@undb/table"
import { toast } from "svelte-sonner"
import { gridViewStore } from "../grid-view.store"
import { Maximize2Icon } from "lucide-svelte"
import * as Dialog from "$lib/components/ui/dialog"
import Tiptap from "$lib/components/tiptap/tiptap.svelte"
export let tableId: string
export let field: LongTextField
export let value: string
export let recordId: string
export let isEditing: boolean
export let isSelected: boolean
export let readonly: boolean
export let onValueChange: (value: string) => void
Expand All @@ -19,6 +23,7 @@
mutationFn: trpc.record.update.mutate,
onSuccess(data, variables, context) {
el?.blur()
open = false
gridViewStore.exitEditing()
onValueChange(value)
},
Expand All @@ -33,34 +38,77 @@
el.focus()
}
}
let open = false
</script>

{#if isEditing}
<div class="absolute -bottom-10 left-0 right-0 top-0">
<textarea
rows={3}
bind:this={el}
bind:value
on:blur={() => {
gridViewStore.exitEditing()
}}
class={cn(
$$restProps.class,
"focus-visible:ring-ring h-full w-full rounded-none border px-2 text-xs outline-none focus:bg-white",
)}
on:change={() => {
$updateCell.mutate({
tableId,
id: recordId,
values: { [field.id.value]: value },
})
}}
/>
{#if field.allowRichText}
<div class={cn("relative overflow-hidden", $$restProps.class)}>
{#if isEditing || isSelected}
<div class="flex w-full justify-between overflow-hidden text-left">
<button type="button" on:click={() => (open = true)} class="flex-1 items-start text-left">
<span class="inline-flex flex-1 self-start truncate">
{@html value}
</span>
</button>
<button on:click={() => (open = true)}>
<Maximize2Icon class="text-muted-foreground h-3 w-3" />
</button>
</div>
{:else if value}
<div class="inline-flex flex-1 items-center truncate">
{@html value}
</div>
{/if}
</div>
{:else}
<div class={cn("truncate", $$restProps.class)}>
{#if value}
{value}
<div class={cn("relative", $$restProps.class, isEditing && "border-none")}>
{#if isEditing}
<div class="absolute -bottom-10 left-0 right-0 top-0">
<textarea
rows={3}
bind:this={el}
bind:value
on:blur={() => {
gridViewStore.exitEditing()
}}
class={cn(
$$restProps.class,
"focus-visible:ring-ring h-full w-full rounded-none border px-2 text-xs outline-none focus:bg-white",
)}
on:change={() => {
$updateCell.mutate({
tableId,
id: recordId,
values: { [field.id.value]: value },
})
}}
/>
</div>
{:else if value}
<div class="truncate">
{value}
</div>
{/if}
</div>
{/if}

<Dialog.Root
bind:open
onOpenChange={(open) => {
if (!open) {
$updateCell.mutate({
tableId,
id: recordId,
values: { [field.id.value]: value },
})
}
}}
>
<Dialog.Content
hideCloseButton
class="flex h-[calc(100vh-200px)] max-w-3xl flex-col overflow-y-auto p-2 md:w-[1000px]"
>
<Tiptap {readonly} class="h-full flex-1" bind:value />
</Dialog.Content>
</Dialog.Root>
92 changes: 92 additions & 0 deletions apps/frontend/src/lib/components/tiptap/tiptap.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.tiptap {
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
border: 1px solid hsl(var(--border));
height: 100%;

:first-child {
margin-top: 0;
}

/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;

li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}

/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 1rem;
margin-bottom: 1rem;
text-wrap: pretty;
}

h1 {
font-size: 1.8rem;
}

h2 {
font-size: 1.6rem;
}

h3 {
font-size: 1.4rem;
}

h4,
h5,
h6 {
font-size: 1rem;
}

/* Code and preformatted text styles */
code {
background-color: hsl(var(--secondary));
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}

pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;

code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}

blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}

hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
}
121 changes: 121 additions & 0 deletions apps/frontend/src/lib/components/tiptap/tiptap.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script lang="ts">
import "./tiptap.css"
import { onMount, onDestroy, createEventDispatcher } from "svelte"
import { Editor } from "@tiptap/core"
import StarterKit from "@tiptap/starter-kit"
import { Button } from "../ui/button"
import { cn } from "$lib/utils"
import { BoldIcon, CodeIcon, ItalicIcon } from "lucide-svelte"
let element: HTMLDivElement
let editor: Editor
export let readonly = false
const dispatch = createEventDispatcher()
export let value: string | undefined
onMount(() => {
editor = new Editor({
element: element,
editable: !readonly,
extensions: [StarterKit],
content: value,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
},
onBlur(props) {
const html = props.editor.getHTML()
value = html
dispatch("blur", html)
},
onUpdate(props) {
const html = props.editor.getHTML()
value = html
dispatch("change", html)
},
})
})
onDestroy(() => {
if (editor) {
editor.destroy()
}
})
</script>

{#if editor && !readonly}
<div class="flex h-10 items-center justify-between">
<slot />

<div class="flex items-center gap-1">
<Button
size="sm"
type="button"
variant={editor.isActive("bold") ? "default" : "ghost"}
class="h-7 w-7 px-2"
on:click={() => editor.chain().focus().toggleBold().run()}
>
<BoldIcon class="h-full w-full font-bold" />
</Button>
<Button
size="sm"
type="button"
variant={editor.isActive("italic") ? "default" : "ghost"}
class="h-7 w-7 px-2"
on:click={() => editor.chain().focus().toggleItalic().run()}
>
<ItalicIcon class="h-full w-full" />
</Button>
<Button
size="sm"
type="button"
variant={editor.isActive("code") ? "default" : "ghost"}
class="h-7 w-7 px-2"
on:click={() => editor.chain().focus().toggleCode().run()}
>
<CodeIcon class="h-full w-full" />
</Button>
<Button
size="sm"
type="button"
variant={editor.isActive("heading", { level: 1 }) ? "default" : "ghost"}
class="h-7 w-7"
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
>
H1
</Button>
<Button
size="sm"
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
variant={editor.isActive("heading", { level: 2 }) ? "default" : "ghost"}
class="h-7 w-7"
>
H2
</Button>
<Button
size="sm"
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
variant={editor.isActive("heading", { level: 3 }) ? "default" : "ghost"}
class="h-7 w-7"
>
H3
</Button>
<Button
size="sm"
type="button"
on:click={() => editor.chain().focus().setParagraph().run()}
variant={editor.isActive("paragraph") ? "default" : "ghost"}
class="h-7 w-7"
>
P
</Button>
</div>
</div>
{/if}

<div class={$$restProps.class} bind:this={element} />
Loading

0 comments on commit b6ebbcb

Please sign in to comment.