import { LocationDescriptor, MeatballMenuApi } from "@heydovetail/ui-components"; import { Plugin } from "prosemirror-state"; import React from "react"; import { style } from "typestyle-react"; import { getPluginStateOrThrow as getRangePluginStateOrThrow } from "../range/RangePlugin"; import { HighlightRange, RangeType } from "../range/types"; import { EditorSchema } from "../schema"; import { el } from "../util/dom"; import { PluginDescriptor } from "../util/PluginDescriptor"; import { PortalProviderApi } from "../util/PortalProvider"; import { focusRange } from "./commands"; import { getPluginStateOrThrow as getHighlightWidgetPluginStateOrThrow } from "./HighlightWidgetPlugin"; import { removeTagFromHighlight } from "./operations"; import { HighlightSidebar } from "./react/HighlightSidebar"; import { getPluginStateOrThrow as getEditablePluginStateOrThrow } from "../editable/EditablePlugin"; export interface PluginState { readonly container: HTMLDivElement; readonly visible: boolean; readonly width: number; } function pluginStateEq(a: PluginState, b: PluginState) { return a.visible === b.visible && a.width === b.width; } const { key, getPluginState, getPluginStateOrThrow, setPluginState } = new PluginDescriptor( "HighlightSidebarPlugin" ); export { getPluginStateOrThrow, setPluginState }; export class HighlightSidebarPlugin extends Plugin { constructor( portalProviderApi: PortalProviderApi, generateTagURL: (tagId: string) => LocationDescriptor, onCopyHighlightURL?: (rangeId: string) => void, tagQuickEditRenderer?: (tagId: string, menuApi: MeatballMenuApi) => React.ReactNode ) { super({ key, state: { init: (): PluginState => ({ container: el( "div", style({ position: "absolute", top: 0, left: "calc(100% + 16px)", height: "100%" }) ), visible: true, width: 0 }), apply(tr, cur: PluginState): PluginState { const nextPluginState = getPluginState(tr); return nextPluginState !== null ? nextPluginState : cur; } }, view: view => { const initialPluginState = getPluginStateOrThrow(view.state); const initialRangePluginState = getRangePluginStateOrThrow(view.state); const { tagLookup: initialTagLookup } = getHighlightWidgetPluginStateOrThrow(view.state); const { editable: initialEditable } = getEditablePluginStateOrThrow(view.state); let raf: number | null = null; const recalculateWidth = () => { const prev = getPluginStateOrThrow(view.state); if (prev.width !== window!.innerWidth - prev.container.getBoundingClientRect().left - 16) { setPluginState(view, prev => ({ ...prev, width: window!.innerWidth - prev.container.getBoundingClientRect().left - 16 })); } raf = window!.requestAnimationFrame(recalculateWidth); }; raf = window!.requestAnimationFrame(recalculateWidth); const container = initialPluginState.container; let parentTopOffset = 0; let initialWidth = 0; if (view.dom.parentElement !== null) { view.dom.parentElement.insertBefore(container, view.dom.parentElement.firstChild); initialWidth = window!.innerWidth - container.getBoundingClientRect().left - 16; parentTopOffset = view.dom.parentElement.getBoundingClientRect().top; } const highlightedRanges = initialRangePluginState.sortedRanges .filter((range): range is HighlightRange => range.type === RangeType.HIGHLIGHT) .map(range => ({ ...range, topOffset: view.coordsAtPos(range.from).top - parentTopOffset })); const onRemoveTag = ({ from, to, tagId }: { from: number; to: number; tagId: string }) => view.dispatch(removeTagFromHighlight(view.state, { from, to }, tagId)); const onFocusRange = (rangeId: string | null) => focusRange(rangeId)(view.state, view.dispatch); portalProviderApi.render( , container ); return { update: (view, prevState) => { const prevPluginState = getPluginStateOrThrow(prevState); const newPluginState = getPluginStateOrThrow(view.state); const prevRangePluginState = getRangePluginStateOrThrow(prevState); const newRangePluginState = getRangePluginStateOrThrow(view.state); const { tagLookup } = getHighlightWidgetPluginStateOrThrow(view.state); const { editable } = getEditablePluginStateOrThrow(view.state); const onRemoveTag = ({ from, to, tagId }: { from: number; to: number; tagId: string }) => view.dispatch(removeTagFromHighlight(view.state, { from, to }, tagId)); const onFocusRange = (rangeId: string | null) => focusRange(rangeId)(view.state, view.dispatch); if ( !pluginStateEq(prevPluginState, newPluginState) || prevState.doc !== view.state.doc || JSON.stringify(prevRangePluginState.sortedRanges) !== JSON.stringify(newRangePluginState) ) { if (view.dom.parentElement !== null) { parentTopOffset = view.dom.parentElement.getBoundingClientRect().top; } const highlightedRanges = newRangePluginState.sortedRanges .filter((range): range is HighlightRange => range.type === RangeType.HIGHLIGHT) .map(range => ({ ...range, topOffset: view.coordsAtPos(range.from).top - parentTopOffset })); portalProviderApi.render( , container ); } }, destroy: () => { if (container.parentElement !== null) { container.parentElement.removeChild(container); } if (raf !== null) { window!.cancelAnimationFrame(raf); } portalProviderApi.destroy(container); } }; } }); } }