UNPKG

@atlaskit/editor-plugin-annotation

Version:

Annotation plugin for @atlaskit/editor-core

251 lines (238 loc) 11.4 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import { pluginFactory } from '@atlaskit/editor-common/utils'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import reducer from './reducer'; import { decorationKey, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils'; var handleDocChanged = function handleDocChanged(tr, prevPluginState) { if (!tr.getMeta('replaceDocument')) { return getSelectionChangedHandler(false)(tr, prevPluginState); } return _objectSpread(_objectSpread({}, prevPluginState), {}, { dirtyAnnotations: true }); }; /** * Creates a handleDocChanged function with its own deleted annotations cache. * This ensures each editor instance has its own cache, avoiding cross-contamination. */ var createHandleDocChanged = function createHandleDocChanged() { // Cache for preserving deleted annotation resolved states through delete/undo cycles // This lives in closure scope per editor instance to avoid serialization and reduce state size var deletedAnnotationsCache = {}; return function (tr, prevPluginState) { if (tr.getMeta('replaceDocument')) { return _objectSpread(_objectSpread({}, prevPluginState), {}, { dirtyAnnotations: true }); } var updatedState = getSelectionChangedHandler(false)(tr, prevPluginState); // Collect annotation IDs currently in the document var annotationIdsInDocument = new Set(); tr.doc.descendants(function (node) { node.marks.forEach(function (mark) { if (mark.type.name === 'annotation') { annotationIdsInDocument.add(mark.attrs.id); } }); }); var annotationIdsInState = Object.keys(prevPluginState.annotations); // Early return if annotations haven't changed var annotationsHaveChanged = annotationIdsInDocument.size !== annotationIdsInState.length || annotationIdsInState.some(function (id) { return !annotationIdsInDocument.has(id); }); if (!annotationsHaveChanged) { return updatedState; } // Cache deleted annotations to be able to restore their resolved states on undo var updatedAnnotations = {}; annotationIdsInState.forEach(function (id) { if (!annotationIdsInDocument.has(id)) { deletedAnnotationsCache[id] = prevPluginState.annotations[id]; } }); // Update annotations to match document state, preserving resolved states through delete/undo // Only include annotations that have a known resolved state - don't default new annotations to false // as this would cause them to briefly appear as unresolved before the provider sets their actual state annotationIdsInDocument.forEach(function (id) { var _prevPluginState$anno; var knownState = (_prevPluginState$anno = prevPluginState.annotations[id]) !== null && _prevPluginState$anno !== void 0 ? _prevPluginState$anno : deletedAnnotationsCache[id]; if (knownState !== undefined) { updatedAnnotations[id] = knownState; } }); return _objectSpread(_objectSpread({}, updatedState), {}, { annotations: updatedAnnotations }); }; }; /** * We clear bookmark on the following conditions: * 1. if current selection is an empty selection, or * 2. if the current selection and bookmark selection are different * @param tr * @param editorState * @param bookmark * @example */ export var shouldClearBookMarkCheck = function shouldClearBookMarkCheck(tr, editorState, bookmark) { if (editorState.selection.empty || !bookmark) { return true; } else if (editorState.selection instanceof NodeSelection) { var bookmarkSelection = bookmark === null || bookmark === void 0 ? void 0 : bookmark.resolve(tr.doc); if (bookmarkSelection instanceof NodeSelection) { var selectionNode = editorState.selection.node; var bookmarkNode = bookmarkSelection.node; /** * Currently, after updating the alt text of a mediaSingle node, * the selection moves to the media node. * (then will append a transaction to its parent node) */ if (selectionNode.type.name === 'media' && bookmarkNode.type.name === 'mediaSingle') { var _bookmarkNode$firstCh; return !((_bookmarkNode$firstCh = bookmarkNode.firstChild) !== null && _bookmarkNode$firstCh !== void 0 && _bookmarkNode$firstCh.eq(selectionNode)); } else { return !bookmarkNode.eq(selectionNode); } } } // by default we discard bookmark return true; }; var getSelectionChangeHandlerOld = function getSelectionChangeHandlerOld(reopenCommentView) { return function (tr, pluginState) { if (pluginState.skipSelectionHandling) { return _objectSpread(_objectSpread({}, pluginState), {}, { skipSelectionHandling: false }, reopenCommentView && { isInlineCommentViewClosed: false }); } if ( // If pluginState.selectedAnnotations is annotations of block node, i.e. when a new comment is created, // we keep it as it is so that we can show comment view component with the newly created comment isBlockNodeAnnotationsSelected(tr.selection, pluginState.selectedAnnotations)) { return _objectSpread(_objectSpread({}, pluginState), reopenCommentView && { isInlineCommentViewClosed: false }); } var selectedAnnotations = findAnnotationsInSelection(tr.selection, tr.doc); if (selectedAnnotations.length === 0) { return _objectSpread(_objectSpread({}, pluginState), {}, { selectedAnnotations: selectedAnnotations, isInlineCommentViewClosed: true, selectAnnotationMethod: undefined }); } if (isSelectedAnnotationsChanged(selectedAnnotations, pluginState.selectedAnnotations)) { return _objectSpread(_objectSpread({}, pluginState), {}, { selectedAnnotations: selectedAnnotations, selectAnnotationMethod: undefined }, reopenCommentView && { isInlineCommentViewClosed: false }); } return _objectSpread(_objectSpread(_objectSpread({}, pluginState), reopenCommentView && { isInlineCommentViewClosed: false }), {}, { selectAnnotationMethod: undefined }); }; }; var getSelectionChangeHandlerNew = function getSelectionChangeHandlerNew(reopenCommentView) { return function (tr, pluginState) { if (pluginState.skipSelectionHandling) { return _objectSpread(_objectSpread({}, pluginState), {}, { skipSelectionHandling: false }, reopenCommentView && { isInlineCommentViewClosed: false }); } var selectedAnnotations = findAnnotationsInSelection(tr.selection, tr.doc); // NOTE: I've left this commented code here as a reference that the previous old code would reset the selected annotations // if the selection is empty. If this is no longer needed, we can remove this code. // if (selectedAnnotations.length === 0) { // return { // ...pluginState, // pendingSelectedAnnotations: selectedAnnotations, // pendingSelectedAnnotationsUpdateCount: // pluginState.pendingSelectedAnnotationsUpdateCount + 1, // isInlineCommentViewClosed: true, // selectAnnotationMethod: undefined, // }; // } if (isSelectedAnnotationsChanged(selectedAnnotations, pluginState.pendingSelectedAnnotations)) { return _objectSpread(_objectSpread({}, pluginState), {}, { pendingSelectedAnnotations: selectedAnnotations, pendingSelectedAnnotationsUpdateCount: pluginState.pendingSelectedAnnotationsUpdateCount + 1 }, reopenCommentView && { isInlineCommentViewClosed: false }); } return _objectSpread(_objectSpread(_objectSpread({}, pluginState), reopenCommentView && { isInlineCommentViewClosed: false }), {}, { selectAnnotationMethod: undefined }); }; }; var getSelectionChangedHandler = function getSelectionChangedHandler(reopenCommentView) { return function (tr, pluginState) { return pluginState.isAnnotationManagerEnabled ? // if platform_editor_comments_api_manager == true getSelectionChangeHandlerNew(reopenCommentView)(tr, pluginState) : // else if platform_editor_comments_api_manager == false getSelectionChangeHandlerOld(reopenCommentView)(tr, pluginState); }; }; // Create the handler with cache once at module level var handleDocChangedWithSync = createHandleDocChanged(); var getDocChangedHandler = function getDocChangedHandler(tr, prevPluginState) { // Check feature flag at runtime to support test variants if (expValEquals('platform_editor_annotations_sync_on_docchange', 'isEnabled', true)) { return handleDocChangedWithSync(tr, prevPluginState); } return handleDocChanged(tr, prevPluginState); }; var dest = pluginFactory(inlineCommentPluginKey, reducer, { onSelectionChanged: getSelectionChangedHandler(true), onDocChanged: getDocChangedHandler, mapping: function mapping(tr, pluginState, editorState) { var draftDecorationSet = pluginState.draftDecorationSet, bookmark = pluginState.bookmark; var mappedDecorationSet = DecorationSet.empty, mappedBookmark; var hasMappedDecorations = false; if (draftDecorationSet) { mappedDecorationSet = draftDecorationSet.map(tr.mapping, tr.doc); } hasMappedDecorations = mappedDecorationSet.find(undefined, undefined, function (spec) { return Object.values(decorationKey).includes(spec.key); }).length > 0; // When changes to decoration target make decoration invalid (e.g. delete text, add mark to node), // we need to reset bookmark to hide create component and to avoid invalid draft being published // We only perform this change when document selection has changed. if (!hasMappedDecorations && shouldClearBookMarkCheck(tr, editorState, bookmark)) { return _objectSpread(_objectSpread({}, pluginState), {}, { draftDecorationSet: mappedDecorationSet, bookmark: undefined }); } if (bookmark) { mappedBookmark = bookmark.map(tr.mapping); } // return same pluginState if mappings did not change if (mappedBookmark === bookmark && mappedDecorationSet === draftDecorationSet) { return pluginState; } return _objectSpread(_objectSpread({}, pluginState), {}, { draftDecorationSet: mappedDecorationSet, bookmark: mappedBookmark }); } }); export var createPluginState = dest.createPluginState; export var createCommand = dest.createCommand;