Skip to content

volstas/trix-svelte

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

trix-svelte

Svelte Wrappers for trix

If you want this to work with prerendering, add this to your vite.config.ts:

ssr: {
		noExternal: ['@ernestasthedev/trix']
	},

Need little bit of CSS in your app.cs file, unless you do your own toolbar.

@layer components {
  .trix-button {
    @apply py-1.5 px-2 bg-gray-200 hover:bg-gray-300 border-gray-300;
  }

  .trix-button.trix-active {
    @apply bg-gray-500 border-gray-600 text-white;
  }

  .basic-list ul {
    list-style-type: disc;
    margin-left: 1rem;
  }

  .basic-list ol {
    list-style-type: decimal;
    margin-left: 1rem;
  }
}

The .basic-list is needed because tailwind strips all default styles so we need to re-add them. If you use tailwind typography, you could use .prose class instead to get all default stuff back.

editor-toolbar.svelte:

Depends on Skeleton from shadcn

I have the 'hideBullets' which hides the list buttons from the toolbar, feel free to remove it if not needed

<script lang="ts">
	import { browser } from '$app/environment';
	import { cn } from '$lib/utils.js';
	import { Skeleton } from '$lib/components/ui/skeleton';
	import TrixStarter from '@ernestasthedev/trix';

	let {
		id,
		hideBullets,
		class: classname
	}: {
		id: string;
		hideBullets?: boolean;
		class?: string;
	} = $props();

	function setupTrixToolbar() {
		return `<div class="row flex">
      <span class="flex" data-trix-button-group="text-tools">
        <button type="button" class="trix-button  rounded-xl rounded-r-none border-r" data-trix-attribute="bold" data-trix-key="b" title="Bold" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
            <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
          </svg>
        </button>
        <button type="button" class="trix-button rounded-none border-x" data-trix-attribute="italic" data-trix-key="i" title="Italic" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="19" y1="4" x2="10" y2="4"></line>
            <line x1="14" y1="20" x2="5" y2="20"></line>
            <line x1="15" y1="4" x2="9" y2="20"></line>
          </svg>
        </button>
        <button type="button" class="trix-button rounded-none border-x" data-trix-attribute="strike" title="Strike" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="18" y1="12" x2="6" y2="12"></line>
            <line x1="18" y1="6" x2="6" y2="18"></line>
          </svg>
        </button>
        <button type="button" class="trix-button border-l rounded-xl rounded-l-none" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="Link" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
            <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
          </svg>
        </button>
      </span>

	<span class="flex ml-2 [.no-bullets_&]:hidden" data-trix-button-group="text-tools">
        <button type="button" class="trix-button border-r rounded-xl rounded-r-none" data-trix-attribute="bullet" title="Bulleted List" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="8" y1="6" x2="21" y2="6"></line>
            <line x1="8" y1="12" x2="21" y2="12"></line>
            <line x1="8" y1="18" x2="21" y2="18"></line>
            <line x1="3" y1="6" x2="3.01" y2="6"></line>
            <line x1="3" y1="12" x2="3.01" y2="12"></line>
            <line x1="3" y1="18" x2="3.01" y2="18"></line>
          </svg>
        </button>
        <button type="button" class="trix-button border-l rounded-xl rounded-l-none" data-trix-attribute="number" title="Numbered List" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="10" y1="6" x2="21" y2="6"></line>
            <line x1="10" y1="12" x2="21" y2="12"></line>
            <line x1="10" y1="18" x2="21" y2="18"></line>
            <path d="M4 6h1v4"></path>
            <path d="M4 10h2"></path>
            <path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"></path>
          </svg>
        </button>
      </span>

      <span class="flex-grow"></span>

      <span class="flex" data-trix-button-group="history-tools">
        <button type="button" class="trix-button border-r rounded-xl rounded-r-none" data-trix-action="undo" data-trix-key="z" title="Undo" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M3 7v6h6"></path>
            <path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path>
          </svg>
        </button>
        <button type="button" class="trix-button rounded-xl rounded-l-none" data-trix-action="redo" data-trix-key="shift+z" title="Redo" tabindex="-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M21 7v6h-6"></path>
            <path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"></path>
          </svg>
        </button>
      </span>
    </div>

    <div class="trix-dialogs" data-trix-dialogs>
      <div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
        <div class="flex flex-col space-y-2 p-2">
          <input type="url" name="href" class="border rounded p-1 text-sm" placeholder="URL or email" aria-label="URL" data-trix-input>
          <div class="flex space-x-2">
            <button type="button" class="trix-button rounded-xl text-sm" data-trix-method="setAttribute">Link</button>
            <button type="button" class="trix-button rounded-xl text-sm"data-trix-method="removeAttribute">Unlink</button>
          </div>
        </div>
      </div>
    </div>`;
	}

	$effect.pre(() => {
		document.addEventListener('trix-before-initialize', () => {
			(window as any).Trix.config.toolbar.getDefaultHTML = setupTrixToolbar;
		});
	});

	let loaded = $state(false);
</script>

{#if !loaded}
	<div class="row flex">
		<Skeleton class={cn(`h-7 w-[8.4rem] rounded-xl`, classname)} />
		<Skeleton class={cn(`ml-2 h-7 w-[4.2rem] rounded-xl`, classname)} />
		<span class="flex-grow"></span>
		<Skeleton class={cn(`ml-2 h-7 w-[4.2rem] rounded-xl`, classname)} />
	</div>
{/if}
{#if browser}
	{#await (async () => {
		await TrixStarter.init();
		loaded = true;
	})() then}
		<trix-toolbar {id} class={cn(`w-full ${hideBullets ? 'no-bullets' : ''}`, classname)}
		></trix-toolbar>
	{/await}
{/if}

editor.svelte

Depends on TextArea from shadcn. My textArea is modified a bit so there is likely some code here that will break, so just remove it. I'll clean it up to behave like vanilla shadcn stuff later

<script lang="ts">
	import { browser } from '$app/environment';
	import TrixStarter from '@ernestasthedev/trix';
	import { cn } from '$lib/utils.js';
	import { Textarea } from '$lib/components/ui/textarea/index.js';

	let {
		id,
		input,
		value = $bindable(),
		placeholder,
		toolbar,
		rows = 1,
		class: classname,
		previewDisabled = false
	}: {
		id: string;
		input: string;
		value: string;
		toolbar: string;
		rows?: number;
		placeholder?: string;
		class?: string;
		previewDisabled?: boolean;
	} = $props();

	let loaded = $state(false);
	let trixEditorElement: HTMLElement | null = $state(null);
	let textAreaElement: HTMLTextAreaElement | null = $state(null);

	let cursorPosition: { start: number; end: number } = $state({ start: 0, end: 0 });

	function swapCursor() {
		const textareaElement = document.getElementById(input) as HTMLTextAreaElement;
		loaded = true;
		if (textareaElement && trixEditorElement && document.activeElement === textareaElement) {
			trixEditorElement.focus();
			(trixEditorElement as any).editor.setSelectedRange([
				cursorPosition.start,
				cursorPosition.end
			]);
		}
	}

	$effect.pre(() => {
		document.addEventListener('trix-before-initialize', (e) => {
			if (e.target instanceof HTMLElement && e.target.id === id) {
				textAreaElement = document.getElementById(input) as HTMLTextAreaElement;
				cursorPosition = {
					start: textAreaElement.selectionStart,
					end: textAreaElement.selectionEnd
				};
			}
		});
		document.addEventListener('trix-initialize', (e) => {
			if (e.target instanceof HTMLElement && e.target.id === id) {
				swapCursor();
			}
		});
	});
</script>

<Textarea
	{placeholder}
	class={cn(`${loaded ? 'hidden' : ''} pl-1 pr-0 w-full resize-none`, classname)}
	id={input}
	blended
	expand
	{rows}
	disabled={previewDisabled}
	bind:value
/>
{#if browser}
	{#await (async () => {
		await TrixStarter.init();
	})() then}
		<trix-editor
			{id}
			{input}
			{placeholder}
			{toolbar}
			class={cn(
				'border-x-0 border-t-0 border-b-2 p-2 pl-1 pr-0 break-all border-input placeholder:text-muted-foreground focus-visible:ring-ring bg-transparent  focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
				classname
			)}
			ontrix-change={(e: any) => {
				value = e.target.value;
			}}
			bind:this={trixEditorElement}
		></trix-editor>
	{/await}
{/if}

Usage

If we assume this is part of a form using superForms:

<EditorToolbar id="foo"></EditorToolbar>
<Editor toolbar="foo" id="bar" input={attrs.id} bind:value={$formData.editor}></Editor>

Then to validate input, in your script add this:

	$effect.pre(() => {
		document.addEventListener('trix-change', (element: any) => {
			form.validate(element?.target?.id);
		});
	});

About

Svelte Wrappers for tri

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published