mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-12-04 11:49:02 +08:00
feat: make the example much better
This commit is contained in:
parent
bd6c257bf6
commit
01d6dc89a8
@ -1,31 +1,31 @@
|
|||||||
import { Editor, FloatingMenu, useEditorState } from '@tiptap/react'
|
import { Editor, FloatingMenu, useEditorState } from '@tiptap/react'
|
||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
|
|
||||||
// import { useFocusMenubar } from './useFocusMenubar.js'
|
import { useFocusMenubar } from './useFocusMenubar.js'
|
||||||
|
|
||||||
export function InsertMenu({ editor }: { editor: Editor }) {
|
export function InsertMenu({ editor }: { editor: Editor }) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const editorState = useEditorState({
|
const { activeNodeType } = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
selector: ctx => {
|
selector: ctx => {
|
||||||
|
const activeNode = ctx.editor.state.selection.$from.node(1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
activeNodeType: activeNode?.type.name ?? 'paragraph',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// // Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
// Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||||
// useFocusMenubar({
|
const { focusButton } = useFocusMenubar({
|
||||||
// editor,
|
editor,
|
||||||
// ref: containerRef,
|
ref: containerRef,
|
||||||
// onEscape: () => {
|
onEscape: () => {
|
||||||
// // On escape, focus the editor & dismiss the menu by moving the selection to the end of the selection
|
// On escape, focus the editor
|
||||||
// editor.chain().focus().command(({ tr }) => {
|
editor.chain().focus().run()
|
||||||
// tr.setSelection(Selection.near(tr.selection.$to))
|
},
|
||||||
// return true
|
})
|
||||||
// }).run()
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingMenu
|
<FloatingMenu
|
||||||
@ -33,16 +33,49 @@ export function InsertMenu({ editor }: { editor: Editor }) {
|
|||||||
shouldShow={null}
|
shouldShow={null}
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
role="menubar"
|
role="menubar"
|
||||||
|
aria-label="Insert Element menu"
|
||||||
|
className='floating-menu'
|
||||||
// Types are broken here, since we import jsx from vue-2
|
// Types are broken here, since we import jsx from vue-2
|
||||||
ref={containerRef as any}
|
ref={containerRef as any}
|
||||||
// This is a raw HTML element, so we can't use onFocus
|
onFocus={e => {
|
||||||
onfocus={() => {
|
// The ref we have is to the container, not the menu itself
|
||||||
// Focus the first button when the menu bar is focused
|
if (containerRef.current === e.target?.parentNode) {
|
||||||
containerRef.current?.querySelector('button')?.focus()
|
// Focus the first button when the menu bar is focused
|
||||||
|
focusButton(containerRef.current?.querySelector('button'))
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
TST
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
className={activeNodeType === 'bulletList' ? 'is-active' : ''}
|
||||||
|
aria-label="Bullet List"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Bullet list
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
className={activeNodeType === 'orderedList' ? 'is-active' : ''}
|
||||||
|
aria-label="Ordered List"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Ordered List
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
aria-label="Horizontal rule"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Horizontal rule
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||||
|
aria-label="Hard break"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Hard break
|
||||||
|
</button>
|
||||||
</FloatingMenu>
|
</FloatingMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,39 @@
|
|||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from '@tiptap/core'
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from 'react'
|
||||||
|
|
||||||
import { NodeTypeDropdown } from "./NodeTypeDropdown.js";
|
import { NodeTypeDropdown } from './NodeTypeDropdown.js'
|
||||||
import { useFocusMenubar } from "./useFocusMenubar.js";
|
import { useFocusMenubar } from './useFocusMenubar.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An accessible menu bar for the editor
|
* An accessible menu bar for the editor
|
||||||
*/
|
*/
|
||||||
export const MenuBar = ({ editor }: { editor: Editor }) => {
|
export const MenuBar = ({ editor }: { editor: Editor }) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useFocusMenubar({
|
useFocusMenubar({
|
||||||
ref: containerRef,
|
ref: containerRef,
|
||||||
editor,
|
editor,
|
||||||
onKeydown: (event) => {
|
onKeydown: event => {
|
||||||
// Handle focus on alt + f10
|
// Handle focus on alt + f10
|
||||||
if (event.altKey && event.key === "F10") {
|
if (event.altKey && event.key === 'F10') {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
containerRef.current?.querySelector("button")?.focus();
|
containerRef.current?.querySelector('button')?.focus()
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="control-group" role="toolbar" aria-orientation="horizontal" ref={containerRef}>
|
<div className="control-group" role="toolbar" aria-orientation="horizontal" ref={containerRef}>
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
<NodeTypeDropdown editor={editor} />
|
<NodeTypeDropdown editor={editor} />
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label="Horizontal rule"
|
|
||||||
>
|
|
||||||
Horizontal rule
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label="Hard break"
|
|
||||||
>
|
|
||||||
Hard break
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
disabled={!editor.can().chain().focus().undo().run()}
|
disabled={!editor.can().chain().focus().undo()
|
||||||
|
.run()}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label="Undo"
|
aria-label="Undo"
|
||||||
>
|
>
|
||||||
@ -55,7 +41,8 @@ export const MenuBar = ({ editor }: { editor: Editor }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
disabled={!editor.can().chain().focus().redo().run()}
|
disabled={!editor.can().chain().focus().redo()
|
||||||
|
.run()}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label="Redo"
|
aria-label="Redo"
|
||||||
>
|
>
|
||||||
@ -63,5 +50,5 @@ export const MenuBar = ({ editor }: { editor: Editor }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
@ -15,7 +15,7 @@ export function NodeTypeDropdown({ editor }: { editor: Editor }) {
|
|||||||
const activeNode = ctx.editor.state.selection.$from.node(1)
|
const activeNode = ctx.editor.state.selection.$from.node(1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeNodeType: activeNode.type.name.slice(0, 1).toUpperCase() + activeNode.type.name.slice(1) || 'Paragraph',
|
activeNodeType: activeNode?.type.name ?? 'paragraph',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -53,7 +53,7 @@ export function NodeTypeDropdown({ editor }: { editor: Editor }) {
|
|||||||
}`}
|
}`}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
Node Type: {activeNodeType}
|
Node Type: {activeNodeType.slice(0, 1).toUpperCase() + activeNodeType.slice(1)}
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
|
@ -30,7 +30,7 @@ export function TextMenu({ editor }: { editor: Editor }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
// Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||||
useFocusMenubar({
|
const { focusButton } = useFocusMenubar({
|
||||||
editor,
|
editor,
|
||||||
ref: containerRef,
|
ref: containerRef,
|
||||||
onEscape: () => {
|
onEscape: () => {
|
||||||
@ -46,14 +46,18 @@ export function TextMenu({ editor }: { editor: Editor }) {
|
|||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
shouldShow={null}
|
shouldShow={null}
|
||||||
|
aria-label="Text formatting menu"
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
role="menubar"
|
role="menubar"
|
||||||
|
className='bubble-menu'
|
||||||
// Types are broken here, since we import jsx from vue-2
|
// Types are broken here, since we import jsx from vue-2
|
||||||
ref={containerRef as any}
|
ref={containerRef as any}
|
||||||
// This is a raw HTML element, so we can't use onFocus
|
onFocus={e => {
|
||||||
onfocus={() => {
|
// The ref we have is to the container, not the menu itself
|
||||||
// Focus the first button when the menu bar is focused
|
if (containerRef.current === e.target?.parentNode) {
|
||||||
containerRef.current?.querySelector('button')?.focus()
|
// Focus the first button when the menu bar is focused
|
||||||
|
focusButton(containerRef.current?.querySelector('button'))
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
@ -1,47 +1,41 @@
|
|||||||
import "./styles.scss";
|
import './styles.scss'
|
||||||
|
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
// import Placeholder from '@tiptap/extension-placeholder'
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, useEditor } from '@tiptap/react'
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import { TextMenu } from "./TextMenu";
|
import { InsertMenu } from './InsertMenu.jsx'
|
||||||
import { MenuBar } from "./MenuBar.jsx";
|
import { MenuBar } from './MenuBar.jsx'
|
||||||
import { InsertMenu } from "./InsertMenu";
|
import { TextMenu } from './TextMenu.jsx'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Placeholder.configure({
|
|
||||||
placeholder: "Use Alt + F10 to focus the menu bar",
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
immediatelyRender: false,
|
content: `
|
||||||
|
<h2>Accessibility Demo</h2>
|
||||||
|
<p>Tab into the demo & navigate around with only your keyboard</p>
|
||||||
|
<p>Use <code>Alt + F10</code> to focus the menu bar</p>
|
||||||
|
`,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: (state): Record<string, string> => {
|
attributes: {
|
||||||
return {
|
// Make sure the editor is announced as a rich text editor
|
||||||
// Make sure the editor is announced as a rich text editor
|
'aria-label': 'Rich Text Editor',
|
||||||
"aria-label": "Rich Text Editor",
|
// editor accepts multiline input
|
||||||
// editor accepts multiline input
|
'aria-multiline': 'true',
|
||||||
"aria-multiline": "true",
|
'aria-readonly': 'false',
|
||||||
// dynamically set the aria-readonly attribute
|
|
||||||
"aria-readonly": editor?.isEditable ? "false" : "true",
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="application">
|
<div role="application" className="editor-application">
|
||||||
<MenuBar editor={editor} />
|
<MenuBar editor={editor} />
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
<TextMenu editor={editor} />
|
<TextMenu editor={editor} />
|
||||||
<InsertMenu editor={editor} />
|
<InsertMenu editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
@ -14,11 +14,70 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Bubble menu */
|
||||||
|
.bubble-menu {
|
||||||
|
background-color: var(--white);
|
||||||
|
border: 1px solid var(--gray-1);
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
display: flex;
|
||||||
|
padding: 0.2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: var(--purple);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--purple-contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating menu */
|
||||||
|
.floating-menu {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
padding: 0.1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: unset;
|
||||||
|
padding: 0.275rem 0.425rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--purple);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--purple-contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Basic editor styles */
|
/* Basic editor styles */
|
||||||
.tiptap {
|
.tiptap {
|
||||||
:first-child {
|
:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: 1px solid var(--purple);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* List styles */
|
/* List styles */
|
||||||
ul,
|
ul,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
* Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||||
@ -46,6 +46,62 @@ export function useFocusMenubar({
|
|||||||
onKeydown,
|
onKeydown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focusNextButton = useCallback((el = document.activeElement) => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = Array.from(containerRef.current.querySelectorAll('button'))
|
||||||
|
const index = elements.findIndex(element => element === el)
|
||||||
|
|
||||||
|
// Find the next enabled button
|
||||||
|
for (let i = index + 1; i <= elements.length; i += 1) {
|
||||||
|
if (!elements[i % elements.length].disabled) {
|
||||||
|
elements[i % elements.length].focus()
|
||||||
|
return elements[i % elements.length]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [containerRef])
|
||||||
|
|
||||||
|
const focusPreviousButton = useCallback((el = document.activeElement) => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = Array.from(containerRef.current.querySelectorAll('button'))
|
||||||
|
const index = elements.findIndex(element => element === el)
|
||||||
|
|
||||||
|
// Find the previous enabled button
|
||||||
|
for (let i = index - 1; i >= -1; i -= 1) {
|
||||||
|
// If we reach the beginning, start from the end
|
||||||
|
if (i < 0) {
|
||||||
|
i = elements.length - 1
|
||||||
|
}
|
||||||
|
if (!elements[i].disabled) {
|
||||||
|
elements[i].focus()
|
||||||
|
return elements[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [containerRef])
|
||||||
|
|
||||||
|
const focusButton = useCallback((el: HTMLButtonElement | null | undefined, direction: 'forwards' | 'backwards' = 'forwards') => {
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!el.disabled) {
|
||||||
|
el.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (direction === 'forwards') {
|
||||||
|
focusNextButton(el)
|
||||||
|
}
|
||||||
|
if (direction === 'backwards') {
|
||||||
|
focusPreviousButton(el)
|
||||||
|
}
|
||||||
|
}, [focusNextButton, focusPreviousButton])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
@ -60,44 +116,25 @@ export function useFocusMenubar({
|
|||||||
const elements = Array.from(containerRef.current.querySelectorAll('button'))
|
const elements = Array.from(containerRef.current.querySelectorAll('button'))
|
||||||
const isFocusedOnButton = elements.includes(event.target as HTMLButtonElement)
|
const isFocusedOnButton = elements.includes(event.target as HTMLButtonElement)
|
||||||
|
|
||||||
// Allow to escape to the editor
|
|
||||||
if (isFocusedOnButton || event.target === containerRef.current) {
|
if (isFocusedOnButton || event.target === containerRef.current) {
|
||||||
|
// Allow to escape to the editor
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
callbacks.current.onEscape(editor)
|
callbacks.current.onEscape(editor)
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isFocusedOnButton) {
|
|
||||||
// Handle arrow navigation within the menu bar
|
// Handle arrow navigation within the menu bar
|
||||||
if (event.key === 'ArrowRight') {
|
if (event.key === 'ArrowRight') {
|
||||||
const index = elements.indexOf(event.target as HTMLButtonElement)
|
if (focusNextButton(event.target as HTMLButtonElement)) {
|
||||||
|
event.preventDefault()
|
||||||
// Find the next enabled button
|
return true
|
||||||
for (let i = index + 1; i <= elements.length; i += 1) {
|
|
||||||
if (!elements[i % elements.length].disabled) {
|
|
||||||
event.preventDefault()
|
|
||||||
elements[i % elements.length].focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
const index = elements.indexOf(event.target as HTMLButtonElement)
|
if (focusPreviousButton(event.target as HTMLButtonElement)) {
|
||||||
|
event.preventDefault()
|
||||||
// Find the previous enabled button
|
return true
|
||||||
for (let i = index - 1; i >= -1; i -= 1) {
|
|
||||||
// If we reach the beginning, start from the end
|
|
||||||
if (i < 0) {
|
|
||||||
i = elements.length - 1
|
|
||||||
}
|
|
||||||
if (!elements[i].disabled) {
|
|
||||||
event.preventDefault()
|
|
||||||
elements[i].focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,5 +145,7 @@ export function useFocusMenubar({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
}
|
}
|
||||||
}, [containerRef, editor])
|
}, [containerRef, editor, focusNextButton, focusPreviousButton])
|
||||||
|
|
||||||
|
return { focusNextButton, focusPreviousButton, focusButton }
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,6 @@ export class FloatingMenuView {
|
|||||||
const isRootDepth = $anchor.depth === 1
|
const isRootDepth = $anchor.depth === 1
|
||||||
const isEmptyTextBlock = $anchor.parent.isTextblock && !$anchor.parent.type.spec.code && !$anchor.parent.textContent
|
const isEmptyTextBlock = $anchor.parent.isTextblock && !$anchor.parent.type.spec.code && !$anchor.parent.textContent
|
||||||
|
|
||||||
console.log('should ran')
|
|
||||||
if (
|
if (
|
||||||
!view.hasFocus()
|
!view.hasFocus()
|
||||||
|| !empty
|
|| !empty
|
||||||
@ -94,7 +93,6 @@ export class FloatingMenuView {
|
|||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.log(true)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -173,7 +171,6 @@ export class FloatingMenuView {
|
|||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('should show', shouldShow)
|
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
this.shouldShow = shouldShow
|
this.shouldShow = shouldShow
|
||||||
}
|
}
|
||||||
|
@ -11,74 +11,84 @@ export type BubbleMenuProps = Omit<
|
|||||||
'element' | 'editor'
|
'element' | 'editor'
|
||||||
> & {
|
> & {
|
||||||
editor: BubbleMenuPluginProps['editor'] | null;
|
editor: BubbleMenuPluginProps['editor'] | null;
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
updateDelay?: number;
|
updateDelay?: number;
|
||||||
resizeDelay?: number;
|
resizeDelay?: number;
|
||||||
options?: BubbleMenuPluginProps['options'];
|
options?: BubbleMenuPluginProps['options'];
|
||||||
} & Partial<Omit<HTMLDivElement, 'children' | 'class'>>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(({
|
export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
||||||
pluginKey = 'bubbleMenu',
|
(
|
||||||
editor,
|
{
|
||||||
updateDelay,
|
pluginKey = 'bubbleMenu',
|
||||||
resizeDelay,
|
editor,
|
||||||
shouldShow = null,
|
|
||||||
options,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}, ref) => {
|
|
||||||
const menuEl = useRef(Object.assign(document.createElement('div'), restProps))
|
|
||||||
|
|
||||||
if (typeof ref === 'function') {
|
|
||||||
ref(menuEl.current)
|
|
||||||
} else if (ref) {
|
|
||||||
ref.current = menuEl.current
|
|
||||||
}
|
|
||||||
|
|
||||||
const { editor: currentEditor } = useCurrentEditor()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const bubbleMenuElement = menuEl.current
|
|
||||||
|
|
||||||
bubbleMenuElement.style.visibility = 'hidden'
|
|
||||||
bubbleMenuElement.style.position = 'absolute'
|
|
||||||
|
|
||||||
if (editor?.isDestroyed || currentEditor?.isDestroyed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachToEditor = editor || currentEditor
|
|
||||||
|
|
||||||
if (!attachToEditor) {
|
|
||||||
console.warn(
|
|
||||||
'BubbleMenu component is not rendered inside of an editor component or does not have editor prop.',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin = BubbleMenuPlugin({
|
|
||||||
updateDelay,
|
updateDelay,
|
||||||
resizeDelay,
|
resizeDelay,
|
||||||
editor: attachToEditor,
|
shouldShow = null,
|
||||||
element: bubbleMenuElement,
|
|
||||||
pluginKey,
|
|
||||||
shouldShow,
|
|
||||||
options,
|
options,
|
||||||
})
|
children,
|
||||||
|
...restProps
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const menuEl = useRef(document.createElement('div'))
|
||||||
|
|
||||||
attachToEditor.registerPlugin(plugin)
|
if (typeof ref === 'function') {
|
||||||
|
ref(menuEl.current)
|
||||||
return () => {
|
} else if (ref) {
|
||||||
attachToEditor.unregisterPlugin(pluginKey)
|
ref.current = menuEl.current
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
if (bubbleMenuElement.parentNode) {
|
|
||||||
bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [editor, currentEditor])
|
|
||||||
|
|
||||||
return createPortal(<>{children}</>, menuEl.current)
|
const { editor: currentEditor } = useCurrentEditor()
|
||||||
})
|
|
||||||
|
useEffect(() => {
|
||||||
|
const bubbleMenuElement = menuEl.current
|
||||||
|
|
||||||
|
bubbleMenuElement.style.visibility = 'hidden'
|
||||||
|
bubbleMenuElement.style.position = 'absolute'
|
||||||
|
|
||||||
|
if (editor?.isDestroyed || currentEditor?.isDestroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachToEditor = editor || currentEditor
|
||||||
|
|
||||||
|
if (!attachToEditor) {
|
||||||
|
console.warn(
|
||||||
|
'BubbleMenu component is not rendered inside of an editor component or does not have editor prop.',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = BubbleMenuPlugin({
|
||||||
|
updateDelay,
|
||||||
|
resizeDelay,
|
||||||
|
editor: attachToEditor,
|
||||||
|
element: bubbleMenuElement,
|
||||||
|
pluginKey,
|
||||||
|
shouldShow,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|
||||||
|
attachToEditor.registerPlugin(plugin)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
attachToEditor.unregisterPlugin(pluginKey)
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (bubbleMenuElement.parentNode) {
|
||||||
|
bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [editor, currentEditor])
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
menuEl.current,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -11,10 +11,8 @@ export type FloatingMenuProps = Omit<
|
|||||||
'element' | 'editor'
|
'element' | 'editor'
|
||||||
> & {
|
> & {
|
||||||
editor: FloatingMenuPluginProps['editor'] | null;
|
editor: FloatingMenuPluginProps['editor'] | null;
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
options?: FloatingMenuPluginProps['options'];
|
options?: FloatingMenuPluginProps['options'];
|
||||||
} & Partial<Omit<HTMLDivElement, 'children' | 'class'>>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(({
|
export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(({
|
||||||
pluginKey = 'floatingMenu',
|
pluginKey = 'floatingMenu',
|
||||||
@ -24,7 +22,7 @@ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const menuEl = useRef(Object.assign(document.createElement('div'), restProps))
|
const menuEl = useRef(document.createElement('div'))
|
||||||
|
|
||||||
if (typeof ref === 'function') {
|
if (typeof ref === 'function') {
|
||||||
ref(menuEl.current)
|
ref(menuEl.current)
|
||||||
@ -74,5 +72,12 @@ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [editor, currentEditor])
|
}, [editor, currentEditor])
|
||||||
|
|
||||||
return createPortal(<>{children}</>, menuEl.current)
|
return createPortal(
|
||||||
|
<div
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
menuEl.current,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user