@atlaskit/editor-plugin-block-menu
Version:
BlockMenu plugin for @atlaskit/editor-core
151 lines (146 loc) • 5.09 kB
JavaScript
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { CellSelection } from '@atlaskit/editor-tables';
import { NODE_CATEGORY_BY_TYPE } from './types';
/**
* Determines if a node is a text node (heading or paragraph).
* Text nodes can have their content converted to paragraphs when they can't be wrapped directly.
*/
export const isTextNode = node => {
const category = NODE_CATEGORY_BY_TYPE[node.type.name];
return category === 'text';
};
export const isListNode = node => {
return ['bulletList', 'orderedList', 'taskList'].includes(node.type.name);
};
export const getSelectedNode = selection => {
if (selection instanceof NodeSelection) {
return {
node: selection.node,
pos: selection.$from.pos,
start: 0,
// ?
depth: selection.$from.depth
};
}
if (selection instanceof CellSelection) {
const tableSelected = findParentNodeOfType(selection.$from.doc.type.schema.nodes.table)(selection);
return tableSelected;
}
if (selection instanceof TextSelection) {
const {
blockquote,
bulletList,
orderedList,
taskList,
codeBlock,
paragraph,
heading
} = selection.$from.doc.type.schema.nodes;
const quoteSelected = findParentNodeOfType([blockquote])(selection);
if (quoteSelected) {
return quoteSelected;
}
const codeBlockSelected = findParentNodeOfType([codeBlock])(selection);
if (codeBlockSelected) {
return codeBlockSelected;
}
const listSelected = findParentNodeOfType([bulletList, taskList, orderedList])(selection);
if (listSelected) {
return listSelected;
}
const paragraphOrHeading = findParentNodeOfType([paragraph, heading])(selection);
if (paragraphOrHeading) {
return paragraphOrHeading;
}
}
return undefined;
};
export const getTargetNodeTypeNameInContext = (nodeTypeName, isNested, parentNode) => {
if (parentNode && isNested && (parentNode.type.name === 'layoutColumn' || parentNode.type.name === 'bodiedSyncBlock')) {
return nodeTypeName;
}
if (nodeTypeName === 'expand' && isNested) {
return 'nestedExpand';
}
return nodeTypeName;
};
/**
* Converts a nestedExpand to a regular expand node.
* NestedExpands can only exist inside expands, so when breaking out or placing
* in containers that don't support nesting, they must be converted.
*/
export const convertNestedExpandToExpand = (node, schema) => {
var _node$attrs;
const expandType = schema.nodes.expand;
if (!expandType) {
return null;
}
return expandType.createAndFill({
title: ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.title) || '',
localId: crypto.randomUUID()
}, node.content);
};
/**
* Converts an expand to a nestedExpand node.
* When placing an expand inside another expand, it must become a nestedExpand
* since expand cannot be a direct child of expand.
*/
export const convertExpandToNestedExpand = (node, schema) => {
var _node$attrs2;
const nestedExpandType = schema.nodes.nestedExpand;
if (!nestedExpandType) {
return null;
}
return nestedExpandType.createAndFill({
title: ((_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.title) || '',
localId: crypto.randomUUID()
}, node.content);
};
/**
* Converts a text node (heading, paragraph) to a paragraph preserving its inline content.
* This is used when a text node can't be wrapped directly in the target container
* (e.g., heading can't go in blockquote, so it becomes a paragraph).
*/
export const convertTextNodeToParagraph = (node, schema) => {
var _schema$nodes$paragra;
// If it's already a paragraph, return as-is
if (node.type.name === 'paragraph') {
return node;
}
// Convert heading (or other text node) to paragraph with same inline content
return (_schema$nodes$paragra = schema.nodes.paragraph.createAndFill({}, node.content)) !== null && _schema$nodes$paragra !== void 0 ? _schema$nodes$paragra : null;
};
export const getBlockNodesInRange = range => {
if (range.startIndex === range.endIndex) {
return [];
}
if (range.endIndex - range.startIndex <= 1) {
return [range.parent.child(range.startIndex)];
}
const blockNodes = [];
for (let i = range.startIndex; i < range.endIndex; i++) {
if (range.parent.child(i).isBlock) {
blockNodes.push(range.parent.child(i));
}
}
return blockNodes;
};
/**
* Iterates over a nodes children and extracting text content, removing all other inline content and converting
* hardbreaks to newlines.
*
* @param node - The node to create text content from (should be paragraph)
* @returns The text content string.
*/
export const createTextContent = node => {
const textContent = node.children.map(child => {
if (child.isText) {
return child.text;
} else if (child.type.name === 'hardBreak') {
return '\n';
}
return '';
});
return textContent.join('');
};