@atlaskit/editor-plugin-annotation
Version:
Annotation plugin for @atlaskit/editor-core
335 lines (325 loc) • 14.1 kB
JavaScript
import { AnnotationTypes } from '@atlaskit/adf-schema';
import { getAnnotationInlineNodeTypes, getRangeInlineNodeNames } from '@atlaskit/editor-common/utils';
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
import { setInlineCommentDraftState, createAnnotation, setSelectedAnnotation, closeComponent, setHoveredAnnotation, removeInlineCommentFromDoc } from '../editor-commands';
import { AnnotationSelectionType } from '../types';
import { inlineCommentPluginKey, isSelectionValid } from './utils';
const ERROR_REASON_DRAFT_NOT_STARTED = 'draft-not-started';
const ERROR_REASON_DRAFT_IN_PROGRESS = 'draft-in-progress';
const ERROR_REASON_RANGE_MISSING = 'range-no-longer-exists';
const ERROR_REASON_RANGE_INVALID = 'invalid-range';
const ERROR_REASON_ID_INVALID = 'id-not-valid';
const domRefFromPos = (view, position) => {
let dom;
try {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
dom = findDomRefAtPos(position, view.domAtPos.bind(view));
} catch (error) {
// eslint-disable-next-line no-console
console.warn(error);
return undefined;
}
return dom;
};
export const allowAnnotation = (editorView, _options) => () => {
const {
isDrafting,
draftDecorationSet
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (isDrafting) {
return false;
}
const decoration = draftDecorationSet === null || draftDecorationSet === void 0 ? void 0 : draftDecorationSet.find();
// If a draft decoration exists then we should block the user from creating a new one.
if (!!(decoration !== null && decoration !== void 0 && decoration.length)) {
return false;
}
return isSelectionValid(editorView.state) === AnnotationSelectionType.VALID;
};
export const startDraft = (editorView, options) => () => {
var _getRangeInlineNodeNa, _options$annotationMa2;
const {
isDrafting,
selectedAnnotations
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (isDrafting) {
return {
success: false,
reason: ERROR_REASON_DRAFT_IN_PROGRESS
};
}
if (!!(selectedAnnotations !== null && selectedAnnotations !== void 0 && selectedAnnotations.length)) {
// if there are selected annotations when starting a draft, we need to clear the selected annotations
// before we start the draft.
closeComponent()(editorView.state, editorView.dispatch);
// not only that but we need to also deselect any other annotations that are currently selected.
selectedAnnotations === null || selectedAnnotations === void 0 ? void 0 : selectedAnnotations.forEach(annotation => {
var _options$annotationMa, _getAnnotationInlineN;
(_options$annotationMa = options.annotationManager) === null || _options$annotationMa === void 0 ? void 0 : _options$annotationMa.emit({
name: 'annotationSelectionChanged',
data: {
annotationId: annotation.id,
isSelected: false,
inlineNodeTypes: (_getAnnotationInlineN = getAnnotationInlineNodeTypes(editorView.state, annotation.id)) !== null && _getAnnotationInlineN !== void 0 ? _getAnnotationInlineN : []
}
});
});
}
setInlineCommentDraftState(options.editorAnalyticsAPI, undefined, options.api)(true)(editorView.state, editorView.dispatch);
const {
draftDecorationSet
} = inlineCommentPluginKey.getState(editorView.state) || {};
const decorations = draftDecorationSet === null || draftDecorationSet === void 0 ? void 0 : draftDecorationSet.find();
// If the matching decorations is not found containing the id, it means something went wrong with the draft.
if (!(decorations !== null && decorations !== void 0 && decorations.length)) {
return {
success: false,
reason: ERROR_REASON_RANGE_INVALID
};
}
const from = decorations[0].from;
const targetElement = domRefFromPos(editorView, from);
const inlineNodeTypes = (_getRangeInlineNodeNa = getRangeInlineNodeNames({
doc: editorView.state.doc,
pos: {
from,
to: decorations[decorations.length - 1].to
}
})) !== null && _getRangeInlineNodeNa !== void 0 ? _getRangeInlineNodeNa : [];
(_options$annotationMa2 = options.annotationManager) === null || _options$annotationMa2 === void 0 ? void 0 : _options$annotationMa2.emit({
name: 'draftAnnotationStarted',
data: {
targetElement,
actionResult: undefined,
inlineNodeTypes
}
});
return {
success: true,
targetElement,
// In Editor the action result is undefined, because the editor will perform the transaction on the document.
actionResult: undefined,
inlineNodeTypes
};
};
export const clearDraft = (editorView, options) => () => {
const {
isDrafting,
draftDecorationSet
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (!isDrafting) {
return {
success: false,
reason: ERROR_REASON_DRAFT_NOT_STARTED
};
}
const decorations = draftDecorationSet === null || draftDecorationSet === void 0 ? void 0 : draftDecorationSet.find();
if (!(decorations !== null && decorations !== void 0 && decorations.length)) {
return {
success: false,
reason: ERROR_REASON_DRAFT_NOT_STARTED
};
}
setInlineCommentDraftState(options.editorAnalyticsAPI, undefined, options.api)(false)(editorView.state, editorView.dispatch);
!editorView.hasFocus() && editorView.focus();
return {
success: true
};
};
export const applyDraft = (editorView, options) => id => {
var _options$annotationMa3, _getAnnotationInlineN2;
const {
isDrafting,
draftDecorationSet,
bookmark
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (!isDrafting) {
return {
success: false,
reason: ERROR_REASON_DRAFT_NOT_STARTED
};
}
const decorations = draftDecorationSet === null || draftDecorationSet === void 0 ? void 0 : draftDecorationSet.find();
if (!(decorations !== null && decorations !== void 0 && decorations.length) || !bookmark) {
return {
success: false,
reason: ERROR_REASON_RANGE_MISSING
};
}
const from = decorations[0].from;
createAnnotation(options.editorAnalyticsAPI, options.api)(id, AnnotationTypes.INLINE_COMMENT, options.provider.supportedBlockNodes)(editorView.state, editorView.dispatch);
!editorView.hasFocus() && editorView.focus();
// Using the original decoration from position we should be able to locate the new target element.
// This is because the new annotation will be created at the same position as the draft decoration.
const targetElement = domRefFromPos(editorView, from);
// When a draft is applied it is automatically selected, so we need to set the selected annotation.
// emit the event for the selected annotation.
(_options$annotationMa3 = options.annotationManager) === null || _options$annotationMa3 === void 0 ? void 0 : _options$annotationMa3.emit({
name: 'annotationSelectionChanged',
data: {
annotationId: id,
isSelected: true,
inlineNodeTypes: (_getAnnotationInlineN2 = getAnnotationInlineNodeTypes(editorView.state, id)) !== null && _getAnnotationInlineN2 !== void 0 ? _getAnnotationInlineN2 : []
}
});
return {
success: true,
// Get the dom element from the newly created annotation and return it here.
targetElement,
// In Editor this is undefined, because the editor will update the document.
actionResult: undefined
};
};
export const getDraft = (editorView, _options) => () => {
var _getRangeInlineNodeNa2;
const {
isDrafting,
draftDecorationSet
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (!isDrafting) {
return {
success: false,
reason: ERROR_REASON_DRAFT_NOT_STARTED
};
}
const decorations = draftDecorationSet === null || draftDecorationSet === void 0 ? void 0 : draftDecorationSet.find();
if (!(decorations !== null && decorations !== void 0 && decorations.length)) {
return {
success: false,
reason: ERROR_REASON_DRAFT_NOT_STARTED
};
}
const from = decorations[0].from;
const targetElement = domRefFromPos(editorView, from);
const inlineNodeTypes = (_getRangeInlineNodeNa2 = getRangeInlineNodeNames({
doc: editorView.state.doc,
pos: {
from,
to: decorations[decorations.length - 1].to
}
})) !== null && _getRangeInlineNodeNa2 !== void 0 ? _getRangeInlineNodeNa2 : [];
return {
success: true,
inlineNodeTypes,
targetElement,
actionResult: undefined
};
};
export const setIsAnnotationSelected = (editorView, options) => (id, isSelected) => {
var _selectedAnnotations$;
const {
annotations,
isDrafting,
selectedAnnotations
} = inlineCommentPluginKey.getState(editorView.state) || {};
if (isDrafting) {
return {
success: false,
reason: ERROR_REASON_DRAFT_IN_PROGRESS
};
}
// If there is no annotation state with this id then we can assume the annotation is invalid.
if (!(annotations !== null && annotations !== void 0 && annotations.hasOwnProperty(id))) {
return {
success: false,
reason: ERROR_REASON_ID_INVALID
};
}
const isCurrentlySelectedIndex = (_selectedAnnotations$ = selectedAnnotations === null || selectedAnnotations === void 0 ? void 0 : selectedAnnotations.findIndex(annotation => annotation.id === id)) !== null && _selectedAnnotations$ !== void 0 ? _selectedAnnotations$ : -1;
const isCurrentlySelected = isCurrentlySelectedIndex !== -1;
if (isSelected !== isCurrentlySelected) {
// the annotation is selection is changing.
if (isCurrentlySelected && !isSelected) {
var _options$annotationMa4, _getAnnotationInlineN3;
// the selected annotaion is being unselected, so we need to close the view.
closeComponent()(editorView.state, editorView.dispatch);
(_options$annotationMa4 = options.annotationManager) === null || _options$annotationMa4 === void 0 ? void 0 : _options$annotationMa4.emit({
name: 'annotationSelectionChanged',
data: {
annotationId: id,
isSelected: false,
inlineNodeTypes: (_getAnnotationInlineN3 = getAnnotationInlineNodeTypes(editorView.state, id)) !== null && _getAnnotationInlineN3 !== void 0 ? _getAnnotationInlineN3 : []
}
});
} else if (!isCurrentlySelected && isSelected) {
var _options$annotationMa6, _getAnnotationInlineN5;
// the annotation is currently not selected and is being selected, so we need to open the view.
setSelectedAnnotation(id)(editorView.state, editorView.dispatch);
// the current annotations are going to be unselected. So we need to notify listeners of this change also.
selectedAnnotations === null || selectedAnnotations === void 0 ? void 0 : selectedAnnotations.forEach(annotation => {
if (annotation.id !== id) {
var _options$annotationMa5, _getAnnotationInlineN4;
(_options$annotationMa5 = options.annotationManager) === null || _options$annotationMa5 === void 0 ? void 0 : _options$annotationMa5.emit({
name: 'annotationSelectionChanged',
data: {
annotationId: annotation.id,
isSelected: false,
inlineNodeTypes: (_getAnnotationInlineN4 = getAnnotationInlineNodeTypes(editorView.state, annotation.id)) !== null && _getAnnotationInlineN4 !== void 0 ? _getAnnotationInlineN4 : []
}
});
}
});
// Lastly we need to emit the event for the selected annotation.
(_options$annotationMa6 = options.annotationManager) === null || _options$annotationMa6 === void 0 ? void 0 : _options$annotationMa6.emit({
name: 'annotationSelectionChanged',
data: {
annotationId: id,
isSelected: true,
inlineNodeTypes: (_getAnnotationInlineN5 = getAnnotationInlineNodeTypes(editorView.state, id)) !== null && _getAnnotationInlineN5 !== void 0 ? _getAnnotationInlineN5 : []
}
});
}
}
return {
success: true,
isSelected
};
};
export const setIsAnnotationHovered = (editorView, _options) => (id, isHovered) => {
var _hoveredAnnotations$f;
const {
annotations,
hoveredAnnotations
} = inlineCommentPluginKey.getState(editorView.state) || {};
// If there is no annotation state with this id then we can assume the annotation is invalid.
if (!(annotations !== null && annotations !== void 0 && annotations.hasOwnProperty(id))) {
return {
success: false,
reason: ERROR_REASON_ID_INVALID
};
}
const isCurrentlyHoveredIndex = (_hoveredAnnotations$f = hoveredAnnotations === null || hoveredAnnotations === void 0 ? void 0 : hoveredAnnotations.findIndex(annotation => annotation.id === id)) !== null && _hoveredAnnotations$f !== void 0 ? _hoveredAnnotations$f : -1;
const isCurrentlyHovered = isCurrentlyHoveredIndex !== -1;
if (isHovered !== isCurrentlyHovered) {
// the annotation in hovered is changing.
if (isCurrentlyHovered && !isHovered) {
// the hovered annotaion is being unhovered, so we should remove the hover state.
setHoveredAnnotation('')(editorView.state, editorView.dispatch);
} else if (!isCurrentlyHovered && isHovered) {
// the annotation is currently not hovered and is being hovered.
setHoveredAnnotation(id)(editorView.state, editorView.dispatch);
}
}
return {
success: true,
isHovered
};
};
export const clearAnnotation = (editorView, options) => id => {
const {
annotations
} = inlineCommentPluginKey.getState(editorView.state) || {};
// If there is no annotation state with this id then we can assume the annotation is invalid.
if (!(annotations !== null && annotations !== void 0 && annotations.hasOwnProperty(id))) {
return {
success: false,
reason: ERROR_REASON_ID_INVALID
};
}
removeInlineCommentFromDoc(options.editorAnalyticsAPI)(id, options.provider.supportedBlockNodes)(editorView.state, editorView.dispatch);
return {
success: true,
actionResult: undefined
};
};