feat: migrate auth from supabase to better auth (#596)
* feat: add README for @cio/db package and remove existingAccounts.json * load session, load profile and organization, add create profile hook * chore: cache session request The trick was passing all the cookies to the backend. Previously I was only passing the better-auth.session_token and not better-auth.session_data. Read about the difference here https://www.better-auth.com/docs/concepts/cookies#custom-cookies * feat: login, logout, onboarding, verify email, reset password. * feat: complete change email and email verification * feat: standardise how we make rpc calls and create org endpoints * fix ui errors * fix: handle potential undefined customerId and standardize avatar URL property * refactor: standardize custom domain handling and update telegram chat ID references * chore: add tailwind prefix to @cio/ui this fixed the media query issues because @cio/ui classes were overriding this dashboard tailwind build * chore: clean up org sidebar * chore: persist mode state * feat: implement order breadcrumbs * chore: upgrade to better auth 1.4 * chore: address common auth flow bugs and setup app for others to setup locally.
This commit is contained in:
@@ -68,7 +68,7 @@ You can also set HTML content programmatically:
|
||||
<p>This content was loaded after the editor was ready.</p>
|
||||
<code>console.log('Hello, world!');</code>
|
||||
`;
|
||||
|
||||
|
||||
if (editor) {
|
||||
editor.commands.setContent(htmlString);
|
||||
}
|
||||
@@ -79,14 +79,9 @@ You can also set HTML content programmatically:
|
||||
}
|
||||
</script>
|
||||
|
||||
<Editor
|
||||
bind:content
|
||||
onEditorReady={handleEditorReady}
|
||||
/>
|
||||
<Editor bind:content onEditorReady={handleEditorReady} />
|
||||
|
||||
<button on:click={loadHtmlContent}>
|
||||
Load HTML Content
|
||||
</button>
|
||||
<button on:click={loadHtmlContent}> Load HTML Content </button>
|
||||
```
|
||||
|
||||
## Advanced Usage with All Props
|
||||
@@ -105,7 +100,7 @@ You can also set HTML content programmatically:
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
let editable = true;
|
||||
let showToolBar = true;
|
||||
|
||||
@@ -120,7 +115,7 @@ You can also set HTML content programmatically:
|
||||
}
|
||||
</script>
|
||||
|
||||
<Editor
|
||||
<Editor
|
||||
bind:content
|
||||
bind:editable
|
||||
{showToolBar}
|
||||
@@ -148,16 +143,12 @@ You can also set HTML content programmatically:
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h2>Article</h2>
|
||||
<Editor
|
||||
bind:content={article}
|
||||
contentStorageKey="article-content"
|
||||
enablePersistence={true}
|
||||
/>
|
||||
<Editor bind:content={article} contentStorageKey="article-content" enablePersistence={true} />
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2>Notes</h2>
|
||||
<Editor
|
||||
<Editor
|
||||
bind:content={notes}
|
||||
showToolBar={false}
|
||||
contentStorageKey="notes-content"
|
||||
@@ -170,30 +161,30 @@ You can also set HTML content programmatically:
|
||||
|
||||
## Props Reference
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `Content` | Default doc with placeholder | Editor content (bindable) |
|
||||
| `showToolBar` | `boolean` | `true` | Whether to show the toolbar |
|
||||
| `editable` | `boolean` | `true` | Whether the editor is editable |
|
||||
| `enablePersistence` | `boolean` | `false` | Enable localStorage persistence |
|
||||
| `contentStorageKey` | `string` | `'edra-content'` | localStorage key for content |
|
||||
| `editableStorageKey` | `string` | `'edra-editable'` | localStorage key for editable state |
|
||||
| `class` | `string` | Default wrapper classes | CSS classes for wrapper |
|
||||
| `editorClass` | `string` | `'h-[32rem] overflow-auto'` | CSS classes for editor |
|
||||
| `onContentChange` | `(content: Content) => void` | `undefined` | Callback when content changes |
|
||||
| `onEditorReady` | `(editor: Editor) => void` | `undefined` | Callback when editor is ready |
|
||||
| `onEditorDestroy` | `() => void` | `undefined` | Callback when editor is destroyed |
|
||||
| Prop | Type | Default | Description |
|
||||
| -------------------- | ---------------------------- | ---------------------------- | ----------------------------------- |
|
||||
| `content` | `Content` | Default doc with placeholder | Editor content (bindable) |
|
||||
| `showToolBar` | `boolean` | `true` | Whether to show the toolbar |
|
||||
| `editable` | `boolean` | `true` | Whether the editor is editable |
|
||||
| `enablePersistence` | `boolean` | `false` | Enable localStorage persistence |
|
||||
| `contentStorageKey` | `string` | `'edra-content'` | localStorage key for content |
|
||||
| `editableStorageKey` | `string` | `'edra-editable'` | localStorage key for editable state |
|
||||
| `class` | `string` | Default wrapper classes | CSS classes for wrapper |
|
||||
| `editorClass` | `string` | `'h-[32rem] overflow-auto'` | CSS classes for editor |
|
||||
| `onContentChange` | `(content: Content) => void` | `undefined` | Callback when content changes |
|
||||
| `onEditorReady` | `(editor: Editor) => void` | `undefined` | Callback when editor is ready |
|
||||
| `onEditorDestroy` | `() => void` | `undefined` | Callback when editor is destroyed |
|
||||
|
||||
## Callbacks
|
||||
|
||||
| Callback | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| Callback | Type | Description |
|
||||
| ----------------- | ---------------------------- | ---------------------------------- |
|
||||
| `onContentChange` | `(content: Content) => void` | Called when editor content changes |
|
||||
| `onEditorReady` | `(editor: Editor) => void` | Called when editor is ready |
|
||||
| `onEditorDestroy` | `() => void` | Called when editor is destroyed |
|
||||
| `onEditorReady` | `(editor: Editor) => void` | Called when editor is ready |
|
||||
| `onEditorDestroy` | `() => void` | Called when editor is destroyed |
|
||||
|
||||
```svelte
|
||||
<Editor
|
||||
<Editor
|
||||
content={myContent}
|
||||
onContentChange={(content) => {
|
||||
console.log('New content:', content);
|
||||
@@ -225,7 +216,7 @@ You can also set HTML content programmatically:
|
||||
<script>
|
||||
import { Editor } from './editor.svelte';
|
||||
import type { Content } from '@tiptap/core';
|
||||
|
||||
|
||||
let content: Content = { type: 'doc', content: [] };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { Editor } from '@tiptap/core';
|
||||
import { Icon } from '@lucide/svelte';
|
||||
|
||||
export interface EdraToolBarCommands {
|
||||
name: string;
|
||||
icon: typeof Icon;
|
||||
tooltip?: string;
|
||||
shortCut?: string;
|
||||
onClick?: (editor: Editor) => void;
|
||||
isActive?: (editor: Editor) => boolean;
|
||||
clickable?: (editor: Editor) => boolean;
|
||||
name: string;
|
||||
icon: typeof Icon;
|
||||
tooltip?: string;
|
||||
shortCut?: string;
|
||||
onClick?: (editor: Editor) => void;
|
||||
isActive?: (editor: Editor) => boolean;
|
||||
clickable?: (editor: Editor) => boolean;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { BubbleMenuPlugin, type BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { BubbleMenuPlugin, type BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
interface Props
|
||||
extends Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> {
|
||||
editor?: Editor;
|
||||
children?: Snippet<[]>;
|
||||
class?: string;
|
||||
style?: string;
|
||||
pluginKey?: string;
|
||||
updateDelay?: number;
|
||||
resizeDelay?: number;
|
||||
}
|
||||
interface Props extends Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> {
|
||||
editor?: Editor;
|
||||
children?: Snippet<[]>;
|
||||
class?: string;
|
||||
style?: string;
|
||||
pluginKey?: string;
|
||||
updateDelay?: number;
|
||||
resizeDelay?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
editor,
|
||||
shouldShow = null,
|
||||
class: className = '',
|
||||
style = '',
|
||||
children,
|
||||
updateDelay,
|
||||
resizeDelay,
|
||||
pluginKey = 'bubbleMenu',
|
||||
options,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
let {
|
||||
editor,
|
||||
shouldShow = null,
|
||||
class: className = '',
|
||||
style = '',
|
||||
children,
|
||||
updateDelay,
|
||||
resizeDelay,
|
||||
pluginKey = 'bubbleMenu',
|
||||
options,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
let element = $state<HTMLElement>();
|
||||
let element = $state<HTMLElement>();
|
||||
|
||||
onMount(() => {
|
||||
if (!element) return;
|
||||
onMount(() => {
|
||||
if (!element) return;
|
||||
|
||||
element.style.position = 'absolute';
|
||||
element.style.visibility = 'hidden';
|
||||
element.style.position = 'absolute';
|
||||
element.style.visibility = 'hidden';
|
||||
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.warn('BubbleMenu component does not have editor prop or editor is destroyed.');
|
||||
return;
|
||||
}
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.warn('BubbleMenu component does not have editor prop or editor is destroyed.');
|
||||
return;
|
||||
}
|
||||
|
||||
const plugin = BubbleMenuPlugin({
|
||||
pluginKey,
|
||||
editor,
|
||||
element,
|
||||
shouldShow,
|
||||
updateDelay,
|
||||
resizeDelay,
|
||||
options
|
||||
});
|
||||
const plugin = BubbleMenuPlugin({
|
||||
pluginKey,
|
||||
editor,
|
||||
element,
|
||||
shouldShow,
|
||||
updateDelay,
|
||||
resizeDelay,
|
||||
options
|
||||
});
|
||||
|
||||
editor.registerPlugin(plugin);
|
||||
editor.registerPlugin(plugin);
|
||||
|
||||
return () => {
|
||||
if (editor && !editor.isDestroyed) {
|
||||
editor.unregisterPlugin(pluginKey);
|
||||
}
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
if (editor && !editor.isDestroyed) {
|
||||
editor.unregisterPlugin(pluginKey);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={element} class={`bubble-menu-wrapper ${className}`} {style} {...restProps}>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type { Icon } from '@lucide/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { NodeViewWrapper } from 'svelte-tiptap';
|
||||
import type { Icon } from '@lucide/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { NodeViewWrapper } from 'svelte-tiptap';
|
||||
|
||||
interface Props {
|
||||
icon?: typeof Icon;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
class?: string;
|
||||
children?: Snippet<[]>;
|
||||
}
|
||||
interface Props {
|
||||
icon?: typeof Icon;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
class?: string;
|
||||
children?: Snippet<[]>;
|
||||
}
|
||||
|
||||
const { icon, title, onClick, class: className = '', children }: Props = $props();
|
||||
const { icon, title, onClick, class: className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper
|
||||
as="div"
|
||||
contenteditable="false"
|
||||
class={`media-placeholder ${className}`}
|
||||
onclick={onClick}
|
||||
style="user-select: none;"
|
||||
as="div"
|
||||
contenteditable="false"
|
||||
class={`media-placeholder ${className}`}
|
||||
onclick={onClick}
|
||||
style="user-select: none;"
|
||||
>
|
||||
{#if !children && icon && title}
|
||||
{@const Icon = icon}
|
||||
<Icon />
|
||||
<div contenteditable="false">{title}</div>
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
{#if !children && icon && title}
|
||||
{@const Icon = icon}
|
||||
<Icon />
|
||||
<div contenteditable="false">{title}</div>
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</NodeViewWrapper>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/* Base TipTap Editor Styles with Light/Dark Theme Support */
|
||||
|
||||
.tiptap :first-child {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* For Placeholder */
|
||||
.tiptap .is-empty::before {
|
||||
pointer-events: none;
|
||||
float: left;
|
||||
height: 0;
|
||||
color: var(--border-color-hover);
|
||||
content: attr(data-placeholder);
|
||||
pointer-events: none;
|
||||
float: left;
|
||||
height: 0;
|
||||
color: var(--border-color-hover);
|
||||
content: attr(data-placeholder);
|
||||
}
|
||||
|
||||
/* Heading Styles */
|
||||
@@ -20,435 +20,435 @@
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
line-height: 1.2;
|
||||
margin-top: 1rem;
|
||||
text-wrap: pretty;
|
||||
line-height: 1.2;
|
||||
margin-top: 1rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.tiptap h1,
|
||||
.tiptap h2 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tiptap h1 {
|
||||
scroll-margin: 5rem;
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.015em;
|
||||
scroll-margin: 5rem;
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.tiptap h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tiptap h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap blockquote {
|
||||
margin: 1rem 0rem;
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 0.75rem;
|
||||
font-style: italic;
|
||||
color: var(--blockquote-color);
|
||||
position: relative;
|
||||
margin: 1rem 0rem;
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 0.75rem;
|
||||
font-style: italic;
|
||||
color: var(--blockquote-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tiptap blockquote::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.25rem;
|
||||
background-color: var(--blockquote-border);
|
||||
border-radius: 0.5rem;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.25rem;
|
||||
background-color: var(--blockquote-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tiptap blockquote p {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Horizontal Rule */
|
||||
.tiptap hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 1rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Inline Code */
|
||||
.tiptap code:not(pre code) {
|
||||
background-color: var(--code-bg);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
background-color: var(--code-bg);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* List Styling */
|
||||
|
||||
.tiptap ul,
|
||||
.tiptap ol {
|
||||
padding: 0 1rem;
|
||||
margin: 0.5rem 1rem 0.5rem 0.4rem;
|
||||
padding: 0 1rem;
|
||||
margin: 0.5rem 1rem 0.5rem 0.4rem;
|
||||
}
|
||||
|
||||
.tiptap ul li p,
|
||||
.tiptap ol li p {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Task List Styling */
|
||||
.tiptap ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] li {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] li > label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] li > div {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li[data-checked='true'] div {
|
||||
color: var(--task-completed-color);
|
||||
text-decoration: line-through;
|
||||
color: var(--task-completed-color);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
position: relative;
|
||||
top: 0.25rem;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
cursor: pointer;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
top: 0.25rem;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
cursor: pointer;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Color Swatches */
|
||||
.color {
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.color::before {
|
||||
margin-bottom: 0.15rem;
|
||||
margin-right: 0.1rem;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
background-color: var(--color);
|
||||
content: ' ';
|
||||
margin-bottom: 0.15rem;
|
||||
margin-right: 0.1rem;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
background-color: var(--color);
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
/* Code Block Styling */
|
||||
.tiptap pre {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
overflow: auto;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
overflow: auto;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tiptap pre code {
|
||||
flex: 1;
|
||||
border-radius: 0 !important;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono';
|
||||
flex: 1;
|
||||
border-radius: 0 !important;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono';
|
||||
}
|
||||
|
||||
/* Drag Handle Styling */
|
||||
.drag-handle {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
padding-right: 0.5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
opacity: 100;
|
||||
transition-property: opacity;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||
color: var(--border-color-hover);
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
padding-right: 0.5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
opacity: 100;
|
||||
transition-property: opacity;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||
color: var(--border-color-hover);
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.drag-handle.hide {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.drag-handle {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.drag-handle {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
/* Math Equations (KaTeX) */
|
||||
.katex:hover {
|
||||
background-color: var(--code-bg);
|
||||
background-color: var(--code-bg);
|
||||
}
|
||||
|
||||
.katex.result {
|
||||
border-bottom: 1px dashed var(--highlight-border);
|
||||
background-color: var(--highlight-color);
|
||||
border-bottom: 1px dashed var(--highlight-border);
|
||||
background-color: var(--highlight-color);
|
||||
}
|
||||
|
||||
/* Table Styling */
|
||||
.ProseMirror .tableWrapper {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror table {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--table-border);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
.ProseMirror table td,
|
||||
.ProseMirror table th {
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
border: 1px solid var(--table-border);
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
border: 1px solid var(--table-border);
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ProseMirror table td:first-of-type:not(a),
|
||||
.ProseMirror table th:first-of-type:not(a) {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ProseMirror table td p,
|
||||
.ProseMirror table th p {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ProseMirror table td p + p,
|
||||
.ProseMirror table th p + p {
|
||||
margin-top: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.ProseMirror table th {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ProseMirror table .column-resize-handle {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -0.25rem;
|
||||
bottom: -2px;
|
||||
display: flex;
|
||||
width: 0.5rem;
|
||||
background-color: var(--table-border);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -0.25rem;
|
||||
bottom: -2px;
|
||||
display: flex;
|
||||
width: 0.5rem;
|
||||
background-color: var(--table-border);
|
||||
}
|
||||
|
||||
.ProseMirror table .column-resize-handle::before {
|
||||
content: '';
|
||||
margin-left: 0.5rem;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: var(--table-border);
|
||||
content: '';
|
||||
margin-left: 0.5rem;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: var(--table-border);
|
||||
}
|
||||
|
||||
.ProseMirror table .selectedCell {
|
||||
border-style: double;
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-selected);
|
||||
border-style: double;
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-selected);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column,
|
||||
.ProseMirror table .grip-row {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--table-bg-selected);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--table-bg-selected);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column {
|
||||
top: -0.75rem;
|
||||
left: 0;
|
||||
margin-left: -1px;
|
||||
height: 0.75rem;
|
||||
width: calc(100% + 1px);
|
||||
border-left: 1px solid var(--table-border);
|
||||
top: -0.75rem;
|
||||
left: 0;
|
||||
margin-left: -1px;
|
||||
height: 0.75rem;
|
||||
width: calc(100% + 1px);
|
||||
border-left: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column:hover::before,
|
||||
.ProseMirror table .grip-column.selected::before {
|
||||
content: '';
|
||||
width: 0.625rem;
|
||||
content: '';
|
||||
width: 0.625rem;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column:hover {
|
||||
background-color: var(--table-bg-hover);
|
||||
background-color: var(--table-bg-hover);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column:hover::before {
|
||||
border-bottom: 2px dotted var(--border-color-hover);
|
||||
border-bottom: 2px dotted var(--border-color-hover);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column.first {
|
||||
border-top-left-radius: 0.125rem;
|
||||
border-color: transparent;
|
||||
border-top-left-radius: 0.125rem;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column.last {
|
||||
border-top-right-radius: 0.125rem;
|
||||
border-top-right-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column.selected {
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-hover);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-hover);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-column.selected::before {
|
||||
border-bottom: 2px dotted var(--border-color-hover);
|
||||
border-bottom: 2px dotted var(--border-color-hover);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row {
|
||||
left: -0.75rem;
|
||||
top: 0;
|
||||
margin-top: -1px;
|
||||
height: calc(100% + 1px);
|
||||
width: 0.75rem;
|
||||
border-top: 1px solid var(--table-border);
|
||||
left: -0.75rem;
|
||||
top: 0;
|
||||
margin-top: -1px;
|
||||
height: calc(100% + 1px);
|
||||
width: 0.75rem;
|
||||
border-top: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row:hover::before,
|
||||
.ProseMirror table .grip-row.selected::before {
|
||||
content: '';
|
||||
height: 0.625rem;
|
||||
content: '';
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row:hover {
|
||||
background-color: var(--table-bg-hover);
|
||||
background-color: var(--table-bg-hover);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row:hover::before {
|
||||
border-left: 2px dotted var(--border-color-hover);
|
||||
border-left: 2px dotted var(--border-color-hover);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row.first {
|
||||
border-top-left-radius: 0.125rem;
|
||||
border-color: transparent;
|
||||
border-top-left-radius: 0.125rem;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row.last {
|
||||
border-bottom-left-radius: 0.125rem;
|
||||
border-bottom-left-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row.selected {
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-hover);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-bg-hover);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.ProseMirror table .grip-row.selected::before {
|
||||
border-left: 2px dotted var(--border-color-hover);
|
||||
border-left: 2px dotted var(--border-color-hover);
|
||||
}
|
||||
|
||||
.tiptap .search-result {
|
||||
background-color: var(--search-result-bg);
|
||||
color: black;
|
||||
background-color: var(--search-result-bg);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tiptap .search-result-current {
|
||||
background-color: var(--search-result-current-bg);
|
||||
color: black;
|
||||
background-color: var(--search-result-current-bg);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
background-color: var(--codeblock-bg);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
background-color: var(--codeblock-bg);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.code-wrapper-tile {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 0.25rem;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 0.25rem;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.code-wrapper:hover .code-wrapper-tile {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tiptap iframe {
|
||||
aspect-ratio: 16 / 9;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
@@ -122,12 +122,17 @@
|
||||
</script>
|
||||
|
||||
{#if browser}
|
||||
<div class={cn('bg-background z-50 flex size-full w-full flex-col rounded-md border border-dashed', className)}>
|
||||
<div
|
||||
class={cn(
|
||||
'ui:bg-background ui:z-50 ui:flex ui:size-full ui:w-full ui:flex-col ui:rounded-md ui:border ui:border-dashed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{#if editor && !editor.isDestroyed}
|
||||
{#if showToolBar}
|
||||
<div transition:slide>
|
||||
<EdraToolBar
|
||||
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5"
|
||||
class="ui:bg-secondary/50 ui:flex ui:w-full ui:items-center ui:overflow-x-auto ui:border-b ui:border-dashed ui:p-0.5"
|
||||
{editor}
|
||||
/>
|
||||
</div>
|
||||
@@ -137,7 +142,7 @@
|
||||
<EdraDragHandleExtended {editor} />
|
||||
{/if}
|
||||
<EdraEditor
|
||||
class={cn('h-[32rem] overflow-auto p-4', editorClass)}
|
||||
class={cn('ui:h-128 ui:overflow-auto ui:p-4', editorClass)}
|
||||
bind:editor
|
||||
{editable}
|
||||
{content}
|
||||
|
||||
@@ -26,383 +26,372 @@ import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/p
|
||||
import { Node as PMNode } from '@tiptap/pm/model';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
search: {
|
||||
/**
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set case sensitivity in extension.
|
||||
*/
|
||||
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Reset current search result to first instance.
|
||||
*/
|
||||
resetIndex: () => ReturnType;
|
||||
/**
|
||||
* @description Find next instance of search result.
|
||||
*/
|
||||
nextSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Find previous instance of search result.
|
||||
*/
|
||||
previousSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Replace first instance of search result with given replace term.
|
||||
*/
|
||||
replace: () => ReturnType;
|
||||
/**
|
||||
* @description Replace all instances of search result with given replace term.
|
||||
*/
|
||||
replaceAll: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
search: {
|
||||
/**
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set case sensitivity in extension.
|
||||
*/
|
||||
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Reset current search result to first instance.
|
||||
*/
|
||||
resetIndex: () => ReturnType;
|
||||
/**
|
||||
* @description Find next instance of search result.
|
||||
*/
|
||||
nextSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Find previous instance of search result.
|
||||
*/
|
||||
previousSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Replace first instance of search result with given replace term.
|
||||
*/
|
||||
replace: () => ReturnType;
|
||||
/**
|
||||
* @description Replace all instances of search result with given replace term.
|
||||
*/
|
||||
replaceAll: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface TextNodesWithPosition {
|
||||
text: string;
|
||||
pos: number;
|
||||
text: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s,
|
||||
caseSensitive ? 'gu' : 'gui'
|
||||
);
|
||||
return RegExp(disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s, caseSensitive ? 'gu' : 'gui');
|
||||
};
|
||||
|
||||
interface ProcessedSearches {
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
}
|
||||
|
||||
function processSearches(
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
): ProcessedSearches {
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
|
||||
if (!searchTerm) {
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.empty,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
if (!searchTerm) {
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.empty,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
doc?.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (textNodesWithPosition[index]) {
|
||||
textNodesWithPosition[index] = {
|
||||
text: textNodesWithPosition[index].text + node.text,
|
||||
pos: textNodesWithPosition[index].pos
|
||||
};
|
||||
} else {
|
||||
textNodesWithPosition[index] = {
|
||||
text: `${node.text}`,
|
||||
pos
|
||||
};
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
doc?.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (textNodesWithPosition[index]) {
|
||||
textNodesWithPosition[index] = {
|
||||
text: textNodesWithPosition[index].text + node.text,
|
||||
pos: textNodesWithPosition[index].pos
|
||||
};
|
||||
} else {
|
||||
textNodesWithPosition[index] = {
|
||||
text: `${node.text}`,
|
||||
pos
|
||||
};
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
|
||||
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||
|
||||
for (const element of textNodesWithPosition) {
|
||||
const { text, pos } = element;
|
||||
const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim());
|
||||
for (const element of textNodesWithPosition) {
|
||||
const { text, pos } = element;
|
||||
const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim());
|
||||
|
||||
for (const m of matches) {
|
||||
if (m[0] === '') break;
|
||||
for (const m of matches) {
|
||||
if (m[0] === '') break;
|
||||
|
||||
if (m.index !== undefined) {
|
||||
results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m.index !== undefined) {
|
||||
results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const r = results[i];
|
||||
const className =
|
||||
i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass;
|
||||
const decoration: Decoration = Decoration.inline(r.from, r.to, {
|
||||
class: className
|
||||
});
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const r = results[i];
|
||||
const className = i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass;
|
||||
const decoration: Decoration = Decoration.inline(r.from, r.to, {
|
||||
class: className
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
}
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
const replace = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
) => {
|
||||
const firstResult = results[0];
|
||||
const firstResult = results[0];
|
||||
|
||||
if (!firstResult) return;
|
||||
if (!firstResult) return;
|
||||
|
||||
const { from, to } = results[0];
|
||||
const { from, to } = results[0];
|
||||
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
};
|
||||
|
||||
const rebaseNextResult = (
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
): [number, Range[]] | null => {
|
||||
const nextIndex = index + 1;
|
||||
const nextIndex = index + 1;
|
||||
|
||||
if (!results[nextIndex]) return null;
|
||||
if (!results[nextIndex]) return null;
|
||||
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
|
||||
const { from, to } = results[nextIndex];
|
||||
const { from, to } = results[nextIndex];
|
||||
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
|
||||
return [offset, results];
|
||||
return [offset, results];
|
||||
};
|
||||
|
||||
const replaceAll = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
) => {
|
||||
let offset = 0;
|
||||
let offset = 0;
|
||||
|
||||
let resultsCopy = results.slice();
|
||||
let resultsCopy = results.slice();
|
||||
|
||||
if (!resultsCopy.length) return;
|
||||
if (!resultsCopy.length) return;
|
||||
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(tr);
|
||||
}
|
||||
if (dispatch) {
|
||||
dispatch(tr);
|
||||
}
|
||||
};
|
||||
|
||||
export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin');
|
||||
|
||||
export interface SearchAndReplaceOptions {
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceStorage {
|
||||
searchTerm: string;
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
searchTerm: string;
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
}
|
||||
|
||||
export const SearchAndReplace = Extension.create<SearchAndReplaceOptions, SearchAndReplaceStorage>({
|
||||
name: 'searchAndReplace',
|
||||
name: 'searchAndReplace',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
replaceTerm: '',
|
||||
results: [],
|
||||
lastSearchTerm: '',
|
||||
caseSensitive: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0
|
||||
};
|
||||
},
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
replaceTerm: '',
|
||||
results: [],
|
||||
lastSearchTerm: '',
|
||||
caseSensitive: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const nextIndex = resultIndex + 1;
|
||||
const nextIndex = resultIndex + 1;
|
||||
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const prevIndex = resultIndex - 1;
|
||||
const prevIndex = resultIndex - 1;
|
||||
|
||||
if (results[prevIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = results.length - 1;
|
||||
}
|
||||
if (results[prevIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = results.length - 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
replace:
|
||||
() =>
|
||||
({ editor, state, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
replace:
|
||||
() =>
|
||||
({ editor, state, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
|
||||
const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastCaseSensitive = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t);
|
||||
const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastCaseSensitive = (t: boolean) => (editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t);
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: searchAndReplacePluginKey,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply({ doc, docChanged }, oldState) {
|
||||
const {
|
||||
searchTerm,
|
||||
lastSearchTerm,
|
||||
caseSensitive,
|
||||
lastCaseSensitive,
|
||||
resultIndex,
|
||||
lastResultIndex
|
||||
} = editor.storage.searchAndReplace;
|
||||
return [
|
||||
new Plugin({
|
||||
key: searchAndReplacePluginKey,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply({ doc, docChanged }, oldState) {
|
||||
const { searchTerm, lastSearchTerm, caseSensitive, lastCaseSensitive, resultIndex, lastResultIndex } =
|
||||
editor.storage.searchAndReplace;
|
||||
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchAndReplace;
|
||||
|
||||
@@ -2,146 +2,137 @@ import { Node, nodeInputRule } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
export interface AudioOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
audio: {
|
||||
/**
|
||||
* Set a audio node
|
||||
*/
|
||||
setAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a audio
|
||||
*/
|
||||
toggleAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a audio
|
||||
*/
|
||||
removeAudio: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
audio: {
|
||||
/**
|
||||
* Set a audio node
|
||||
*/
|
||||
setAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a audio
|
||||
*/
|
||||
toggleAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a audio
|
||||
*/
|
||||
removeAudio: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const AUDIO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
||||
|
||||
export const Audio = Node.create<AudioOptions>({
|
||||
name: 'audio',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
atom: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'audio',
|
||||
getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
name: 'audio',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
atom: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'audio',
|
||||
getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'audio',
|
||||
{ controls: 'true', style: 'width: 100%;', ...HTMLAttributes },
|
||||
['source', HTMLAttributes]
|
||||
];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setAudio:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(
|
||||
`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`
|
||||
),
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['audio', { controls: 'true', style: 'width: 100%;', ...HTMLAttributes }, ['source', HTMLAttributes]];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setAudio:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`),
|
||||
|
||||
toggleAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: AUDIO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
toggleAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: AUDIO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('audioDropPlugin'),
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('audioDropPlugin'),
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles =
|
||||
event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
|
||||
if (!hasFiles) return false;
|
||||
if (!hasFiles) return false;
|
||||
|
||||
const audios = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
/audio/i.test(file.type)
|
||||
);
|
||||
const audios = Array.from(event.dataTransfer.files).filter((file) => /audio/i.test(file.type));
|
||||
|
||||
if (audios.length === 0) return false;
|
||||
if (audios.length === 0) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
|
||||
audios.forEach((audio) => {
|
||||
const reader = new FileReader();
|
||||
audios.forEach((audio) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.audio.create({ src: readerEvent.target?.result });
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.audio.create({ src: readerEvent.target?.result });
|
||||
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(audio);
|
||||
});
|
||||
reader.readAsDataURL(audio);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,62 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface AudioPlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
audioPlaceholder: {
|
||||
/**
|
||||
* Inserts an audio placeholder
|
||||
*/
|
||||
insertAudioPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
audioPlaceholder: {
|
||||
/**
|
||||
* Inserts an audio placeholder
|
||||
*/
|
||||
insertAudioPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const AudioPlaceholder = (
|
||||
component: Component<NodeViewProps>
|
||||
): Node<AudioPlaceholderOptions> =>
|
||||
Node.create<AudioPlaceholderOptions>({
|
||||
name: 'audio-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
export const AudioPlaceholder = (component: Component<NodeViewProps>): Node<AudioPlaceholderOptions> =>
|
||||
Node.create<AudioPlaceholderOptions>({
|
||||
name: 'audio-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertAudioPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'audio-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertAudioPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'audio-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,26 +3,26 @@ import { EditorView } from '@tiptap/pm/view';
|
||||
import * as pmView from '@tiptap/pm/view';
|
||||
|
||||
function getPmView() {
|
||||
try {
|
||||
return pmView;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return pmView;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeForClipboard(view: EditorView, slice: Slice) {
|
||||
// Newer Tiptap/ProseMirror
|
||||
if (view && typeof view.serializeForClipboard === 'function') {
|
||||
return view.serializeForClipboard(slice);
|
||||
}
|
||||
// Newer Tiptap/ProseMirror
|
||||
if (view && typeof view.serializeForClipboard === 'function') {
|
||||
return view.serializeForClipboard(slice);
|
||||
}
|
||||
|
||||
// Older version fallback
|
||||
const proseMirrorView = getPmView();
|
||||
// Older version fallback
|
||||
const proseMirrorView = getPmView();
|
||||
|
||||
if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === 'function') {
|
||||
return proseMirrorView.__serializeForClipboard(view, slice);
|
||||
}
|
||||
if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === 'function') {
|
||||
return proseMirrorView.__serializeForClipboard(view, slice);
|
||||
}
|
||||
|
||||
throw new Error('No supported clipboard serialization method found.');
|
||||
throw new Error('No supported clipboard serialization method found.');
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { Node } from '@tiptap/core';
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean;
|
||||
HTMLAttributes: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
allowFullscreen: boolean;
|
||||
HTMLAttributes: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
/**
|
||||
* Add an iframe with src
|
||||
*/
|
||||
setIframe: (options: { src: string }) => ReturnType;
|
||||
removeIframe: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
/**
|
||||
* Add an iframe with src
|
||||
*/
|
||||
setIframe: (options: { src: string }) => ReturnType;
|
||||
removeIframe: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper'
|
||||
}
|
||||
};
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
frameborder: {
|
||||
default: 0
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen
|
||||
}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
frameborder: {
|
||||
default: 0
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe'
|
||||
}
|
||||
];
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe'
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]];
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create(options);
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create(options);
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
}
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
removeIframe:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
}
|
||||
return true;
|
||||
},
|
||||
removeIframe:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,54 +3,54 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface IFramePlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframePlaceholder: {
|
||||
/**
|
||||
* Inserts a IFrame placeholder
|
||||
*/
|
||||
insertIFramePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
iframePlaceholder: {
|
||||
/**
|
||||
* Inserts a IFrame placeholder
|
||||
*/
|
||||
insertIFramePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const IFramePlaceholder = (content: Component<NodeViewProps>) =>
|
||||
Node.create<IFramePlaceholderOptions>({
|
||||
name: 'iframe-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
Node.create<IFramePlaceholderOptions>({
|
||||
name: 'iframe-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertIFramePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'iframe-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertIFramePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'iframe-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,33 +4,33 @@ import type { Component } from 'svelte';
|
||||
import type { NodeViewProps, Node } from '@tiptap/core';
|
||||
|
||||
export const ImageExtended = (component: Component<NodeViewProps>): Node<ImageOptions, unknown> => {
|
||||
return Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
default: null
|
||||
},
|
||||
align: {
|
||||
default: 'left'
|
||||
}
|
||||
};
|
||||
},
|
||||
addNodeView: () => {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
}
|
||||
}).configure({
|
||||
allowBase64: true
|
||||
});
|
||||
return Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
default: null
|
||||
},
|
||||
align: {
|
||||
default: 'left'
|
||||
}
|
||||
};
|
||||
},
|
||||
addNodeView: () => {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
}
|
||||
}).configure({
|
||||
allowBase64: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,62 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface ImagePlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
imagePlaceholder: {
|
||||
/**
|
||||
* Inserts an image placeholder
|
||||
*/
|
||||
insertImagePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
imagePlaceholder: {
|
||||
/**
|
||||
* Inserts an image placeholder
|
||||
*/
|
||||
insertImagePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ImagePlaceholder = (
|
||||
component: Component<NodeViewProps>
|
||||
): Node<ImagePlaceholderOptions> =>
|
||||
Node.create<ImagePlaceholderOptions>({
|
||||
name: 'image-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
export const ImagePlaceholder = (component: Component<NodeViewProps>): Node<ImagePlaceholderOptions> =>
|
||||
Node.create<ImagePlaceholderOptions>({
|
||||
name: 'image-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertImagePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'image-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertImagePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'image-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TableRow as TiptapTableRow } from '@tiptap/extension-table';
|
||||
|
||||
export const TableRow = TiptapTableRow.extend({
|
||||
allowGapCursor: false,
|
||||
content: '(tableCell | tableHeader)*'
|
||||
allowGapCursor: false,
|
||||
content: '(tableCell | tableHeader)*'
|
||||
});
|
||||
|
||||
export default TableRow;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Table as TiptapTable } from '@tiptap/extension-table';
|
||||
|
||||
export const Table = TiptapTable.configure({
|
||||
resizable: true,
|
||||
lastColumnResizable: true,
|
||||
allowTableNodeSelection: true
|
||||
resizable: true,
|
||||
lastColumnResizable: true,
|
||||
allowTableNodeSelection: true
|
||||
});
|
||||
|
||||
export default Table;
|
||||
|
||||
@@ -2,146 +2,137 @@ import { Node, nodeInputRule } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
export interface VideoOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
/**
|
||||
* Set a video node
|
||||
*/
|
||||
setVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a video
|
||||
*/
|
||||
toggleVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a video
|
||||
*/
|
||||
removeVideo: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
/**
|
||||
* Set a video node
|
||||
*/
|
||||
setVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a video
|
||||
*/
|
||||
toggleVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a video
|
||||
*/
|
||||
removeVideo: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
||||
|
||||
export const Video = Node.create<VideoOptions>({
|
||||
name: 'video',
|
||||
group: 'block',
|
||||
content: 'inline*',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'video',
|
||||
getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
name: 'video',
|
||||
group: 'block',
|
||||
content: 'inline*',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'video',
|
||||
getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'video',
|
||||
{ controls: 'true', style: 'width: fit-content;', ...HTMLAttributes },
|
||||
['source', HTMLAttributes]
|
||||
];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(
|
||||
`<video controls="true" autoplay="false" style="width: fit-content" src="${src}" />`
|
||||
),
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['video', { controls: 'true', style: 'width: fit-content;', ...HTMLAttributes }, ['source', HTMLAttributes]];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(`<video controls="true" autoplay="false" style="width: fit-content" src="${src}" />`),
|
||||
|
||||
toggleVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: VIDEO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
toggleVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: VIDEO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('videoDropPlugin'),
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('videoDropPlugin'),
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles =
|
||||
event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
|
||||
if (!hasFiles) return false;
|
||||
if (!hasFiles) return false;
|
||||
|
||||
const videos = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
/video/i.test(file.type)
|
||||
);
|
||||
const videos = Array.from(event.dataTransfer.files).filter((file) => /video/i.test(file.type));
|
||||
|
||||
if (videos.length === 0) return false;
|
||||
if (videos.length === 0) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
|
||||
videos.forEach((video) => {
|
||||
const reader = new FileReader();
|
||||
videos.forEach((video) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.video.create({ src: readerEvent.target?.result });
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.video.create({ src: readerEvent.target?.result });
|
||||
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(video);
|
||||
});
|
||||
reader.readAsDataURL(video);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,60 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface VideoPlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
videoPlaceholder: {
|
||||
/**
|
||||
* Inserts a video placeholder
|
||||
*/
|
||||
insertVideoPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
videoPlaceholder: {
|
||||
/**
|
||||
* Inserts a video placeholder
|
||||
*/
|
||||
insertVideoPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const VideoPlaceholder = (content: Component<NodeViewProps>) =>
|
||||
Node.create<VideoPlaceholderOptions>({
|
||||
name: 'video-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
Node.create<VideoPlaceholderOptions>({
|
||||
name: 'video-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertVideoPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'video-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertVideoPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'video-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* One Dark and Light Theme for Highlight.js using Pure CSS */
|
||||
/* Light Theme (Default) */
|
||||
.tiptap pre code {
|
||||
color: #383a42;
|
||||
color: #383a42;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
font-style: italic;
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
color: #a0a1a7;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
@@ -20,7 +20,7 @@
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #e45649;
|
||||
color: #e45649;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
@@ -31,12 +31,12 @@
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #986801;
|
||||
color: #986801;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
color: #c18401;
|
||||
color: #c18401;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
@@ -44,133 +44,133 @@
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #50a14f;
|
||||
color: #50a14f;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #4078f2;
|
||||
color: #4078f2;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #a626a4;
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
/* Cyan */
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
color: #0184bc;
|
||||
font-style: italic;
|
||||
color: #0184bc;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
color: #a626a4;
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-subst {
|
||||
color: #383a42;
|
||||
color: #383a42;
|
||||
}
|
||||
|
||||
/* Line highlights */
|
||||
.hljs-addition {
|
||||
background-color: #e6ffed;
|
||||
background-color: #e6ffed;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background-color: #ffeef0;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
/* Dark Theme (All dark styles consolidated in one media query) */
|
||||
html.dark {
|
||||
.tiptap pre code {
|
||||
color: #abb2bf;
|
||||
}
|
||||
.tiptap pre code {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6370;
|
||||
}
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6370;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #e06c75;
|
||||
}
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #d19a66;
|
||||
}
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #d19a66;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
color: #e5c07b;
|
||||
}
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
color: #e5c07b;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #98c379;
|
||||
}
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #98c379;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #61afef;
|
||||
}
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #61afef;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #c678dd;
|
||||
}
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
/* Cyan */
|
||||
.hljs-emphasis {
|
||||
color: #56b6c2;
|
||||
}
|
||||
/* Cyan */
|
||||
.hljs-emphasis {
|
||||
color: #56b6c2;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
/* Base styles */
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-subst {
|
||||
color: #abb2bf;
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-subst {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
/* Line highlights */
|
||||
.hljs-addition {
|
||||
background-color: #283428;
|
||||
}
|
||||
/* Line highlights */
|
||||
.hljs-addition {
|
||||
background-color: #283428;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background-color: #342828;
|
||||
}
|
||||
.hljs-deletion {
|
||||
background-color: #342828;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,74 +2,74 @@ import { flushSync, mount, unmount } from 'svelte';
|
||||
import type { Editor, NodeViewProps } from '@tiptap/core';
|
||||
|
||||
interface RendererOptions<P extends Record<string, unknown>> {
|
||||
editor: Editor;
|
||||
props: P;
|
||||
editor: Editor;
|
||||
props: P;
|
||||
}
|
||||
|
||||
type App = ReturnType<typeof mount>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
class SvelteRenderer<R = unknown, P extends Record<string, any> = object> {
|
||||
id: string;
|
||||
component: App;
|
||||
editor: Editor;
|
||||
props: P;
|
||||
element: HTMLElement;
|
||||
ref: R | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mnt: Record<any, any> | null = null;
|
||||
id: string;
|
||||
component: App;
|
||||
editor: Editor;
|
||||
props: P;
|
||||
element: HTMLElement;
|
||||
ref: R | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mnt: Record<any, any> | null = null;
|
||||
|
||||
constructor(component: App, { props, editor }: RendererOptions<P>) {
|
||||
this.id = Math.floor(Math.random() * 0xffffffff).toString();
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
this.editor = editor;
|
||||
constructor(component: App, { props, editor }: RendererOptions<P>) {
|
||||
this.id = Math.floor(Math.random() * 0xffffffff).toString();
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
this.editor = editor;
|
||||
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add('svelte-renderer');
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add('svelte-renderer');
|
||||
|
||||
if (this.editor.isInitialized) {
|
||||
// On first render, we need to flush the render synchronously
|
||||
// Renders afterwards can be async, but this fixes a cursor positioning issue
|
||||
flushSync(() => {
|
||||
this.render();
|
||||
});
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
if (this.editor.isInitialized) {
|
||||
// On first render, we need to flush the render synchronously
|
||||
// Renders afterwards can be async, but this fixes a cursor positioning issue
|
||||
flushSync(() => {
|
||||
this.render();
|
||||
});
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
render(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.mnt = mount(this.component as any, {
|
||||
target: this.element,
|
||||
props: {
|
||||
props: this.props
|
||||
}
|
||||
});
|
||||
}
|
||||
render(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.mnt = mount(this.component as any, {
|
||||
target: this.element,
|
||||
props: {
|
||||
props: this.props
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateProps(props: Partial<NodeViewProps>): void {
|
||||
Object.assign(this.props, props);
|
||||
this.destroy();
|
||||
this.render();
|
||||
}
|
||||
updateProps(props: Partial<NodeViewProps>): void {
|
||||
Object.assign(this.props, props);
|
||||
this.destroy();
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateAttributes(attributes: Record<string, string>): void {
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
this.element.setAttribute(key, attributes[key]);
|
||||
});
|
||||
this.destroy();
|
||||
this.render();
|
||||
}
|
||||
updateAttributes(attributes: Record<string, string>): void {
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
this.element.setAttribute(key, attributes[key]);
|
||||
});
|
||||
this.destroy();
|
||||
this.render();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.mnt) {
|
||||
unmount(this.mnt);
|
||||
} else {
|
||||
unmount(this.component);
|
||||
}
|
||||
}
|
||||
destroy(): void {
|
||||
if (this.mnt) {
|
||||
unmount(this.mnt);
|
||||
} else {
|
||||
unmount(this.component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SvelteRenderer;
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
</script>
|
||||
|
||||
<MediaExtended bind:mediaRef {...rest}>
|
||||
{@const node = rest.node}
|
||||
<audio
|
||||
bind:this={mediaRef}
|
||||
src={node.attrs.src}
|
||||
controls
|
||||
title={node.attrs.title}
|
||||
class="m-0 w-full"
|
||||
>
|
||||
</audio>
|
||||
{@const node = rest.node}
|
||||
<audio bind:this={mediaRef} src={node.attrs.src} controls title={node.attrs.title} class="ui:m-0 ui:w-full"> </audio>
|
||||
</MediaExtended>
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
>{defaultLanguage}
|
||||
<ChevronDown class="!size-2" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="h-96 w-40 overflow-auto" contenteditable="false">
|
||||
<DropdownMenu.Content class="ui:h-96 ui:w-40 ui:overflow-auto" contenteditable="false">
|
||||
{#each languages as language (language)}
|
||||
<DropdownMenu.Item
|
||||
contenteditable="false"
|
||||
data-current={defaultLanguage === language}
|
||||
class="data-[current=true]:bg-muted"
|
||||
class="ui:data-[current=true]:bg-muted"
|
||||
textValue={language}
|
||||
onclick={() => {
|
||||
defaultLanguage = language;
|
||||
@@ -53,17 +53,17 @@
|
||||
>
|
||||
<span>{language}</span>
|
||||
{#if defaultLanguage === language}
|
||||
<Check class="ml-auto" />
|
||||
<Check class="ui:ml-auto" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Button variant="ghost" class="text-muted-foreground size-4 p-0" onclick={copyCode}>
|
||||
<Button variant="ghost" class="ui:text-muted-foreground ui:size-4 ui:p-0" onclick={copyCode}>
|
||||
{#if isCopying}
|
||||
<Check class="size-3 text-green-500" />
|
||||
<Check class="ui:size-3 ui:text-green-500" />
|
||||
{:else}
|
||||
<Copy class="size-3" />
|
||||
<Copy class="ui:size-3" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>{tooltip}</span>
|
||||
<span class="text-muted-foreground">{shortCut}</span>
|
||||
<span class="ui:text-muted-foreground">{shortCut}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
</script>
|
||||
|
||||
<MediaExtended bind:mediaRef {...rest}>
|
||||
{@const node = rest.node}
|
||||
<iframe bind:this={mediaRef} class="m-0 w-full" {...node.attrs}> </iframe>
|
||||
{@const node = rest.node}
|
||||
<iframe bind:this={mediaRef} class="ui:m-0 ui:w-full" {...node.attrs}> </iframe>
|
||||
</MediaExtended>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
</script>
|
||||
|
||||
<MediaExtended bind:mediaRef {...rest}>
|
||||
{@const node = rest.node}
|
||||
<img
|
||||
bind:this={mediaRef}
|
||||
src={node.attrs.src}
|
||||
alt={node.attrs.alt}
|
||||
title={node.attrs.title}
|
||||
class="m-0 object-cover"
|
||||
/>
|
||||
{@const node = rest.node}
|
||||
<img
|
||||
bind:this={mediaRef}
|
||||
src={node.attrs.src}
|
||||
alt={node.attrs.alt}
|
||||
title={node.attrs.title}
|
||||
class="ui:m-0 ui:object-cover"
|
||||
/>
|
||||
</MediaExtended>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<NodeViewWrapper
|
||||
id="resizable-container-media"
|
||||
class={cn(
|
||||
'relative my-2 flex flex-col rounded-md border-2 border-transparent',
|
||||
'ui:relative ui:my-2 ui:flex ui:flex-col ui:rounded-md ui:border-2 ui:border-transparent',
|
||||
selected ? 'border-muted-foreground' : '',
|
||||
node.attrs.align === 'left' && 'left-0 -translate-x-0',
|
||||
node.attrs.align === 'center' && 'left-1/2 -translate-x-1/2',
|
||||
@@ -149,7 +149,7 @@
|
||||
)}
|
||||
style={`width: ${node.attrs.width}`}
|
||||
>
|
||||
<div class={cn('group relative flex flex-col rounded-md', resizing && '')}>
|
||||
<div class={cn('ui:relative ui:flex ui:flex-col ui:rounded-md group', resizing && '')}>
|
||||
{@render children()}
|
||||
|
||||
{#if editor.isEditable}
|
||||
@@ -157,7 +157,7 @@
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-start p-2"
|
||||
class="ui:absolute ui:inset-y-0 ui:z-20 ui:flex ui:w-[25px] ui:cursor-col-resize ui:items-center ui:justify-start ui:p-2"
|
||||
style="left: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'left');
|
||||
@@ -167,7 +167,7 @@
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="bg-muted z-20 h-[70px] w-1 rounded-xl border opacity-0 transition-all group-hover:opacity-100"
|
||||
class="ui:bg-muted ui:z-20 ui:h-[70px] ui:w-1 ui:rounded-xl ui:border ui:opacity-0 ui:transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-end p-2"
|
||||
class="ui:absolute ui:inset-y-0 ui:z-20 ui:flex ui:w-[25px] ui:cursor-col-resize ui:items-center ui:justify-end ui:p-2"
|
||||
style="right: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'right');
|
||||
@@ -185,48 +185,48 @@
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="bg-muted z-20 h-[70px] w-1 rounded-xl border opacity-0 transition-all group-hover:opacity-100"
|
||||
class="ui:bg-muted ui:z-20 ui:h-[70px] ui:w-1 ui:rounded-xl ui:border ui:opacity-0 ui:transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class={cn(
|
||||
'bg-background/50 absolute -top-2 left-[calc(50%-3rem)] flex items-center gap-1 rounded border p-1 opacity-0 backdrop-blur-sm transition-opacity',
|
||||
'ui:bg-background/50 ui:absolute ui:-top-2 ui:left-[calc(50%-3rem)] ui:flex ui:items-center ui:gap-1 ui:rounded ui:border ui:p-1 ui:opacity-0 ui:backdrop-blur-sm ui:transition-opacity',
|
||||
!resizing && 'group-hover:opacity-100',
|
||||
openedMore && 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'left' && 'bg-muted')}
|
||||
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'left' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'left' })}
|
||||
title="Align Left"
|
||||
>
|
||||
<AlignLeft class="size-4" />
|
||||
<AlignLeft class="ui:size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'center' && 'bg-muted')}
|
||||
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'center' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'center' })}
|
||||
title="Align Center"
|
||||
>
|
||||
<AlignCenter class="size-4" />
|
||||
<AlignCenter class="ui:size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'right' && 'bg-muted')}
|
||||
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'right' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'right' })}
|
||||
title="Align Right"
|
||||
>
|
||||
<AlignRight class="size-4" />
|
||||
<AlignRight class="ui:size-4" />
|
||||
</Button>
|
||||
<DropdownMenu.Root bind:open={openedMore} onOpenChange={(value) => (openedMore = value)}>
|
||||
<DropdownMenu.Trigger
|
||||
class={buttonVariants({ variant: 'ghost', class: 'size-6 p-0' })}
|
||||
title="More Options"
|
||||
>
|
||||
<EllipsisVertical class="size-4" />
|
||||
<EllipsisVertical class="ui:size-4" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="start" alignOffset={-90} class="mt-1 overflow-auto text-sm">
|
||||
<DropdownMenu.Content align="start" alignOffset={-90} class="ui:mt-1 ui:overflow-auto ui:text-sm">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (node.attrs.title === null || node.attrs.title.trim() === '')
|
||||
@@ -235,14 +235,14 @@
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Captions class="mr-1 size-4" /> Caption
|
||||
<Captions class="ui:mr-1 ui:size-4" /> Caption
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
duplicateContent(editor, node);
|
||||
}}
|
||||
>
|
||||
<CopyIcon class="mr-1 size-4" /> Duplicate
|
||||
<CopyIcon class="ui:mr-1 ui:size-4" /> Duplicate
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
@@ -251,15 +251,15 @@
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Fullscreen class="mr-1 size-4" /> Full Screen
|
||||
<Fullscreen class="ui:mr-1 ui:size-4" /> Full Screen
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
deleteNode();
|
||||
}}
|
||||
class="text-destructive"
|
||||
class="ui:text-destructive"
|
||||
>
|
||||
<Trash class="mr-1 size-4" /> Delete
|
||||
<Trash class="ui:mr-1 ui:size-4" /> Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
@@ -96,10 +96,10 @@
|
||||
{#if items.length}
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
class="bg-popover flex max-h-80 flex-col gap-1 overflow-auto scroll-smooth rounded border"
|
||||
class="ui:bg-popover ui:flex ui:max-h-80 ui:flex-col ui:gap-1 ui:overflow-auto ui:scroll-smooth ui:rounded ui:border"
|
||||
>
|
||||
{#each items as grp, groupIndex (groupIndex)}
|
||||
<span class="text-muted-foreground p-2 text-xs">{grp.title}</span>
|
||||
<span class="ui:text-muted-foreground ui:p-2 ui:text-xs">{grp.title}</span>
|
||||
|
||||
{#each grp.commands as command, commandIndex (commandIndex)}
|
||||
{@const Icon = command.icon}
|
||||
@@ -107,7 +107,7 @@
|
||||
<Button
|
||||
id={`${groupIndex}-${commandIndex}`}
|
||||
variant="ghost"
|
||||
class={cn('h-8 w-full justify-start gap-2 rounded-sm', isActive && 'bg-muted')}
|
||||
class={cn('ui:h-8 ui:w-full ui:justify-start ui:gap-2 ui:rounded-sm', isActive && 'bg-muted')}
|
||||
onclick={() => selectItem(groupIndex, commandIndex)}
|
||||
>
|
||||
<Icon />
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import MediaExtended from './MediaExtended.svelte';
|
||||
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
const { ...rest }: NodeViewProps = $props();
|
||||
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
let mediaRef = $state<HTMLElement>();
|
||||
</script>
|
||||
|
||||
<MediaExtended bind:mediaRef {...rest}>
|
||||
{@const node = rest.node}
|
||||
<video
|
||||
bind:this={mediaRef}
|
||||
src={node.attrs.src}
|
||||
controls
|
||||
title={node.attrs.title}
|
||||
class="m-0 object-cover"
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{@const node = rest.node}
|
||||
<video bind:this={mediaRef} src={node.attrs.src} controls title={node.attrs.title} class="ui:m-0 ui:object-cover">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
</MediaExtended>
|
||||
|
||||
@@ -33,12 +33,12 @@
|
||||
class={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'icon',
|
||||
class: cn('gap-0')
|
||||
class: cn('ui:gap-0')
|
||||
})}
|
||||
class:bg-muted={isActive}
|
||||
>
|
||||
<AlignMentIcon />
|
||||
<ChevronDown class="text-muted-foreground !size-2" />
|
||||
<ChevronDown class="ui:text-muted-foreground !size-2" />
|
||||
</div>
|
||||
</EdraToolTip>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<EdraToolTip tooltip="Font Size">
|
||||
<Button variant="ghost" class={cn('gap-0.5 !px-2', className)}>
|
||||
<Button variant="ghost" class={cn('ui:gap-0.5 !px-2', className)}>
|
||||
<span>{currentLabel}</span>
|
||||
<ChevronDown class="text-muted-foreground !size-2" />
|
||||
<ChevronDown class="ui:text-muted-foreground !size-2" />
|
||||
</Button>
|
||||
</EdraToolTip>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="h-fit w-fit" portalProps={{ disabled: true, to: undefined }}>
|
||||
<DropdownMenu.Content class="ui:h-fit ui:w-fit" portalProps={{ disabled: true, to: undefined }}>
|
||||
{#each FONT_SIZE as fontSize (fontSize)}
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
class={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'icon',
|
||||
class: cn('gap-0')
|
||||
class: cn('ui:gap-0')
|
||||
})}
|
||||
class:bg-muted={isActive}
|
||||
>
|
||||
<HeadingIcon />
|
||||
<ChevronDown class="text-muted-foreground !size-2" />
|
||||
<ChevronDown class="ui:text-muted-foreground !size-2" />
|
||||
</div>
|
||||
</EdraToolTip>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
@@ -36,18 +36,18 @@
|
||||
class={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'icon',
|
||||
class: cn('gap-0.5', className)
|
||||
class: cn('ui:gap-0.5', className)
|
||||
})}
|
||||
style={`color: ${currentColor}; background-color: ${currentHighlight}50;`}
|
||||
>
|
||||
<span>A</span>
|
||||
<ChevronDown class="text-muted-foreground !size-2" />
|
||||
<ChevronDown class="ui:text-muted-foreground !size-2" />
|
||||
</div>
|
||||
</EdraToolTip>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="size-fit shadow-lg" portalProps={{ disabled: true, to: undefined }}>
|
||||
<div class="text-muted-foreground my-2 text-xs">Text Colors</div>
|
||||
<div class="grid grid-cols-5 gap-2">
|
||||
<Popover.Content class="ui:size-fit ui:shadow-lg" portalProps={{ disabled: true, to: undefined }}>
|
||||
<div class="ui:text-muted-foreground ui:my-2 ui:text-xs">Text Colors</div>
|
||||
<div class="ui:grid ui:gap-2 grid-cols-5">
|
||||
{#each colors as color (color)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -72,8 +72,8 @@
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text-muted-foreground my-2 text-xs">Highlight Colors</div>
|
||||
<div class="grid grid-cols-5 gap-2">
|
||||
<div class="ui:text-muted-foreground ui:my-2 ui:text-xs">Highlight Colors</div>
|
||||
<div class="ui:grid ui:gap-2 grid-cols-5">
|
||||
{#each colors as color (color)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -84,26 +84,29 @@
|
||||
>
|
||||
<Popover.Trigger>
|
||||
<EdraToolTip tooltip="Search and Replace">
|
||||
<Button variant="ghost" size="icon" class="gap-0.5">
|
||||
<Button variant="ghost" size="icon" class="ui:gap-0.5">
|
||||
<Search />
|
||||
<ChevronDown class="text-muted-foreground !size-2" />
|
||||
<ChevronDown class="ui:text-muted-foreground !size-2" />
|
||||
</Button>
|
||||
</EdraToolTip>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="flex w-fit items-center gap-1 p-2" portalProps={{ disabled: true, to: undefined }}>
|
||||
<Popover.Content
|
||||
class="ui:flex ui:w-fit ui:items-center ui:gap-1 ui:p-2"
|
||||
portalProps={{ disabled: true, to: undefined }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('transition-transform', showMore && 'bg-muted rotate-90')}
|
||||
class={cn('ui:transition-transform', showMore && 'bg-muted rotate-90')}
|
||||
onclick={() => (showMore = !showMore)}
|
||||
title="Show More"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<div class="flex size-full flex-col gap-1">
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<Input placeholder="Search..." bind:value={searchText} oninput={() => updateSearchTerm()} class="w-40" />
|
||||
<span class="text-muted-foreground text-sm">{searchCount > 0 ? searchIndex + 1 : 0}/{searchCount} </span>
|
||||
<div class="ui:flex ui:size-full ui:flex-col ui:gap-1">
|
||||
<div class="ui:flex ui:w-full ui:items-center ui:gap-1">
|
||||
<Input placeholder="Search..." bind:value={searchText} oninput={() => updateSearchTerm()} class="ui:w-40" />
|
||||
<span class="ui:text-muted-foreground ui:text-sm">{searchCount > 0 ? searchIndex + 1 : 0}/{searchCount} </span>
|
||||
<EdraToolTip tooltip="Case Sensitive">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -129,8 +132,8 @@
|
||||
</EdraToolTip>
|
||||
</div>
|
||||
{#if showMore}
|
||||
<div transition:slide class="flex w-full items-center gap-1">
|
||||
<Input placeholder="Replace..." bind:value={replaceText} oninput={() => updateSearchTerm()} class="w-40" />
|
||||
<div transition:slide class="ui:flex ui:w-full ui:items-center ui:gap-1">
|
||||
<Input placeholder="Replace..." bind:value={replaceText} oninput={() => updateSearchTerm()} class="ui:w-40" />
|
||||
<EdraToolTip tooltip="Replace">
|
||||
<Button variant="ghost" size="icon" onclick={replace}>
|
||||
<Replace />
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</script>
|
||||
|
||||
<div class="drag-handle">
|
||||
<Button variant="ghost" class="!size-6 rounded-sm p-0" onclick={() => (open = true)}>
|
||||
<Button variant="ghost" class="ui:rounded-sm ui:p-0 !size-6" onclick={() => (open = true)}>
|
||||
<GripVertical />
|
||||
</Button>
|
||||
<DropdownMenu.Root bind:open>
|
||||
@@ -95,7 +95,7 @@
|
||||
Copy to clipboard
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete}>
|
||||
<Delete class="text-destructive" />
|
||||
<Delete class="ui:text-destructive" />
|
||||
Delete Node
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -109,5 +109,5 @@
|
||||
focusEditor(editor, event);
|
||||
}
|
||||
}}
|
||||
class={cn('edra-editor h-full w-full cursor-auto px-4 *:outline-none', className)}
|
||||
class={cn('edra-editor ui:h-full ui:w-full ui:cursor-auto ui:px-4 *:outline-none', className)}
|
||||
></div>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
options={{
|
||||
strategy: 'fixed'
|
||||
}}
|
||||
class="bg-popover flex h-fit w-fit items-center gap-1 rounded border p-1 shadow-lg"
|
||||
class="ui:bg-popover ui:flex ui:h-fit ui:w-fit ui:items-center ui:gap-1 ui:rounded ui:border ui:p-1 ui:shadow-lg"
|
||||
>
|
||||
<Button variant="link" href={link} class="max-w-80 p-1" target="_blank">
|
||||
<span class="w-full overflow-hidden text-ellipsis">
|
||||
<Button variant="link" href={link} class="ui:max-w-80 ui:p-1" target="_blank">
|
||||
<span class="ui:w-full ui:overflow-hidden ui:text-ellipsis">
|
||||
{link}
|
||||
</span>
|
||||
</Button>
|
||||
@@ -36,7 +36,7 @@
|
||||
variant="ghost"
|
||||
title="Copy Link"
|
||||
size="icon"
|
||||
class="z-50"
|
||||
class="ui:z-50"
|
||||
onclick={() => {
|
||||
navigator.clipboard.writeText(link);
|
||||
}}
|
||||
|
||||
@@ -90,7 +90,10 @@
|
||||
{editor}
|
||||
pluginKey="link-bubble-menu"
|
||||
{shouldShow}
|
||||
class={cn('edra-bubble-menu bg-popover flex h-fit w-fit items-center rounded-lg p-0.5', className)}
|
||||
class={cn(
|
||||
'edra-bubble-menu ui:bg-popover ui:flex ui:h-fit ui:w-fit ui:items-center ui:rounded-lg ui:p-0.5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
from: props.from
|
||||
});
|
||||
}}
|
||||
class="bg-background flex h-fit w-fit items-center gap-1 rounded border shadow-lg"
|
||||
class="ui:bg-background ui:flex ui:h-fit ui:w-fit ui:items-center ui:gap-1 ui:rounded ui:border ui:shadow-lg"
|
||||
>
|
||||
<EdraToolTip tooltip="Add Column After">
|
||||
<Button variant="ghost" size="icon" onclick={() => editor.chain().focus().addColumnAfter().run()}>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
from: props.from
|
||||
});
|
||||
}}
|
||||
class="bg-background flex h-fit w-fit items-center gap-1 rounded border shadow-lg"
|
||||
class="ui:bg-background ui:flex ui:h-fit ui:w-fit ui:items-center ui:gap-1 ui:rounded ui:border ui:shadow-lg"
|
||||
>
|
||||
<EdraToolTip tooltip="Add Row After">
|
||||
<Button variant="ghost" size="icon" onclick={() => editor.chain().focus().addRowAfter().run()}>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
:root {
|
||||
--border-color: var(--muted);
|
||||
--border-color-hover: var(--muted-foreground);
|
||||
--blockquote-border: var(--border);
|
||||
--blockquote-color: var(--muted-foreground);
|
||||
--codeblock-bg: var(--muted);
|
||||
--code-bg: var(--muted);
|
||||
--table-border: var(--border);
|
||||
--table-bg-selected: color-mix(in oklch, var(--muted) 60%, transparent);
|
||||
--table-bg-hover: color-mix(in oklch, var(--muted) 30%, transparent);
|
||||
--task-completed-color: var(--muted-foreground);
|
||||
--highlight-color: rgba(0, 255, 0, 0.1);
|
||||
--highlight-border: green;
|
||||
--search-result-bg: yellow;
|
||||
--search-result-current-bg: orange;
|
||||
--border-color: var(--muted);
|
||||
--border-color-hover: var(--muted-foreground);
|
||||
--blockquote-border: var(--border);
|
||||
--blockquote-color: var(--muted-foreground);
|
||||
--codeblock-bg: var(--muted);
|
||||
--code-bg: var(--muted);
|
||||
--table-border: var(--border);
|
||||
--table-bg-selected: color-mix(in oklch, var(--muted) 60%, transparent);
|
||||
--table-bg-hover: color-mix(in oklch, var(--muted) 30%, transparent);
|
||||
--task-completed-color: var(--muted-foreground);
|
||||
--highlight-color: rgba(0, 255, 0, 0.1);
|
||||
--highlight-border: green;
|
||||
--search-result-bg: yellow;
|
||||
--search-result-current-bg: orange;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--codeblock-bg: color-mix(in oklch, var(--muted) 30%, transparent);
|
||||
--codeblock-bg: color-mix(in oklch, var(--muted) 30%, transparent);
|
||||
}
|
||||
|
||||
.tiptap a {
|
||||
color: oklch(62.3% 0.214 259.815);
|
||||
text-decoration: underline;
|
||||
color: oklch(62.3% 0.214 259.815);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user