import mapKeys from 'lodash.mapkeys'; import { forwardRef, useLayoutEffect, useImperativeHandle, useRef, useState } from 'react'; import { useFirstMountState } from 'react-use'; import { HandleEvents } from '../helpers/event'; import { Overlay } from '../overlay'; import type { UIEventHandlers } from '../types/event'; import { useNavermaps } from '../use-navermaps'; import { omitUndefined } from '../utils/omit-undefined'; import { getUncontrolledKey, makeUncontrolledKeyMap, UncontrolledKey } from '../utils/uncontrolled'; const primitiveKeys = [ 'animation', 'icon', 'shape', 'title', 'cursor', 'clickable', 'draggable', 'visible', 'zIndex', ] as const; const locationalKeys = ['position'] as const; const uncontrolledKeyMap = makeUncontrolledKeyMap(locationalKeys); const kvoKeys = [ ...primitiveKeys, ...locationalKeys, ] as const; const kvoEvents = kvoKeys.map(key => `${key}_changed`); const uiEvents = [ 'mousedown', 'mouseup', 'click', 'dblclick', 'rightclick', 'mouseover', 'mouseout', 'dragstart', 'drag', 'dragend', ] as const; const events = [...uiEvents, ...kvoEvents]; type MarkerKVO = { /** Animation??? */ animation: naver.maps.Animation; position: naver.maps.Coord | naver.maps.CoordLiteral; icon: string | naver.maps.ImageIcon | naver.maps.SymbolIcon | naver.maps.HtmlIcon; shape: naver.maps.MarkerShape; title: string; cursor: string; clickable: boolean; draggable: boolean; visible: boolean; zIndex: number; }; type UncontrolledProps = { [key in typeof locationalKeys[number] as UncontrolledKey]: MarkerKVO[key]; }; // TODO: Fix DefinitelyTyped type MarkerOptions = Partial; export type Props = MarkerOptions & Partial & UIEventHandlers & { onAnimationChanged?: (value: naver.maps.Animation) => void; onPositionChanged?: (value: naver.maps.Coord) => void; onIconChanged?: (value: string | naver.maps.ImageIcon | naver.maps.HtmlIcon | naver.maps.SymbolIcon) => void; onShapeChanged?: (event: naver.maps.MarkerShape) => void; onTitleChanged?: (event: string) => void; onCursorChanged?: (event: string) => void; onClickableChanged?: (event: boolean) => void; onDraggableChanged?: (event: boolean) => void; onVisibleChanged?: (event: boolean) => void; /** * hello yeah * @param event helo? * @returns */ onZIndexChanged?: (event: number) => void; }; function makeInitialOption(props: Props) { const uncontrolledProps = Object.fromEntries(Object.entries(props).filter(([key]) => key in uncontrolledKeyMap)) as Record; const prefixCleared = mapKeys(uncontrolledProps, (_, key) => uncontrolledKeyMap[key as keyof typeof uncontrolledKeyMap]); const kvoProps = Object.fromEntries(kvoKeys.map(key => [key, props[key]])) as Partial; return omitUndefined({ ...kvoProps, ...prefixCleared }); } function isLocationalKey(key: string): key is typeof locationalKeys[number] { return locationalKeys.includes(key as typeof locationalKeys[number]); } function isEqualKvo(kvo: any, target: any) { if (kvo === undefined) { return false; } if (kvo === target) { return true; } try { return kvo.equals(target); } catch { return kvo === target; } } export const Marker = forwardRef(function Marker(props, ref) { const navermaps = useNavermaps(); const [marker] = useState(() => new navermaps.Marker(makeInitialOption(props) as naver.maps.MarkerOptions)); useImperativeHandle(ref, () => marker); // make dirties const isFirst = useFirstMountState(); const dirtiesRef = useRef>({}); dirtiesRef.current = getDirties(); function getDirties() { // initialize의 option과 중복되지 않도록 첫 렌더시 제외한다. if (isFirst) { return {}; } return kvoKeys.reduce((acc, key) => { if (props[key] === undefined) { return acc; } if (isLocationalKey(key) && props[getUncontrolledKey(key)] !== undefined) { return acc; } const kvos = marker.getOptions(key); if (isEqualKvo(kvos[key], props[key])) { return acc; } return { ...acc, [key]: props[key], }; }, {}); } function pickDirties(keys: readonly string[]) { return Object.fromEntries(keys.map(key => [key, dirtiesRef.current[key as keyof typeof dirtiesRef.current]])) as Pick; } // side effects useLayoutEffect(() => { const { position } = pickDirties(['position']); if (position) { marker.setPosition(position); } }, [dirtiesRef.current['position']]); useLayoutEffect(() => { const dirties = pickDirties(primitiveKeys); if (Object.values(dirties).length < 1) { return; } marker.setOptions(dirties as naver.maps.MarkerOptions); }, primitiveKeys.map(key => dirtiesRef.current[key])); return ( ); });