Add floating menu (bold, italic, link) (#867)
* Add floating menu (bold, italic, link) * Sanitize and href-ify user input
This commit is contained in:
parent
2351403674
commit
22d2248951
|
@ -23,8 +23,15 @@ import { Link } from '@tiptap/extension-link'
|
|||
import { Mention } from '@tiptap/extension-mention'
|
||||
import Iframe from './tiptap-iframe'
|
||||
import TiptapTweet from './tiptap-tweet-type'
|
||||
import { find } from 'linkifyjs'
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
/** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */
|
||||
export function getUrl(text: string) {
|
||||
const results = find(text, 'url')
|
||||
return results.length ? results[0].href : null
|
||||
}
|
||||
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
const matches = (text.match(regex) || []).map((match) =>
|
||||
|
|
|
@ -2,6 +2,7 @@ import CharacterCount from '@tiptap/extension-character-count'
|
|||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import {
|
||||
useEditor,
|
||||
BubbleMenu,
|
||||
EditorContent,
|
||||
JSONContent,
|
||||
Content,
|
||||
|
@ -24,13 +25,19 @@ import Iframe from 'common/util/tiptap-iframe'
|
|||
import TiptapTweet from './editor/tiptap-tweet'
|
||||
import { EmbedModal } from './editor/embed-modal'
|
||||
import {
|
||||
CheckIcon,
|
||||
CodeIcon,
|
||||
PhotographIcon,
|
||||
PresentationChartLineIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { MarketModal } from './editor/market-modal'
|
||||
import { insertContent } from './editor/utils'
|
||||
import { Tooltip } from './tooltip'
|
||||
import BoldIcon from 'web/lib/icons/bold-icon'
|
||||
import ItalicIcon from 'web/lib/icons/italic-icon'
|
||||
import LinkIcon from 'web/lib/icons/link-icon'
|
||||
import { getUrl } from 'common/util/parse'
|
||||
|
||||
const DisplayImage = Image.configure({
|
||||
HTMLAttributes: {
|
||||
|
@ -141,6 +148,66 @@ function isValidIframe(text: string) {
|
|||
return /^<iframe.*<\/iframe>$/.test(text)
|
||||
}
|
||||
|
||||
function FloatingMenu(props: { editor: Editor | null }) {
|
||||
const { editor } = props
|
||||
|
||||
const [url, setUrl] = useState<string | null>(null)
|
||||
|
||||
if (!editor) return null
|
||||
|
||||
// current selection
|
||||
const isBold = editor.isActive('bold')
|
||||
const isItalic = editor.isActive('italic')
|
||||
const isLink = editor.isActive('link')
|
||||
|
||||
const setLink = () => {
|
||||
const href = url && getUrl(url)
|
||||
if (href) {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href }).run()
|
||||
}
|
||||
}
|
||||
|
||||
const unsetLink = () => editor.chain().focus().unsetLink().run()
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
editor={editor}
|
||||
className="flex gap-2 rounded-sm bg-slate-700 p-1 text-white"
|
||||
>
|
||||
{url === null ? (
|
||||
<>
|
||||
<button onClick={() => editor.chain().focus().toggleBold().run()}>
|
||||
<BoldIcon className={clsx('h-5', isBold && 'text-indigo-200')} />
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().toggleItalic().run()}>
|
||||
<ItalicIcon
|
||||
className={clsx('h-5', isItalic && 'text-indigo-200')}
|
||||
/>
|
||||
</button>
|
||||
<button onClick={() => (isLink ? unsetLink() : setUrl(''))}>
|
||||
<LinkIcon className={clsx('h-5', isLink && 'text-indigo-200')} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="h-5 border-0 bg-inherit text-sm !shadow-none !ring-0"
|
||||
placeholder="Type or paste a link"
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => (setLink(), setUrl(null))}>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button onClick={() => (unsetLink(), setUrl(null))}>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</BubbleMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextEditor(props: {
|
||||
editor: Editor | null
|
||||
upload: ReturnType<typeof useUploadMutation>
|
||||
|
@ -155,6 +222,7 @@ export function TextEditor(props: {
|
|||
{/* hide placeholder when focused */}
|
||||
<div className="relative w-full [&:focus-within_p.is-empty]:before:content-none">
|
||||
<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} />
|
||||
<EditorContent editor={editor} />
|
||||
{/* Toolbar, with buttons for images and embeds */}
|
||||
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
||||
|
|
20
web/lib/icons/bold-icon.tsx
Normal file
20
web/lib/icons/bold-icon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
// from Feather: https://feathericons.com/
|
||||
export default function BoldIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
||||
<path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
21
web/lib/icons/italic-icon.tsx
Normal file
21
web/lib/icons/italic-icon.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// from Feather: https://feathericons.com/
|
||||
export default function ItalicIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<line x1="19" y1="4" x2="10" y2="4"></line>
|
||||
<line x1="14" y1="20" x2="5" y2="20"></line>
|
||||
<line x1="15" y1="4" x2="9" y2="20"></line>
|
||||
</svg>
|
||||
)
|
||||
}
|
20
web/lib/icons/link-icon.tsx
Normal file
20
web/lib/icons/link-icon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
// from Feather: https://feathericons.com/
|
||||
export default function LinkIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user