packages/remirror__react-hooks/src/use-multi-positioner.ts
import { RefCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { omitUndefined } from '@remirror/core';
import {
defaultAbsolutePosition,
ElementsAddedProps,
getPositioner,
PositionerExtension,
PositionerParam,
PositionerPosition,
} from '@remirror/extension-positioner';
import { useExtension } from '@remirror/react-core';
import { usePrevious } from './use-previous';
export interface UseMultiPositionerReturn<Data = any> extends PositionerPosition {
/**
* This ref must be applied to the component that is being positioned in order
* to correctly obtain the position data.
*/
ref: RefCallback<HTMLElement>;
/**
* The element that the ref has found.
*/
element?: HTMLElement;
/**
* A key to uniquely identify this positioner. This can be applied to the
* react element.
*/
key: string;
/**
* Metadata associated with the position
*/
data: Data;
}
/**
* A positioner for your editor. This returns an array of active positions and
* is useful for tracking the positions of multiple items in the editor.
*
* ```ts
* import { Positioner } from 'remirror/extensions';
* import { useMultiPositioner } from '@remirror/react';
*
* const positioner = Positioner.create({
* ...config, // custom config
* })
*
* const MenuComponent: FC = () => {
* const positions = usePositioner(positioner, []);
*
* return (
* <>
* {
* positions.map(({ ref, bottom, left, key }) => (
* <div style={{ bottom, left }} ref={ref} key={key}>
* <MenuIcon {...options} />
* </div>
* )
* }
* </>
* )
* };
* ```
*
* @param positioner - the positioner which will be used
* @param deps - an array of dependencies which will cause the hook to rerender
* with an updated positioner. This is the only way to update the positioner.
*/
export function useMultiPositioner<Data = any>(
positioner: PositionerParam,
deps: unknown[],
): Array<UseMultiPositionerReturn<Data>> {
interface CollectElementRef {
ref: RefCallback<HTMLElement>;
id: string;
data: Data;
}
const [state, setState] = useState<ElementsAddedProps[]>([]);
const [memoizedPositioner, setMemoizedPositioner] = useState(() => getPositioner(positioner));
const [collectRefs, setCollectRefs] = useState<CollectElementRef[]>([]);
const positionerRef = useRef(positioner);
const previousPositioner = usePrevious(memoizedPositioner);
positionerRef.current = positioner;
useExtension(
PositionerExtension,
({ addCustomHandler }) => {
const positioner = getPositioner(positionerRef.current);
const dispose = addCustomHandler('positioner', positioner);
setMemoizedPositioner(positioner);
return dispose;
},
deps,
);
// Add the positioner update handlers.
useLayoutEffect(() => {
const disposeUpdate = memoizedPositioner.addListener('update', (options) => {
const items: CollectElementRef[] = [];
for (const { id, data, setElement } of options) {
const ref: RefCallback<HTMLElement> = (element) => {
if (!element) {
return;
}
setElement(element);
};
items.push({ id, data, ref });
}
setCollectRefs(items);
});
const disposeDone = memoizedPositioner.addListener('done', (options) => {
setState(options);
});
if (previousPositioner?.recentUpdate) {
memoizedPositioner.onActiveChanged(previousPositioner?.recentUpdate);
}
return () => {
disposeUpdate();
disposeDone();
};
}, [memoizedPositioner, previousPositioner]);
return useMemo(() => {
const positions: UseMultiPositionerReturn[] = [];
for (const [index, { ref, data, id: key }] of collectRefs.entries()) {
const stateValue = state[index];
const { element, position = {} } = stateValue ?? {};
const absolutePosition = { ...defaultAbsolutePosition, ...omitUndefined(position) };
positions.push({ ref, element, data, key, ...absolutePosition });
}
return positions;
}, [collectRefs, state]);
}