Tooltip (selection size example) in React

I’ve been learning/evaluating the framework the past couple of days and I have re-implement the Tooltip example that’s available in the docs.

The approach I took was to allow the plugin class to control and render a css-component (in this case implemented with glamorous). I’m sure the code can be improved, anyways, here it is.

SelectionSizeTooltipPlugin.js:

import React from 'react';
import { render as renderReact } from 'react-dom';
import { Plugin } from 'prosemirror-state';
import { SelectionSizeTooltip } from './components/SelectionSizeTooltip';
import { areStatesEqual } from '../../utils';

class SelectionSizeTooltipPlugin {
	constructor(view) {
		this.tooltip = document.createElement('div');
		view.dom.parentNode.appendChild(this.tooltip);

		this.update(view, null);
	}

	render(view) {
		const { from, to, empty } = view.state.selection;

		const start = view.coordsAtPos(from);
		const end = view.coordsAtPos(to);
		const box = this.tooltip.offsetParent.getBoundingClientRect();
		const left = Math.max((start.left + end.left) / 2, start.left + 3);

		const leftSpacing = `${left - box.left}px`;
		const bottomSpacing = `${box.bottom - start.top}px`;
		const numberOfCharactersInSelection = to - from;

		return (
			<SelectionSizeTooltip
				display={empty ? 'none' : undefined}
				left={leftSpacing}
				bottom={bottomSpacing}
			>
				{numberOfCharactersInSelection}
			</SelectionSizeTooltip>
		);
	}

	update(view, lastState) {
		const { state } = view;

		if (areStatesEqual(state, lastState)) return;

		const tooltipComponent = this.render(view);
		renderReact(tooltipComponent, this.tooltip);
	}

	destroy() {
		this.tooltip.remove();
	}
}

const selectionSizeTooltipPlugin = new Plugin({
	view(editorView) {
		return new SelectionSizeTooltipPlugin(editorView);
	},
});

export { selectionSizeTooltipPlugin };

SelectionSizeTooltip.js:

/* eslint-disable no-useless-return */
import PropTypes from 'prop-types';
import glamorous from 'glamorous';

const SelectionSizeTooltip = glamorous('div', { propsAreCssOverrides: true })({
	position: 'absolute',
	pointerEvents: 'none',
	zIndex: 20,
	background: 'white',
	border: '1px solid silver',
	borderRadius: '2px',
	padding: '2px 10px',
	marginBottom: '7px',
	transform: 'translateX(-50%)',
	'&::before': {
		content: '',
		height: 0,
		width: 0,
		position: 'absolute',
		left: '50%',
		marginLeft: '-5px',
		bottom: '-6px',
		border: '5px solid transparent',
		borderBottomWidth: 0,
		borderTopColor: 'silver',
	},
	'&::after': {
		content: '',
		height: 0,
		width: 0,
		position: 'absolute',
		left: '50%',
		marginLeft: '-5px',
		bottom: '-4.5px',
		border: '5px solid transparent',
		borderBottomWidth: 0,
		borderTopColor: 'white',
	},
});

SelectionSizeTooltip.propTypes = {
	display: PropTypes.string,
	children: PropTypes.number.isRequired,
	left: PropTypes.string.isRequired,
	bottom: PropTypes.string.isRequired,
};

SelectionSizeTooltip.defaultProps = {
	display: undefined,
};

export { SelectionSizeTooltip };

I’d much appreciate if someone had input on how to improve the cycle of rendering/updating. It would be nice to replace createElement, this.update(view, null) and/or reactDOM.render with something more declarative.

Also interested in hearing if someone managed to implement a plugin where the plugin class is a react component or if it makes more sense to have something in between like in my approach.

Hopefully this is useful to someone!

I implemented a plugin that basically display a tooltip for a link node .

image

my implementation is similar to what you posted, and there might be some improvement that you could try.

  • always use React.PureComponent to avoid excessive redundant rendering. Note that ProseMirror always gives you you the immutable data so it work really well with React.PureComponent.
  • make sure that you call ReactDOM.unmountComponentAtNode(el) when the plugin’s view is destroyed to avoid memory leak.

thanks.

1 Like