UNPKG

@atlaskit/editor-common

Version:

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

278 lines (261 loc) • 9.15 kB
import { AnnotationTypes } from '@atlaskit/adf-schema'; import { fg } from '@atlaskit/platform-feature-flags'; export const canApplyAnnotationOnRange = (rangeSelection, doc, schema) => { const { from, to } = rangeSelection; if (isNaN(from + to) || to - from <= 0 || to < 0 || from < 0) { return false; } let foundInvalid = false; doc.nodesBetween(rangeSelection.from, rangeSelection.to, (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')) { const 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 const getAnnotationIdsFromRange = (rangeSelection, doc, schema) => { const { from, to } = rangeSelection; const annotations = new Set(); doc.nodesBetween(from, to, node => { if (!node.marks) { return true; } node.marks.forEach(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) { const { schema: { marks: { annotation: annotationMark } } } = state; 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; } let hasAnnotation = false; slice.content.forEach(node => { hasAnnotation = hasAnnotation || hasAnnotationMark(node, state); // return early if annotation found already if (hasAnnotation) { return true; } // check annotations in descendants node.descendants(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({ doc, pos }) { if (!fg('editor_inline_comments_on_inline_nodes')) { return undefined; } const nodeNames = new Set(); try { doc.nodesBetween(pos.from, pos.to, 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) const sortedNames = [...nodeNames].sort(); return sortedNames; } catch { // 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({ doc, pos }) { if (!fg('cc_comments_log_draft_annotation_ancestor_nodes')) { return undefined; } const 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 const resolvedFromPos = doc.resolve(pos.from); const resolvedToPos = doc.resolve(pos.to); // Collect ancestors at the 'from' position for (let depth = resolvedFromPos.depth; depth > 0; depth--) { const 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 const seenParents = new Set(); doc.nodesBetween(pos.from, pos.to, (_node, _nodePos, parent) => { // Collect parent chain using the parent parameter const 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 (let depth = resolvedToPos.depth; depth > 0; depth--) { const ancestorNode = resolvedToPos.node(depth); nodeNames.add(ancestorNode.type.name); } } return [...nodeNames]; } catch { // 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; } const mark = state.schema.marks.annotation.create({ id: annotationId, annotationType: AnnotationTypes.INLINE_COMMENT }); const inlineNodeNames = new Set(); state.doc.descendants((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 [...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) { const annotationMarks = pos.marks().filter(mark => 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) { const { text, paragraph } = schema.nodes; let hasContent = false; selection.content().content.descendants(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) { const { text, paragraph } = schema.nodes; let hasContent = false; selection.content().content.descendants(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; }