UNPKG

@atlaskit/editor-plugin-annotation

Version:

Annotation plugin for @atlaskit/editor-core

523 lines (511 loc) 28.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.inlineCommentPlugin = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _adfSchema = require("@atlaskit/adf-schema"); var _analytics = require("@atlaskit/editor-common/analytics"); var _safePlugin = require("@atlaskit/editor-common/safe-plugin"); var _utils = require("@atlaskit/editor-common/utils"); var _view = require("@atlaskit/editor-prosemirror/view"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _editorCommands = require("../editor-commands"); var _utils2 = require("../editor-commands/utils"); var _nodeviews = require("../nodeviews"); var _annotationManagerHooks = require("./annotation-manager-hooks"); var _pluginFactory = require("./plugin-factory"); var _toolbar = require("./toolbar"); var _utils3 = require("./utils"); var fetchProviderStates = /*#__PURE__*/function () { var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(provider, annotationIds) { var data, result; return _regenerator.default.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: if (!(!provider || !provider.getState)) { _context.next = 2; break; } return _context.abrupt("return", {}); case 2: _context.next = 4; return provider.getState(annotationIds); case 4: data = _context.sent; result = {}; data.forEach(function (annotation) { if (annotation.annotationType === _adfSchema.AnnotationTypes.INLINE_COMMENT) { result[annotation.id] = annotation.state.resolved; } }); return _context.abrupt("return", result); case 8: case "end": return _context.stop(); } }, _callee); })); return function fetchProviderStates(_x, _x2) { return _ref.apply(this, arguments); }; }(); // fetchState is unable to return a command as it's runs async and may dispatch at a later time // Requires `editorView` instead of the decomposition as the async means state may end up stale var fetchState = /*#__PURE__*/function () { var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(provider, annotationIds, editorView, editorAnalyticsAPI) { var inlineCommentStates, _ref3, annotationsLoaded; return _regenerator.default.wrap(function _callee2$(_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return fetchProviderStates(provider, annotationIds); case 2: inlineCommentStates = _context2.sent; if (!(Object.keys(inlineCommentStates).length === 0)) { _context2.next = 7; break; } _ref3 = (0, _utils3.getPluginState)(editorView.state) || {}, annotationsLoaded = _ref3.annotationsLoaded; if (!annotationsLoaded && (0, _platformFeatureFlags.fg)('confluence_frontend_new_dangling_comments_ux')) { (0, _editorCommands.setInlineCommentsFetched)()(editorView.state, editorView.dispatch); } return _context2.abrupt("return"); case 7: if (editorView.dispatch) { (0, _editorCommands.updateInlineCommentResolvedState)(editorAnalyticsAPI)(inlineCommentStates)(editorView.state, editorView.dispatch); } case 8: case "end": return _context2.stop(); } }, _callee2); })); return function fetchState(_x3, _x4, _x5, _x6) { return _ref2.apply(this, arguments); }; }(); var initialState = function initialState() { var disallowOnWhitespace = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var featureFlagsPluginState = arguments.length > 1 ? arguments[1] : undefined; var isAnnotationManagerEnabled = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; return { annotationsLoaded: false, annotations: {}, selectedAnnotations: [], hoveredAnnotations: [], mouseData: { isSelecting: false }, disallowOnWhitespace: disallowOnWhitespace, isInlineCommentViewClosed: false, isVisible: true, skipSelectionHandling: false, featureFlagsPluginState: featureFlagsPluginState, isDrafting: false, pendingSelectedAnnotations: [], pendingSelectedAnnotationsUpdateCount: 0, isAnnotationManagerEnabled: isAnnotationManagerEnabled }; }; var hideToolbar = function hideToolbar(state, dispatch) { return function () { (0, _editorCommands.updateMouseState)({ isSelecting: true })(state, dispatch); }; }; // Subscribe to updates from consumer var onResolve = function onResolve(editorAnalyticsAPI) { return function (state, dispatch) { return function (annotationId) { (0, _editorCommands.updateInlineCommentResolvedState)(editorAnalyticsAPI)((0, _defineProperty2.default)({}, annotationId, true), _analytics.RESOLVE_METHOD.CONSUMER)(state, dispatch); }; }; }; var onUnResolve = function onUnResolve(editorAnalyticsAPI) { return function (state, dispatch) { return function (annotationId) { (0, _editorCommands.updateInlineCommentResolvedState)(editorAnalyticsAPI)((0, _defineProperty2.default)({}, annotationId, false))(state, dispatch); }; }; }; var onMouseUp = function onMouseUp(state, dispatch) { return function (e) { var _ref4 = (0, _utils3.getPluginState)(state) || {}, mouseData = _ref4.mouseData; if (mouseData !== null && mouseData !== void 0 && mouseData.isSelecting) { (0, _editorCommands.updateMouseState)({ isSelecting: false })(state, dispatch); } }; }; var onSetVisibility = function onSetVisibility(view) { return function (isVisible) { var state = view.state, dispatch = view.dispatch; (0, _editorCommands.setInlineCommentsVisibility)(isVisible)(state, dispatch); if (isVisible) { // PM retains focus when we click away from the editor. // This will restore the visual aspect of the selection, // otherwise it will seem a floating toolbar will appear // for no reason. view.focus(); } }; }; var inlineCommentPlugin = exports.inlineCommentPlugin = function inlineCommentPlugin(options) { var provider = options.provider, featureFlagsPluginState = options.featureFlagsPluginState, annotationManager = options.annotationManager; return new _safePlugin.SafePlugin({ key: _utils3.inlineCommentPluginKey, state: (0, _pluginFactory.createPluginState)(options.dispatch, initialState(provider.disallowOnWhitespace, featureFlagsPluginState, !!annotationManager)), view: function view(editorView) { var allowAnnotationFn; var startDraftFn; var clearDraftFn; var applyDraftFn; var getDraftFn; var setIsAnnotationSelectedFn; var setIsAnnotationHoveredFn; var clearAnnotationFn; if (annotationManager) { allowAnnotationFn = (0, _annotationManagerHooks.allowAnnotation)(editorView, options); startDraftFn = (0, _annotationManagerHooks.startDraft)(editorView, options); clearDraftFn = (0, _annotationManagerHooks.clearDraft)(editorView, options); applyDraftFn = (0, _annotationManagerHooks.applyDraft)(editorView, options); getDraftFn = (0, _annotationManagerHooks.getDraft)(editorView, options); setIsAnnotationSelectedFn = (0, _annotationManagerHooks.setIsAnnotationSelected)(editorView, options); setIsAnnotationHoveredFn = (0, _annotationManagerHooks.setIsAnnotationHovered)(editorView, options); clearAnnotationFn = (0, _annotationManagerHooks.clearAnnotation)(editorView, options); annotationManager.hook('allowAnnotation', allowAnnotationFn); annotationManager.hook('startDraft', startDraftFn); annotationManager.hook('clearDraft', clearDraftFn); annotationManager.hook('applyDraft', applyDraftFn); annotationManager.hook('getDraft', getDraftFn); annotationManager.hook('setIsAnnotationSelected', setIsAnnotationSelectedFn); annotationManager.hook('setIsAnnotationHovered', setIsAnnotationHoveredFn); annotationManager.hook('clearAnnotation', clearAnnotationFn); } // Get initial state // Need to pass `editorView` to mitigate editor state going stale fetchState(provider, (0, _utils3.getAllAnnotations)(editorView.state.doc), editorView, options.editorAnalyticsAPI); var resolve = function resolve(annotationId) { return onResolve(options.editorAnalyticsAPI)(editorView.state, editorView.dispatch)(annotationId); }; var unResolve = function unResolve(annotationId) { return onUnResolve(options.editorAnalyticsAPI)(editorView.state, editorView.dispatch)(annotationId); }; var mouseUp = function mouseUp(event) { return onMouseUp(editorView.state, editorView.dispatch)(event); }; var setVisibility = function setVisibility(isVisible) { return onSetVisibility(editorView)(isVisible); }; var setSelectedAnnotationFn = function setSelectedAnnotationFn(annotationId) { if (!annotationId) { (0, _editorCommands.closeComponent)()(editorView.state, editorView.dispatch); } else { (0, _editorCommands.setSelectedAnnotation)(annotationId)(editorView.state, editorView.dispatch); } }; var setHoveredAnnotationFn = function setHoveredAnnotationFn(annotationId) { if (!annotationId) { (0, _editorCommands.closeComponent)()(editorView.state, editorView.dispatch); } else { (0, _editorCommands.setHoveredAnnotation)(annotationId)(editorView.state, editorView.dispatch); } }; var removeHoveredannotationFn = function removeHoveredannotationFn() { (0, _editorCommands.setHoveredAnnotation)('')(editorView.state, editorView.dispatch); }; var closeInlineCommentFn = function closeInlineCommentFn() { (0, _editorCommands.closeComponent)()(editorView.state, editorView.dispatch); }; var updateSubscriber = provider.updateSubscriber; if (updateSubscriber) { updateSubscriber.on('resolve', resolve).on('delete', resolve).on('unresolve', unResolve).on('create', unResolve).on('setvisibility', setVisibility).on('setselectedannotation', setSelectedAnnotationFn).on('sethoveredannotation', setHoveredAnnotationFn).on('removehoveredannotation', removeHoveredannotationFn).on('closeinlinecomment', closeInlineCommentFn); } // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners editorView.root.addEventListener('mouseup', mouseUp); /** * This flag is used to prevent the preemptive gate from being called multiple times while a check is in-flight. * If a check is still pending then it's most likely because the product is busy and trying to block the * selection of an annotation. */ var isPreemptiveGateActive = false; return { update: function update(view, _prevState) { var _prevSelectedAnnotati; var _ref5 = (0, _utils3.getPluginState)(view.state) || {}, selectedAnnotations = _ref5.selectedAnnotations, annotations = _ref5.annotations, isDrafting = _ref5.isDrafting, bookmark = _ref5.bookmark; var _ref6 = (0, _utils3.getPluginState)(_prevState) || {}, prevSelectedAnnotations = _ref6.selectedAnnotations; var selectedAnnotationId = selectedAnnotations && selectedAnnotations.length !== 0 && selectedAnnotations[0].id ? selectedAnnotations[0].id : undefined; // If the new state has an unresolved selected annotation, and it's different from // the previous one then... if ( //This checks the selected annotation is different from the previous one selectedAnnotationId && selectedAnnotationId !== (prevSelectedAnnotations === null || prevSelectedAnnotations === void 0 || (_prevSelectedAnnotati = prevSelectedAnnotations[0]) === null || _prevSelectedAnnotati === void 0 ? void 0 : _prevSelectedAnnotati.id) && // This ensures that the selected annotation is unresolved annotations && annotations[selectedAnnotationId] === false) { var _options$selectCommen, _options$viewInlineCo; // ...we mark the select annotation experience as complete. // The selectComponentExperience is using a simplified object, which is why it's type asserted. (_options$selectCommen = options.selectCommentExperience) === null || _options$selectCommen === void 0 || _options$selectCommen.selectAnnotation.complete(selectedAnnotationId); // ...and start a new UFO press trace since the selected comment is changing (_options$viewInlineCo = options.viewInlineCommentTraceUFOPress) === null || _options$viewInlineCo === void 0 || _options$viewInlineCo.call(options); } var api = options.api; if (isDrafting && !view.state.selection.eq(_prevState.selection) && Boolean(api === null || api === void 0 ? void 0 : api.toolbar)) { // It is possible that user update selection while having a active draft, // so we need to reset the user intent to allow inline text toolbar to be visible api === null || api === void 0 || api.core.actions.execute(function (_ref7) { var tr = _ref7.tr; if ((0, _toolbar.shouldSuppressFloatingToolbar)({ state: view.state, bookmark: bookmark })) { (0, _utils2.setUserIntent)(api, tr); } else { (0, _utils2.resetUserIntent)(api, tr); } return tr; }); } if (annotationManager) { // In the Editor, Annotations can be selected in three ways: // 1. By clicking on the annotation in the editor // 2. By using the annotation manager to select the annotation // 3. By moving the cursor to the annotation and using the keyboard to select it // Item 1 & 3 need to be protected by the preemptive gate. This is because these actions could be performed by a user // at a time when changing the selection could cause data loss. // The following preemptive check is designed to cover these items. var _ref8 = (0, _utils3.getPluginState)(view.state) || {}, pendingSelectedAnnotations = _ref8.pendingSelectedAnnotations, pendingSelectedAnnotationsUpdateCount = _ref8.pendingSelectedAnnotationsUpdateCount; var _ref9 = (0, _utils3.getPluginState)(_prevState) || {}, prevPendingSelectedAnnotationsUpdateCount = _ref9.pendingSelectedAnnotationsUpdateCount; if (!isPreemptiveGateActive && pendingSelectedAnnotationsUpdateCount !== prevPendingSelectedAnnotationsUpdateCount && !!(pendingSelectedAnnotations !== null && pendingSelectedAnnotations !== void 0 && pendingSelectedAnnotations.length)) { // Need to set a lock to avoid calling gate multiple times. The lock will be released // when the preemptive gate is complete. isPreemptiveGateActive = true; annotationManager.checkPreemptiveGate().then(function (canSelectAnnotation) { var _ref0 = (0, _utils3.getPluginState)(view.state) || {}, isDrafting = _ref0.isDrafting, latestPendingSelectedAnnotations = _ref0.pendingSelectedAnnotations, latestSelectedAnnotations = _ref0.selectedAnnotations; if (canSelectAnnotation) { if (isDrafting) { // The user must have chosen to discard there draft. So before we flush the pending selections // we need to clear the draft if there is one. (0, _editorCommands.setInlineCommentDraftState)(options.editorAnalyticsAPI, undefined, options.api)(false)(view.state, view.dispatch); } // Flush the pending selections into the selected annotations list. (0, _editorCommands.flushPendingSelections)(options.editorAnalyticsAPI)(true)(view.state, view.dispatch); latestSelectedAnnotations === null || latestSelectedAnnotations === void 0 || latestSelectedAnnotations.filter(function (annotation) { return (latestPendingSelectedAnnotations === null || latestPendingSelectedAnnotations === void 0 ? void 0 : latestPendingSelectedAnnotations.findIndex(function (pendingAnnotation) { return pendingAnnotation.id === annotation.id; })) === -1; }).forEach(function (annotation) { var _getAnnotationInlineN; annotationManager.emit({ name: 'annotationSelectionChanged', data: { annotationId: annotation.id, isSelected: false, inlineNodeTypes: (_getAnnotationInlineN = (0, _utils.getAnnotationInlineNodeTypes)(editorView.state, annotation.id)) !== null && _getAnnotationInlineN !== void 0 ? _getAnnotationInlineN : [] } }); }); // Notify the annotation manager that the pending selection has changed. latestPendingSelectedAnnotations === null || latestPendingSelectedAnnotations === void 0 || latestPendingSelectedAnnotations.forEach(function (_ref1) { var _getAnnotationInlineN2; var id = _ref1.id; annotationManager.emit({ name: 'annotationSelectionChanged', data: { annotationId: id, isSelected: true, inlineNodeTypes: (_getAnnotationInlineN2 = (0, _utils.getAnnotationInlineNodeTypes)(view.state, id)) !== null && _getAnnotationInlineN2 !== void 0 ? _getAnnotationInlineN2 : [] } }); }); } else { // Clears the pending selections if the preemptive gate returns false. // We should need to worry about dispatching change events here because the pending selections // are being aborted and the selections will remain unchanged. (0, _editorCommands.flushPendingSelections)(options.editorAnalyticsAPI)(false)(view.state, view.dispatch); } }).catch(function (error) { // If an error has occured we will clear any pending selections to avoid accidentally setting the wrong thing. (0, _editorCommands.flushPendingSelections)(options.editorAnalyticsAPI)(false, 'pending-selection-preemptive-gate-error')(view.state, view.dispatch); }).finally(function () { isPreemptiveGateActive = false; }); } } var _ref10 = (0, _utils3.getPluginState)(view.state) || {}, dirtyAnnotations = _ref10.dirtyAnnotations; if (!dirtyAnnotations) { return; } (0, _editorCommands.clearDirtyMark)()(view.state, view.dispatch); fetchState(provider, (0, _utils3.getAllAnnotations)(view.state.doc), view, options.editorAnalyticsAPI); }, destroy: function destroy() { // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners editorView.root.removeEventListener('mouseup', mouseUp); if (updateSubscriber) { updateSubscriber.off('resolve', resolve).off('delete', resolve).off('unresolve', unResolve).off('create', unResolve).off('setvisibility', setVisibility).off('setselectedannotation', setSelectedAnnotationFn).off('sethoveredannotation', setHoveredAnnotationFn).off('removehoveredannotation', removeHoveredannotationFn).off('closeinlinecomment', closeInlineCommentFn); } if (annotationManager) { annotationManager.unhook('allowAnnotation', allowAnnotationFn); annotationManager.unhook('startDraft', startDraftFn); annotationManager.unhook('clearDraft', clearDraftFn); annotationManager.unhook('applyDraft', applyDraftFn); annotationManager.unhook('getDraft', getDraftFn); annotationManager.unhook('setIsAnnotationSelected', setIsAnnotationSelectedFn); annotationManager.unhook('setIsAnnotationHovered', setIsAnnotationHoveredFn); annotationManager.unhook('clearAnnotation', clearAnnotationFn); } } }; }, props: { handleDOMEvents: { mousedown: function mousedown(view) { var pluginState = (0, _utils3.getPluginState)(view.state); if (!(pluginState !== null && pluginState !== void 0 && pluginState.mouseData.isSelecting)) { hideToolbar(view.state, view.dispatch)(); } return false; }, dragstart: function dragstart(view, event) { // Mouseup won't be triggered after dropping // Hence, update the mouse data to cancel selecting when drag starts return onMouseUp(view.state, view.dispatch)(event); }, click: function click(view, event) { var _event$target$closest, _pluginState$selected; if (!(event.target instanceof HTMLElement)) { return false; } // Find the nearest ancestor (or self) with the data-id attribute var annotationId = (_event$target$closest = event.target.closest('[data-id]')) === null || _event$target$closest === void 0 ? void 0 : _event$target$closest.getAttribute('data-id'); if (!annotationId) { return false; } var pluginState = (0, _utils3.getPluginState)(view.state); var isSelected = pluginState === null || pluginState === void 0 || (_pluginState$selected = pluginState.selectedAnnotations) === null || _pluginState$selected === void 0 ? void 0 : _pluginState$selected.some(function (selectedAnnotation) { return selectedAnnotation.id === annotationId; }); // If the annotation is selected and the inline comment view is open, do nothing // as the user is already in the comment view. if (isSelected && !(pluginState !== null && pluginState !== void 0 && pluginState.isInlineCommentViewClosed)) { return false; } var _ref11 = pluginState || {}, annotations = _ref11.annotations; var isUnresolved = annotations && annotations[annotationId] === false; if (!isUnresolved) { return false; } if (annotationManager) { var _pluginState$pendingS; // The manager disable setting the selected annotation on click because in the editor this is already // handled by the selection update handler. When the manager is enabled, and a selection changes it's pushed into // the pendingSelectedAnnotations array. This is then used to update the selection when the preemptive gate // is released. var isPendingSelection = pluginState === null || pluginState === void 0 || (_pluginState$pendingS = pluginState.pendingSelectedAnnotations) === null || _pluginState$pendingS === void 0 ? void 0 : _pluginState$pendingS.some(function (selectedAnnotation) { return selectedAnnotation.id === annotationId; }); // If the annotation is selected and the inline comment view is open, do nothing // as the user is already in the comment view. if (isPendingSelection) { return false; } (0, _editorCommands.setPendingSelectedAnnotation)(annotationId)(view.state, view.dispatch); } else { (0, _editorCommands.setSelectedAnnotation)(annotationId)(view.state, view.dispatch); } return true; } }, decorations: function decorations(state) { // highlight comments, depending on state var _ref12 = (0, _utils3.getPluginState)(state) || {}, draftDecorationSet = _ref12.draftDecorationSet, annotations = _ref12.annotations, selectedAnnotations = _ref12.selectedAnnotations, isVisible = _ref12.isVisible, isInlineCommentViewClosed = _ref12.isInlineCommentViewClosed, hoveredAnnotations = _ref12.hoveredAnnotations; var decorations = draftDecorationSet !== null && draftDecorationSet !== void 0 ? draftDecorationSet : _view.DecorationSet.empty; var focusDecorations = []; // TODO: EDITOR-760 - This needs to be optimised, it's not a good idea to scan the entire document // everytime we need to update the decorations. This handler will be called alot. We should be caching // the decorations in plugin state and only updating them when required. state.doc.descendants(function (node, pos) { var _provider$supportedBl; // Inline comment on mediaInline is not supported as part of comments on media project // Thus, we skip the decoration for mediaInline node if (node.type.name === 'mediaInline') { return false; } var isSupportedBlockNode = node.isBlock && ((_provider$supportedBl = provider.supportedBlockNodes) === null || _provider$supportedBl === void 0 ? void 0 : _provider$supportedBl.includes(node.type.name)); node.marks.filter(function (mark) { return mark.type === state.schema.marks.annotation; }).forEach(function (mark) { if (isVisible) { var isUnresolved = !!annotations && annotations[mark.attrs.id] === false; var isSelected = !isInlineCommentViewClosed && !!(selectedAnnotations !== null && selectedAnnotations !== void 0 && selectedAnnotations.some(function (selectedAnnotation) { return selectedAnnotation.id === mark.attrs.id; })); var isHovered = !isInlineCommentViewClosed && !!(hoveredAnnotations !== null && hoveredAnnotations !== void 0 && hoveredAnnotations.some(function (hoveredAnnotation) { return hoveredAnnotation.id === mark.attrs.id; })); if (isSupportedBlockNode) { focusDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, { class: "".concat((0, _nodeviews.getBlockAnnotationViewClassname)(isUnresolved, isSelected), " ").concat(isUnresolved) }, { key: _utils3.decorationKey.block })); } else { if ((0, _platformFeatureFlags.fg)('editor_inline_comments_on_inline_nodes')) { if (node.isText) { focusDecorations.push(_view.Decoration.inline(pos, pos + node.nodeSize, { class: "".concat((0, _nodeviews.getAnnotationViewClassname)(isUnresolved, isSelected, isHovered), " ").concat(isUnresolved), nodeName: 'span' })); } else { focusDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, { class: "".concat((0, _nodeviews.getAnnotationViewClassname)(isUnresolved, isSelected, isHovered), " ").concat(isUnresolved) }, { key: _utils3.decorationKey.block })); } } else { focusDecorations.push(_view.Decoration.inline(pos, pos + node.nodeSize, { class: "".concat((0, _nodeviews.getAnnotationViewClassname)(isUnresolved, isSelected, isHovered), " ").concat(isUnresolved), nodeName: 'span' })); } } } }); }); decorations = decorations.add(state.doc, focusDecorations); return decorations; } } }); };