@atlaskit/renderer
Version:
Renderer component
381 lines (372 loc) • 11.4 kB
JavaScript
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);
}
}