Skip to content

Commit

Permalink
feat: created editor and stories
Browse files Browse the repository at this point in the history
  • Loading branch information
g-saracca committed Oct 22, 2024
1 parent 902409f commit 1bf7310
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@use 'sass:color';
@import 'src/lib/assets/styles/design-tokens/colors.module';

.editor-actions-wrapper {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding-inline: 0.5rem;
}

.editor-actions-button {
display: grid;
place-items: center;

&.selected {
background-color: color.adjust($dv-secondary-color, $blackness: 30%);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { useRef, useState } from 'react'
import { Editor } from '@tiptap/react'
import { ButtonGroup } from '../button-group/ButtonGroup'
import { Button } from '../button/Button'
import {
BlockquoteRight,
Code,
Link,
ListOl,
ListUl,
TypeBold,
TypeH1,
TypeH2,
TypeH3,
TypeItalic,
TypeStrikethrough,
TypeUnderline
} from 'react-bootstrap-icons'
import styles from './EditorActions.module.scss'
import { Modal } from '../modal/Modal'
import { Form } from '../form/Form'
import { Col } from '../grid/Col'

const EDITOR_FORMATS = {
heading: 'heading',
bold: 'bold',
italic: 'italic',
underline: 'underline',
strike: 'strike',
code: 'code',
link: 'link',
blockquote: 'blockquote',
bulletList: 'bulletList',
orderedList: 'orderedList'
} as const

interface EditorActionsProps {
editor: Editor
disabled?: boolean
}

export const EditorActions = ({ editor, disabled }: EditorActionsProps) => {
const [linkDialogOpen, setLinkDialogOpen] = useState(false)
const linkTextfieldRef = useRef<HTMLInputElement>(null)

const handleOpenLinkDialog = () => setLinkDialogOpen(true)
const handleCloseLinkDialog = () => setLinkDialogOpen(false)

const handleOKLinkDialog = () => {
const url = linkTextfieldRef.current?.value

if (url) {
editor.chain().focus().extendMarkRange(EDITOR_FORMATS.link).setLink({ href: url }).run()
} else {
editor.chain().focus().extendMarkRange(EDITOR_FORMATS.link).unsetLink().run()
}
setLinkDialogOpen(false)
}

const handleToggleH1 = () => editor.chain().focus().toggleHeading({ level: 1 }).run()
const isActiveH1 = !disabled && editor.isActive(EDITOR_FORMATS.heading, { level: 1 })

const handleToggleH2 = () => editor.chain().focus().toggleHeading({ level: 2 }).run()
const isActiveH2 = !disabled && editor.isActive(EDITOR_FORMATS.heading, { level: 2 })

const handleToggleH3 = () => editor.chain().focus().toggleHeading({ level: 3 }).run()
const isActiveH3 = !disabled && editor.isActive(EDITOR_FORMATS.heading, { level: 3 })

const handleToggleBold = () => editor.chain().focus().toggleBold().run()
const isActiveBold = !disabled && editor.isActive(EDITOR_FORMATS.bold)

const handleToggleItalic = () => editor.chain().focus().toggleItalic().run()
const isActiveItalic = !disabled && editor.isActive(EDITOR_FORMATS.italic)

const handleToggleUnderline = () => editor.chain().focus().toggleUnderline().run()
const isActiveUnderline = !disabled && editor.isActive(EDITOR_FORMATS.underline)

const handleToggleStrike = () => editor.chain().focus().toggleStrike().run()
const isActiveStrike = !disabled && editor.isActive(EDITOR_FORMATS.strike)

const handleToggleCode = () => editor.chain().focus().toggleCode().run()
const isActiveCode = !disabled && editor.isActive(EDITOR_FORMATS.code)

const handleToggleBlockquote = () => editor.chain().focus().toggleBlockquote().run()
const isActiveBlockquote = !disabled && editor.isActive(EDITOR_FORMATS.blockquote)

const handleToggleBulletList = () => editor.chain().focus().toggleBulletList().run()
const isActiveBulletList = !disabled && editor.isActive(EDITOR_FORMATS.bulletList)

const handleToggleOrderedList = () => editor.chain().focus().toggleOrderedList().run()
const isActiveOrderedList = !disabled && editor.isActive(EDITOR_FORMATS.orderedList)

return (
<>
<div className={styles['editor-actions-wrapper']}>
{/* Headings */}
<ButtonGroup>
<Button
onClick={handleToggleH1}
className={`${styles['editor-actions-button']} ${isActiveH1 ? styles.selected : ''}`}
aria-pressed={isActiveH1}
aria-label="Heading 1"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeH1 size={18} />}
/>
<Button
onClick={handleToggleH2}
className={`${styles['editor-actions-button']} ${isActiveH2 ? styles.selected : ''}`}
aria-pressed={isActiveH2}
aria-label="Heading 2"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeH2 size={18} />}
/>
<Button
onClick={handleToggleH3}
className={`${styles['editor-actions-button']} ${isActiveH3 ? styles.selected : ''}`}
aria-pressed={isActiveH3}
aria-label="Heading 3"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeH3 size={18} />}
/>
</ButtonGroup>

{/* Font styles */}
<ButtonGroup>
<Button
onClick={handleToggleBold}
className={`${styles['editor-actions-button']} ${isActiveBold ? styles.selected : ''}`}
aria-pressed={isActiveBold}
aria-label="Bold"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeBold size={18} />}
/>
<Button
onClick={handleToggleItalic}
className={`${styles['editor-actions-button']} ${
isActiveItalic ? styles.selected : ''
}`}
aria-pressed={isActiveItalic}
aria-label="Italic"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeItalic size={18} />}
/>

<Button
onClick={handleToggleUnderline}
className={`${styles['editor-actions-button']} ${
isActiveUnderline ? styles.selected : ''
}`}
aria-pressed={isActiveUnderline}
aria-label="Underline"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeUnderline size={18} />}
/>
<Button
onClick={handleToggleStrike}
className={`${styles['editor-actions-button']} ${
isActiveStrike ? styles.selected : ''
}`}
aria-pressed={isActiveStrike}
aria-label="Underline"
disabled={disabled}
variant="secondary"
size="sm"
icon={<TypeStrikethrough size={18} />}
/>
</ButtonGroup>

{/* Lists */}
<ButtonGroup>
<Button
onClick={handleToggleBulletList}
className={`${styles['editor-actions-button']} ${
isActiveBulletList ? styles.selected : ''
}`}
aria-pressed={isActiveBulletList}
aria-label="Unordered list"
disabled={disabled}
variant="secondary"
size="sm"
icon={<ListUl size={18} />}
/>
<Button
onClick={handleToggleOrderedList}
className={`${styles['editor-actions-button']} ${
isActiveOrderedList ? styles.selected : ''
}`}
aria-pressed={isActiveOrderedList}
aria-label="Ordered list"
disabled={disabled}
variant="secondary"
size="sm"
icon={<ListOl size={18} />}
/>
</ButtonGroup>

{/* Extras */}
<ButtonGroup>
<Button
onClick={handleToggleCode}
className={`${styles['editor-actions-button']} ${isActiveCode ? styles.selected : ''}`}
aria-pressed={isActiveCode}
aria-label="Code"
disabled={disabled}
variant="secondary"
size="sm"
icon={<Code size={18} />}
/>
<Button
onClick={handleOpenLinkDialog}
className={`${styles['editor-actions-button']}`}
aria-label="Open modal to add link"
disabled={disabled}
variant="secondary"
size="sm"
icon={<Link size={18} />}
/>
<Button
onClick={handleToggleBlockquote}
className={`${styles['editor-actions-button']} ${
isActiveBlockquote ? styles.selected : ''
}`}
aria-pressed={isActiveBlockquote}
aria-label="Blockquote"
disabled={disabled}
variant="secondary"
size="sm"
icon={<BlockquoteRight size={18} />}
/>
</ButtonGroup>
</div>
<Modal show={linkDialogOpen} onHide={handleCloseLinkDialog} size="lg">
<Modal.Header>
<Modal.Title>Add Link</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group controlId="link-url" as={Col}>
<Form.Group.Label column>URL</Form.Group.Label>
<Col>
<Form.Group.Input type="text" ref={linkTextfieldRef} />
</Col>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button onClick={handleOKLinkDialog} variant="primary">
OK
</Button>
<Button onClick={handleCloseLinkDialog} variant="secondary">
Cancel
</Button>
</Modal.Footer>
</Modal>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@import 'src/lib/assets/styles/design-tokens/colors.module';

.rich-text-editor-wrapper {
display: flex;
flex-direction: column;
padding-top: 0.5rem;
border: solid 1px $dv-border-color;
border-radius: 6px;
}

.editor-content-wrapper {
display: flex;
flex: 1;
flex-direction: column;

& > div {
display: flex;
flex: 1;
flex-direction: column;
}

.rich-text-editor-content {
flex: 1;
min-height: 200px;
max-height: 350px;
margin: 0.5rem;
padding: 6px 12px;
overflow-y: auto;
border: solid 1px $dv-border-color;
border-radius: 4px;

&.ProseMirror-focused {
border-color: #99bddb;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(51 122 183 / 25%);
}

blockquote {
padding: 1px 10px;
background-color: rgba(0 0 0 / 15%);
border-left: solid 5px gray;
border-radius: 4px;
}

code {
padding: 0.1em 0.35em;
font-size: 85%;
background-color: rgba(0 0 0 / 10%);
border-radius: 0.375rem;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect } from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Link from '@tiptap/extension-link'
import { EditorActions } from './EditorActions'
import './RichTextEditor.scss'

export interface RichTextFieldProps {
initialValue?: string | undefined
onChange: (value: string) => void
error?: string
disabled?: boolean
}

export const RichTextEditor = ({ initialValue, onChange, disabled }: RichTextFieldProps) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3]
}
}),
Underline,
Link.configure({
openOnClick: false,
autolink: true,
linkOnPaste: true
})
],
content: initialValue,
editorProps: {
attributes: {
class: 'rich-text-editor-content'
}
},
onUpdate: ({ editor }) => onChange(editor.getHTML())
})

useEffect(() => {
if (editor) editor.setEditable(disabled ? false : true, false)
}, [disabled, editor])

if (!editor) return null

return (
<div className="rich-text-editor-wrapper">
<EditorActions editor={editor} disabled={disabled} />
<div className="editor-content-wrapper">
<EditorContent editor={editor} />
</div>
</div>
)
}
Loading

0 comments on commit 1bf7310

Please sign in to comment.