Editor improvements (#735)

* Allow focus on all parts of editor

* Fix background and text colors

* Restrict height of image in comment

* Remove "Type *markdown*" placeholder

it's a little misleading (can't do markdown links) and messes with focus

to be replaced with a highlight menu in the future
This commit is contained in:
Sinclair Chen 2022-08-09 19:04:55 -07:00 committed by GitHub
parent c07daafb8d
commit 0b9ca6b7ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 31 deletions

View File

@ -65,7 +65,7 @@ function ProfileComment(props: { comment: Comment; className?: string }) {
/>{' '} />{' '}
<RelativeTimestamp time={createdTime} /> <RelativeTimestamp time={createdTime} />
</p> </p>
<Content content={content || text} /> <Content content={content || text} smallImage />
</div> </div>
</Row> </Row>
) )

View File

@ -3,7 +3,6 @@ import Placeholder from '@tiptap/extension-placeholder'
import { import {
useEditor, useEditor,
EditorContent, EditorContent,
FloatingMenu,
JSONContent, JSONContent,
Content, Content,
Editor, Editor,
@ -11,13 +10,11 @@ import {
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
import { Mention } from '@tiptap/extension-mention'
import clsx from 'clsx' import clsx from 'clsx'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Linkify } from './linkify' import { Linkify } from './linkify'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import { exhibitExts } from 'common/util/parse'
import { FileUploadButton } from './file-upload-button' import { FileUploadButton } from './file-upload-button'
import { linkClass } from './site-link' import { linkClass } from './site-link'
import { useUsers } from 'web/hooks/use-users' import { useUsers } from 'web/hooks/use-users'
@ -31,6 +28,18 @@ import { Button } from './button'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
const DisplayImage = Image.configure({
HTMLAttributes: {
class: 'max-h-60',
},
})
const DisplayLink = Link.configure({
HTMLAttributes: {
class: clsx('no-underline !text-indigo-700', linkClass),
},
})
const proseClass = clsx( const proseClass = clsx(
'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
'font-light prose-a:font-light prose-blockquote:font-light' 'font-light prose-a:font-light prose-blockquote:font-light'
@ -64,15 +73,11 @@ export function useTextEditor(props: {
Placeholder.configure({ Placeholder.configure({
placeholder, placeholder,
emptyEditorClass: emptyEditorClass:
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text',
}), }),
CharacterCount.configure({ limit: max }), CharacterCount.configure({ limit: max }),
Image, simple ? DisplayImage : Image,
Link.configure({ DisplayLink,
HTMLAttributes: {
class: clsx('no-underline !text-indigo-700', linkClass),
},
}),
DisplayMention.configure({ DisplayMention.configure({
suggestion: mentionSuggestion(users), suggestion: mentionSuggestion(users),
}), }),
@ -132,15 +137,7 @@ export function TextEditor(props: {
<> <>
{/* hide placeholder when focused */} {/* hide placeholder when focused */}
<div className="relative w-full [&:focus-within_p.is-empty]:before:content-none"> <div className="relative w-full [&:focus-within_p.is-empty]:before:content-none">
{editor && ( <div className="rounded-lg border border-gray-300 bg-white shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500">
<FloatingMenu
editor={editor}
className={clsx(proseClass, '-ml-2 mr-2 w-full text-slate-300 ')}
>
Type <em>*markdown*</em>
</FloatingMenu>
)}
<div className="rounded-lg border border-gray-300 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500">
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{/* Toolbar, with buttons for images and embeds */} {/* Toolbar, with buttons for images and embeds */}
<div className="flex h-9 items-center gap-5 pl-4 pr-1"> <div className="flex h-9 items-center gap-5 pl-4 pr-1">
@ -168,7 +165,14 @@ export function TextEditor(props: {
<span className="sr-only">Embed an iframe</span> <span className="sr-only">Embed an iframe</span>
</button> </button>
</div> </div>
<div className="ml-auto" /> {/* Spacer that also focuses editor on click */}
<div
className="grow cursor-text self-stretch"
onMouseDown={() =>
editor?.chain().focus('end').createParagraphNear().run()
}
aria-hidden
/>
{children} {children}
</div> </div>
</div> </div>
@ -258,14 +262,19 @@ const useUploadMutation = (editor: Editor | null) =>
} }
) )
function RichContent(props: { content: JSONContent | string }) { function RichContent(props: {
const { content } = props content: JSONContent | string
smallImage?: boolean
}) {
const { content, smallImage } = props
const editor = useEditor({ const editor = useEditor({
editorProps: { attributes: { class: proseClass } }, editorProps: { attributes: { class: proseClass } },
extensions: [ extensions: [
// replace tiptap's Mention with ours, to add style and link StarterKit,
...exhibitExts.filter((ex) => ex.name !== Mention.name), smallImage ? DisplayImage : Image,
DisplayLink,
DisplayMention, DisplayMention,
Iframe,
], ],
content, content,
editable: false, editable: false,
@ -276,13 +285,16 @@ function RichContent(props: { content: JSONContent | string }) {
} }
// backwards compatibility: we used to store content as strings // backwards compatibility: we used to store content as strings
export function Content(props: { content: JSONContent | string }) { export function Content(props: {
content: JSONContent | string
smallImage?: boolean
}) {
const { content } = props const { content } = props
return typeof content === 'string' ? ( return typeof content === 'string' ? (
<div className="whitespace-pre-line font-light leading-relaxed"> <div className="whitespace-pre-line font-light leading-relaxed">
<Linkify text={content} /> <Linkify text={content} />
</div> </div>
) : ( ) : (
<RichContent content={content} /> <RichContent {...props} />
) )
} }

View File

@ -254,7 +254,7 @@ export function FeedComment(props: {
/> />
</div> </div>
<div className="mt-2 text-[15px] text-gray-700"> <div className="mt-2 text-[15px] text-gray-700">
<Content content={content || text} /> <Content content={content || text} smallImage />
</div> </div>
<Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Row className="mt-2 items-center gap-6 text-xs text-gray-500">
<Tipper comment={comment} tips={tips ?? {}} /> <Tipper comment={comment} tips={tips ?? {}} />
@ -394,8 +394,8 @@ export function CommentInput(props: {
/> />
</div> </div>
<div className={'min-w-0 flex-1'}> <div className={'min-w-0 flex-1'}>
<div className="pl-0.5 text-sm text-gray-500"> <div className="pl-0.5 text-sm">
<div className={'mb-1'}> <div className="mb-1 text-gray-500">
{mostRecentCommentableBet && ( {mostRecentCommentableBet && (
<BetStatusText <BetStatusText
contract={contract} contract={contract}

View File

@ -338,7 +338,7 @@ const GroupMessage = memo(function GroupMessage_(props: {
</Row> </Row>
<div className="mt-2 text-black"> <div className="mt-2 text-black">
{comments.map((comment) => ( {comments.map((comment) => (
<Content content={comment.content || comment.text} /> <Content content={comment.content || comment.text} smallImage />
))} ))}
</div> </div>
<Row> <Row>