mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-19 23:11:30 +02:00
feat: feedback (#156)
* minimal tiptap * wip * img edit block * wip * fix
This commit is contained in:
15
web/components/minimal-tiptap/hooks/use-image-load.ts
Normal file
15
web/components/minimal-tiptap/hooks/use-image-load.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const useImageLoad = (src: string) => {
|
||||
const [imgSize, setImgSize] = React.useState({ width: 0, height: 0 })
|
||||
|
||||
React.useEffect(() => {
|
||||
const img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
setImgSize({ width: img.width, height: img.height })
|
||||
}
|
||||
}, [src])
|
||||
|
||||
return imgSize
|
||||
}
|
||||
107
web/components/minimal-tiptap/hooks/use-minimal-tiptap.ts
Normal file
107
web/components/minimal-tiptap/hooks/use-minimal-tiptap.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as React from "react"
|
||||
import { StarterKit } from "@tiptap/starter-kit"
|
||||
import type { Content, UseEditorOptions } from "@tiptap/react"
|
||||
import { useEditor } from "@tiptap/react"
|
||||
import type { Editor } from "@tiptap/core"
|
||||
import { Typography } from "@tiptap/extension-typography"
|
||||
import { Placeholder } from "@tiptap/extension-placeholder"
|
||||
import { TextStyle } from "@tiptap/extension-text-style"
|
||||
import {
|
||||
Link,
|
||||
Image,
|
||||
HorizontalRule,
|
||||
CodeBlockLowlight,
|
||||
Selection,
|
||||
Color,
|
||||
UnsetAllMarks,
|
||||
ResetMarksOnEnter
|
||||
} from "../extensions"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { getOutput } from "../utils"
|
||||
import { useThrottle } from "../hooks/use-throttle"
|
||||
|
||||
export interface UseMinimalTiptapEditorProps extends UseEditorOptions {
|
||||
value?: Content
|
||||
output?: "html" | "json" | "text"
|
||||
placeholder?: string
|
||||
editorClassName?: string
|
||||
throttleDelay?: number
|
||||
onUpdate?: (content: Content) => void
|
||||
onBlur?: (content: Content) => void
|
||||
}
|
||||
|
||||
const createExtensions = (placeholder: string) => [
|
||||
StarterKit.configure({
|
||||
horizontalRule: false,
|
||||
codeBlock: false,
|
||||
paragraph: { HTMLAttributes: { class: "text-node" } },
|
||||
heading: { HTMLAttributes: { class: "heading-node" } },
|
||||
blockquote: { HTMLAttributes: { class: "block-node" } },
|
||||
bulletList: { HTMLAttributes: { class: "list-node" } },
|
||||
orderedList: { HTMLAttributes: { class: "list-node" } },
|
||||
code: { HTMLAttributes: { class: "inline", spellcheck: "false" } },
|
||||
dropcursor: { width: 2, class: "ProseMirror-dropcursor border" }
|
||||
}),
|
||||
Link,
|
||||
Image,
|
||||
Color,
|
||||
TextStyle,
|
||||
Selection,
|
||||
Typography,
|
||||
UnsetAllMarks,
|
||||
HorizontalRule,
|
||||
ResetMarksOnEnter,
|
||||
CodeBlockLowlight,
|
||||
Placeholder.configure({ placeholder: () => placeholder })
|
||||
]
|
||||
|
||||
export const useMinimalTiptapEditor = ({
|
||||
value,
|
||||
output = "html",
|
||||
placeholder = "",
|
||||
editorClassName,
|
||||
throttleDelay = 1000,
|
||||
onUpdate,
|
||||
onBlur,
|
||||
...props
|
||||
}: UseMinimalTiptapEditorProps) => {
|
||||
const throttledSetValue = useThrottle((value: Content) => onUpdate?.(value), throttleDelay)
|
||||
|
||||
const handleUpdate = React.useCallback(
|
||||
(editor: Editor) => {
|
||||
throttledSetValue(getOutput(editor, output))
|
||||
},
|
||||
[output, throttledSetValue]
|
||||
)
|
||||
|
||||
const handleCreate = React.useCallback(
|
||||
(editor: Editor) => {
|
||||
if (value && editor.isEmpty) {
|
||||
editor.commands.setContent(value)
|
||||
}
|
||||
},
|
||||
[value]
|
||||
)
|
||||
|
||||
const handleBlur = React.useCallback((editor: Editor) => onBlur?.(getOutput(editor, output)), [output, onBlur])
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: createExtensions(placeholder!),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
autocomplete: "off",
|
||||
autocorrect: "off",
|
||||
autocapitalize: "off",
|
||||
class: cn("focus:outline-none", editorClassName)
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => handleUpdate(editor),
|
||||
onCreate: ({ editor }) => handleCreate(editor),
|
||||
onBlur: ({ editor }) => handleBlur(editor),
|
||||
...props
|
||||
})
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
export default useMinimalTiptapEditor
|
||||
25
web/components/minimal-tiptap/hooks/use-theme.ts
Normal file
25
web/components/minimal-tiptap/hooks/use-theme.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const useTheme = () => {
|
||||
const [isDarkMode, setIsDarkMode] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
setIsDarkMode(darkModeMediaQuery.matches)
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
const newDarkMode = e.matches
|
||||
setIsDarkMode(newDarkMode)
|
||||
}
|
||||
|
||||
darkModeMediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
return () => {
|
||||
darkModeMediaQuery.removeEventListener('change', handleChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isDarkMode
|
||||
}
|
||||
|
||||
export default useTheme
|
||||
34
web/components/minimal-tiptap/hooks/use-throttle.ts
Normal file
34
web/components/minimal-tiptap/hooks/use-throttle.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useRef, useCallback } from 'react'
|
||||
|
||||
export function useThrottle<T extends (...args: any[]) => void>(
|
||||
callback: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
const lastRan = useRef(Date.now())
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
return useCallback(
|
||||
(...args: Parameters<T>) => {
|
||||
const handler = () => {
|
||||
if (Date.now() - lastRan.current >= delay) {
|
||||
callback(...args)
|
||||
lastRan.current = Date.now()
|
||||
} else {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
timeoutRef.current = setTimeout(
|
||||
() => {
|
||||
callback(...args)
|
||||
lastRan.current = Date.now()
|
||||
},
|
||||
delay - (Date.now() - lastRan.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handler()
|
||||
},
|
||||
[callback, delay]
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user