So I am trying to create this basic functionality but I feel, the documentation here is not up to par.
While I did manage to invent a way to create my own toolbar, I think it’s quite hacky how it works. I guess creating a plugin is the way to go, but the integration to the current editor DOM is done by appending the plugin’s node to it? Well here’s my current implementation:
// ToolbarPlugin.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { Plugin } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { Toolbar } from './Toolbar'
export const toolbarPlugin = new Plugin({
view(editorView: EditorView) {
return new ToolbarPluginView(editorView)
}
})
interface IPluginProps {
editorView: EditorView
}
class ToolbarPluginView<IPluginProps> {
dom: HTMLElement
ref: React.RefObject<any>
view: EditorView
constructor(editorView: EditorView) {
this.view = editorView
this.ref = React.createRef()
this.dom = document.createElement('span')
this.view.dom.parentNode!.appendChild(this.dom)
this.dom.classList.add('plugin__dom')
ReactDOM.render(
<Toolbar ref={this.ref} editorView={this.view}/>,
this.dom
)
}
update() {
console.log('update ToolbarPluginView')
}
destroy() {
ReactDOM.unmountComponentAtNode(this.dom)
}
}
// Toolbar.tsx
import React from 'react'
import { toggleMark, setBlockType, wrapIn } from 'prosemirror-commands'
import { schema } from 'prosemirror-schema-basic'
import { EditorView } from 'prosemirror-view'
import styled, { StyledComponentClass } from 'styled-components'
import {
MdFormatBold, MdFormatItalic,
} from 'react-icons/md'
import { ITheme } from '../../../types/theme'
import { MarkButton } from './MarkButton'
interface IProps {
editorView: EditorView
}
interface IState {
}
export class Toolbar extends React.Component<IProps, IState> {
shouldComponentUpdate(nextProps: IProps, nextState: IState) {
console.log('shouldComponentUpdate Toolbar')
return true
}
render() {
const { editorView } = this.props
return (
<Container>
<h2>Toolbar</h2>
<MarksRow>
<MarkButton
active={toggleMark(schema.marks.strong)(editorView.state, undefined)}
name="bold"
icon={<MdFormatBold size={24}/>}
onClick={() => {
debugger
toggleMark(schema.marks.strong)(editorView.state, editorView.dispatch)
editorView.focus()
}}
/>
<MarkButton
active={toggleMark(schema.marks.em)(editorView.state, undefined)}
name="italic"
icon={<MdFormatItalic size={24}/>}
onClick={() => {
toggleMark(schema.marks.em)(editorView.state, editorView.dispatch)
editorView.focus()
}}
/>
</MarksRow>
</Container>
)
}
}
const Container = styled.div`
background: #7ab4ff;
& > h2 {
margin: 0 0 5px 0;
}
`
const MarksRow = styled.div`
display: flex;
`
// MarkButton.tsx
import React from 'react'
import styled from 'styled-components'
interface IProps {
className?: string
active: boolean
name: string
icon: React.ReactNode
onClick: () => void
}
function MarkButtonEl(props: IProps) {
const { className, active, name, icon, onClick } = props
return (
<Button
className={className}
active={active}
name={name}
onClick={onClick}
>
<SvgWrapper>
{icon}
</SvgWrapper>
</Button>
)
}
interface IButtonProps { active: boolean }
const SvgWrapper = styled.span`
display: flex;
`
const Button = styled.button`
background: ${(props: IButtonProps) => props.active ? '#f0f8ff' : ''};
border: ${(props: IButtonProps) => props.active ? '1px solid #5a6ecd' : '1px solid transparent'};
cursor: pointer;
display: flex;
margin-right: 5px;
padding: 1px;
&:hover {
background: #f0f8ff;
opacity: 0.6;
}
`
export const MarkButton = styled(MarkButtonEl)``
There I am creating a ToolbarPluginView, that I append with its contents to the editorview with this.view.dom.parentNode!.appendChild(this.dom)
. Which I think is a bit magical way of doing it. Also it always appends it underneath the editor, so I guess I should add some specific slot for this toolbar. Or just create the toolbar using a regular React component, passing along the editorView. What is best practise in this case?
As a side note, I think it’s fun, that ProseMirror is low-level with its DOM manipulation, but the complexity of managing DOM with its quirks and interactions is not something I would want to do by hand. It would be nice to have examples of basic PM functionality using different JS frameworks.
Anyway, moving on to the real problems. I did not find out how to get the currently active marks. I think this should be pretty basic information, but I couldn’t find it. This let active = command(this.editorView.state, null, this.editorView)
did not work. Also if I assumed correctly, I should also manually call the Toolbar to update either on every transaction or by first computing have the marks changed? The calling of editorView function such as toggleMark(schema.marks.strong)(editorView.state, undefined)
in all MarkButtons is probably bad practise. Or just store the currently active marks somewhere and pass them down to the Toolbar?
I think there’s also a bug in the toggleMark
-method, as if I activate both marks I can’t toggle either of them back. Text stays bolded and italic.
Third problem I have are the inconsistencies in the API. In the example menu the commands are executed as such: command(this.editorView.state, null, this.editorView)
. This, according to my typings, is not allowed. There’s no third argument and the second is either dispatch or undefined. I’ve found these inconsistencies to be quite widespread and eg in the schema examples there are never checks for the type of the node in getAttrs(node: string | Node)
. It’s just assumed it’s always either a Node or string, somehow. I don’t know how. Oral tradition?
I can provide an example if required.