@atlaskit/editor-plugin-annotation
Version:
Annotation plugin for @atlaskit/editor-core
200 lines (198 loc) • 9.01 kB
JavaScript
import React from 'react';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, MODE } from '@atlaskit/editor-common/analytics';
import { ToolTipContent, addInlineComment } from '@atlaskit/editor-common/keymaps';
import { currentMediaNodeWithPos } from '@atlaskit/editor-common/media-single';
import { annotationMessages } from '@atlaskit/editor-common/messages';
import { calculateToolbarPositionAboveSelection, calculateToolbarPositionTrackHead, getRangeInlineNodeNames } from '@atlaskit/editor-common/utils';
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
import CommentIcon from '@atlaskit/icon/core/comment';
import { fg } from '@atlaskit/platform-feature-flags';
import { setInlineCommentDraftState } from '../editor-commands';
import { AnnotationSelectionType, AnnotationTestIds } from '../types';
import { getPluginState, isSelectionValid, resolveDraftBookmark } from './utils';
const getValidNodes = state => {
const {
schema
} = state;
const {
annotation
} = schema.marks;
return Object.keys(schema.nodes).reduce((acc, current) => {
const type = schema.nodes[current];
if (type.allowsMarkType(annotation)) {
acc.push(type);
}
return acc;
}, []);
};
/**
* Should suppress toolbars when the user is creating an inline comment
* This only applies when the selection range exactly matches the bookmark range
* which should be the case immediately after the comment button is clicked
* if the user creates a different selection range, the floating toolbar should still be shown
* @param root0
* @param root0.state
* @param root0.bookmark
* @example
*/
export const shouldSuppressFloatingToolbar = ({
state,
bookmark
}) => {
if (!bookmark) {
return false;
}
const {
tr
} = state;
const resolvedBookmark = bookmark.resolve(tr.doc);
const isSelectionMatchingBookmark = resolvedBookmark.to === tr.selection.to && resolvedBookmark.from === tr.selection.from;
return isSelectionMatchingBookmark;
};
export const buildSuppressedToolbar = state => {
return {
items: [],
nodeType: getValidNodes(state),
title: 'Annotation suppressed toolbar'
};
};
export const buildToolbar = editorAnalyticsAPI => ({
state,
intl,
isToolbarAbove = false,
_supportedNodes = [],
api,
createCommentExperience,
annotationManager,
onCommentButtonMount,
getCanAddComments = () => true,
contentType
}) => {
var _api$connectivity, _api$connectivity$sha, _api$connectivity$sha2;
const selectionValid = isSelectionValid(state);
const isMediaSelected = currentMediaNodeWithPos(state);
// comments on media can only be added via media floating toolbar
if (isMediaSelected || selectionValid === AnnotationSelectionType.INVALID) {
return undefined;
}
const createCommentMessage = intl.formatMessage(annotationMessages.createComment);
const commentDisabledMessage = intl.formatMessage(fg('editor_inline_comments_on_inline_nodes') ? annotationMessages.createCommentDisabled : annotationMessages.createCommentInvalid);
const canAddComments = getCanAddComments();
const isCommentButtonDisabled = !canAddComments || selectionValid === AnnotationSelectionType.DISABLED;
const disabledTooltipContent = !canAddComments ? intl.formatMessage(annotationMessages.noPermissionToAddComment, {
contentType
}) : commentDisabledMessage;
const createComment = {
type: 'button',
showTitle: true,
disabled: isCommentButtonDisabled || isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity = api.connectivity) === null || _api$connectivity === void 0 ? void 0 : (_api$connectivity$sha = _api$connectivity.sharedState) === null || _api$connectivity$sha === void 0 ? void 0 : (_api$connectivity$sha2 = _api$connectivity$sha.currentState()) === null || _api$connectivity$sha2 === void 0 ? void 0 : _api$connectivity$sha2.mode),
testId: AnnotationTestIds.floatingToolbarCreateButton,
interactionName: 'start-inline-comment-action',
icon: CommentIcon,
iconFallback: CommentIcon,
tooltipContent: isCommentButtonDisabled ? disabledTooltipContent : /*#__PURE__*/React.createElement(ToolTipContent, {
description: createCommentMessage,
keymap: addInlineComment
}),
title: createCommentMessage,
onMount: () => {
var _getRangeInlineNodeNa;
if (fg('confluence_frontend_preload_inline_comment_editor')) {
onCommentButtonMount && onCommentButtonMount();
}
// Check if the selection includes an non-text inline node
const inlineCommentPluginState = getPluginState(state);
const inlineNodeNames = (_getRangeInlineNodeNa = getRangeInlineNodeNames({
doc: state.doc,
pos: resolveDraftBookmark(state, inlineCommentPluginState === null || inlineCommentPluginState === void 0 ? void 0 : inlineCommentPluginState.bookmark)
})) !== null && _getRangeInlineNodeNa !== void 0 ? _getRangeInlineNodeNa : [];
const isNonTextInlineNodeInludedInComment = inlineNodeNames.filter(nodeName => nodeName !== 'text').length > 0;
if (editorAnalyticsAPI) {
editorAnalyticsAPI.fireAnalyticsEvent({
action: ACTION.VIEWED,
actionSubject: ACTION_SUBJECT.BUTTON,
actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT,
eventType: EVENT_TYPE.UI,
attributes: {
isNonTextInlineNodeInludedInComment,
isDisabled: selectionValid === AnnotationSelectionType.DISABLED,
inputMethod: INPUT_METHOD.FLOATING_TB,
mode: MODE.EDITOR
}
});
}
},
onClick: (state, dispatch) => {
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.fireAnalyticsEvent({
action: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.BUTTON,
actionSubjectId: ACTION_SUBJECT_ID.CREATE_INLINE_COMMENT_FROM_HIGHLIGHT_ACTIONS_MENU,
eventType: EVENT_TYPE.UI,
attributes: {
source: 'highlightActionsMenu',
pageMode: 'edit'
}
});
if (annotationManager) {
annotationManager.checkPreemptiveGate().then(canStartDraft => {
if (canStartDraft) {
createCommentExperience === null || createCommentExperience === void 0 ? void 0 : createCommentExperience.start({
attributes: {
pageClass: 'editor',
commentType: 'inline',
entryPoint: 'highlightActions'
}
});
createCommentExperience === null || createCommentExperience === void 0 ? void 0 : createCommentExperience.initExperience.start();
const result = annotationManager.startDraft();
if (!result.success) {
// Fire an analytics event to indicate that the user has clicked the button
// but the action was not completed, the result should contain a reason.
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.fireAnalyticsEvent({
action: ACTION.ERROR,
actionSubject: ACTION_SUBJECT.ANNOTATION,
actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT,
eventType: EVENT_TYPE.OPERATIONAL,
attributes: {
errorReason: `toolbar-start-draft-failed/${result.reason}`
}
});
}
}
}).catch(() => {
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.fireAnalyticsEvent({
action: ACTION.ERROR,
actionSubject: ACTION_SUBJECT.ANNOTATION,
actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT,
eventType: EVENT_TYPE.OPERATIONAL,
attributes: {
errorReason: `toolbar-start-draft-preemptive-gate-error`
}
});
});
return true;
} else {
createCommentExperience === null || createCommentExperience === void 0 ? void 0 : createCommentExperience.start({
attributes: {
pageClass: 'editor',
commentType: 'inline',
entryPoint: 'highlightActions'
}
});
createCommentExperience === null || createCommentExperience === void 0 ? void 0 : createCommentExperience.initExperience.start();
return setInlineCommentDraftState(editorAnalyticsAPI)(true)(state, dispatch);
}
},
supportsViewMode: true // TODO: MODES-3950 - Clean up this floating toolbar view mode logic,
};
const toolbarTitle = intl.formatMessage(annotationMessages.toolbar);
const calcToolbarPosition = isToolbarAbove ? calculateToolbarPositionAboveSelection : calculateToolbarPositionTrackHead;
const onPositionCalculated = calcToolbarPosition(toolbarTitle);
return {
title: toolbarTitle,
nodeType: getValidNodes(state),
items: [createComment],
onPositionCalculated,
pluginName: 'annotation'
};
};