UNPKG

@atlaskit/editor-plugin-annotation

Version:

Annotation plugin for @atlaskit/editor-core

335 lines (325 loc) 14.1 kB
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 }; };