UNPKG

@atlaskit/editor-plugin-annotation

Version:

Annotation plugin for @atlaskit/editor-core

253 lines (241 loc) • 12.4 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; 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/plugin-state-factory'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import reducer from './reducer'; import { decorationKey, getAnnotationDiscovery, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils'; /** * Creates a document-sync handler with a deleted annotations cache in its closure. */ var createHandleDocChanged = function createHandleDocChanged() { // Cache for preserving deleted annotation resolved states through delete/undo cycles // This stays outside plugin state so the undo cache is not serialized. var deletedAnnotationsCache = {}; return function (tr, prevPluginState) { if (tr.getMeta('replaceDocument')) { return _objectSpread(_objectSpread({}, prevPluginState), {}, { dirtyAnnotations: true }); } var updatedState = getSelectionChangedHandler(false)(tr, prevPluginState); var legacyContentExperimentEnabled = isExperimentEnabled('platform_editor_fix_lcm_editor_annotations'); var currentDiscovery = legacyContentExperimentEnabled ? getAnnotationDiscovery(tr.doc) : undefined; var previousAnnotationIds = currentDiscovery ? new Set(getAnnotationDiscovery(tr.before).annotationIds) : undefined; var newNestedAnnotationIds = currentDiscovery ? currentDiscovery.nestedAnnotationIds.filter(function (id) { return !(previousAnnotationIds !== null && previousAnnotationIds !== void 0 && previousAnnotationIds.has(id)) && prevPluginState.annotations[id] === undefined && deletedAnnotationsCache[id] === undefined; }) : []; // Collect annotation IDs currently in the document var annotationIdsInDocument = currentDiscovery ? new Set([].concat(_toConsumableArray(currentDiscovery.rootAnnotationIds), _toConsumableArray(currentDiscovery.nestedAnnotationIds))) : new Set(); if (!currentDiscovery) { 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 newNestedAnnotationIds.length > 0 ? _objectSpread(_objectSpread({}, updatedState), {}, { dirtyAnnotations: true }) : 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 }, newNestedAnnotationIds.length > 0 && { dirtyAnnotations: true }); }; }; /** * 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); }; }; var createAnnotationPluginFactory = function createAnnotationPluginFactory(onDocChanged) { return pluginFactory(inlineCommentPluginKey, reducer, { onSelectionChanged: getSelectionChangedHandler(true), onDocChanged: onDocChanged, 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 }); } }); }; var handleDocChangedWithSync = createHandleDocChanged(); // Keep the control cohort's existing module-level cache lifetime unchanged. var legacyDest = createAnnotationPluginFactory(handleDocChangedWithSync); var commandFactory = pluginFactory(inlineCommentPluginKey, reducer); export var createPluginState = function createPluginState(dispatch, initialState) { return (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations') ? createAnnotationPluginFactory(createHandleDocChanged()) : legacyDest).createPluginState(dispatch, initialState); }; export var createCommand = commandFactory.createCommand;