import type { Content, Editor } from '@tiptap/core' import { Decoration, DecorationSet } from '@tiptap/pm/view' import type { EditorState, Transaction } from '@tiptap/pm/state' import type { EditorView } from '@tiptap/pm/view' import { browser } from '$app/environment' import type { Snippet } from 'svelte' export interface ShouldShowProps { editor: Editor element: HTMLElement view: EditorView state: EditorState oldState?: EditorState from: number to: number } export const findColors = (doc: Node) => { const hexColor = /(#[0-9a-f]{3,6})\b/gi const decorations: Decoration[] = [] doc.descendants((node, position) => { if (!node.text) { return } Array.from(node.text.matchAll(hexColor)).forEach((match) => { const color = match[0] const index = match.index || 0 const from = position + index const to = from + color.length const decoration = Decoration.inline(from, to, { class: 'color', style: `--color: ${color}` }) decorations.push(decoration) }) }) return DecorationSet.create(doc, decorations) } /** * Check if the current browser is mac or not */ export const isMac = browser ? navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('Mac OS X') : false /** * Dupilcate content at the current selection * @param editor Editor instance * @param node Node to be duplicated */ export const duplicateContent = (editor: Editor, node: Node) => { const { view } = editor const { state } = view const { selection } = state editor .chain() .insertContentAt(selection.to, node.toJSON(), { updateSelection: true }) .focus(selection.to) .run() } /** * Function to handle paste event of an image * @param editor Editor - editor instance * @param maxSize number - max size of the image to be pasted in MB, default is 2MB */ export function getHandlePaste(editor: Editor, maxSize: number = 2) { return (view: EditorView, event: ClipboardEvent) => { const item = event.clipboardData?.items[0] if (item?.type.indexOf('image') !== 0) { return } const file = item.getAsFile() if (file === null || file.size === undefined) return const filesize = (file?.size / 1024 / 1024).toFixed(4) if (filesize && Number(filesize) > maxSize) { window.alert(`too large image! filesize: ${filesize} mb`) return } const reader = new FileReader() reader.readAsDataURL(file) reader.onload = (e) => { if (e.target?.result) { editor.commands.insertContent({ type: 'image', attrs: { src: e.target.result as string, alt: '', mediaId: null // No media ID for pasted images } }) } } } } /** * Sets focus on the editor and moves the cursor to the clicked text position, * defaulting to the end of the document if the click is outside any text. * * @param editor - Editor instance * @param event - Optional MouseEvent or KeyboardEvent triggering the focus */ export function focusEditor(editor: Editor | undefined, event?: MouseEvent | KeyboardEvent) { if (!editor) return // Check if there is a text selection already (i.e. a non-empty selection) const selection = window.getSelection() if (selection && selection.toString().length > 0) { // Focus the editor without modifying selection editor.chain().focus().run() return } if (event instanceof MouseEvent) { const { clientX, clientY } = event const pos = editor.view.posAtCoords({ left: clientX, top: clientY })?.pos if (pos == null) { // If not a valid position, move cursor to the end of the document const endPos = editor.state.doc.content.size editor.chain().focus().setTextSelection(endPos).run() } else { editor.chain().focus().setTextSelection(pos).run() } } else { editor.chain().focus().run() } } /** * Props for Edra's editor component */ export interface EdraProps { class?: string content?: Content editable?: boolean limit?: number editor?: Editor showSlashCommands?: boolean showLinkBubbleMenu?: boolean showTableBubbleMenu?: boolean /** * Callback function to be called when the content is updated * @param content */ onUpdate?: (props: { editor: Editor; transaction: Transaction }) => void children?: Snippet<[]> }