@atlaskit/editor-plugin-tasks-and-decisions
Version:
Tasks and decisions plugin for @atlaskit/editor-core
498 lines (473 loc) • 19.3 kB
JavaScript
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;
};