UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

267 lines (250 loc) • 9.58 kB
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; }