import { Plugin } from "prosemirror-state"; import React from "react"; import { style } from "typestyle-react"; import { UxCommand } from "../constants"; import { EditorSchema, schema } from "../schema"; import { el } from "../util/dom"; import { PluginDescriptor } from "../util/PluginDescriptor"; import { PortalProviderApi } from "../util/PortalProvider"; import { uxCommands } from "../uxCommands"; import { isBqActive, isHActive, isMarkActive, isOlActive, isUlActive } from "./isActive"; import { ToolbarStalker } from "./react/ToolbarStalker"; export interface PluginState { readonly container: HTMLDivElement; readonly active: { readonly b: boolean; readonly bq: boolean; readonly h1: boolean; readonly h2: boolean; readonly i: boolean; readonly ol: boolean; readonly s: boolean; readonly u: boolean; readonly ul: boolean; }; } function pluginStateEq(a: PluginState, b: PluginState) { return JSON.stringify(a) === JSON.stringify(b); } const { key, getPluginState, getPluginStateOrThrow, setPluginState } = new PluginDescriptor("ToolbarPlugin"); export { getPluginStateOrThrow, setPluginState }; export class ToolbarPlugin extends Plugin { constructor(portalProviderApi: PortalProviderApi) { super({ key, state: { init: (): PluginState => ({ container: el( "div", // Elements that are `position:sticky` are only free to move around // within their parent. This means that if we want the toolbar to // stalk the full height of the editor, a simple approach is to make // it a sibling to the editor content. // // 56px is the height of the (sticky) NoteHeader in Dovetail // 32px is the gap we want between the NoteHeader and the toolbar // Thus 56 + 32 = 88 style({ position: ["-webkit-sticky", "sticky"], top: 88 }) ), active: { b: false, bq: false, h1: false, h2: false, i: false, ol: false, s: false, u: false, ul: false } }), apply(tr, cur: PluginState, _oldState, newState): PluginState { const nextPluginState = getPluginState(tr); return nextPluginState !== null ? nextPluginState : tr.docChanged || tr.selectionSet ? { ...cur, active: { b: isMarkActive(newState, schema.marks.b), bq: isBqActive(newState), h1: isHActive(newState, 1), h2: isHActive(newState, 2), i: isMarkActive(newState, schema.marks.i), ol: isOlActive(newState), s: isMarkActive(newState, schema.marks.s), u: isMarkActive(newState, schema.marks.u), ul: isUlActive(newState) } } : cur; } }, view: view => { const initialPluginState = getPluginStateOrThrow(view.state); const container = initialPluginState.container; const handleUxCommand = (uxCommand: UxCommand) => uxCommands[uxCommand](view.state, tr => view.dispatch(tr)); if (view.dom.parentElement !== null) { view.dom.parentElement.insertBefore(container, view.dom.parentElement.firstChild); } if (view.someProp("editable") == null || view.someProp("editable")(view.state)) { portalProviderApi.render( , container ); } return { update: (view, prevState) => { const prevPluginState = getPluginStateOrThrow(prevState); const newPluginState = getPluginStateOrThrow(view.state); if ( (view.someProp("editable") == null || view.someProp("editable")(view.state)) && !pluginStateEq(prevPluginState, newPluginState) ) { portalProviderApi.render( , container ); } }, destroy: () => { if (container.parentElement !== null) { container.parentElement.removeChild(container); } portalProviderApi.destroy(container); } }; } }); } }