UNPKG

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

Version:

Tasks and decisions plugin for @atlaskit/editor-core

498 lines (473 loc) 19.3 kB
import { GapCursorSelection } from '@atlaskit/editor-common/selection'; import { findFarthestParentNode, isListNode } from '@atlaskit/editor-common/utils'; import { NodeRange } from '@atlaskit/editor-prosemirror/model'; import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { liftTarget } from '@atlaskit/editor-prosemirror/transform'; import { findParentNodeClosestToPos, findParentNodeOfTypeClosestToPos, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { stateKey } from './plugin-key'; import { ACTIONS } from './types'; import { findBlockTaskItem } from './utils'; export var 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(findParentNodeOfTypeClosestToPos(state.selection.$from, [decisionItem, taskItem, blockTaskItem])); } return hasParentNodeOfType([decisionItem, taskItem, blockTaskItem])(state.selection); }; export var 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; }; export var 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; }; export var isInsideTask = function isInsideTask(state) { var _state$schema$nodes2 = state.schema.nodes, taskItem = _state$schema$nodes2.taskItem, blockTaskItem = _state$schema$nodes2.blockTaskItem; return hasParentNodeOfType([taskItem, blockTaskItem])(state.selection); }; export var isInsideDecision = function isInsideDecision(state) { var decisionItem = state.schema.nodes.decisionItem; return hasParentNodeOfType([decisionItem])(state.selection); }; export var 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. */ export var 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 = 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 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); * ``` */ export var 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 = findFarthestParentNode(function (node) { return node.type === taskList; })($from); if (!furthestParent) { return null; } if (hasParentNodeOfType([blockTaskItem])(selection)) { var blockTaskItemNode = 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 */ export var 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 = 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. */ export var 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 */ export var subtreeHeight = function subtreeHeight($from, $to, types) { var root = 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`. */ export var 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`) */ export var liftBlock = function liftBlock(tr, $from, $to) { var blockRange = getBlockRange({ $from: $from, $to: $to }); if (!blockRange) { return null; } // ensure we can actually lift var target = liftTarget(blockRange); if (typeof target !== 'number') { return null; } return tr.lift(blockRange, target).scrollIntoView(); }; export function getTaskItemDataAtPos(view) { var state = view.state; var selection = state.selection, schema = state.schema; var $from = selection.$from; if (expValEquals('platform_editor_blocktaskitem_patch_1', 'isEnabled', true)) { var _schema$nodes2 = schema.nodes, taskItem = _schema$nodes2.taskItem, blockTaskItem = _schema$nodes2.blockTaskItem; var maybeTask = 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 }; } } } export function getAllTaskItemsDataInRootTaskList(view) { var state = view.state; var schema = state.schema; var $fromPos = state.selection.$from; var isInTaskItem = 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 = 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 || expValEquals('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) && blockTaskItem && node.type === blockTaskItem) { allTaskItems.push({ node: node, pos: pos + rootTaskListStartPos, index: index }); } }); return allTaskItems; } } export 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 = expValEquals('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) ? (_findParentNodeOfType = 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; } } export 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 }; } export function focusCheckbox(view, taskItemData) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; if (taskItemData) { tr.setMeta(stateKey, { action: ACTIONS.FOCUS_BY_LOCALID, data: taskItemData.localId }); dispatch(tr); } } export 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 (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 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 (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 TextSelection(doc.resolve(pos + 2))); // else, this is an ordinary task item with inline content } else { tr.setSelection(new TextSelection(doc.resolve(pos + 1))); } tr.setMeta(stateKey, { action: ACTIONS.FOCUS_BY_LOCALID, data: localId }); dispatch(tr); } export function removeCheckboxFocus(view) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; view.focus(); dispatch(tr.setMeta(stateKey, { action: ACTIONS.FOCUS_BY_LOCALID })); } export function openRequestEditPopupAt(view, pos) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; dispatch(tr.setMeta(stateKey, { action: ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data: pos })); } export function closeRequestEditPopupAt(view) { var state = view.state, dispatch = view.dispatch; var tr = state.tr; dispatch(tr.setMeta(stateKey, { action: ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data: null })); } export function findFirstParentListNode($pos) { var currentNode = $pos.doc.nodeAt($pos.pos); var listNodePosition = null; if (isListNode(currentNode)) { listNodePosition = $pos.pos; } else { var result = findParentNodeClosestToPos($pos, 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 }; } export var 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; }; export var 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; };