UNPKG

@atlaskit/editor-common

Version:

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

221 lines (215 loc) • 7.96 kB
import clamp from 'lodash/clamp'; import { ReplaceStep } from '@atlaskit/editor-prosemirror/transform'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isEmptyParagraph } from './editor-core-utils'; export const getStepRange = transaction => { let from = -1; let to = -1; transaction.mapping.maps.forEach((stepMap, index) => { stepMap.forEach((oldStart, oldEnd) => { const newStart = transaction.mapping.slice(index).map(oldStart, -1); const newEnd = transaction.mapping.slice(index).map(oldEnd); const docSize = transaction.doc.content.size; from = clamp(newStart < from || from === -1 ? newStart : from, 0, docSize); to = clamp(newEnd > to || to === -1 ? newEnd : to, 0, docSize); }); }); if (from !== -1) { return { from, to }; } return null; }; // Checks to see if the parent node is the document, ie not contained within another entity export function hasDocAsParent($anchor) { return $anchor.depth === 1; } /** * Checks if a node looks like an empty document */ export function isEmptyDocument(node) { const nodeChild = node.content.firstChild; if (node.childCount !== 1 || !nodeChild) { return false; } return isEmptyParagraph(nodeChild); } export function bracketTyped(state) { const { selection } = state; const { $cursor, $anchor } = selection; if (!$cursor) { return false; } const node = $cursor.nodeBefore; if (!node) { return false; } if (node.type.name === 'text' && node.text === '{') { const paragraphNode = $anchor.node(); return paragraphNode.marks.length === 0 && hasDocAsParent($anchor); } return false; } export function nodesBetweenChanged(tr, f, startPos) { const stepRange = getStepRange(tr); if (!stepRange) { return; } tr.doc.nodesBetween(stepRange.from, stepRange.to, f, startPos); } /** * Returns false if node contains only empty inline nodes and hardBreaks. */ export function hasVisibleContent(node) { const isInlineNodeHasVisibleContent = inlineNode => { return inlineNode.isText ? !!inlineNode.textContent.trim() : inlineNode.type.name !== 'hardBreak'; }; if (node.isInline) { return isInlineNodeHasVisibleContent(node); } else if (node.isBlock && (node.isLeaf || node.isAtom)) { return true; } else if (!node.childCount) { return false; } for (let index = 0; index < node.childCount; index++) { const child = node.child(index); const invisibleNodeTypes = ['paragraph', 'text', 'hardBreak']; if (!invisibleNodeTypes.includes(child.type.name) || hasVisibleContent(child)) { return true; } } return false; } export const isSelectionEndOfParagraph = state => state.selection.$to.parent.type === state.schema.nodes.paragraph && state.selection.$to.pos === state.doc.resolve(state.selection.$to.pos).end(); function getChangedNodesIn({ tr, doc }) { const nodes = []; const stepRange = getStepRange(tr); if (!stepRange) { return nodes; } const from = Math.min(doc.nodeSize - 2, stepRange.from); const to = Math.min(doc.nodeSize - 2, stepRange.to); doc.nodesBetween(from, to, (node, pos) => { nodes.push({ node, pos }); }); return nodes; } export function getChangedNodes(tr) { return getChangedNodesIn({ tr: tr, doc: tr.doc }); } // When document first load in Confluence, initially it is an empty document // and Collab service triggers a transaction to replace the empty document with the real document that should be rendered. // isReplaceDocumentOperation is checking if the transaction is the one that replace the empty document with the real document export const isReplaceDocOperation = (transactions, oldState) => { return transactions.some(tr => { if (tr.getMeta('replaceDocument')) { return true; } const hasStepReplacingEntireDocument = tr.steps.some(step => { if (!(step instanceof ReplaceStep)) { return false; } const isStepReplacingFromDocStart = step.from === 0; const isStepReplacingUntilTheEndOfDocument = step.to === oldState.doc.content.size; if (!isStepReplacingFromDocStart || !isStepReplacingUntilTheEndOfDocument) { return false; } return true; }); return hasStepReplacingEntireDocument; }); }; function marksEqualInOrder(m1, m2) { if (m1.length !== m2.length) return false; return m1.every((m, i) => m.eq(m2[i])); } function marksEqualIgnoringOrder(m1, m2) { if (m1.length !== m2.length) { return false; } const m2Used = new Set(); for (const mark1 of m1) { const idx = m2.findIndex((mark2, i) => !m2Used.has(i) && mark1.eq(mark2)); if (idx === -1) { return false; } m2Used.add(idx); } return true; } /** * Compares two ProseMirror documents for equality, ignoring attributes * which don't affect the document structure. * * This is almost a copy of the .eq() PM function - tweaked to ignore attrs * * @param doc1 PMNode * @param doc2 PMNode * @param attributesToIgnore Specific array of attribute keys to ignore - defaults to ignoring all * @param opts.ignoreMarkOrder If mark order should be ignored to still be equal (e.g. reversed annotation marks). When not provided, controlled by platform_editor_are_nodes_equal_ignore_mark_order feature gate (defaults to true when gate is on). * @returns boolean */ export function areNodesEqualIgnoreAttrs(node1, node2, attributesToIgnore, opts) { var _opts$ignoreMarkOrder; const ignoreMarkOrder = (_opts$ignoreMarkOrder = opts === null || opts === void 0 ? void 0 : opts.ignoreMarkOrder) !== null && _opts$ignoreMarkOrder !== void 0 ? _opts$ignoreMarkOrder : expValEquals('platform_editor_are_nodes_equal_ignore_mark_order', 'isEnabled', true); if (node1.isText) { if (ignoreMarkOrder) { return node1.text === node2.text && marksEqualIgnoringOrder(node1.marks, node2.marks); } return node1.eq(node2); } const marksEqual = ignoreMarkOrder ? marksEqualIgnoringOrder(node1.marks, node2.marks) : marksEqualInOrder(node1.marks, node2.marks); // If no attributes to ignore, compare all attributes if (!attributesToIgnore || attributesToIgnore.length === 0) { if (expValEquals('platform_editor_are_nodes_equal_ignore_mark_order', 'isEnabled', true)) { return node1 === node2 || node1.hasMarkup(node2.type, node1.attrs, node1.marks) && marksEqual && areFragmentsEqual(node1.content, node2.content, undefined, opts); } else { return node1 === node2 || node1.hasMarkup(node2.type, node1.attrs, node2.marks) && areFragmentsEqual(node1.content, node2.content); } } // Build attrs to compare by excluding ignored attributes const attrsToCompare = expValEquals('platform_editor_show_diff_fix_missing_attrs', 'isEnabled', true) ? { ...node2.attrs } : node2.attrs; const ignoreSet = new Set(attributesToIgnore); for (const key in node2.attrs) { if (ignoreSet.has(key)) { attrsToCompare[key] = node1.attrs[key]; } } if (expValEquals('platform_editor_are_nodes_equal_ignore_mark_order', 'isEnabled', true)) { return node1 === node2 || node1.type === node2.type && node1.hasMarkup(node2.type, attrsToCompare, node1.marks) && marksEqual && areFragmentsEqual(node1.content, node2.content, attributesToIgnore, opts); } else { return node1 === node2 || node1.hasMarkup(node2.type, attrsToCompare, node2.marks) && areFragmentsEqual(node1.content, node2.content, attributesToIgnore); } } function areFragmentsEqual(frag1, frag2, attributesToIgnore, opts) { if (frag1.content.length !== frag2.content.length) { return false; } let childrenEqual = true; frag1.content.forEach((child, i) => { const otherChild = frag2.child(i); if (child === otherChild || otherChild && areNodesEqualIgnoreAttrs(child, otherChild, attributesToIgnore, opts)) { return; } childrenEqual = false; }); return childrenEqual; }