jedmund-svelte/src/lib/components/edra/utils.ts
Justin Edmund 2bbc306762 refactor: restructure routing - albums at /albums/[slug], photos at /photos/[id]
- Move album routes from /photos/[slug] to /albums/[slug]
- Simplify photo permalinks from /photos/p/[id] to /photos/[id]
- Remove album-scoped photo route /photos/[albumSlug]/[photoId]
- Update all component references to use new routes
- Simplify content.ts to always use direct photo permalinks
- Update PhotoItem, MasonryPhotoGrid, ThreeColumnPhotoGrid components
- Update UniverseAlbumCard and admin AlbumForm view links
- Remove album context from photo navigation

Breaking change: URLs have changed
- Albums: /photos/[slug] → /albums/[slug]
- Photos: /photos/p/[id] → /photos/[id]

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-24 10:35:21 +01:00

158 lines
4.1 KiB
TypeScript

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<[]>
}