Skip to content

Commit 001d1cb

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents dee5015 + 455ce7b commit 001d1cb

File tree

12 files changed

+259
-239
lines changed

12 files changed

+259
-239
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script lang="ts">
2+
import { buttonVariants } from '../ui/button/button.svelte';
3+
import { Checkbox } from '../ui/checkbox/index.js';
4+
import * as Dialog from '../ui/dialog/index.js';
5+
import Settings from 'lucide-svelte/icons/settings';
6+
import { Label } from '../ui/label/index.js';
7+
8+
interface Props {
9+
showToolBar: boolean;
10+
showBubbleMenus: boolean;
11+
editable: boolean;
12+
}
13+
14+
let {
15+
showToolBar = $bindable(true),
16+
showBubbleMenus = $bindable(true),
17+
editable = $bindable(true)
18+
}: Props = $props();
19+
</script>
20+
21+
<Dialog.Root>
22+
<Dialog.Trigger class={buttonVariants({ variant: 'outline' })}>
23+
<Settings />
24+
<span>Demo Settings</span>
25+
</Dialog.Trigger>
26+
<Dialog.Content class="h-fit max-h-[95vh] w-80 max-w-[95vw] p-4">
27+
<Dialog.Header class="mb-4">
28+
<Dialog.Title>Demo Edra Settings</Dialog.Title>
29+
</Dialog.Header>
30+
<div class="flex flex-col items-start gap-6">
31+
<div class="flex w-full items-center gap-2">
32+
<Checkbox id="toolbar" bind:checked={showToolBar} />
33+
<Label for="toolbar" class="w-full cursor-pointer text-sm font-medium leading-none"
34+
>Show Editor Toolbar</Label
35+
>
36+
</div>
37+
<div class="flex w-full items-center gap-2">
38+
<Checkbox id="menus" bind:checked={showBubbleMenus} />
39+
<Label for="menus" class="w-full cursor-pointer text-sm font-medium leading-none"
40+
>Show Editor Menus</Label
41+
>
42+
</div>
43+
<div class="flex w-full items-center gap-2">
44+
<Checkbox id="editable" bind:checked={editable} />
45+
<Label for="editable" class="w-full cursor-pointer text-sm font-medium leading-none"
46+
>Editable</Label
47+
>
48+
</div>
49+
</div>
50+
</Dialog.Content>
51+
</Dialog.Root>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script lang="ts">
2+
import * as Dialog from '$lib/components/ui/dialog/index.js';
3+
import FileJson from 'lucide-svelte/icons/file-json';
4+
import { Button, buttonVariants } from '../ui/button/index.js';
5+
import ShikiCode from './shiki-code.svelte';
6+
import { toast } from 'svelte-sonner';
7+
import Expand from 'lucide-svelte/icons/maximize-2';
8+
import Restore from 'lucide-svelte/icons/minimize-2';
9+
import { cn } from '$lib/utils.js';
10+
11+
interface Props {
12+
code: string;
13+
}
14+
15+
let { code }: Props = $props();
16+
17+
let expand = $state(false);
18+
19+
function handleExpand() {
20+
expand = !expand;
21+
}
22+
</script>
23+
24+
<Dialog.Root>
25+
<Dialog.Trigger class={buttonVariants({ variant: 'outline' })}>
26+
<FileJson /> Show Output
27+
</Dialog.Trigger>
28+
<Dialog.Content
29+
class={cn(
30+
'h-[95%] w-[95vw] transition-all duration-500 sm:h-[80%] sm:min-w-[50%]',
31+
expand && 'sm:h-[95%] sm:min-w-[95vw]'
32+
)}
33+
>
34+
<Button
35+
variant="ghost"
36+
class="absolute right-12 top-4 hidden size-4 rounded-sm bg-muted p-0 text-muted-foreground opacity-70 hover:opacity-100 sm:inline-flex"
37+
title={expand ? 'Restore' : 'Expand'}
38+
onclick={handleExpand}
39+
>
40+
{#if expand}
41+
<Restore class="!size-3" />
42+
{:else}
43+
<Expand class="!size-3" />
44+
{/if}
45+
</Button>
46+
<Dialog.Header>
47+
<Dialog.Title>JSON Output</Dialog.Title>
48+
<Dialog.Description>Observe the JSON output of the editor content</Dialog.Description>
49+
</Dialog.Header>
50+
<ShikiCode class="size-full overflow-auto" {code} lang="json" />
51+
<Button
52+
variant="outline"
53+
class="ml-auto w-fit"
54+
onclick={() => {
55+
navigator.clipboard.writeText(code);
56+
toast.success(`Copied to clipboard`);
57+
}}>Copy JSON Output</Button
58+
>
59+
</Dialog.Content>
60+
</Dialog.Root>

src/lib/edra/headless/editor.svelte

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { type Editor } from '@tiptap/core';
3-
import { onDestroy, onMount } from 'svelte';
3+
import { onMount } from 'svelte';
44
55
import { initiateEditor } from '../editor.js';
66
import './style.css';
@@ -44,13 +44,14 @@
4444
let {
4545
class: className = '',
4646
content = undefined,
47-
showBubbleMenu = true,
48-
allowedBubbleMenuCommands = [],
49-
showSlashCommands = true,
47+
editable = true,
48+
showBubbleMenus = true,
5049
limit = undefined,
5150
editable = true,
5251
editor = $bindable<Editor | undefined>(),
53-
onUpdate
52+
showSlashCommand = true,
53+
onUpdate,
54+
children
5455
}: EdraProps = $props();
5556
5657
let element = $state<HTMLElement>();
@@ -76,7 +77,7 @@
7677
AudioExtended(AudioExtendedComponent),
7778
ImageExtended(ImageExtendedComponent),
7879
VideoExtended(VideoExtendedComponent),
79-
...(showSlashCommands ? [slashcommand(SlashCommandList)] : [])
80+
...(showSlashCommand ? [slashcommand(SlashCommandList)] : [])
8081
],
8182
{
8283
editable,
@@ -87,10 +88,7 @@
8788
}
8889
}
8990
);
90-
});
91-
92-
onDestroy(() => {
93-
editor?.destroy();
91+
return () => editor?.destroy();
9492
});
9593
9694
</script>
@@ -100,11 +98,11 @@
10098
{/if}
10199

102100
<div class={`edra ${className}`}>
103-
{#if editor && showBubbleMenu}
101+
{@render children?.()}
102+
{#if editor && showBubbleMenus}
104103
<LinkMenu {editor} />
105104
<TableRowMenu {editor} />
106105
<TableColMenu {editor} />
107-
<BubbleMenu {editor} {allowedBubbleMenuCommands} />
108106
{/if}
109107
{#if !editor}
110108
<div class="edra-loading">

src/lib/edra/headless/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default as Edra } from './editor.svelte';
22
export { default as EdraToolbar } from './toolbar.svelte';
3+
export { default as EdraBubbleMenu } from './menus/bubble-menu.svelte';

src/lib/edra/headless/menus/bubble-menu.svelte

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import { commands } from '../../commands/commands.js';
55
import EdraToolBarIcon from '../components/EdraToolBarIcon.svelte';
66
import type { ShouldShowProps } from '../../utils.js';
7+
import type { Snippet } from 'svelte';
78
89
interface Props {
10+
class?: string;
911
editor: Editor;
10-
allowedBubbleMenuCommands?: string[];
12+
children?: Snippet<[]>;
1113
}
12-
const { editor, allowedBubbleMenuCommands }: Props = $props();
14+
const { class: className = '', editor, children }: Props = $props();
1315
1416
const bubbleMenuCommands = [
1517
...commands['text-formatting'].commands,
@@ -72,7 +74,7 @@
7274

7375
<BubbleMenu
7476
{editor}
75-
class="bubble-menu-wrapper"
77+
class={`bubble-menu-wrapper ${className}`}
7678
{shouldShow}
7779
pluginKey="bubble-menu"
7880
updateDelay={100}
@@ -98,36 +100,17 @@
98100
maxWidth: 'calc(100vw - 16px)'
99101
}}
100102
>
101-
{#each Object.keys(commands) as groupKey}
102-
{#if allowedBubbleMenuCommands && allowedBubbleMenuCommands.length > 0}
103-
<!-- If allowedCommands has top-level name, show all commands within that group, else filter by individual commands -->
104-
{@const groupCommands = commands[groupKey].commands}
105-
{@const filteredCommands = allowedBubbleMenuCommands.includes(groupKey)
106-
? groupCommands
107-
: groupCommands.filter((command) => allowedBubbleMenuCommands.includes(command.name))}
108-
{#if filteredCommands.length > 0}
109-
{#each filteredCommands as command}
110-
<EdraToolBarIcon {command} {editor} />
111-
{/each}
112-
{/if}
113-
{:else}
114-
<!-- If no allowedCommands are passed, use default exclusions -->
115-
{#if !excludeCommands.includes(groupKey)}
116-
{#each commands[groupKey].commands as command}
117-
<EdraToolBarIcon {command} {editor} />
118-
{/each}
119-
{/if}
120-
{/if}
121-
{/each}
122-
103+
{#if children}
104+
{@render children()}
105+
{:else}
106+
{#each bubbleMenuCommands as command}
107+
<EdraToolBarIcon {command} {editor} />
108+
{/each}
123109

124-
{#if !allowedBubbleMenuCommands || allowedBubbleMenuCommands.length === 0 || allowedBubbleMenuCommands.includes('fontSize')}
125110
<EdraToolBarIcon command={fontCommands[0]} {editor} />
126111
<span>{editor.getAttributes('textStyle').fontSize ?? '16px'}</span>
127112
<EdraToolBarIcon command={fontCommands[1]} {editor} />
128-
{/if}
129113

130-
{#if !allowedBubbleMenuCommands || allowedBubbleMenuCommands.length === 0 || allowedBubbleMenuCommands.includes('color')}
131114
<EdraToolBarIcon
132115
command={colorCommands[0]}
133116
{editor}
@@ -145,9 +128,6 @@
145128
}
146129
}}
147130
/>
148-
{/if}
149-
150-
{#if !allowedBubbleMenuCommands || allowedBubbleMenuCommands.length === 0 || allowedBubbleMenuCommands.includes('highlight')}
151131
<EdraToolBarIcon
152132
command={colorCommands[1]}
153133
{editor}

src/lib/edra/headless/toolbar.svelte

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,79 @@
22
import type { Editor } from '@tiptap/core';
33
import { commands } from '../commands/commands.js';
44
import EdraToolBarIcon from './components/EdraToolBarIcon.svelte';
5-
import SearchAndReplace from './components/SearchAndReplace.svelte';
5+
import SearcnAndReplace from './components/SearcnAndReplace.svelte';
66
import type { Snippet } from 'svelte';
7-
import { getOrderedToolbarItems } from '../utils.js';
87
98
interface Props {
109
class?: string;
1110
editor: Editor;
12-
allowedCommands?: string[];
13-
children?: Snippet;
11+
children?: Snippet<[]>;
1412
}
1513
16-
const { class: className = '', editor, allowedCommands, children }: Props = $props();
14+
const { class: className = '', editor, children }: Props = $props();
1715
1816
// Special components that are handled separately
1917
const specialComponents = ['fontSize', 'quickColor', 'searchAndReplace'];
2018
const toolbarItems = getOrderedToolbarItems(allowedCommands, commands, specialComponents) as Array<{ type: string; command?: any }>;
2119
let showSearchAndReplace = $state(false);
2220
const colorCommands = commands.colors.commands;
2321
const fontCommands = commands.fonts.commands;
24-
22+
const excludedCommands = ['colors', 'fonts'];
2523
</script>
2624

2725
<div class={`edra-toolbar ${className}`}>
28-
{#if !showSearchAndReplace}
29-
{#each toolbarItems as item}
30-
{#if item.type === 'command'}
31-
<EdraToolBarIcon command={item.command} {editor} />
32-
{:else if item.type === 'fontSize'}
33-
<EdraToolBarIcon command={fontCommands[0]} {editor} />
34-
<span>{editor.getAttributes('textStyle').fontSize ?? '16px'}</span>
35-
<EdraToolBarIcon command={fontCommands[1]} {editor} />
36-
{:else if item.type === 'quickColor'}
37-
<EdraToolBarIcon
38-
command={colorCommands[0]}
39-
{editor}
40-
style={`color: ${editor.getAttributes('textStyle').color};`}
41-
onclick={() => {
42-
const color = editor.getAttributes('textStyle').color;
43-
const hasColor = editor.isActive('textStyle', { color });
44-
if (hasColor) {
45-
editor.chain().focus().unsetColor().run();
46-
} else {
47-
const color = prompt('Enter the color of the text:');
48-
if (color !== null) {
49-
editor.chain().focus().setColor(color).run();
50-
}
26+
{#if children}
27+
{@render children()}
28+
{:else}
29+
{#if !showSearchAndReplace}
30+
{#each Object.keys(commands).filter((key) => !excludedCommands.includes(key)) as keys}
31+
{@const groups = commands[keys].commands}
32+
{#each groups as command}
33+
<EdraToolBarIcon {command} {editor} />
34+
{/each}
35+
<span class="separator"></span>
36+
{/each}
37+
38+
<EdraToolBarIcon command={fontCommands[0]} {editor} />
39+
<span>{editor.getAttributes('textStyle').fontSize ?? '16px'}</span>
40+
<EdraToolBarIcon command={fontCommands[1]} {editor} />
41+
42+
<span class="separator"></span>
43+
44+
<EdraToolBarIcon
45+
command={colorCommands[0]}
46+
{editor}
47+
style={`color: ${editor.getAttributes('textStyle').color};`}
48+
onclick={() => {
49+
const color = editor.getAttributes('textStyle').color;
50+
const hasColor = editor.isActive('textStyle', { color });
51+
if (hasColor) {
52+
editor.chain().focus().unsetColor().run();
53+
} else {
54+
const color = prompt('Enter the color of the text:');
55+
if (color !== null) {
56+
editor.chain().focus().setColor(color).run();
5157
}
52-
}}
53-
/>
54-
{:else if item.type === 'searchAndReplace'}
55-
<EdraToolBarIcon
56-
command={colorCommands[1]}
57-
{editor}
58-
style={`background-color: ${editor.getAttributes('highlight').color};`}
59-
onclick={() => {
60-
const hasHightlight = editor.isActive('highlight');
61-
if (hasHightlight) {
62-
editor.chain().focus().unsetHighlight().run();
63-
} else {
64-
const color = prompt('Enter the color of the highlight:');
65-
if (color !== null) {
66-
editor.chain().focus().setHighlight({ color }).run();
67-
}
58+
}
59+
}}
60+
/>
61+
<EdraToolBarIcon
62+
command={colorCommands[1]}
63+
{editor}
64+
style={`background-color: ${editor.getAttributes('highlight').color};`}
65+
onclick={() => {
66+
const hasHightlight = editor.isActive('highlight');
67+
if (hasHightlight) {
68+
editor.chain().focus().unsetHighlight().run();
69+
} else {
70+
const color = prompt('Enter the color of the highlight:');
71+
if (color !== null) {
72+
editor.chain().focus().setHighlight({ color }).run();
6873
}
69-
}}
70-
/>
71-
{/if}
72-
{/each}
74+
}
75+
}}
76+
/>
77+
{/if}
78+
<SearcnAndReplace {editor} bind:show={showSearchAndReplace} />
7379
{/if}
74-
<SearchAndReplace {editor} bind:show={showSearchAndReplace} />
75-
{@render children?.()}
7680
</div>

0 commit comments

Comments
 (0)