@atlaskit/editor-plugin-block-type
Version:
BlockType plugin for @atlaskit/editor-core
324 lines (313 loc) • 12.5 kB
JavaScript
import { anyMarkActive } from '@atlaskit/editor-common/mark';
import { createBlockTaskItem } from '@atlaskit/editor-common/transforms';
import { createRule, createWrappingJoinRule } from '@atlaskit/editor-common/utils';
import { hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { WRAPPER_BLOCK_TYPES, FORMATTING_NODE_TYPES, FORMATTING_MARK_TYPES } from './block-types';
export var isNodeAWrappingBlockNode = function isNodeAWrappingBlockNode(node) {
if (!node) {
return false;
}
return WRAPPER_BLOCK_TYPES.some(function (blockNode) {
return blockNode.name === node.type.name;
});
};
export var createJoinNodesRule = function createJoinNodesRule(match, nodeType) {
return createWrappingJoinRule({
nodeType: nodeType,
match: match,
getAttrs: {},
joinPredicate: function joinPredicate(_, node) {
return node.type === nodeType;
}
});
};
export var createWrappingTextBlockRule = function createWrappingTextBlockRule(_ref) {
var match = _ref.match,
nodeType = _ref.nodeType,
getAttrs = _ref.getAttrs;
var handler = function handler(state, match, start, end) {
var fixedStart = Math.max(start, 1);
var $start = state.doc.resolve(fixedStart);
var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
var nodeBefore = $start.node(-1);
if (nodeBefore && !nodeBefore.canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType)) {
return null;
}
return state.tr.delete(fixedStart, end).setBlockType(fixedStart, fixedStart, nodeType, attrs);
};
return createRule(match, handler);
};
/**
* Function will create a list of wrapper blocks present in a selection.
*/
function getSelectedWrapperNodes(state) {
var nodes = [];
if (state.selection) {
var _state$selection = state.selection,
$from = _state$selection.$from,
$to = _state$selection.$to,
empty = _state$selection.empty;
var _state$schema$nodes = state.schema.nodes,
blockquote = _state$schema$nodes.blockquote,
panel = _state$schema$nodes.panel,
orderedList = _state$schema$nodes.orderedList,
bulletList = _state$schema$nodes.bulletList,
listItem = _state$schema$nodes.listItem,
caption = _state$schema$nodes.caption,
codeBlock = _state$schema$nodes.codeBlock,
decisionItem = _state$schema$nodes.decisionItem,
decisionList = _state$schema$nodes.decisionList,
taskItem = _state$schema$nodes.taskItem,
taskList = _state$schema$nodes.taskList;
var wrapperNodes = [blockquote, panel, orderedList, bulletList, listItem, codeBlock, decisionItem, decisionList, taskItem, taskList];
wrapperNodes.push(caption);
if (empty && expValEquals('platform_editor_small_font_size', 'isEnabled', true)) {
for (var depth = 0; depth <= $from.depth; depth++) {
var node = $from.node(depth);
if (node.isBlock && wrapperNodes.indexOf(node.type) >= 0) {
nodes.push(node.type);
}
}
return nodes;
}
state.doc.nodesBetween($from.pos, $to.pos, function (node) {
if (node.isBlock && wrapperNodes.indexOf(node.type) >= 0) {
nodes.push(node.type);
}
});
}
return nodes;
}
/**
* Function will check if changing block types: Paragraph, Heading is enabled.
*/
export function areBlockTypesDisabled(state) {
var allowFontSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var nodesTypes = getSelectedWrapperNodes(state);
var _state$schema$nodes2 = state.schema.nodes,
panel = _state$schema$nodes2.panel,
blockquote = _state$schema$nodes2.blockquote,
bulletList = _state$schema$nodes2.bulletList,
orderedList = _state$schema$nodes2.orderedList,
listItem = _state$schema$nodes2.listItem,
taskList = _state$schema$nodes2.taskList,
taskItem = _state$schema$nodes2.taskItem;
var isSmallFontSizeEnabled = allowFontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
var excludedTypes = isSmallFontSizeEnabled ? [panel, bulletList, orderedList, listItem, taskList, taskItem] : [panel];
var disallowedWrapperTypes = nodesTypes.filter(function (type) {
return !excludedTypes.includes(type);
});
if (isSmallFontSizeEnabled) {
var selectionInsideList = isSelectionInsideListNode(state);
var selectionInsideQuote = isSelectionInsideBlockquote(state);
// Inside a blockquote (but not a list nested within one): the blockquote itself isn't a
// disallowing wrapper, but anything else is.
if (selectionInsideQuote && !selectionInsideList) {
return disallowedWrapperTypes.some(function (type) {
return type !== blockquote;
});
}
return disallowedWrapperTypes.length > 0;
}
if (editorExperiment('platform_editor_blockquote_in_text_formatting_menu', true)) {
var hasQuote = false;
var hasNestedListInQuote = false;
var _state$selection2 = state.selection,
$from = _state$selection2.$from,
$to = _state$selection2.$to;
state.doc.nodesBetween($from.pos, $to.pos, function (node) {
if (node.type === blockquote) {
hasQuote = true;
node.descendants(function (child) {
if (child.type === bulletList || child.type === orderedList) {
hasNestedListInQuote = true;
return false;
}
return true;
});
}
return !hasNestedListInQuote;
});
return disallowedWrapperTypes.length > 0 && (!hasQuote || hasNestedListInQuote);
}
return disallowedWrapperTypes.length > 0;
}
/**
* Checks if the current selection is inside a list node (bulletList, orderedList, or taskList).
* Used to determine which text styles should be enabled when the small font size experiment is active.
*/
export function isSelectionInsideListNode(state) {
if (!state.selection) {
return false;
}
var _state$selection3 = state.selection,
$from = _state$selection3.$from,
$to = _state$selection3.$to;
var _state$schema$nodes3 = state.schema.nodes,
bulletList = _state$schema$nodes3.bulletList,
orderedList = _state$schema$nodes3.orderedList,
taskList = _state$schema$nodes3.taskList;
var listNodeTypes = [bulletList, orderedList, taskList];
var insideList = false;
state.doc.nodesBetween($from.pos, $to.pos, function (node) {
if (node.isBlock && listNodeTypes.indexOf(node.type) >= 0) {
insideList = true;
return false;
}
return true;
});
return insideList || listNodeTypes.some(function (nodeType) {
return hasParentNodeOfType(nodeType)(state.selection);
});
}
export function isSelectionInsideBlockquote(state) {
if (!state.selection) {
return false;
}
var _state$selection4 = state.selection,
$from = _state$selection4.$from,
$to = _state$selection4.$to;
var blockquote = state.schema.nodes.blockquote;
// For collapsed selections, check if the cursor is inside a blockquote
if ($from.pos === $to.pos) {
return hasParentNodeOfType(blockquote)(state.selection);
}
// For range selections, check if any node in the range is a blockquote,
// or if the selection starts/ends inside a blockquote
var insideQuote = false;
state.doc.nodesBetween($from.pos, $to.pos, function (node) {
if (node.type === blockquote) {
insideQuote = true;
return false;
}
return true;
});
return insideQuote || hasParentNodeOfType(blockquote)(state.selection);
}
var blockStylingIsPresent = function blockStylingIsPresent(state) {
var _state$selection5 = state.selection,
from = _state$selection5.from,
to = _state$selection5.to;
var isBlockStyling = false;
state.doc.nodesBetween(from, to, function (node) {
if (FORMATTING_NODE_TYPES.indexOf(node.type.name) !== -1) {
isBlockStyling = true;
return false;
}
return true;
});
return isBlockStyling;
};
var marksArePresent = function marksArePresent(state) {
var activeMarkTypes = FORMATTING_MARK_TYPES.filter(function (mark) {
if (!!state.schema.marks[mark]) {
var _state$selection6 = state.selection,
$from = _state$selection6.$from,
empty = _state$selection6.empty;
var marks = state.schema.marks;
if (empty) {
return !!marks[mark].isInSet(state.storedMarks || $from.marks());
}
return anyMarkActive(state, marks[mark]);
}
return false;
});
return activeMarkTypes.length > 0;
};
export var checkFormattingIsPresent = function checkFormattingIsPresent(state) {
return marksArePresent(state) || blockStylingIsPresent(state);
};
export var hasBlockQuoteInOptions = function hasBlockQuoteInOptions(dropdownOptions) {
return !!dropdownOptions.find(function (blockType) {
return blockType.name === 'blockquote';
});
};
/**
* Returns a { from, to } range that extends the selection boundaries outward
* to include the entirety of any list nodes at either end. If the selection
* start is inside a list, `from` is pulled back to the list's start; if the
* selection end is inside a list, `to` is pushed forward to the list's end.
* Non-list content in the middle is included as-is.
*/
export function getSelectionRangeExpandedToLists(tr) {
var selection = tr.selection;
var _tr$doc$type$schema$n = tr.doc.type.schema.nodes,
bulletList = _tr$doc$type$schema$n.bulletList,
orderedList = _tr$doc$type$schema$n.orderedList,
taskList = _tr$doc$type$schema$n.taskList;
var listNodeTypes = [bulletList, orderedList, taskList];
var from = selection.from;
var to = selection.to;
// Walk up from the selection start to find the outermost list node.
// We do NOT break at the first list found because task lists nest differently
// from bullet/ordered lists:
// - bullet/ordered: bulletList > listItem > bulletList (nested inside listItem)
// - task: taskList > taskList (nested as direct children)
// For task lists, breaking at the first list would only capture the innermost
// taskList, missing sibling task items in parent lists. By continuing to walk
// up, we find the outermost list and include all nested content.
for (var depth = selection.$from.depth; depth > 0; depth--) {
var node = selection.$from.node(depth);
if (listNodeTypes.indexOf(node.type) >= 0) {
from = selection.$from.before(depth);
}
}
for (var _depth = selection.$to.depth; _depth > 0; _depth--) {
var _node = selection.$to.node(_depth);
if (listNodeTypes.indexOf(_node.type) >= 0) {
to = selection.$to.after(_depth);
}
}
return {
from: from,
to: to
};
}
/**
* Converts all taskItem nodes within the given range to blockTaskItem nodes.
*
* taskItem nodes contain inline content directly, which cannot hold block-level
* marks like fontSize. blockTaskItem nodes wrap content in paragraphs, which can
* hold block marks. This conversion is needed when applying small text formatting
* to task lists.
*
* The inline content of each taskItem is wrapped in a paragraph node, and the
* taskItem is replaced with a blockTaskItem that preserves the original attributes
* (localId, state).
*
* Collects taskItem positions in a forward pass over the unmutated document,
* then applies replacements in reverse document order so positions remain valid
* without needing remapping or doc snapshots.
*/
export function convertTaskItemsToBlockTaskItems(tr, from, to) {
var _tr$doc$type$schema$n2 = tr.doc.type.schema.nodes,
taskItem = _tr$doc$type$schema$n2.taskItem,
blockTaskItem = _tr$doc$type$schema$n2.blockTaskItem;
if (!blockTaskItem || !taskItem) {
return;
}
// Collect taskItem positions from the current (unmutated) document
var taskItemsToConvert = [];
tr.doc.nodesBetween(from, to, function (node, pos) {
if (node.type === taskItem) {
taskItemsToConvert.push({
pos: pos,
node: node
});
}
});
// Replace in reverse document order so earlier positions remain valid
for (var i = taskItemsToConvert.length - 1; i >= 0; i--) {
var _taskItemsToConvert$i = taskItemsToConvert[i],
pos = _taskItemsToConvert$i.pos,
node = _taskItemsToConvert$i.node;
var blockTaskNode = createBlockTaskItem({
attrs: node.attrs,
content: node.content,
schema: tr.doc.type.schema
});
tr.replaceWith(pos, pos + node.nodeSize, blockTaskNode);
}
}