UNPKG

@atlaskit/renderer

Version:
381 lines (372 loc) • 11.4 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import { AnnotationTypes } from '@atlaskit/adf-schema'; import { canApplyAnnotationOnRange, getAnnotationIdsFromRange, getAnnotationInlineNodeTypes, isEmptyTextSelectionRenderer } from '@atlaskit/editor-common/utils'; import { JSONTransformer } from '@atlaskit/editor-json-transformer'; import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { AddNodeMarkStep, RemoveMarkStep, RemoveNodeMarkStep } from '@atlaskit/editor-prosemirror/transform'; import { fg } from '@atlaskit/platform-feature-flags'; import { createAnnotationStep, getPosFromRange } from '../steps'; import { getRendererRangeInlineNodeNames, getRendererRangeAncestorNodeNames } from './get-renderer-range-inline-node-names'; import { getIndexMatch } from './matches-utils'; import { getSelectionContext } from './selection'; export default class RendererActions { // Any kind of refence is allowed // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(initFromContext = false) { // This is our psuedo feature flag for now // This module can only be used when wrapped with // the <RendererContext> component for now. _defineProperty(this, "initFromContext", false); this.initFromContext = initFromContext; this.transformer = new JSONTransformer(); } //#region private _privateRegisterRenderer(ref, doc, schema, onAnalyticsEvent) { if (!this.initFromContext) { return; } else if (!this.ref) { this.ref = ref; } else if (this.ref !== ref) { throw new Error("Renderer has already been registered! It's not allowed to re-register with another new Renderer instance."); } this.doc = doc; this.schema = schema; this.onAnalyticsEvent = onAnalyticsEvent; } _privateUnregisterRenderer() { this.doc = undefined; this.ref = undefined; this.schema = undefined; } /** * Validate whether we can create an annotation between two positions */ _privateValidatePositionsForAnnotation(from, to) { if (!this.doc || !this.schema) { return false; } const currentSelection = TextSelection.create(this.doc, from, to); if (isEmptyTextSelectionRenderer(currentSelection, this.schema)) { return false; } const result = canApplyAnnotationOnRange({ from, to }, this.doc, this.schema); return result; } //#endregion deleteAnnotation(annotationId, annotationType) { if (!this.doc || !this.schema || !this.schema.marks.annotation) { return false; } const mark = this.schema.marks.annotation.create({ id: annotationId, annotationType }); let from; let to; let nodePos; let step; this.doc.descendants((node, pos) => { const found = mark.isInSet(node.marks); if (found && node.type.name === 'media') { nodePos = pos; } if (found && !from) { // Set both here incase it only spans one node. from = pos; to = pos + node.nodeSize; } else if (found && from) { // If the mark spans multiple nodes, // we'll keep setting the end until no longer found. to = pos + node.nodeSize; } return true; }); if (nodePos !== undefined) { step = new RemoveNodeMarkStep(nodePos, mark); } else { if (from === undefined || to === undefined) { return false; } step = new RemoveMarkStep(from, to, mark); } const { doc, failed } = step.apply(this.doc); if (this.onAnalyticsEvent) { const payload = { action: ACTION.DELETED, actionSubject: ACTION_SUBJECT.ANNOTATION, actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT, eventType: EVENT_TYPE.TRACK, attributes: { inlineNodeNames: step instanceof RemoveMarkStep ? getRendererRangeInlineNodeNames({ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pos: { from: from, to: to }, actions: this }) : undefined } }; this.onAnalyticsEvent(payload); } if (!failed && doc) { return { step, doc: this.transformer.encode(doc) }; } return false; } annotate(range, annotationId, _annotationType) { if (!this.doc || !this.schema || !this.schema.marks.annotation) { return false; } const pos = getPosFromRange(range); if (!pos) { return false; } const { from, to } = pos; const validPositions = this._privateValidatePositionsForAnnotation(from, to); if (!validPositions) { return false; } return this.applyAnnotation(pos, { annotationId, annotationType: AnnotationTypes.INLINE_COMMENT }); } isValidAnnotationRange(range) { if (!range) { return false; } if (fg('editor_inline_comments_on_inline_nodes')) { if (this.isRendererWithinRange(range)) { return false; } } const pos = getPosFromRange(range); if (!pos || !this.doc) { return false; } return this._privateValidatePositionsForAnnotation(pos.from, pos.to); } isRangeAnnotatable(range) { try { var _startContainer$paren, _endContainer$parentE; if (!range) { return false; } const { startContainer, endContainer } = range; if ((_startContainer$paren = startContainer.parentElement) !== null && _startContainer$paren !== void 0 && _startContainer$paren.closest('.ak-renderer-extension') || (_endContainer$parentE = endContainer.parentElement) !== null && _endContainer$parentE !== void 0 && _endContainer$parentE.closest('.ak-renderer-extension')) { return false; } return this.isValidAnnotationRange(range); } catch { // isValidAnnotationRange can fail when called inside nested renderers. // while isRendererWithinRange guards against this to some degree -- the classnames // are controlled by product -- and we don't have platform guarantees on them. // // Currently there is a mix of logic across the platform and confluence on determining // positions that are annotatable. This is a defensive check to ensure we don't throw an error // in cases where the range is not valid. return false; } } // eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883 /** * This is replaced by `isRangeAnnotatable`. * * @deprecated **/ isRendererWithinRange(range) { const { startContainer, endContainer } = range; if (startContainer.parentElement && startContainer.parentElement.closest('.ak-renderer-extension') || endContainer.parentElement && endContainer.parentElement.closest('.ak-renderer-extension')) { return true; } return false; } isValidAnnotationPosition(pos) { if (!pos || !this.doc) { return false; } return this._privateValidatePositionsForAnnotation(pos.from, pos.to); } /** * Note: False indicates that the selection not able to be calculated. */ getPositionFromRange(range) { if (!this.doc || !this.schema || !range) { return false; } return getPosFromRange(range); } getSelectionContext() { return getSelectionContext({ doc: this.doc, schema: this.schema }); } getAnnotationMarks() { const { schema, doc } = this; if (!schema || !doc) { return []; } const { marks: { annotation: annotationMarkType } } = schema; if (!annotationMarkType) { return []; } const marks = []; doc.descendants(node => { const annotationsMark = node.marks.filter(m => m.type === annotationMarkType); if (!annotationsMark || !annotationsMark.length) { return true; } marks.push(...annotationsMark); return false; }); const uniqueMarks = new Map(); marks.forEach(m => { uniqueMarks.set(m.attrs.id, m); }); return Array.from(uniqueMarks.values()); } getAnnotationsByPosition(range) { if (!this.doc || !this.schema) { return []; } const pos = getPosFromRange(range); if (!pos || !this.doc) { return []; } return getAnnotationIdsFromRange(pos, this.doc, this.schema); } applyAnnotation(pos, annotation) { if (!this.doc || !pos || !this.schema) { return false; } const { from, to } = pos; const { annotationId, annotationType } = annotation; let step; let targetNodeType; // As part of fix for RAP, `from` points to the position right before media node // hence, -1 is not needed const beforeNodePos = from; const possibleNode = this.doc.nodeAt(beforeNodePos); if ((possibleNode === null || possibleNode === void 0 ? void 0 : possibleNode.type.name) === 'media') { targetNodeType = 'media'; step = new AddNodeMarkStep(beforeNodePos, this.schema.marks.annotation.create({ id: annotationId, type: annotationType })); } else { const resolvedNode = this.doc.resolve(from).node(); // annotation is technically on text, but the context is caption targetNodeType = resolvedNode.type.name === 'caption' ? 'caption' : 'text'; step = createAnnotationStep(from, to, { annotationId, annotationType, schema: this.schema }); } const { doc, failed } = step.apply(this.doc); if (failed || !doc) { return false; } const originalSelection = doc.textBetween(from, to); const { numMatches, matchIndex, blockNodePos } = getIndexMatch(this.doc, this.schema, originalSelection, from); return { step, doc: this.transformer.encode(doc), inlineNodeTypes: getRendererRangeInlineNodeNames({ actions: this, pos: { from, to } }), ancestorNodeTypes: getRendererRangeAncestorNodeNames({ actions: this, pos: { from, to } }), originalSelection, numMatches, matchIndex, pos: blockNodePos, targetNodeType }; } generateAnnotationIndexMatch(pos) { if (!this.doc || !pos || !this.schema) { return false; } const { from, to } = pos; const originalSelection = this.doc.textBetween(from, to); const { numMatches, matchIndex, blockNodePos } = getIndexMatch(this.doc, this.schema, originalSelection, from); return { originalSelection, numMatches, matchIndex, pos: blockNodePos }; } // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format // TODO: Do not forget to remove `undefined` when the `editor_inline_comments_on_inline_nodes` is removed. getInlineNodeTypes(annotationId) { if (!this.doc || !this.schema) { return []; } return getAnnotationInlineNodeTypes({ doc: this.doc, schema: this.schema }, annotationId); } }