import { Plugin } from "prosemirror-state"; import React from "react"; import { getPluginStateOrThrow as getRangePluginStateOrThrow } from "../range/RangePlugin"; import { HighlightRange, RangeType } from "../range/types"; import { EditorSchema } from "../schema"; import { getPluginStateOrThrow as getHighlightWidgetPluginStateOrThrow } from "./HighlightWidgetPlugin"; import { el, isMouseMove } from "../util/dom"; import { PluginDescriptor } from "../util/PluginDescriptor"; import { PortalProviderApi } from "../util/PortalProvider"; import { HighlightHover } from "./react/HighlightHover"; import { ITag } from "@heydovetail/ui-components/lib.es2015/components/Highlighter"; export interface PluginState { readonly container: HTMLDivElement; readonly timeout: NodeJS.Timer | null; readonly mousePosition: { x: number; y: number } | null; } function pluginStateEq(a: PluginState, b: PluginState) { return JSON.stringify(a) === JSON.stringify(b); } const { key, getPluginState, getPluginStateOrThrow, setPluginState } = new PluginDescriptor( "HighlightHoverPlugin" ); export class HighlightHoverPlugin extends Plugin { constructor(portalProviderApi: PortalProviderApi) { super({ key, state: { init: (): PluginState => ({ container: el("div"), mousePosition: null, timeout: null }), apply: (tr, curr: PluginState): PluginState => { const nextPluginState = getPluginState(tr); return nextPluginState !== null ? nextPluginState : curr; } }, view: view => { const initialPluginState = getPluginStateOrThrow(view.state); const container = initialPluginState.container; if (view.dom.parentElement !== null) { view.dom.parentElement.insertBefore(container, view.dom.parentElement.firstChild); } return { update: (view, prevState) => { const prevPluginState = getPluginStateOrThrow(prevState); const newPluginState = getPluginStateOrThrow(view.state); if (!pluginStateEq(prevPluginState, newPluginState)) { const { mousePosition } = newPluginState; const { widgetState, tagLookup } = getHighlightWidgetPluginStateOrThrow(view.state); const rangeState = getRangePluginStateOrThrow(view.state); if (mousePosition !== null && view.dom.parentElement !== null) { const position = view.posAtCoords({ left: mousePosition.x, top: mousePosition.y }); if ( position != null && (widgetState.type !== "tagging" || (position.pos < widgetState.selection.from || position.pos > widgetState.selection.to)) ) { const anchorPosition = { x: mousePosition.x - view.dom.parentElement.getBoundingClientRect().left, y: mousePosition.y - view.dom.parentElement.getBoundingClientRect().top }; const tagIds = rangeState.sortedRanges .filter( (range): range is HighlightRange => position.pos >= range.from && position.pos <= range.to && range.type === RangeType.HIGHLIGHT ) .reduce((accum, prev) => [...accum, ...prev.attrs.tagIds], []); portalProviderApi.render( tagLookup.get(id)).filter((tag): tag is ITag => tag !== undefined)} />, container ); } else { portalProviderApi.destroy(container); } } else { portalProviderApi.destroy(container); } } }, destroy: () => { if (container.parentElement !== null) { container.parentElement.removeChild(container); } } }; }, props: { handleDOMEvents: { mousemove: (view, event) => { if (isMouseMove(event)) { const prevPluginState = getPluginStateOrThrow(view.state); if (prevPluginState.timeout !== null) { clearTimeout(prevPluginState.timeout); } setPluginState(view, prevPluginState => ({ ...prevPluginState, mousePosition: null, timeout: setTimeout(() => { setPluginState(view, prevPluginState => ({ ...prevPluginState, mousePosition: { x: event.clientX, y: event.clientY } })); }, 300) })); } return false; }, mouseout: view => { const prevPluginState = getPluginStateOrThrow(view.state); if (prevPluginState.timeout !== null) { clearTimeout(prevPluginState.timeout); } setPluginState(view, prevPluginState => ({ ...prevPluginState, mousePosition: null, timeout: null })); return false; } } } }); } }