UNPKG

@atlaskit/editor-plugin-block-type

Version:

BlockType plugin for @atlaskit/editor-core

337 lines (325 loc) 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.areBlockTypesDisabled = areBlockTypesDisabled; exports.checkFormattingIsPresent = void 0; exports.convertTaskItemsToBlockTaskItems = convertTaskItemsToBlockTaskItems; exports.createWrappingTextBlockRule = exports.createJoinNodesRule = void 0; exports.getSelectionRangeExpandedToLists = getSelectionRangeExpandedToLists; exports.isNodeAWrappingBlockNode = exports.hasBlockQuoteInOptions = void 0; exports.isSelectionInsideBlockquote = isSelectionInsideBlockquote; exports.isSelectionInsideListNode = isSelectionInsideListNode; var _mark = require("@atlaskit/editor-common/mark"); var _transforms = require("@atlaskit/editor-common/transforms"); var _utils = require("@atlaskit/editor-common/utils"); var _utils2 = require("@atlaskit/editor-prosemirror/utils"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); var _blockTypes = require("./block-types"); var isNodeAWrappingBlockNode = exports.isNodeAWrappingBlockNode = function isNodeAWrappingBlockNode(node) { if (!node) { return false; } return _blockTypes.WRAPPER_BLOCK_TYPES.some(function (blockNode) { return blockNode.name === node.type.name; }); }; var createJoinNodesRule = exports.createJoinNodesRule = function createJoinNodesRule(match, nodeType) { return (0, _utils.createWrappingJoinRule)({ nodeType: nodeType, match: match, getAttrs: {}, joinPredicate: function joinPredicate(_, node) { return node.type === nodeType; } }); }; var createWrappingTextBlockRule = exports.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 (0, _utils.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 && (0, _expValEquals.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. */ 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 && (0, _expValEquals.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 ((0, _experiments.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. */ 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 (0, _utils2.hasParentNodeOfType)(nodeType)(state.selection); }); } 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 (0, _utils2.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 || (0, _utils2.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 (_blockTypes.FORMATTING_NODE_TYPES.indexOf(node.type.name) !== -1) { isBlockStyling = true; return false; } return true; }); return isBlockStyling; }; var marksArePresent = function marksArePresent(state) { var activeMarkTypes = _blockTypes.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 (0, _mark.anyMarkActive)(state, marks[mark]); } return false; }); return activeMarkTypes.length > 0; }; var checkFormattingIsPresent = exports.checkFormattingIsPresent = function checkFormattingIsPresent(state) { return marksArePresent(state) || blockStylingIsPresent(state); }; var hasBlockQuoteInOptions = exports.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. */ 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. */ 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 = (0, _transforms.createBlockTaskItem)({ attrs: node.attrs, content: node.content, schema: tr.doc.type.schema }); tr.replaceWith(pos, pos + node.nodeSize, blockTaskNode); } }