@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
267 lines (250 loc) • 9.58 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import { AnnotationTypes } from '@atlaskit/adf-schema';
import { fg } from '@atlaskit/platform-feature-flags';
export var canApplyAnnotationOnRange = function canApplyAnnotationOnRange(rangeSelection, doc, schema) {
var from = rangeSelection.from,
to = rangeSelection.to;
if (isNaN(from + to) || to - from <= 0 || to < 0 || from < 0) {
return false;
}
var foundInvalid = false;
doc.nodesBetween(rangeSelection.from, rangeSelection.to, function (node, _pos, parent) {
// Special exception for hardBreak nodes
if (schema.nodes.hardBreak === node.type) {
return false;
}
// For block elements or text nodes, we want to check
// if annotations are allowed inside this tree
// or if we're leaf and not text
if (fg('editor_inline_comments_on_inline_nodes')) {
var isAllowedInlineNode = ['emoji', 'status', 'date', 'mention', 'inlineCard'].includes(node.type.name);
if (node.isInline && !node.isText && !isAllowedInlineNode || node.isLeaf && !node.isText && !isAllowedInlineNode || node.isText && !(parent !== null && parent !== void 0 && parent.type.allowsMarkType(schema.marks.annotation))) {
foundInvalid = true;
return false;
}
} else {
if (node.isInline && !node.isText || node.isLeaf && !node.isText || node.isText && !(parent !== null && parent !== void 0 && parent.type.allowsMarkType(schema.marks.annotation))) {
foundInvalid = true;
return false;
}
}
return true;
});
return !foundInvalid;
};
export var getAnnotationIdsFromRange = function getAnnotationIdsFromRange(rangeSelection, doc, schema) {
var from = rangeSelection.from,
to = rangeSelection.to;
var annotations = new Set();
doc.nodesBetween(from, to, function (node) {
if (!node.marks) {
return true;
}
node.marks.forEach(function (mark) {
if (mark.type === schema.marks.annotation && mark.attrs) {
annotations.add(mark.attrs.id);
}
});
return true;
});
return Array.from(annotations);
};
/*
* verifies if node contains annotation mark
*/
export function hasAnnotationMark(node, state) {
var annotationMark = state.schema.marks.annotation;
return !!(annotationMark && node && node.marks.length && annotationMark.isInSet(node.marks));
}
/*
* verifies that slice contains any annotations
*/
export function containsAnyAnnotations(slice, state) {
if (!slice.content.size) {
return false;
}
var hasAnnotation = false;
slice.content.forEach(function (node) {
hasAnnotation = hasAnnotation || hasAnnotationMark(node, state);
// return early if annotation found already
if (hasAnnotation) {
return true;
}
// check annotations in descendants
node.descendants(function (node) {
if (hasAnnotationMark(node, state)) {
hasAnnotation = true;
return false;
}
return true;
});
});
return hasAnnotation;
}
/**
* This returns a list of node names that are inline nodes in the range.
*/
export function getRangeInlineNodeNames(_ref) {
var doc = _ref.doc,
pos = _ref.pos;
if (!fg('editor_inline_comments_on_inline_nodes')) {
return undefined;
}
var nodeNames = new Set();
try {
doc.nodesBetween(pos.from, pos.to, function (node) {
if (node.isInline) {
nodeNames.add(node.type.name);
}
});
// We sort the list alphabetically to make human consumption of the list easier (in tools like the analytics extension)
var sortedNames = _toConsumableArray(nodeNames).sort();
return sortedNames;
} catch (_unused) {
// Some callers have existing gaps where the positions are not valid,
// and so when called in the current document can throw an error if out of range.
//
// This is a defensive check to ensure we don't throw an error in those cases.
return undefined;
}
}
/**
* This returns a list of ancestor node names that contain the given position.
*/
export function getRangeAncestorNodeNames(_ref2) {
var doc = _ref2.doc,
pos = _ref2.pos;
if (!fg('cc_comments_log_draft_annotation_ancestor_nodes')) {
return undefined;
}
var nodeNames = new Set();
try {
// For a range, we can get ancestors at both from and to positions
// or just use the from position if you want ancestors at the start
var resolvedFromPos = doc.resolve(pos.from);
var resolvedToPos = doc.resolve(pos.to);
// Collect ancestors at the 'from' position
for (var depth = resolvedFromPos.depth; depth > 0; depth--) {
var ancestorNode = resolvedFromPos.node(depth);
nodeNames.add(ancestorNode.type.name);
}
// Use nodesBetween to collect parent types without additional resolve() calls
// This isn't as precise as calling resolve() on each parent, but it's a lot faster
// and should hopefully provide a good approximation of the ancestor nodes
var seenParents = new Set();
doc.nodesBetween(pos.from, pos.to, function (_node, _nodePos, parent) {
// Collect parent chain using the parent parameter
var currentParent = parent;
if (!!currentParent && !seenParents.has(currentParent)) {
seenParents.add(currentParent);
nodeNames.add(currentParent.type.name);
// Note: We can't easily get the parent's parent from this context
// without additional resolve calls, so this approach has limitations
}
return true;
});
// Optionally collect ancestors at the 'to' position if different
// This ensures we capture all ancestor contexts across the range
if (pos.from !== pos.to) {
for (var _depth = resolvedToPos.depth; _depth > 0; _depth--) {
var _ancestorNode = resolvedToPos.node(_depth);
nodeNames.add(_ancestorNode.type.name);
}
}
return _toConsumableArray(nodeNames);
} catch (_unused2) {
// Some callers have existing gaps where the positions are not valid,
// and so when called in the current document can throw an error if out of range.
//
// This is a defensive check to ensure we don't throw an error in those cases.
return undefined;
}
}
/**
* This function returns a list of node types that are wrapped by an annotation mark.
*
* The `undefined` will be returned if `editor_inline_comments_on_inline_nodes` is off.
*
* @todo: Do not forget to remove `undefined` when the
* `editor_inline_comments_on_inline_nodes` is removed.
*/
export function getAnnotationInlineNodeTypes(state, annotationId) {
if (!fg('editor_inline_comments_on_inline_nodes')) {
return undefined;
}
var mark = state.schema.marks.annotation.create({
id: annotationId,
annotationType: AnnotationTypes.INLINE_COMMENT
});
var inlineNodeNames = new Set();
state.doc.descendants(function (node, _pos) {
if (mark.isInSet(node.marks)) {
inlineNodeNames.add(node.type.name);
}
return true;
});
// This sorting is done to make human consumption easier (ie. in dev tools, test snapshots, analytics events, ...)
return _toConsumableArray(inlineNodeNames).sort();
}
/*
Get the annotations marks from the given position and add them to the original marks array if they exist.
Used with the creation of the inline nodes: emoji, status, dates, mentions & inlineCards.
*/
export function getAnnotationMarksForPos(pos) {
var annotationMarks = pos.marks().filter(function (mark) {
return mark.type === pos.doc.type.schema.marks.annotation;
});
return annotationMarks;
}
/**
* Checks if selection contains only empty text
* e.g. when you select across multiple empty paragraphs
*/
export function isEmptyTextSelection(selection, schema) {
var _schema$nodes = schema.nodes,
text = _schema$nodes.text,
paragraph = _schema$nodes.paragraph;
var hasContent = false;
selection.content().content.descendants(function (node) {
// for empty paragraph - consider empty (nothing to comment on)
if (node.type === paragraph && !node.content.size) {
return false;
}
// for not a text or nonempty text - consider nonempty (can comment if the node is supported for annotations)
if (node.type !== text || !node.textContent) {
hasContent = true;
}
return !hasContent;
});
return !hasContent;
}
/**
* This is a modified version of the `isEmptyTextSelection` function (above), fixing some unexpected behavior in the renderer.
*
* This function does NOT consider non-inline nodes as non-empty.
* With this change, the function continues descending into block nodes, like tables or expands. Without this change for
* the renderer, block nodes containing empty text were not considered empty.
*/
export function isEmptyTextSelectionRenderer(selection, schema) {
var _schema$nodes2 = schema.nodes,
text = _schema$nodes2.text,
paragraph = _schema$nodes2.paragraph;
var hasContent = false;
selection.content().content.descendants(function (node) {
// for empty paragraph - consider empty (nothing to comment on)
if (node.type === paragraph && !node.content.size) {
return false;
}
// for inline elements - consider nonempty
if (node.type !== text && node.type.isInline) {
hasContent = true;
}
// for nonempty text - consider nonempty (can comment if the node is supported for annotations)
if (node.type === text && !!node.textContent) {
hasContent = true;
}
// for other non-text nodes - continue descending
return !hasContent;
});
return !hasContent;
}