Skip to content

Commit

Permalink
Add Markdown support for GCN Circulars
Browse files Browse the repository at this point in the history
  • Loading branch information
lpsinger committed Feb 9, 2024
1 parent 47994df commit dd0994f
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 24 deletions.
191 changes: 167 additions & 24 deletions app/routes/_gcn.circulars.edit.$circularId/CircularEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ import {
Textarea,
} from '@trussworks/react-uswds'
import classnames from 'classnames'
import type { ReactNode } from 'react'
import { useContext, useState } from 'react'
import {
type ReactNode,
type RefObject,
useContext,
useRef,
useState,
} from 'react'
import { dedent } from 'ts-dedent'

import { AstroDataContext } from '../_gcn.circulars.$circularId.($version)/AstroDataContext'
Expand All @@ -34,6 +39,11 @@ import {
import { CircularsKeywords } from '~/components/CircularsKeywords'
import Spinner from '~/components/Spinner'

import iconBold from '@gitlab/svgs/dist/sprite_icons/bold.svg'
import iconCode from '@gitlab/svgs/dist/sprite_icons/code.svg'
import iconItalic from '@gitlab/svgs/dist/sprite_icons/italic.svg'
import iconLink from '@gitlab/svgs/dist/sprite_icons/link.svg'

function SyntaxExample({
label,
children,
Expand Down Expand Up @@ -103,6 +113,158 @@ function SyntaxReference() {
)
}

function RichTextareaStyleButton({
children,
inputRef,
startText = '',
endText = '',
...props
}: Omit<Parameters<typeof Button>[0], 'type'> & {
inputRef?: RefObject<HTMLTextAreaElement>
startText: string
endText: string
}) {
return (
<Button
contentEditable={false}
onMouseDown={(e) => {
e.preventDefault()
}}
type="button"
outline
tabIndex={-1}
onClick={() => {
const input = inputRef?.current
if (input) {
const oldText = input.value.substring(
input.selectionStart,
input.selectionEnd
)
const newText = `${startText}${oldText}${endText}`
document.execCommand('insertText', false, newText)
}
}}
{...props}
>
{children}
</Button>
)
}

function RichTextarea({
defaultValue,
className,
onChange,
...props
}: Omit<Parameters<typeof Textarea>[0], 'defaultValue'> & {
defaultValue: string
}) {
const [showPreview, setShowPreview] = useState(false)
const [formatMarkdown, setFormatMarkdown] = useState(false)
const Preview = formatMarkdown ? MarkdownBody : PlainTextBody
const inputRef = useRef<HTMLTextAreaElement>(null)
const [inputFocused, setInputFocused] = useState(false)
const [value, setValue] = useState(defaultValue)

return (
<>
<ButtonGroup>
<SegmentedRadioButtonGroup>
<SegmentedRadioButton
defaultChecked
onClick={() => setShowPreview(false)}
>
Edit
</SegmentedRadioButton>
<SegmentedRadioButton onClick={() => setShowPreview(true)}>
Preview
</SegmentedRadioButton>
</SegmentedRadioButtonGroup>
<SegmentedRadioButtonGroup>
<SegmentedRadioButton
name="format"
value="text/plain"
defaultChecked
onClick={() => {
setFormatMarkdown(false)
}}
>
Plain Text
</SegmentedRadioButton>
<SegmentedRadioButton
name="format"
value="text/markdown"
onClick={() => {
setFormatMarkdown(true)
}}
>
Markdown
</SegmentedRadioButton>
</SegmentedRadioButtonGroup>
{formatMarkdown && !showPreview && (
<ButtonGroup type="segmented">
<RichTextareaStyleButton
startText="**"
endText="**"
disabled={!inputFocused}
inputRef={inputRef}
>
<img src={iconBold} alt="Bold" />
</RichTextareaStyleButton>
<RichTextareaStyleButton
startText="*"
endText="*"
disabled={!inputFocused}
inputRef={inputRef}
title="Italic"
>
<img src={iconItalic} alt="Italic" />
</RichTextareaStyleButton>
<RichTextareaStyleButton
startText="`"
endText="`"
disabled={!inputFocused}
inputRef={inputRef}
title="Code"
>
<img src={iconCode} alt="Code" />
</RichTextareaStyleButton>
<RichTextareaStyleButton
startText="["
endText="]()"
disabled={!inputFocused}
inputRef={inputRef}
title="Hyperlink"
>
<img src={iconLink} alt="Link" />
</RichTextareaStyleButton>
</ButtonGroup>
)}
</ButtonGroup>
<Textarea
hidden={showPreview}
inputRef={inputRef}
defaultValue={defaultValue}
className={classnames('maxw-full', className)}
onChange={(e) => {
setValue(e.target.value)
onChange?.(e)
}}
onFocus={() => {
setInputFocused(true)
}}
onBlur={() => {
setInputFocused(false)
}}
{...props}
/>
{showPreview && (
<Preview className="border padding-1 margin-top-1">{value}</Preview>
)}
</>
)
}

export function CircularEditForm({
formattedContributor,
circularId,
Expand Down Expand Up @@ -130,9 +292,9 @@ export function CircularEditForm({
const bodyValid = bodyIsValid(body)
const [showKeywords, toggleShowKeywords] = useStateToggle(false)
const [showBodySyntax, toggleShowBodySyntax] = useStateToggle(false)
const [showPreview, setShowPreview] = useState(false)
const sending = Boolean(useNavigation().formData)
const valid = subjectValid && bodyValid

return (
<AstroDataContext.Provider value={{ rel: 'noopener', target: '_blank' }}>
<h1>{circularId ? 'Edit' : 'New'} GCN Circular</h1>
Expand Down Expand Up @@ -205,37 +367,18 @@ export function CircularEditForm({
<label hidden htmlFor="body">
Body
</label>
<SegmentedRadioButtonGroup>
<SegmentedRadioButton
defaultChecked
onClick={() => setShowPreview(false)}
>
Edit
</SegmentedRadioButton>
<SegmentedRadioButton onClick={() => setShowPreview(true)}>
Preview
</SegmentedRadioButton>
</SegmentedRadioButtonGroup>
<Textarea
hidden={showPreview}
<RichTextarea
name="body"
id="body"
aria-describedby="bodyDescription"
placeholder={useBodyPlaceholder()}
defaultValue={defaultBody}
required={true}
className={classnames('maxw-full', {
'usa-input--success': bodyValid,
})}
className={bodyValid ? 'usa-input--success' : undefined}
onChange={({ target: { value } }) => {
setBody(value)
}}
/>
{showPreview && (
<PlainTextBody className="border padding-1 margin-top-1">
{body}
</PlainTextBody>
)}
<div className="text-base margin-bottom-1" id="bodyDescription">
<small>
Body text. If this is your first Circular, please review the{' '}
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@aws-sdk/util-dynamodb": "^3.319.0",
"@babel/preset-env": "^7.23.6",
"@babel/preset-typescript": "^7.23.3",
"@gitlab/svgs": "^3.83.0",
"@nasa-gcn/architect-plugin-search": "^1.2.0",
"@nasa-gcn/eslint-config-gitignore": "^0.0.1",
"@remix-run/dev": "^2.6.0",
Expand Down

0 comments on commit dd0994f

Please sign in to comment.