@atlaskit/editor-plugin-annotation
Version:
Annotation plugin for @atlaskit/editor-core
303 lines (299 loc) • 11 kB
JavaScript
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
}
});
};