UNPKG

@atlaskit/renderer

Version:
508 lines (501 loc) • 20.5 kB
import React, { createContext, useContext, useEffect, useMemo, useReducer, useRef } from 'react'; import { AnnotationMarkStates, AnnotationTypes } from '@atlaskit/adf-schema'; import { AnnotationUpdateEvent } from '@atlaskit/editor-common/types'; import { RendererContext } from '../../../ui/RendererActionsContext'; const initState = { isDrafting: false, draftId: undefined, draftMarkRef: undefined, draftActionResult: undefined, annotations: {}, currentSelectedAnnotationId: undefined, currentSelectedMarkRef: undefined, currentHoveredAnnotationId: undefined }; const AnnotationManagerStateContext = /*#__PURE__*/createContext(initState); const AnnotationManagerDispatchContext = /*#__PURE__*/createContext({ annotationManager: undefined, dispatch: () => {} }); function reducer(state, action) { switch (action.type) { case 'reset': { return { ...state, ...initState }; } case 'loadAnnotation': { const currentIds = Object.keys(state.annotations); const uids = Array.from(new Set(currentIds.concat(action.data.map(a => a.id)))); const updates = []; for (const id of uids) { var _state$annotations$id, _state$annotations$id2; const loadedAnnotation = action.data.find(a => a.id === id); if (!loadedAnnotation && ((_state$annotations$id = state.annotations[id]) === null || _state$annotations$id === void 0 ? void 0 : _state$annotations$id.markState) === AnnotationMarkStates.ACTIVE) { // If the annotation is not in the loaded data, we need to remove it from the state. However, // rather then removing it, we will set the mark state to resolved. This is to align it better with // how the editor works. updates.push({ id, markState: AnnotationMarkStates.RESOLVED }); continue; } if (!!(loadedAnnotation !== null && loadedAnnotation !== void 0 && loadedAnnotation.markState) && ((_state$annotations$id2 = state.annotations[id]) === null || _state$annotations$id2 === void 0 ? void 0 : _state$annotations$id2.markState) !== loadedAnnotation.markState) { updates.push({ id, markState: loadedAnnotation.markState }); continue; } } if (updates.length > 0) { return { ...state, annotations: updates.reduce((nextAnnotations, update) => { return { ...nextAnnotations, [update.id]: { ...nextAnnotations[update.id], ...update } }; }, state.annotations) }; } return state; } case 'updateAnnotation': { const current = state.annotations[action.data.id]; const { id, selected, hovered, markState = current === null || current === void 0 ? void 0 : current.markState } = action.data; const updates = []; // If the annotation is not currently in the state, we need to add it to the state. if (!current) { updates.push({ id, markState: markState !== null && markState !== void 0 ? markState : AnnotationMarkStates.ACTIVE }); } // The goal of the following is to enforce a single selection and a single hover state across all annotations. let nextSelectedId = state.currentSelectedAnnotationId; if (selected && nextSelectedId !== id) { nextSelectedId = id; } // If the annotation is currently selected and it's being unselected, we need to remove it from the // current selected annotation id. if (selected === false && nextSelectedId === id) { nextSelectedId = undefined; } let nextHoveredId = state.currentHoveredAnnotationId; if (hovered && nextHoveredId !== id) { nextHoveredId = id; } // If the annotation is currently hovered and it's being unhovered, we need to remove it from the // current hovered annotation id. if (hovered === false && nextHoveredId === id) { nextHoveredId = undefined; } // If the annotations mark state is not the same as the current mark state, we need to update it. if ((current === null || current === void 0 ? void 0 : current.markState) !== markState) { updates.push({ id, markState: markState !== null && markState !== void 0 ? markState : AnnotationMarkStates.ACTIVE }); // If the annotation is currently selected and it's being resolved, then we need to remove it from the // current selected annotation id also. if (markState === AnnotationMarkStates.RESOLVED && nextSelectedId === id) { nextSelectedId = undefined; } } if (updates.length > 0 || nextSelectedId !== state.currentSelectedAnnotationId || nextHoveredId !== state.currentHoveredAnnotationId) { return { ...state, currentSelectedAnnotationId: nextSelectedId, currentHoveredAnnotationId: nextHoveredId, annotations: updates.reduce((nextAnnotations, update) => { return { ...nextAnnotations, [update.id]: { ...nextAnnotations[update.id], ...update } }; }, state.annotations) }; } return state; } case 'resetSelectedAnnotation': { if (state.currentSelectedAnnotationId !== undefined) { return { ...state, currentSelectedAnnotationId: undefined, currentSelectedMarkRef: undefined }; } return state; } case 'resetHoveredAnnotation': { if (state.currentHoveredAnnotationId !== undefined) { return { ...state, currentHoveredAnnotationId: undefined }; } return state; } case 'setDrafting': { if (state.isDrafting !== action.data.isDrafting || state.draftId !== action.data.draftId || state.draftActionResult !== action.data.draftActionResult) { // XXX: When a draft is open the current selected annotation should no longer be selected. We need // to decide what is better UX, // 1 - do we want to deselct the selected annotation on draft open, if so then when draft is closed the s // selected annotation will not come back // 2 - do we want to still allow the selected annotation to be selected when draft is open, however the underlying // mark style just shows the annotation as not selected when a draft is active. Then when a draft closes // we can reopen the previous selected annotation. return { ...state, isDrafting: action.data.isDrafting, draftId: action.data.draftId, draftActionResult: action.data.draftActionResult }; } return state; } case 'setDraftMarkRef': { if (state.draftMarkRef !== action.data.draftMarkRef) { return { ...state, draftMarkRef: action.data.draftMarkRef }; } return state; } case 'setSelectedMarkRef': { if (state.currentSelectedMarkRef !== action.data.markRef) { return { ...state, currentSelectedMarkRef: action.data.markRef }; } return state; } } } export const AnnotationManagerProvider = ({ children, annotationManager, updateSubscriber }) => { const [state, dispatch] = useReducer(reducer, initState); const actionContext = useContext(RendererContext); useEffect(() => { const getDraft = () => { var _state$draftActionRes; if (!state.isDrafting || !state.draftActionResult || !state.draftMarkRef || !state.draftId) { return { success: false, reason: 'draft-not-started' }; } return { success: true, inlineNodeTypes: (_state$draftActionRes = state.draftActionResult.inlineNodeTypes) !== null && _state$draftActionRes !== void 0 ? _state$draftActionRes : [], targetElement: state.draftMarkRef, actionResult: { step: state.draftActionResult.step, doc: state.draftActionResult.doc, inlineNodeTypes: state.draftActionResult.inlineNodeTypes, targetNodeType: state.draftActionResult.targetNodeType, originalSelection: state.draftActionResult.originalSelection, numMatches: state.draftActionResult.numMatches, matchIndex: state.draftActionResult.matchIndex, pos: state.draftActionResult.pos } }; }; annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.hook('getDraft', getDraft); return () => { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.unhook('getDraft', getDraft); }; }, [annotationManager, state.draftId, state.isDrafting, state.draftMarkRef, state.draftActionResult]); // We need to watch for the draft mark element to exist, so we can inform any listeners that the draft has been started // and give them a reference to the element useEffect(() => { if (state.isDrafting && state.draftId && state.draftMarkRef && state.draftActionResult) { var _state$draftActionRes2; annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.emit({ name: 'draftAnnotationStarted', data: { inlineNodeTypes: (_state$draftActionRes2 = state.draftActionResult.inlineNodeTypes) !== null && _state$draftActionRes2 !== void 0 ? _state$draftActionRes2 : [], targetElement: state.draftMarkRef, actionResult: { step: state.draftActionResult.step, doc: state.draftActionResult.doc, inlineNodeTypes: state.draftActionResult.inlineNodeTypes, targetNodeType: state.draftActionResult.targetNodeType, originalSelection: state.draftActionResult.originalSelection, numMatches: state.draftActionResult.numMatches, matchIndex: state.draftActionResult.matchIndex, pos: state.draftActionResult.pos } } }); } }, [annotationManager, state.draftId, state.isDrafting, state.draftMarkRef, state.draftActionResult]); useEffect(() => { const setIsAnnotationSelected = (id, isSelected) => { var _state$annotations; if (state.isDrafting) { return { success: false, reason: 'draft-in-progress' }; } if (!((_state$annotations = state.annotations) !== null && _state$annotations !== void 0 && _state$annotations[id])) { return { success: false, reason: 'id-not-valid' }; } // the annotation is currently not selected and is being selected if (id !== state.currentSelectedAnnotationId && isSelected) { dispatch({ type: 'updateAnnotation', data: { id, selected: true } }); dispatch({ type: 'setSelectedMarkRef', data: { // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- resolve mark node by id in the document markRef: document.getElementById(id) || undefined } }); } // the annotation is currently selected and is being unselected else if (id === state.currentSelectedAnnotationId && !isSelected) { dispatch({ type: 'resetSelectedAnnotation' }); } return { success: true, isSelected }; }; annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.hook('setIsAnnotationSelected', setIsAnnotationSelected); return () => { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.unhook('setIsAnnotationSelected', setIsAnnotationSelected); }; }, [annotationManager, state.isDrafting, state.annotations, state.currentSelectedAnnotationId, state.currentSelectedMarkRef]); const prevSelectedAnnotationId = useRef(undefined); useEffect(() => { if (prevSelectedAnnotationId.current) { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.emit({ name: 'annotationSelectionChanged', data: { annotationId: prevSelectedAnnotationId.current, isSelected: false, inlineNodeTypes: [] } }); } prevSelectedAnnotationId.current = state.currentSelectedAnnotationId; }, [state.currentSelectedAnnotationId, annotationManager]); useEffect(() => { if (state.currentSelectedAnnotationId && state.currentSelectedMarkRef && state.currentSelectedMarkRef.id === state.currentSelectedAnnotationId) { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.emit({ name: 'annotationSelectionChanged', data: { annotationId: state.currentSelectedAnnotationId, isSelected: true, inlineNodeTypes: [] } }); } }, [annotationManager, state.currentSelectedAnnotationId, state.currentSelectedMarkRef]); useEffect(() => { const setIsAnnotationHovered = (id, isHovered) => { var _state$annotations2; if (!((_state$annotations2 = state.annotations) !== null && _state$annotations2 !== void 0 && _state$annotations2[id])) { return { success: false, reason: 'id-not-valid' }; } dispatch({ type: 'updateAnnotation', data: { id, hovered: isHovered } }); return { success: true, isHovered }; }; annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.hook('setIsAnnotationHovered', setIsAnnotationHovered); return () => { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.unhook('setIsAnnotationHovered', setIsAnnotationHovered); }; }, [annotationManager, state.annotations]); useEffect(() => { const clearAnnotation = id => { var _state$annotations3; if (!((_state$annotations3 = state.annotations) !== null && _state$annotations3 !== void 0 && _state$annotations3[id])) { return { success: false, reason: 'id-not-valid' }; } const result = actionContext.deleteAnnotation(id, AnnotationTypes.INLINE_COMMENT); if (!result) { return { success: false, reason: 'clear-failed' }; } const { step, doc } = result; return { success: true, actionResult: { step, doc } }; }; annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.hook('clearAnnotation', clearAnnotation); return () => { annotationManager === null || annotationManager === void 0 ? void 0 : annotationManager.unhook('clearAnnotation', clearAnnotation); }; }, [annotationManager, state.annotations, actionContext]); /** * This is a temporary solution to ensure that the annotation manager state is in sync with the * old updateSubscriber. The updateSubscriber will eventually be deprecated and the state will be managed * by the annotation manager itself. */ useEffect(() => { const onSetAnnotationState = payload => { if (!payload) { return; } Object.values(payload).forEach(annotation => { if (annotation.id && annotation.annotationType === AnnotationTypes.INLINE_COMMENT) { var _annotation$state; dispatch({ type: 'updateAnnotation', data: { id: annotation.id, markState: (_annotation$state = annotation.state) !== null && _annotation$state !== void 0 ? _annotation$state : undefined } }); } }); }; const onAnnotationSelected = payload => { dispatch({ type: 'updateAnnotation', data: { id: payload.annotationId, selected: true } }); }; const onAnnotationHovered = payload => { dispatch({ type: 'updateAnnotation', data: { id: payload.annotationId, hovered: true } }); }; const onAnnotationSelectedRemoved = () => { dispatch({ type: 'resetSelectedAnnotation' }); }; const onAnnotationHoveredRemoved = () => { dispatch({ type: 'resetHoveredAnnotation' }); }; const onAnnotationClick = ({ annotationIds, eventTarget, eventTargetType: _eventTargetType, viewMethod: _viewMethod }) => { dispatch({ type: 'updateAnnotation', data: { id: annotationIds[0], selected: true } }); dispatch({ type: 'setSelectedMarkRef', data: { markRef: eventTarget } }); }; const onAnnotationDeselect = () => { dispatch({ type: 'resetSelectedAnnotation' }); }; updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.SET_ANNOTATION_STATE, onSetAnnotationState); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.SET_ANNOTATION_FOCUS, onAnnotationSelected); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.SET_ANNOTATION_HOVERED, onAnnotationHovered); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.REMOVE_ANNOTATION_FOCUS, onAnnotationSelectedRemoved); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.REMOVE_ANNOTATION_HOVERED, onAnnotationHoveredRemoved); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.ON_ANNOTATION_CLICK, onAnnotationClick); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.on(AnnotationUpdateEvent.DESELECT_ANNOTATIONS, onAnnotationDeselect); return () => { updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.SET_ANNOTATION_STATE, onSetAnnotationState); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.SET_ANNOTATION_FOCUS, onAnnotationSelected); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.SET_ANNOTATION_HOVERED, onAnnotationHovered); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.REMOVE_ANNOTATION_FOCUS, onAnnotationSelectedRemoved); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.REMOVE_ANNOTATION_HOVERED, onAnnotationHoveredRemoved); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.ON_ANNOTATION_CLICK, onAnnotationClick); updateSubscriber === null || updateSubscriber === void 0 ? void 0 : updateSubscriber.off(AnnotationUpdateEvent.DESELECT_ANNOTATIONS, onAnnotationDeselect); }; }, [updateSubscriber]); const dispatchData = useMemo(() => ({ annotationManager, dispatch }), [annotationManager, dispatch]); return /*#__PURE__*/React.createElement(AnnotationManagerStateContext.Provider, { value: state }, /*#__PURE__*/React.createElement(AnnotationManagerDispatchContext.Provider, { value: dispatchData }, children)); }; export const useAnnotationManagerState = () => { return useContext(AnnotationManagerStateContext); }; export const useAnnotationManagerDispatch = () => { return useContext(AnnotationManagerDispatchContext); };