@atlaskit/renderer
Version:
Renderer component
508 lines (501 loc) • 20.5 kB
JavaScript
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);
};