UNPKG

@atlaskit/editor-plugin-annotation

Version:

Annotation plugin for @atlaskit/editor-core

303 lines (299 loc) 11 kB
import { AnnotationTypes } from '@atlaskit/adf-schema'; import { INPUT_METHOD, RESOLVE_METHOD } from '@atlaskit/editor-common/analytics'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { createCommand } from '../pm-plugins/plugin-factory'; import { ACTIONS } from '../pm-plugins/types'; import { getPluginState, inlineCommentPluginKey, isSelectionValid, isSupportedBlockNode } from '../pm-plugins/utils'; import { AnnotationSelectionType } from '../types'; import transform from './transform'; import { resetUserIntent, setUserIntent } from './utils'; export const updateInlineCommentResolvedState = editorAnalyticsAPI => (partialNewState, resolveMethod) => { const command = { type: ACTIONS.UPDATE_INLINE_COMMENT_STATE, data: partialNewState }; const allResolved = Object.values(partialNewState).every(state => state); // TODO: EDF-716 - This is nuking the scroll into view which occurs // when a comment is resolved. The problem is this is called when either a collab user or the current user resolves a comment. // and the problem is since cc is just dispatching an event through the provider to resolve the comment and this // is not comming via NCS, we have not way to know if this is a local or remote transaction. // To quickly fix this problem to unblock live pages this will just stop the transaction causing a scroll when the // resolve method is set. const transformer = (tr, state) => resolveMethod === RESOLVE_METHOD.CONSUMER ? tr.setMeta('scrollIntoView', false) : tr; if (resolveMethod && allResolved) { return createCommand(command, (tr, state) => transformer(transform.addResolveAnalytics(editorAnalyticsAPI)(resolveMethod)(tr, state), state)); } return createCommand(command, transformer); }; export const closeComponent = () => createCommand({ type: ACTIONS.CLOSE_COMPONENT }); export const clearDirtyMark = () => createCommand({ type: ACTIONS.INLINE_COMMENT_CLEAR_DIRTY_MARK }); export const setInlineCommentsFetched = () => createCommand({ type: ACTIONS.SET_INLINE_COMMENTS_FETCHED }); export const flushPendingSelections = editorAnalyticsAPI => (canSetAsSelectedAnnotations, errorReason) => { const command = { type: ACTIONS.FLUSH_PENDING_SELECTIONS, data: { canSetAsSelectedAnnotations } }; if (!!errorReason) { return createCommand(command, (tr, state) => transform.addPreemptiveGateErrorAnalytics(editorAnalyticsAPI)(errorReason)(tr, state)); } return createCommand(command); }; export const setPendingSelectedAnnotation = id => createCommand({ type: ACTIONS.SET_PENDING_SELECTIONS, data: { selectedAnnotations: [{ id, type: AnnotationTypes.INLINE_COMMENT }] } }); const removeInlineCommentFromNode = (id, supportedBlockNodes = [], state, dispatch) => { const { tr, selection } = state; if (selection instanceof NodeSelection && isSupportedBlockNode(selection.node, supportedBlockNodes)) { const { $from } = selection; let currNode = selection.node; let from = $from.start(); // for media annotation, the selection is on media Single if (currNode.type === state.schema.nodes.mediaSingle && currNode.firstChild) { currNode = currNode.firstChild; from = from + 1; } const { annotation: annotationMarkType } = state.schema.marks; const hasAnnotation = currNode.marks.some(mark => mark.type === annotationMarkType); if (!hasAnnotation) { return false; } tr.removeNodeMark(from, annotationMarkType.create({ id, type: AnnotationTypes.INLINE_COMMENT })); if (dispatch) { dispatch(tr); } return true; } return false; }; export const removeInlineCommentNearSelection = (id, supportedNodes = []) => (state, dispatch) => { const { tr, selection: { $from } } = state; if (removeInlineCommentFromNode(id, supportedNodes, state, dispatch)) { return true; } const { annotation: annotationMarkType } = state.schema.marks; const hasAnnotation = $from.marks().some(mark => mark.type === annotationMarkType); if (!hasAnnotation) { return false; } // just remove entire mark from around the node tr.removeMark($from.start(), $from.end(), annotationMarkType.create({ id, type: AnnotationTypes.INLINE_COMMENT })); if (dispatch) { dispatch(tr); } return true; }; export const removeInlineCommentFromDoc = editorAnalyticsAPI => (id, supportedNodes = []) => (state, dispatch) => { const { tr } = state; state.doc.descendants((node, pos) => { // Inline comment on mediaInline is not supported as part of comments on media project // Thus, we skip the decoration for mediaInline node if (node.type.name === 'mediaInline') { return false; } const isSupportedBlockNode = node.isBlock && (supportedNodes === null || supportedNodes === void 0 ? void 0 : supportedNodes.includes(node.type.name)); node.marks.filter(mark => mark.type === state.schema.marks.annotation && mark.attrs.id === id).forEach(mark => { if (isSupportedBlockNode) { tr.removeNodeMark(pos, mark); } else { tr.removeMark(pos, pos + node.nodeSize, mark); } }); }); if (dispatch) { dispatch(transform.addDeleteAnalytics(editorAnalyticsAPI)(tr, state)); return true; } return false; }; const getDraftCommandAction = (drafting, targetType, targetNodeId, supportedBlockNodes, isOpeningMediaCommentFromToolbar) => { return editorState => { // validate selection only when entering draft mode if (drafting && isSelectionValid(editorState) !== AnnotationSelectionType.VALID) { return false; } return { type: ACTIONS.SET_INLINE_COMMENT_DRAFT_STATE, data: { drafting, editorState, targetType, supportedBlockNodes, targetNodeId, isOpeningMediaCommentFromToolbar } }; }; }; /** * Show active inline comments for a given block node, otherwise, * return false if the node has no comments or no unresolved comments. * @param supportedBlockNodes * @example */ export const showInlineCommentForBlockNode = (supportedBlockNodes = []) => (node, viewMethod, isOpeningMediaCommentFromToolbar) => (state, dispatch) => { const pluginState = getPluginState(state); const { annotation } = state.schema.marks; if (node && node.isBlock && supportedBlockNodes.includes(node.type.name)) { const unresolvedAnnotationMarks = ((node === null || node === void 0 ? void 0 : node.marks) || []).filter(mark => mark.type === annotation && !(pluginState !== null && pluginState !== void 0 && pluginState.annotations[mark.attrs.id])).map(mark => ({ id: mark.attrs.id, type: mark.attrs.annotationType })); if (unresolvedAnnotationMarks.length) { if (dispatch) { // bypass createCommand with setMeta // so that external plugins can be aware of if there are active(unresolved) comments associated with the node // i.e. media plugin can use the return result (true/false) to show toggle create comment component dispatch(state.tr.setMeta(inlineCommentPluginKey, { type: ACTIONS.SET_SELECTED_ANNOTATION, data: { selectedAnnotations: unresolvedAnnotationMarks, selectAnnotationMethod: viewMethod, isOpeningMediaCommentFromToolbar } })); return true; } } } return false; }; export const setInlineCommentDraftState = (editorAnalyticsAPI, supportedBlockNodes = [], api) => (drafting, inputMethod = INPUT_METHOD.TOOLBAR, targetType = 'inline', targetNodeId = undefined, isOpeningMediaCommentFromToolbar) => { const commandAction = getDraftCommandAction(drafting, targetType, targetNodeId, supportedBlockNodes, isOpeningMediaCommentFromToolbar); if (Boolean(api === null || api === void 0 ? void 0 : api.toolbar)) { return (state, dispatch) => { const tr = transform.handleDraftState(editorAnalyticsAPI)(drafting, inputMethod)(state.tr, state); const newPluginState = commandAction(state); if (tr && newPluginState) { tr.setMeta(inlineCommentPluginKey, newPluginState); if (drafting) { setUserIntent(api, tr); } else { resetUserIntent(api, tr); } if (dispatch) { dispatch(tr); } } else { return false; } return true; }; } else { return createCommand(commandAction, transform.handleDraftState(editorAnalyticsAPI)(drafting, inputMethod)); } }; export const addInlineComment = (editorAnalyticsAPI, editorAPI) => (id, supportedBlockNodes) => { const commandAction = editorState => ({ type: ACTIONS.ADD_INLINE_COMMENT, data: { drafting: false, inlineComments: { [id]: false }, // Auto make the newly inserted comment selected. // We move the selection to the head of the comment selection. selectedAnnotations: [{ id, type: AnnotationTypes.INLINE_COMMENT }], editorState } }); if (Boolean(editorAPI === null || editorAPI === void 0 ? void 0 : editorAPI.toolbar)) { return (state, dispatch) => { const tr = transform.addInlineComment(editorAnalyticsAPI, editorAPI)(id, supportedBlockNodes)(state.tr, state); tr.setMeta(inlineCommentPluginKey, commandAction(state)); resetUserIntent(editorAPI, tr); if (dispatch) { dispatch(tr); return true; } return false; }; } else { return createCommand(commandAction, transform.addInlineComment(editorAnalyticsAPI, editorAPI)(id, supportedBlockNodes)); } }; export const updateMouseState = mouseData => createCommand({ type: ACTIONS.INLINE_COMMENT_UPDATE_MOUSE_STATE, data: { mouseData } }); export const setSelectedAnnotation = id => createCommand({ type: ACTIONS.SET_SELECTED_ANNOTATION, data: { selectedAnnotations: [{ id, type: AnnotationTypes.INLINE_COMMENT }] } }); export const setHoveredAnnotation = id => createCommand({ type: ACTIONS.SET_HOVERED_ANNOTATION, data: { hoveredAnnotations: [{ id, type: AnnotationTypes.INLINE_COMMENT }] } }); export const createAnnotation = (editorAnalyticsAPI, editorAPI) => (id, annotationType = AnnotationTypes.INLINE_COMMENT, supportedBlockNodes) => (state, dispatch) => { // don't try to add if there are is no temp highlight bookmarked const { bookmark } = getPluginState(state) || {}; if (!bookmark || !dispatch) { return false; } if (annotationType === AnnotationTypes.INLINE_COMMENT) { return addInlineComment(editorAnalyticsAPI, editorAPI)(id, supportedBlockNodes)(state, dispatch); } return false; }; export const setInlineCommentsVisibility = isVisible => { return createCommand({ type: ACTIONS.INLINE_COMMENT_SET_VISIBLE, data: { isVisible } }); };