47cc4a19ea
All checks were successful
Release to Staging / build_and_release (push) Successful in 43s
291 lines
7.4 KiB
TypeScript
291 lines
7.4 KiB
TypeScript
import Link from '@tiptap/extension-link'
|
|
import { Editor, EditorContent, useEditor } from '@tiptap/react'
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
import React, { useEffect } from 'react'
|
|
import '../styles/styles.css'
|
|
import '../styles/tiptap.scss'
|
|
|
|
interface InputFieldProps {
|
|
label: string
|
|
description?: string
|
|
type?: 'text' | 'textarea' | 'richtext'
|
|
placeholder: string
|
|
name: string
|
|
inputMode?: 'url'
|
|
value: string
|
|
error?: string
|
|
onChange: (name: string, value: string) => void
|
|
}
|
|
|
|
export const InputField = React.memo(
|
|
({
|
|
label,
|
|
description,
|
|
type = 'text',
|
|
placeholder,
|
|
name,
|
|
inputMode,
|
|
value,
|
|
error,
|
|
onChange
|
|
}: InputFieldProps) => {
|
|
const handleChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
onChange(name, e.target.value)
|
|
}
|
|
|
|
return (
|
|
<div className='inputLabelWrapperMain'>
|
|
<label className='form-label labelMain'>{label}</label>
|
|
{description && <p className='labelDescriptionMain'>{description}</p>}
|
|
{type === 'textarea' ? (
|
|
<textarea
|
|
className='inputMain'
|
|
placeholder={placeholder}
|
|
name={name}
|
|
value={value}
|
|
onChange={handleChange}
|
|
></textarea>
|
|
) : type === 'richtext' ? (
|
|
<RichTextEditor
|
|
content={value}
|
|
updateContent={(content) => onChange(name, content)}
|
|
/>
|
|
) : (
|
|
<input
|
|
type={type}
|
|
className='inputMain'
|
|
placeholder={placeholder}
|
|
name={name}
|
|
inputMode={inputMode}
|
|
value={value}
|
|
onChange={handleChange}
|
|
/>
|
|
)}
|
|
{error && <InputError message={error} />}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
|
|
type InputErrorProps = {
|
|
message: string
|
|
}
|
|
|
|
export const InputError = ({ message }: InputErrorProps) => {
|
|
if (!message) return null
|
|
|
|
return (
|
|
<div className='errorMain'>
|
|
<div className='errorMainColor'></div>
|
|
<p className='errorMainText'>{message}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface CheckboxFieldProps {
|
|
label: string
|
|
name: string
|
|
isChecked: boolean
|
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
}
|
|
|
|
export const CheckboxField = React.memo(
|
|
({ label, name, isChecked, handleChange }: CheckboxFieldProps) => (
|
|
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
|
<label className='form-label labelMain'>{label}</label>
|
|
<input
|
|
type='checkbox'
|
|
className='CheckboxMain'
|
|
name={name}
|
|
checked={isChecked}
|
|
onChange={handleChange}
|
|
/>
|
|
</div>
|
|
)
|
|
)
|
|
|
|
type RichTextEditorProps = {
|
|
content: string
|
|
updateContent: (updatedContent: string) => void
|
|
}
|
|
|
|
const RichTextEditor = ({ content, updateContent }: RichTextEditorProps) => {
|
|
const editor = useEditor({
|
|
extensions: [StarterKit, Link],
|
|
onUpdate: ({ editor }) => {
|
|
// Update the state when the editor content changes
|
|
updateContent(editor.getHTML())
|
|
},
|
|
content
|
|
})
|
|
|
|
// Update editor content when the `content` prop changes
|
|
useEffect(() => {
|
|
if (editor && editor.getHTML() !== content) {
|
|
editor.commands.setContent(content, false)
|
|
}
|
|
}, [content, editor])
|
|
|
|
return (
|
|
<div className='inputMain'>
|
|
{editor && (
|
|
<>
|
|
<MenuBar editor={editor} />
|
|
<EditorContent editor={editor} />
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type MenuBarProps = {
|
|
editor: Editor
|
|
}
|
|
|
|
const MenuBar = ({ editor }: MenuBarProps) => {
|
|
const setLink = () => {
|
|
// Prompt the user to enter a URL
|
|
let url = prompt('URL')
|
|
|
|
// Check if the user provided a URL
|
|
if (url) {
|
|
// If the URL doesn't start with 'http://' or 'https://',
|
|
// prepend 'https://' to the URL
|
|
if (!/^(http|https):\/\//i.test(url)) {
|
|
url = `https://${url}`
|
|
}
|
|
|
|
return editor.chain().focus().setLink({ href: url }).run()
|
|
}
|
|
|
|
// If no URL was provided (e.g., the user cancels the prompt),
|
|
// return false, indicating that the link was not set.
|
|
return false
|
|
}
|
|
|
|
const unsetLink = () => editor.chain().focus().unsetLink().run()
|
|
|
|
const buttons: MenuBarButtonProps[] = [
|
|
{
|
|
label: 'Bold',
|
|
disabled: !editor.can().chain().focus().toggleBold().run(),
|
|
isActive: editor.isActive('bold'),
|
|
onClick: () => editor.chain().focus().toggleBold().run()
|
|
},
|
|
{
|
|
label: 'Italic',
|
|
disabled: !editor.can().chain().focus().toggleItalic().run(),
|
|
isActive: editor.isActive('italic'),
|
|
onClick: () => editor.chain().focus().toggleItalic().run()
|
|
},
|
|
{
|
|
label: 'Strike',
|
|
disabled: !editor.can().chain().focus().toggleStrike().run(),
|
|
isActive: editor.isActive('strike'),
|
|
onClick: () => editor.chain().focus().toggleStrike().run()
|
|
},
|
|
{
|
|
label: 'Clear marks',
|
|
onClick: () => editor.chain().focus().unsetAllMarks().run()
|
|
},
|
|
{
|
|
label: 'Clear nodes',
|
|
onClick: () => editor.chain().focus().clearNodes().run()
|
|
},
|
|
{
|
|
label: 'Paragraph',
|
|
isActive: editor.isActive('paragraph'),
|
|
onClick: () => editor.chain().focus().toggleStrike().run()
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
...[1, 2, 3, 4, 5, 6].map((level: any) => ({
|
|
label: `H${level}`,
|
|
isActive: editor.isActive('heading', { level }),
|
|
onClick: () => editor.chain().focus().toggleHeading({ level }).run()
|
|
})),
|
|
{
|
|
label: 'Bullet list',
|
|
isActive: editor.isActive('bulletList'),
|
|
onClick: () => editor.chain().focus().toggleBulletList().run()
|
|
},
|
|
{
|
|
label: 'Ordered list',
|
|
isActive: editor.isActive('orderedList'),
|
|
onClick: () => editor.chain().focus().toggleOrderedList().run()
|
|
},
|
|
{
|
|
label: 'Code block',
|
|
isActive: editor.isActive('codeBlock'),
|
|
onClick: () => editor.chain().focus().toggleCodeBlock().run()
|
|
},
|
|
{
|
|
label: 'Blockquote',
|
|
isActive: editor.isActive('blockquote'),
|
|
onClick: () => editor.chain().focus().toggleBlockquote().run()
|
|
},
|
|
{
|
|
label: 'Link',
|
|
isActive: editor.isActive('link'),
|
|
onClick: editor.isActive('link') ? unsetLink : setLink
|
|
},
|
|
{
|
|
label: 'Horizontal rule',
|
|
onClick: () => editor.chain().focus().setHorizontalRule().run()
|
|
},
|
|
{
|
|
label: 'Hard break',
|
|
onClick: () => editor.chain().focus().setHardBreak().run()
|
|
},
|
|
{
|
|
label: 'Undo',
|
|
disabled: !editor.can().chain().focus().undo().run(),
|
|
onClick: () => editor.chain().focus().undo().run()
|
|
},
|
|
{
|
|
label: 'Redo',
|
|
disabled: !editor.can().chain().focus().redo().run(),
|
|
onClick: () => editor.chain().focus().redo().run()
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div className='control-group'>
|
|
<div className='button-group'>
|
|
{buttons.map(({ label, disabled, isActive, onClick }) => (
|
|
<MenuBarButton
|
|
key={label}
|
|
label={label}
|
|
disabled={disabled}
|
|
isActive={isActive}
|
|
onClick={onClick}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface MenuBarButtonProps {
|
|
label: string
|
|
isActive?: boolean
|
|
disabled?: boolean
|
|
onClick: () => boolean
|
|
}
|
|
|
|
const MenuBarButton = ({
|
|
label,
|
|
isActive = false,
|
|
disabled = false,
|
|
onClick
|
|
}: MenuBarButtonProps) => (
|
|
<button
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
className={`btn btnMain btnMainTipTap ${isActive ? 'is-active' : ''}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
)
|