UNPKG

@atlaskit/editor-plugin-tasks-and-decisions

Version:

Tasks and decisions plugin for @atlaskit/editor-core

516 lines (490 loc) 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.closeRequestEditPopupAt = closeRequestEditPopupAt; exports.findFirstParentListNode = findFirstParentListNode; exports.focusCheckbox = focusCheckbox; exports.focusCheckboxAndUpdateSelection = focusCheckboxAndUpdateSelection; exports.getAllTaskItemsDataInRootTaskList = getAllTaskItemsDataInRootTaskList; exports.getCurrentIndentLevel = exports.getBlockRange = void 0; exports.getCurrentTaskItemIndex = getCurrentTaskItemIndex; exports.getTaskItemDataAtPos = getTaskItemDataAtPos; exports.getTaskItemDataToFocus = getTaskItemDataToFocus; exports.liftBlock = exports.isTable = exports.isInsideTaskOrDecisionItem = exports.isInsideTask = exports.isInsideDecision = exports.isInLastTextblockOfBlockTaskItem = exports.isInFirstTextblockOfBlockTaskItem = exports.isEmptyTaskDecision = exports.isActionOrDecisionList = exports.isActionOrDecisionItem = exports.getTaskItemIndex = void 0; exports.openRequestEditPopupAt = openRequestEditPopupAt; exports.removeCheckboxFocus = removeCheckboxFocus; exports.walkOut = exports.subtreeHeight = void 0; var _selection = require("@atlaskit/editor-common/selection"); var _utils = require("@atlaskit/editor-common/utils"); var _model = require("@atlaskit/editor-prosemirror/model"); var _state = require("@atlaskit/editor-prosemirror/state"); var _transform = require("@atlaskit/editor-prosemirror/transform"); var _utils2 = require("@atlaskit/editor-prosemirror/utils"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _pluginKey = require("./plugin-key"); var _types = require("./types"); var _utils3 = require("./utils"); var isInsideTaskOrDecisionItem = exports.isInsideTaskOrDecisionItem = function isInsideTaskOrDecisionItem(state) { var _state$schema$nodes = state.schema.nodes, decisionItem = _state$schema$nodes.decisionItem, taskItem = _state$schema$nodes.taskItem, blockTaskItem = _state$schema$nodes.blockTaskItem; if (blockTaskItem) { return Boolean((0, _utils2.findParentNodeOfTypeClosestToPos)(state.selection.$from, [decisionItem, taskItem, blockTaskItem])); } return (0, _utils2.hasParentNodeOfType)([decisionItem, taskItem, blockTaskItem])(state.selection); }; var isActionOrDecisionList = exports.isActionOrDecisionList = function isActionOrDecisionList(node) { var _node$type$schema$nod = node.type.schema.nodes, taskList = _node$type$schema$nod.taskList, decisionList = _node$type$schema$nod.decisionList; return [taskList, decisionList].indexOf(node.type) > -1; }; var isActionOrDecisionItem = exports.isActionOrDecisionItem = function isActionOrDecisionItem(node) { var _node$type$schema$nod2 = node.type.schema.nodes, taskItem = _node$type$schema$nod2.taskItem, decisionItem = _node$type$schema$nod2.decisionItem, blockTaskItem = _node$type$schema$nod2.blockTaskItem; return [taskItem, decisionItem, blockTaskItem].indexOf(node.type) > -1; }; var isInsideTask = exports.isInsideTask = function isInsideTask(state) { var _state$schema$nodes2 = state.schema.nodes, taskItem = _state$schema$nodes2.taskItem, blockTaskItem = _state$schema$nodes2.blockTaskItem; return (0, _utils2.hasParentNodeOfType)([taskItem, blockTaskItem])(state.selection); }; var isInsideDecision = exports.isInsideDecision = function isInsideDecision(state) { var decisionItem = state.schema.nodes.decisionItem; return (0, _utils2.hasParentNodeOfType)([decisionItem])(state.selection); }; var isTable = exports.isTable = function isTable(node) { if (!node) { return false; } var _node$type$schema$nod3 = node.type.schema.nodes, table = _node$type$schema$nod3.table, tableHeader = _node$type$schema$nod3.tableHeader, tableCell = _node$type$schema$nod3.tableCell, tableRow = _node$type$schema$nod3.tableRow; return [table, tableHeader, tableCell, tableRow].includes(node.type); }; /** * Creates a NodeRange around the given taskItem and the following * ("nested") taskList, if one exists. */ var getBlockRange = exports.getBlockRange = function getBlockRange(_ref) { var $from = _ref.$from, $to = _ref.$to; var _$from$doc$type$schem = $from.doc.type.schema.nodes, taskList = _$from$doc$type$schem.taskList, taskItem = _$from$doc$type$schem.taskItem, blockTaskItem = _$from$doc$type$schem.blockTaskItem, paragraph = _$from$doc$type$schem.paragraph; if (blockTaskItem) { var result = (0, _utils3.findBlockTaskItem)($from); if (result) { var _$prevNode$nodeBefore; var hasParagraph = result.hasParagraph; var blockTaskItemDepth = hasParagraph ? $from.depth - 1 : $from.depth; var blockRangeDepth = blockTaskItemDepth - 1; // Calculate start position of the block range var startPos = $from.start(blockTaskItemDepth) - 1; // Calculate end position and get node after the current selection var endPos = $to.end(); var _$after = $to.doc.resolve(endPos + 1); var afterNode = _$after.nodeAfter; var lastNode = $to.node($to.depth); var endRangePos = $to.start() + lastNode.nodeSize; // Make adjustments for paragraph nodes if (lastNode.type === paragraph) { _$after = $to.doc.resolve(endPos + 2); afterNode = _$after.nodeAfter; } else { blockRangeDepth--; endRangePos--; } // Extend range if there's a sibling taskList if (afterNode && afterNode.type === taskList && _$after.depth === blockTaskItemDepth - 1) { endRangePos += afterNode.nodeSize; } // Check if preceded by another taskItem/blockTaskItem var $prevNode = $from.doc.resolve(startPos - 1); var prevNodeSize = ((_$prevNode$nodeBefore = $prevNode.nodeBefore) === null || _$prevNode$nodeBefore === void 0 ? void 0 : _$prevNode$nodeBefore.nodeSize) || 0; var $prevNodeParent = $from.doc.resolve($prevNode.pos - prevNodeSize - 1); var prevNodeParent = $prevNodeParent.nodeAfter; if (prevNodeParent && [blockTaskItem, taskItem].includes(prevNodeParent.type)) { blockRangeDepth = blockTaskItemDepth - 1; endRangePos -= 2; startPos += 1; } // Create and return the NodeRange return new _model.NodeRange($to.doc.resolve(startPos), $to.doc.resolve(endRangePos), blockRangeDepth); } } var end = $to.end(); var $after = $to.doc.resolve(end + 1); var after = $after.nodeAfter; // ensure the node after is actually just a sibling // $to will be inside the text, so subtract one to get the taskItem it contains in if (after && after.type === taskList && $after.depth === $to.depth - 1) { // it was! include it in our blockRange end += after.nodeSize; } return $from.blockRange($to.doc.resolve(end)); }; /** * Calculates the current indent level of the selection within a task list in the ProseMirror document. * * The indent level is determined by finding the depth difference between the selection and the furthest parent * node of type `taskList`. If the selection is inside a `blockTaskItem`, the calculation is adjusted to avoid * counting nested block items as additional indent levels. * * @param selection - The current ProseMirror selection. * @returns The indent level as a number, or `null` if the selection is not inside a task list. * @example * ```typescript * const indentLevel = getCurrentIndentLevel(editorState.selection); * ``` */ var getCurrentIndentLevel = exports.getCurrentIndentLevel = function getCurrentIndentLevel(selection) { var $from = selection.$from; var _$from$doc$type$schem2 = $from.doc.type.schema.nodes, taskList = _$from$doc$type$schem2.taskList, blockTaskItem = _$from$doc$type$schem2.blockTaskItem; var furthestParent = (0, _utils.findFarthestParentNode)(function (node) { return node.type === taskList; })($from); if (!furthestParent) { return null; } if ((0, _utils2.hasParentNodeOfType)([blockTaskItem])(selection)) { var blockTaskItemNode = (0, _utils.findFarthestParentNode)(function (node) { return node.type === blockTaskItem; })($from); if (blockTaskItemNode) { /** * If we are inside a blockTaskItem, calculate the indent level from the * blockTaskItemNode instead of the selection, in case the selection is * nested inside a blockTaskItem. */ return blockTaskItemNode.depth - furthestParent.depth; } } return $from.depth - furthestParent.depth; }; /** * Finds the index of the current task item in relation to the closest taskList */ var getTaskItemIndex = exports.getTaskItemIndex = function getTaskItemIndex(state) { var $pos = state.selection.$from; var isTaskList = function isTaskList(node) { return (node === null || node === void 0 ? void 0 : node.type.name) === 'taskList'; }; var itemAtPos = (0, _utils2.findParentNodeClosestToPos)($pos, isTaskList); return $pos.index(itemAtPos ? itemAtPos.depth : undefined); }; /** * Walk outwards from a position until we encounter the (inside) start of * the next node, or reach the end of the document. * * @param $startPos Position to start walking from. */ var walkOut = exports.walkOut = function walkOut($startPos) { var $pos = $startPos; // invariant 1: don't walk past the end of the document // invariant 2: we haven't walked to the start of *any* node // parentOffset includes textOffset. while ($pos.pos < $pos.doc.nodeSize - 2 && $pos.parentOffset > 0) { $pos = $pos.doc.resolve($pos.pos + 1); } return $pos; }; /** * Finds the height of a tree-like structure, given any position inside it. * * Traverses from the top of the tree to all leaf nodes, and returns the length * of the longest path. * * This means you can use it with things like taskList, which * do not nest themselves inside taskItems but rather as adjacent children. * * @param $pos Any position inside the tree. * @param types The node types to consider traversable */ var subtreeHeight = exports.subtreeHeight = function subtreeHeight($from, $to, types) { var root = (0, _utils.findFarthestParentNode)(function (node) { return types.indexOf(node.type) > -1; })($from); if (!root) { return -1; } // get the height between the root and the current position var distToParent = $from.depth - root.depth; // include any following taskList since nested lists appear // as siblings // // this is unlike regular bullet lists where the orderedList // appears as descendent of listItem var blockRange = getBlockRange({ $from: $from, $to: $to }); if (!blockRange) { return -1; } // and get the max height from the current position to the // deepest leaf node var maxChildDepth = $from.depth; $from.doc.nodesBetween(blockRange.start, blockRange.end, function (descendent, relPos, _parent) { maxChildDepth = Math.max($from.doc.resolve(relPos).depth, maxChildDepth); // keep descending down the tree if we can if (types.indexOf(descendent.type) > -1) { return true; } }); return distToParent + (maxChildDepth - $from.depth); }; /** * Determines if the current selection is inside an empty taskItem, decisionItem, or blockTaskItem. * * @param state - The current EditorState. * @returns `true` if the taskItem, decisionItem, or blockTaskItem is empty; otherwise, `false`. */ var isEmptyTaskDecision = exports.isEmptyTaskDecision = function isEmptyTaskDecision(state) { var selection = state.selection, schema = state.schema; var _schema$nodes = schema.nodes, paragraph = _schema$nodes.paragraph, blockTaskItem = _schema$nodes.blockTaskItem, decisionItem = _schema$nodes.decisionItem, taskItem = _schema$nodes.taskItem; var $from = selection.$from; var node = $from.node($from.depth); var isEmptyTaskOrDecisionItem = node && (node.type === taskItem || node.type === decisionItem) && node.content.size === 0; // Block task items must contain a single empty paragraph to be considered empty var isInEmptyBlockTaskItem = // If in an empty paragraph that's not at the doc level node.content.size === 0 && node.type === paragraph && $from.depth > 0 && // and it's parent is a blockTaskItem with only this paragraph inside it $from.node($from.depth - 1).type === blockTaskItem && $from.node($from.depth - 1).childCount === 1; return isEmptyTaskOrDecisionItem || isInEmptyBlockTaskItem; }; /** * Lifts a taskItem and any directly following taskList * (taskItem and its "nested children") out one level. * * @param tr Transaction to base steps on * @param $from Start of range you want to lift * @param $to End of range you want to lift (can be same as `$from`) */ var liftBlock = exports.liftBlock = function liftBlock(tr, $from, $to) { var blockRange = getBlockRange({ $from: $from, $to: $to }); if (!blockRange) { return null; } // ensure we can actually lift var target = (0, _transform.liftTarget)(blockRange); if (typeof target !== 'number') { return null; } return tr.lift(blockRange, target).scrollIntoView(); }; function getTaskItemDataAtPos(view) { var state = view.state; var selection = state.selection, schema = state.schema; var $from = selection.$from; if ((0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true)) { var _schema$nodes2 = schema.nodes, taskItem = _schema$nodes2.taskItem, blockTaskItem = _schema$nodes2.blockTaskItem; var maybeTask = (0, _utils2.findParentNodeOfTypeClosestToPos)($from, [taskItem, blockTaskItem]); // current selection has to be inside taskitem if (maybeTask) { return { pos: maybeTask === null || maybeTask === void 0 ? void 0 : maybeTask.pos, localId: maybeTask === null || maybeTask === void 0 ? void 0 : maybeTask.node.attrs.localId }; } } else { var isInTaskItem = $from.node().type === schema.nodes.taskItem; // current selection has to be inside taskitem if (isInTaskItem) { var taskItemPos = $from.before(); return { pos: taskItemPos, localId: $from.node().attrs.localId }; } } } function getAllTaskItemsDataInRootTaskList(view) { var state = view.state; var schema = state.schema; var $fromPos = state.selection.$from; var isInTaskItem = (0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) ? isInsideTask(state) : $fromPos.node().type === schema.nodes.taskItem; // if not inside task item then return undefined; if (!isInTaskItem) { return; } var _schema$nodes3 = schema.nodes, taskList = _schema$nodes3.taskList, taskItem = _schema$nodes3.taskItem, blockTaskItem = _schema$nodes3.blockTaskItem; var rootTaskListData = (0, _utils.findFarthestParentNode)(function (node) { return node.type === taskList; })($fromPos); if (rootTaskListData) { var rootTaskList = rootTaskListData.node; var rootTaskListStartPos = rootTaskListData.start; var allTaskItems = []; rootTaskList.descendants(function (node, pos, parent, index) { if (node.type === taskItem || (0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) && blockTaskItem && node.type === blockTaskItem) { allTaskItems.push({ node: node, pos: pos + rootTaskListStartPos, index: index }); } }); return allTaskItems; } } function getCurrentTaskItemIndex(view, allTaskItems) { var _findParentNodeOfType; var state = view.state; var schema = state.schema; var _schema$nodes4 = schema.nodes, taskItem = _schema$nodes4.taskItem, blockTaskItem = _schema$nodes4.blockTaskItem; var $fromPos = state.selection.$from; var allTaskItemNodes = allTaskItems.map(function (nodeData) { return nodeData.node; }); var currentTaskItem = (0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) ? (_findParentNodeOfType = (0, _utils2.findParentNodeOfTypeClosestToPos)($fromPos, [taskItem, blockTaskItem])) === null || _findParentNodeOfType === void 0 ? void 0 : _findParentNodeOfType.node : $fromPos.node($fromPos.depth); if (currentTaskItem) { var currentTaskItemIndex = allTaskItemNodes.indexOf(currentTaskItem); return currentTaskItemIndex; } else { return -1; } } function getTaskItemDataToFocus(view, direction) { var allTaskItems = getAllTaskItemsDataInRootTaskList(view); // if not inside task item then allTaskItems will be undefined; if (!allTaskItems) { return; } var currentTaskItemIndex = getCurrentTaskItemIndex(view, allTaskItems); if (direction === 'next' ? currentTaskItemIndex === allTaskItems.length - 1 : currentTaskItemIndex === 0) { // checkbox of first or last task item is already focused based on direction. return; } var indexOfTaskItemToFocus = direction === 'next' ? currentTaskItemIndex + 1 : currentTaskItemIndex - 1; var taskItemToFocus = allTaskItems[indexOfTaskItemToFocus]; return { pos: taskItemToFocus.pos, localId: taskItemToFocus.node.attrs.localId }; } function focusCheckbox(view, taskItemData) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; if (taskItemData) { tr.setMeta(_pluginKey.stateKey, { action: _types.ACTIONS.FOCUS_BY_LOCALID, data: taskItemData.localId }); dispatch(tr); } } function focusCheckboxAndUpdateSelection(view, taskItemData) { var _doc$resolve$nodeAfte, _doc$resolve$nodeAfte2; var pos = taskItemData.pos, localId = taskItemData.localId; var state = view.state, dispatch = view.dispatch; var doc = state.doc; var schema = state.schema; var extension = schema.nodes.extension; var tr = state.tr; // if there's an extension at this position, we're in a blockTaskItem, set a gapCursor if ((0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) && extension && ((_doc$resolve$nodeAfte = doc.resolve(pos + 1).nodeAfter) === null || _doc$resolve$nodeAfte === void 0 ? void 0 : _doc$resolve$nodeAfte.type) === extension) { tr.setSelection(new _selection.GapCursorSelection(doc.resolve(pos + 1))); // if there's a textblock at this position, we're in a blockTaskItem, add an extra hop into the content } else if ((0, _expValEquals.expValEquals)('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) && (_doc$resolve$nodeAfte2 = doc.resolve(pos + 1).nodeAfter) !== null && _doc$resolve$nodeAfte2 !== void 0 && _doc$resolve$nodeAfte2.isTextblock) { tr.setSelection(new _state.TextSelection(doc.resolve(pos + 2))); // else, this is an ordinary task item with inline content } else { tr.setSelection(new _state.TextSelection(doc.resolve(pos + 1))); } tr.setMeta(_pluginKey.stateKey, { action: _types.ACTIONS.FOCUS_BY_LOCALID, data: localId }); dispatch(tr); } function removeCheckboxFocus(view) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; view.focus(); dispatch(tr.setMeta(_pluginKey.stateKey, { action: _types.ACTIONS.FOCUS_BY_LOCALID })); } function openRequestEditPopupAt(view, pos) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; dispatch(tr.setMeta(_pluginKey.stateKey, { action: _types.ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data: pos })); } function closeRequestEditPopupAt(view) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; dispatch(tr.setMeta(_pluginKey.stateKey, { action: _types.ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data: null })); } function findFirstParentListNode($pos) { var currentNode = $pos.doc.nodeAt($pos.pos); var listNodePosition = null; if ((0, _utils.isListNode)(currentNode)) { listNodePosition = $pos.pos; } else { var result = (0, _utils2.findParentNodeClosestToPos)($pos, _utils.isListNode); listNodePosition = result && result.pos; } if (listNodePosition == null) { return null; } var node = $pos.doc.nodeAt(listNodePosition); if (!node) { return null; } return { node: node, pos: listNodePosition }; } var isInFirstTextblockOfBlockTaskItem = exports.isInFirstTextblockOfBlockTaskItem = function isInFirstTextblockOfBlockTaskItem(state) { var $from = state.selection.$from; var blockTaskItem = state.schema.nodes.blockTaskItem; return $from.parent.isTextblock && $from.node($from.depth - 1).type === blockTaskItem && $from.index($from.depth - 1) === 0; }; var isInLastTextblockOfBlockTaskItem = exports.isInLastTextblockOfBlockTaskItem = function isInLastTextblockOfBlockTaskItem(state) { var $from = state.selection.$from; var blockTaskItem = state.schema.nodes.blockTaskItem; var parentNode = $from.node($from.depth - 1); return $from.parent.isTextblock && parentNode.type === blockTaskItem && $from.index($from.depth - 1) === parentNode.childCount - 1; };