@atlaskit/editor-plugin-tasks-and-decisions
Version:
Tasks and decisions plugin for @atlaskit/editor-core
739 lines (706 loc) • 38.7 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { uuid } from '@atlaskit/adf-schema';
import { SetAttrsStep } from '@atlaskit/adf-schema/steps';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INDENT_DIRECTION, INDENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { withAnalytics } from '@atlaskit/editor-common/editor-analytics';
import { toggleTaskItemCheckbox, toggleTaskList as toggleTaskListKeymap } from '@atlaskit/editor-common/keymaps';
import { getBlockMarkAttrs, getFirstParagraphBlockMarkAttrs, reconcileBlockMarkForParagraphAtPos } from '@atlaskit/editor-common/lists';
import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
import { deleteEmptyParagraphAndMoveBlockUp, filterCommand as filter, isEmptySelectionAtEnd, isEmptySelectionAtStart } from '@atlaskit/editor-common/utils';
import { autoJoin, chainCommands } from '@atlaskit/editor-prosemirror/commands';
import { keymap } from '@atlaskit/editor-prosemirror/keymap';
import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfType, findParentNodeOfTypeClosestToPos, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { moveSelectedTaskListItems } from './actions/move-selected-task-list-items';
import { joinAtCut, liftSelection, wrapSelectionInTaskList } from './commands';
import { findFirstParentListNode, getBlockRange, getCurrentIndentLevel, getTaskItemIndex, isActionOrDecisionItem, isActionOrDecisionList, isEmptyTaskDecision, isInFirstTextblockOfBlockTaskItem, isInLastTextblockOfBlockTaskItem, isInsideDecision, isInsideTask, isInsideTaskOrDecisionItem, isTable, liftBlock, walkOut } from './helpers';
import { insertTaskDecisionWithAnalytics } from './insert-commands';
import { toggleTaskList } from './toggle-tasklist-commands';
import { findBlockTaskItem, normalizeTaskItemsSelection } from './utils';
var indentationAnalytics = function indentationAnalytics(curIndentLevel, direction, inputMethod) {
return {
action: ACTION.FORMATTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.FORMAT_INDENT,
eventType: EVENT_TYPE.TRACK,
attributes: {
inputMethod: inputMethod,
previousIndentationLevel: curIndentLevel,
newIndentLevel: direction === INDENT_DIRECTION.OUTDENT ? curIndentLevel - 1 : curIndentLevel + 1,
direction: direction,
indentType: INDENT_TYPE.TASK_LIST
}
};
};
var nodeAfter = function nodeAfter($pos) {
return $pos.doc.resolve($pos.end()).nodeAfter;
};
var actionDecisionFollowsOrNothing = function actionDecisionFollowsOrNothing($pos) {
var after = nodeAfter($pos);
return !after || isActionOrDecisionItem(after);
};
var joinTaskDecisionFollowing = function joinTaskDecisionFollowing(state, dispatch) {
// only run if selection is at end of text, and inside a task or decision item
if (!isEmptySelectionAtEnd(state) || !isInsideTaskOrDecisionItem(state) || !dispatch) {
return false;
}
// look for the node after this current one
var $next = walkOut(state.selection.$from);
// if there's no taskItem or taskList following, then
// we just do the normal behaviour
var _state$schema$nodes = state.schema.nodes,
taskList = _state$schema$nodes.taskList,
taskItem = _state$schema$nodes.taskItem,
decisionList = _state$schema$nodes.decisionList,
decisionItem = _state$schema$nodes.decisionItem,
paragraph = _state$schema$nodes.paragraph,
bulletList = _state$schema$nodes.bulletList,
orderedList = _state$schema$nodes.orderedList,
listItem = _state$schema$nodes.listItem;
var parentList = findParentNodeOfTypeClosestToPos($next, [taskList, taskItem, decisionList, decisionItem]);
if (!parentList) {
if ($next.parent.type === paragraph) {
// try to join paragraph and taskList when backspacing
return joinAtCut($next.doc.resolve($next.pos))(state, dispatch);
}
// If the item we are joining is a list
if ($next.parent.type === bulletList || $next.parent.type === orderedList) {
// If the list has an item
if ($next.parent.firstChild && $next.parent.firstChild.type === listItem) {
// Place the cursor at the first listItem
var resolvedStartPos = state.doc.resolve($next.pos + 1);
// Unindent the first listItem.
// As if placing your cursor just after the first dot of the list (before the text)
// and pressing Shift-Tab.
var tr = liftBlock(state.tr, resolvedStartPos, resolvedStartPos);
// If autoJoin not used, two ul/ol elements appear rather than one with multiple li elements
return autoJoin(function (state, dispatch) {
if (tr) {
if (dispatch) {
dispatch(tr);
}
return true;
}
return false;
}, ['bulletList', 'orderedList'])(state, dispatch);
}
}
}
return false;
};
export var getUnindentCommand = function getUnindentCommand(editorAnalyticsAPI) {
return function () {
var inputMethod = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INPUT_METHOD.KEYBOARD;
return filter(isInsideTask, function (state, dispatch) {
var normalizedSelection = normalizeTaskItemsSelection(state.selection);
var curIndentLevel = getCurrentIndentLevel(normalizedSelection);
if (expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
if (!curIndentLevel) {
return true;
}
var outdentTr = moveSelectedTaskListItems(state.tr, -1);
if (outdentTr) {
withAnalytics(editorAnalyticsAPI, indentationAnalytics(curIndentLevel, INDENT_DIRECTION.OUTDENT, inputMethod))(function (_state, d) {
d === null || d === void 0 || d(outdentTr);
return true;
})(state, dispatch);
return true;
}
return false;
}
if (!curIndentLevel || curIndentLevel === 1) {
return false;
}
return withAnalytics(editorAnalyticsAPI, indentationAnalytics(curIndentLevel, INDENT_DIRECTION.OUTDENT, inputMethod))(autoJoin(liftSelection, ['taskList']))(state, dispatch);
});
};
};
// if selection is decision item or first action item in table cell
// then dont consume the Tab, as table-keymap should tab to the next cell
var shouldLetTabThroughInTable = function shouldLetTabThroughInTable(state) {
var curIndentLevel = getCurrentIndentLevel(state.selection);
var curIndex = getTaskItemIndex(state);
var _state$schema$nodes2 = state.schema.nodes,
tableCell = _state$schema$nodes2.tableCell,
tableHeader = _state$schema$nodes2.tableHeader;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var cell = findParentNodeOfType([tableCell, tableHeader])(state.selection);
if ((curIndentLevel === 1 && curIndex === 0 || isInsideDecision(state)) && cell) {
return true;
}
return false;
};
export var getIndentCommand = function getIndentCommand(editorAnalyticsAPI) {
return function () {
var inputMethod = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INPUT_METHOD.KEYBOARD;
return filter(isInsideTask, function (state, dispatch) {
var normalizedSelection = normalizeTaskItemsSelection(state.selection);
var curIndentLevel = getCurrentIndentLevel(normalizedSelection);
if (expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
if (!curIndentLevel) {
return true;
}
var indentTr = moveSelectedTaskListItems(state.tr, 1);
if (indentTr) {
withAnalytics(editorAnalyticsAPI, indentationAnalytics(curIndentLevel, INDENT_DIRECTION.INDENT, inputMethod))(function (_state, d) {
d === null || d === void 0 || d(indentTr);
return true;
})(state, dispatch);
return true;
}
return false;
}
if (!curIndentLevel || curIndentLevel >= 6) {
return true;
}
return withAnalytics(editorAnalyticsAPI, indentationAnalytics(curIndentLevel, INDENT_DIRECTION.INDENT, inputMethod))(autoJoin(wrapSelectionInTaskList, ['taskList']))(state, dispatch);
});
};
};
var backspaceFrom = function backspaceFrom(editorAnalyticsAPI) {
return function ($from) {
return function (state, dispatch) {
var _nodeBefore$firstChil, _nodeBefore$firstChil2;
var _state$schema$nodes3 = state.schema.nodes,
taskList = _state$schema$nodes3.taskList,
blockTaskItem = _state$schema$nodes3.blockTaskItem,
paragraph = _state$schema$nodes3.paragraph;
// Check if selection is inside a blockTaskItem paragraph
var resultOfFindBlockTaskItem = findBlockTaskItem($from);
var isInBlockTaskItemParagraph = resultOfFindBlockTaskItem && (resultOfFindBlockTaskItem === null || resultOfFindBlockTaskItem === void 0 ? void 0 : resultOfFindBlockTaskItem.hasParagraph);
// Get the node before the current position
var beforePos = isInBlockTaskItemParagraph ? $from.before() - 1 : $from.before();
var nodeBefore = $from.doc.resolve(beforePos).nodeBefore;
// Check if the node before is an empty task item
var isEmptyActionOrDecisionItem = nodeBefore && isActionOrDecisionItem(nodeBefore) && nodeBefore.content.size === 0;
var isEmptyBlockTaskItem = blockTaskItem && (nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.type) === blockTaskItem && (nodeBefore === null || nodeBefore === void 0 || (_nodeBefore$firstChil = nodeBefore.firstChild) === null || _nodeBefore$firstChil === void 0 ? void 0 : _nodeBefore$firstChil.type) === paragraph && (nodeBefore === null || nodeBefore === void 0 || (_nodeBefore$firstChil2 = nodeBefore.firstChild) === null || _nodeBefore$firstChil2 === void 0 || (_nodeBefore$firstChil2 = _nodeBefore$firstChil2.content) === null || _nodeBefore$firstChil2 === void 0 ? void 0 : _nodeBefore$firstChil2.size) === 0;
// previous was empty, just delete backwards
if (isEmptyActionOrDecisionItem || isEmptyBlockTaskItem) {
return false;
}
// If nested in a taskList, unindent
var depthFromSelectionToBlockTaskItem = isInBlockTaskItemParagraph ? 2 : 1;
var depthFromSelectionToNestedTaskList = depthFromSelectionToBlockTaskItem + 1;
var parentDepth = $from.depth - depthFromSelectionToNestedTaskList;
if ($from.node(parentDepth).type === taskList) {
return getUnindentCommand(editorAnalyticsAPI)()(state, dispatch);
}
// If at the end of an item, unwrap contents into a paragraph
// we achieve this by slicing the content out, and replacing
if (actionDecisionFollowsOrNothing($from)) {
if (dispatch) {
// If we are in a blockTaskItem paragraph, we need to get the content of the whole blockTaskItem
// So we reduce the depth by 1 to get to the blockTaskItem node content
var taskContent = isInBlockTaskItemParagraph ? state.doc.slice($from.start($from.depth - 1), $from.end($from.depth - 1)).content : state.doc.slice($from.start(), $from.end()).content;
var slice;
try {
slice = taskContent.size ? paragraph.createChecked(undefined, taskContent) : paragraph.createChecked();
// might be end of document after
var tr = splitListItemWith(state.tr, slice, $from, true);
dispatch(tr);
return true;
} catch (_unused) {
// If there's an error creating a paragraph, check if we are in a blockTaskItem
// Block task item's can have non-text content that cannot be wrapped in a paragraph
// So if the selection is in a blockTaskItem, just pass the content as is
if (resultOfFindBlockTaskItem && resultOfFindBlockTaskItem.blockTaskItemNode) {
// Create an array from the fragment to pass into splitListItemWith, as the `content` property is readonly
slice = Array.from(taskContent.content);
var _tr = splitListItemWith(state.tr, slice, $from, true);
dispatch(_tr);
return true;
}
}
}
}
return false;
};
};
};
var backspace = function backspace(editorAnalyticsAPI) {
return filter(isEmptySelectionAtStart, autoJoin(chainCommands(function (state, dispatch) {
return joinAtCut(state.selection.$from)(state, dispatch);
}, filter(isInsideTaskOrDecisionItem, function (state, dispatch) {
return backspaceFrom(editorAnalyticsAPI)(state.selection.$from)(state, dispatch);
})), ['taskList', 'decisionList']));
};
var unindentTaskOrUnwrapTaskDecisionFollowing = function unindentTaskOrUnwrapTaskDecisionFollowing(state, dispatch) {
var _$next$node, _$from$node, _$next$node2, _$from$node2, _$next$node3;
var $from = state.selection.$from,
_state$schema$nodes4 = state.schema.nodes,
taskList = _state$schema$nodes4.taskList,
doc = _state$schema$nodes4.doc,
paragraph = _state$schema$nodes4.paragraph,
blockTaskItem = _state$schema$nodes4.blockTaskItem,
taskItem = _state$schema$nodes4.taskItem,
tr = state.tr;
// only run if cursor is at the end of the node
if (!isEmptySelectionAtEnd(state) || !dispatch) {
return false;
}
// look for the node after this current one
var $next = walkOut($from);
// this is a top-level node it wont have $next.before()
if (!$next.parent || $next.parent.type === doc) {
return false;
}
// get resolved position of parent
var $parentPos = $from.doc.resolve($from.start($from.depth - 1));
var currentNode = $from.node();
var parentNode = $parentPos.node();
// if current position isn't an action or decision item, return false
if (!isActionOrDecisionItem(currentNode) && !isActionOrDecisionItem(parentNode)) {
return false;
}
var resultOfCurrentFindBlockTaskItem = findBlockTaskItem($next);
var isCurrentEmptyBlockTaskItem = false;
if (resultOfCurrentFindBlockTaskItem) {
var _blockTaskItemNode$fi;
var blockTaskItemNode = resultOfCurrentFindBlockTaskItem.blockTaskItemNode;
isCurrentEmptyBlockTaskItem = blockTaskItem && blockTaskItemNode && blockTaskItemNode.childCount === 1 && ((_blockTaskItemNode$fi = blockTaskItemNode.firstChild) === null || _blockTaskItemNode$fi === void 0 ? void 0 : _blockTaskItemNode$fi.type) === paragraph && blockTaskItemNode.firstChild.childCount === 0;
}
var isEmptyActionOrDecisionItem = currentNode && isActionOrDecisionItem(currentNode) && currentNode.childCount === 0;
// If empty item, use default handler
if (isEmptyActionOrDecisionItem || isCurrentEmptyBlockTaskItem) {
return false;
}
// Check if next node is a blockTaskItem paragraph
var resultOfNextFindBlockTaskItem = findBlockTaskItem($next);
var isNextInBlockTaskItemParagraph = resultOfNextFindBlockTaskItem && (resultOfNextFindBlockTaskItem === null || resultOfNextFindBlockTaskItem === void 0 ? void 0 : resultOfNextFindBlockTaskItem.hasParagraph);
// if nested, just unindent
if ($next.node($next.depth - 2).type === taskList ||
// this is for the case when we are on a non-nested item and next one is nested
$next.node($next.depth - 1).type === taskList && $next.parent.type === taskList) {
liftBlock(tr, $next, $next);
dispatch(tr);
return true;
}
var isNextCompatibleWithBlockTaskItem = blockTaskItem && (($next === null || $next === void 0 || (_$next$node = $next.node()) === null || _$next$node === void 0 ? void 0 : _$next$node.type) === taskItem && ($from === null || $from === void 0 || (_$from$node = $from.node()) === null || _$from$node === void 0 ? void 0 : _$from$node.type) === blockTaskItem || ($next === null || $next === void 0 || (_$next$node2 = $next.node()) === null || _$next$node2 === void 0 ? void 0 : _$next$node2.type) === blockTaskItem && ($from === null || $from === void 0 || (_$from$node2 = $from.node()) === null || _$from$node2 === void 0 ? void 0 : _$from$node2.type) === taskItem || [taskItem, blockTaskItem].includes($next === null || $next === void 0 || (_$next$node3 = $next.node()) === null || _$next$node3 === void 0 ? void 0 : _$next$node3.type) && resultOfCurrentFindBlockTaskItem && resultOfCurrentFindBlockTaskItem.blockTaskItemNode);
// if next node is of same type or compatible type, remove the node wrapping and create paragraph
if (!isTable($next.nodeAfter) && isActionOrDecisionItem($from.parent) || resultOfCurrentFindBlockTaskItem && resultOfCurrentFindBlockTaskItem.blockTaskItemNode && actionDecisionFollowsOrNothing($from) && (
// only forward delete if the node is same type or compatible
$next.node().type.name === $from.node().type.name || isNextCompatibleWithBlockTaskItem)) {
if (dispatch) {
// If next node is in a blockTaskItem paragraph, we need to get the content of the whole blockTaskItem
// So we reduce the depth by 1 to get to the blockTaskItem node content
var taskContent = isNextInBlockTaskItemParagraph ? state.doc.slice($next.start($next.depth - 1), $next.end($next.depth - 1)).content : state.doc.slice($next.start(), $next.end()).content;
var slice;
try {
slice = taskContent.size ? paragraph.createChecked(undefined, taskContent) : paragraph.createChecked();
// might be end of document after
var _tr2 = splitListItemWith(state.tr, slice, $next, false);
dispatch(_tr2);
return true;
} catch (_unused2) {
// If there's an error creating a paragraph, check if we are in a blockTaskItem
// Block task item's can have non-text content that cannot be wrapped in a paragraph
// So if the selection is in a blockTaskItem, just pass the content as is
if (resultOfNextFindBlockTaskItem && resultOfNextFindBlockTaskItem.blockTaskItemNode) {
var _$next$node$firstChil;
// Create an array from the fragment to pass into splitListItemWith, as the `content` property is readonly
slice = Array.from(taskContent.content);
var $splitPos = $next;
if ((_$next$node$firstChil = $next.node().firstChild) !== null && _$next$node$firstChil !== void 0 && _$next$node$firstChil.isTextblock) {
// set $next to the resolved position of inside the textblock
$splitPos = $next.doc.resolve($next.pos + 1);
}
var _tr3 = splitListItemWith(state.tr, slice, $splitPos, false);
dispatch(_tr3);
return true;
}
}
}
}
return false;
};
var deleteForwards = autoJoin(chainCommands(deleteEmptyParagraphAndMoveBlockUp(isActionOrDecisionList), joinTaskDecisionFollowing, unindentTaskOrUnwrapTaskDecisionFollowing), ['taskList', 'decisionList']);
var deleteExtraListItem = function deleteExtraListItem(tr, _$from) {
/*
After we replace actionItem with empty list item if there's the anomaly of extra empty list item
the cursor moves inside the first taskItem of splitted taskList
so the extra list item present above the list item containing taskList & cursor
*/
var $currentFrom = tr.selection.$from;
var listItemContainingActionList = tr.doc.resolve($currentFrom.start($currentFrom.depth - 2));
var emptyListItem = tr.doc.resolve(listItemContainingActionList.before() - 1);
tr.delete(emptyListItem.start(), listItemContainingActionList.pos);
};
var processNestedActionItem = function processNestedActionItem(tr, $from, previousListItemPos) {
var parentListNode = findFirstParentListNode($from);
var previousChildCountOfList = parentListNode === null || parentListNode === void 0 ? void 0 : parentListNode.node.childCount;
var currentParentListNode = findFirstParentListNode(tr.doc.resolve(tr.mapping.map($from.pos)));
var currentChildCountOfList = currentParentListNode === null || currentParentListNode === void 0 ? void 0 : currentParentListNode.node.childCount;
/*
While replacing range with empty list item an extra list item gets created in some of the scenarios
After splitting only one extra listItem should be created else an extra listItem is created
*/
if (previousChildCountOfList && currentChildCountOfList && previousChildCountOfList + 1 !== currentChildCountOfList) {
deleteExtraListItem(tr, $from);
}
// Set custom selection for nested action inside lists using previosuly calculated previousListItem position
var stableResolvedPos = tr.doc.resolve(previousListItemPos);
tr.setSelection(TextSelection.create(tr.doc, stableResolvedPos.after() + 2));
};
var splitListItemWith = function splitListItemWith(tr, content, $from, setSelection) {
var _frag$firstChild;
var origDoc = tr.doc;
var _tr$doc$type$schema$n = tr.doc.type.schema.nodes,
blockTaskItem = _tr$doc$type$schema$n.blockTaskItem,
taskList = _tr$doc$type$schema$n.taskList;
var baseDepth = $from.depth;
var $oldAfter = origDoc.resolve($from.after());
var textSelectionModifier = 0;
var replaceFromModifier = 0;
var deleteBlockModifier = 0;
var shouldSplitBlockTaskItem = true;
var isGapCursorSelection = false;
var hasBlockTaskItem = false;
if (blockTaskItem) {
var result = findBlockTaskItem($from);
if (result) {
var blockTaskItemNode = result.blockTaskItemNode,
hasParagraph = result.hasParagraph;
hasBlockTaskItem = !!blockTaskItemNode;
if (blockTaskItemNode) {
var _$oldAfter$nodeAfter, _$posPreviousSibling$;
// If the case there is a paragraph in the block task item we need to
// adjust some calculations
if (hasParagraph) {
baseDepth = $from.depth - 1;
$oldAfter = origDoc.resolve($from.after(baseDepth));
// When we're removing the extra empty task item we need to reduce the range a bit
deleteBlockModifier = 1;
} else {
textSelectionModifier = 1;
isGapCursorSelection = true;
}
textSelectionModifier = 1;
var hasSiblingTaskList = ((_$oldAfter$nodeAfter = $oldAfter.nodeAfter) === null || _$oldAfter$nodeAfter === void 0 ? void 0 : _$oldAfter$nodeAfter.type) === taskList;
if (hasSiblingTaskList) {
// Don't use the split command if there is a sibling taskList
shouldSplitBlockTaskItem = false;
}
var posPreviousSibling = $from.start(hasParagraph ? $from.depth - 1 : $from.depth) - 1;
var $posPreviousSibling = tr.doc.resolve(posPreviousSibling);
var hasPreviousTaskItem = ((_$posPreviousSibling$ = $posPreviousSibling.nodeBefore) === null || _$posPreviousSibling$ === void 0 ? void 0 : _$posPreviousSibling$.type) === blockTaskItem;
if (hasPreviousTaskItem && hasParagraph) {
replaceFromModifier = 1;
}
}
}
}
// split just before the current item
// we can only split if there was a list item before us
var container = $from.node(baseDepth - 2);
var posInList = $from.index(baseDepth - 1);
var shouldSplit = !(!isActionOrDecisionList(container) && posInList === 0) && shouldSplitBlockTaskItem;
var frag = Fragment.from(content);
var isNestedActionInsideLists = frag.childCount === 1 && ((_frag$firstChild = frag.firstChild) === null || _frag$firstChild === void 0 ? void 0 : _frag$firstChild.type.name) === 'listItem';
/*
* We don't split the list item if it's nested inside lists
* to have consistent behaviour and their resolution.
*/
if (shouldSplit && !isNestedActionInsideLists) {
// this only splits a node to delete it, so we probably don't need a random uuid
// but generate one anyway for correctness
tr = tr.split($from.pos, hasBlockTaskItem ? 0 : 1, [{
type: $from.parent.type,
attrs: {
localId: uuid.generate()
}
}]);
}
/*
* In case of nested action inside lists we explicitly set the cursor
* We need to insert it relatively to previous doc structure
* So we calculate the position of previous list item and save that position
* (The cursor can be placed easily next to list item)
*/
var previousListItemPos = isNestedActionInsideLists ? $from.start(baseDepth - 2) : 0;
tr = tr.replace(tr.mapping.map($from.start(baseDepth) - 2 + replaceFromModifier), tr.mapping.map($from.end(baseDepth) + 2), frag.size ? new Slice(frag, 0, 0) : Slice.empty);
if (setSelection && !isNestedActionInsideLists) {
var newPos = $from.pos + 1 - ((shouldSplit ? 0 : 2) + textSelectionModifier);
if (isGapCursorSelection) {
tr = tr.setSelection(new GapCursorSelection(tr.doc.resolve(newPos), Side.LEFT));
} else {
tr = tr.setSelection(new TextSelection(tr.doc.resolve(newPos)));
}
}
// if different levels then we shouldn't lift
if ($oldAfter.depth === baseDepth - 1) {
if ($oldAfter.nodeAfter && isActionOrDecisionList($oldAfter.nodeAfter)) {
// getBlockRange expects to be inside the taskItem
var pos = tr.mapping.map($oldAfter.pos + 2);
var $after = tr.doc.resolve(pos);
var blockRange = getBlockRange({
$from: $after,
$to: tr.doc.resolve($after.after($after.depth - 1) - 1)
});
if (blockRange) {
tr = tr.lift(blockRange, blockRange.depth - 1).scrollIntoView();
}
// After replacing the range there is an empty task item that
// we need to remove.
// We delete 1 past the range of the empty taskItem
// otherwise we hit a bug in prosemirror-transform:
// Cannot read property 'content' of undefined
// If this operation was done on a blockTaskItem we
// have a modifier for the position
tr = tr.deleteRange(pos - 3 - deleteBlockModifier, pos - 1 - deleteBlockModifier);
}
}
if (isNestedActionInsideLists) {
processNestedActionItem(tr, $from, previousListItemPos);
}
return tr;
};
var creatParentListItemFragement = function creatParentListItemFragement(state) {
return state.schema.nodes.listItem.create({}, state.schema.nodes.paragraph.create());
};
var getCurrentBlockTaskFontSizeAttrs = function getCurrentBlockTaskFontSizeAttrs(state, $from) {
if (!expValEquals('platform_editor_small_font_size', 'isEnabled', true)) {
return false;
}
var fontSize = state.schema.marks.fontSize;
if (!fontSize) {
return false;
}
var result = findBlockTaskItem($from);
if (!result) {
return false;
}
return result.hasParagraph ? getBlockMarkAttrs($from.parent, fontSize) : getFirstParagraphBlockMarkAttrs(result.blockTaskItemNode, fontSize);
};
var createTaskItemForCurrentTextSize = function createTaskItemForCurrentTextSize(state, attrs, fontSizeAttrs) {
var _state$schema$nodes5 = state.schema.nodes,
taskItem = _state$schema$nodes5.taskItem,
blockTaskItem = _state$schema$nodes5.blockTaskItem,
paragraph = _state$schema$nodes5.paragraph;
var fontSize = state.schema.marks.fontSize;
if (fontSizeAttrs && blockTaskItem && paragraph && fontSize) {
return blockTaskItem.createChecked(attrs, paragraph.createChecked({}, undefined, [fontSize.create(fontSizeAttrs)]));
}
return taskItem.createAndFill(attrs);
};
var splitListItem = function splitListItem(state, dispatch) {
var tr = state.tr,
$from = state.selection.$from;
var _state$schema$nodes6 = state.schema.nodes,
listItem = _state$schema$nodes6.listItem,
blockTaskItem = _state$schema$nodes6.blockTaskItem,
taskItem = _state$schema$nodes6.taskItem,
paragraph = _state$schema$nodes6.paragraph;
var currentBlockTaskFontSizeAttrs = getCurrentBlockTaskFontSizeAttrs(state, $from);
if (actionDecisionFollowsOrNothing($from)) {
if (dispatch) {
// If previous node is a blockTaskItem we just want to delete the existing node and replace it with a paragraph
var nodeBefore = state.doc.resolve($from.pos - 1).nodeBefore;
if (blockTaskItem && (nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.type) === blockTaskItem) {
if ($from.parent.type === taskItem) {
var nodeSize = $from.parent.nodeSize;
tr.delete($from.pos - Math.floor(nodeSize / 2), $from.pos + Math.ceil(nodeSize / 2));
tr.insert($from.pos, paragraph.createChecked());
dispatch(tr);
return true;
}
}
if (hasParentNodeOfType(listItem)(tr.selection)) {
// if we're inside a list item, then we pass in a fragment containing a new list item not a paragraph
dispatch(splitListItemWith(tr, creatParentListItemFragement(state), $from, true));
return true;
}
var splitTr = splitListItemWith(tr, paragraph.createChecked(), $from, true);
if (currentBlockTaskFontSizeAttrs) {
reconcileBlockMarkForParagraphAtPos(splitTr, splitTr.selection.from, state.schema.marks.fontSize, currentBlockTaskFontSizeAttrs);
}
dispatch(splitTr);
}
return true;
}
return false;
};
var enter = function enter(editorAnalyticsAPI, getContextIdentifier) {
return filter(isInsideTaskOrDecisionItem, chainCommands(filter(isEmptyTaskDecision, chainCommands(getUnindentCommand(editorAnalyticsAPI)(), splitListItem)), function (state, dispatch) {
var selection = state.selection,
schema = state.schema;
var _schema$nodes = schema.nodes,
decisionItem = _schema$nodes.decisionItem,
taskItem = _schema$nodes.taskItem,
blockTaskItem = _schema$nodes.blockTaskItem;
var $from = selection.$from,
$to = selection.$to;
var node = $from.node($from.depth);
var nodeType = node && node.type;
// Get the parent node type if the current node type is not one of the task or decision items
// This is required to handle blockTaskItem
if (![decisionItem, taskItem, blockTaskItem].includes(nodeType)) {
var _findParentNodeOfType;
var possibleNodeType = (_findParentNodeOfType = findParentNodeOfType([decisionItem, taskItem, blockTaskItem])(selection)) === null || _findParentNodeOfType === void 0 || (_findParentNodeOfType = _findParentNodeOfType.node) === null || _findParentNodeOfType === void 0 ? void 0 : _findParentNodeOfType.type;
if (possibleNodeType) {
nodeType = possibleNodeType;
}
}
var listType = [taskItem, blockTaskItem].includes(nodeType) ? 'taskList' : 'decisionList';
var addItem = function addItem(_ref) {
var tr = _ref.tr,
itemLocalId = _ref.itemLocalId;
// ED-8932: When cursor is at the beginning of a task item, instead of split, we insert above.
if ($from.pos === $to.pos && $from.parentOffset === 0 && (nodeType !== blockTaskItem || !$from.parent.isTextblock || isInFirstTextblockOfBlockTaskItem(state))) {
var newTask = nodeType.createAndFill({
localId: itemLocalId
});
if (newTask) {
if (nodeType === blockTaskItem) {
var blockTaskItemNode = findParentNodeOfType([blockTaskItem])(selection);
// New task items for blockTaskItem should be taskItem
// We want to prevent creating new blockTaskItems as much as possible
var newTaskItem = taskItem.createAndFill({
localId: itemLocalId
});
if (!blockTaskItemNode || !newTaskItem) {
return tr;
}
return tr.insert(blockTaskItemNode.pos, newTaskItem);
}
// Current position will point to text node, but we want to insert above the taskItem node
var insertPos = $from.pos - 1;
tr.insert(insertPos, newTask);
// Place cursor on the newly inserted empty task item above
// when nested inside another taskList.
if (expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
var taskListType = schema.nodes.taskList;
var parentTaskList = $from.node($from.depth - 1);
var grandparent = $from.depth >= 3 ? $from.node($from.depth - 2) : null;
if ((parentTaskList === null || parentTaskList === void 0 ? void 0 : parentTaskList.type) === taskListType && (grandparent === null || grandparent === void 0 ? void 0 : grandparent.type) === taskListType) {
tr.setSelection(TextSelection.create(tr.doc, insertPos + 1));
}
}
return tr;
}
}
/**
* For blockTaskItem, must handle it differently because it can have a different depth
*/
if (nodeType === blockTaskItem) {
var _$from$parent;
var _blockTaskItemNode = findParentNodeOfType([blockTaskItem])(selection);
if (!_blockTaskItemNode) {
return tr;
}
// If the selection is a gap cursor at the end of the blockTaskItem,
// we should insert a new taskItem.
if ((!$from.parent.isTextblock || isInLastTextblockOfBlockTaskItem(state)) && $from.parentOffset === $from.parent.nodeSize - 2) {
var _currentBlockTaskFontSizeAttrs = getCurrentBlockTaskFontSizeAttrs(state, $from);
var newTaskNode = createTaskItemForCurrentTextSize(state, {
localId: itemLocalId
}, _currentBlockTaskFontSizeAttrs);
if (newTaskNode) {
tr.insert(_blockTaskItemNode.pos + _blockTaskItemNode.node.nodeSize, newTaskNode);
// Move the cursor to the end of the newly inserted blockTaskItem
tr.setSelection(TextSelection.create(tr.doc, _blockTaskItemNode.pos + _blockTaskItemNode.node.nodeSize + 1));
return tr;
}
}
// Split near the depth of the current selection
var splitTr = tr.split($from.pos, $from !== null && $from !== void 0 && (_$from$parent = $from.parent) !== null && _$from$parent !== void 0 && _$from$parent.isTextblock ? 2 : 1, [{
type: blockTaskItem,
attrs: {
localId: itemLocalId
}
}]);
var currentBlockTaskFontSizeAttrs = getCurrentBlockTaskFontSizeAttrs(state, $from);
if (currentBlockTaskFontSizeAttrs) {
reconcileBlockMarkForParagraphAtPos(splitTr, splitTr.selection.from, state.schema.marks.fontSize, currentBlockTaskFontSizeAttrs);
}
return splitTr;
}
return tr.split($from.pos, 1, [{
type: nodeType,
attrs: {
localId: itemLocalId
}
}]);
};
var insertTr = insertTaskDecisionWithAnalytics(editorAnalyticsAPI, getContextIdentifier)(state, listType, INPUT_METHOD.KEYBOARD, addItem);
if (insertTr && dispatch) {
insertTr.scrollIntoView();
dispatch(insertTr);
}
return true;
}));
};
var cmdOptEnter = filter(isInsideTaskOrDecisionItem, function (state, dispatch) {
var selection = state.selection,
schema = state.schema;
var taskItem = schema.nodes.taskItem;
var $from = selection.$from;
var node = $from.node($from.depth);
var nodeType = node && node.type;
var nodePos = $from.before($from.depth);
if (nodeType === taskItem) {
var tr = state.tr;
tr.step(new SetAttrsStep(nodePos, {
state: node.attrs.state === 'TODO' ? 'DONE' : 'TODO',
localId: node.attrs.localId
}));
if (tr && dispatch) {
dispatch(tr);
}
}
return true;
});
export function keymapPlugin(_schema, api, allowNestedTasks, consumeTabs) {
var _api$analytics4, _api$analytics5;
var getContextIdentifier = function getContextIdentifier() {
var _api$contextIdentifie;
return api === null || api === void 0 || (_api$contextIdentifie = api.contextIdentifier) === null || _api$contextIdentifie === void 0 || (_api$contextIdentifie = _api$contextIdentifie.sharedState.currentState()) === null || _api$contextIdentifie === void 0 ? void 0 : _api$contextIdentifie.contextIdentifierProvider;
};
var indentHandlers = {
'Shift-Tab': filter([isInsideTaskOrDecisionItem, function (state) {
return !shouldLetTabThroughInTable(state);
}], function (state, dispatch) {
var _api$analytics;
return getUnindentCommand(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(INPUT_METHOD.KEYBOARD)(state, dispatch) || !!consumeTabs;
}),
Tab: filter([isInsideTaskOrDecisionItem, function (state) {
return !shouldLetTabThroughInTable(state);
}], function (state, dispatch) {
var _api$analytics2;
return getIndentCommand(api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions)(INPUT_METHOD.KEYBOARD)(state, dispatch) || !!consumeTabs;
})
};
var defaultHandlers = consumeTabs ? {
'Shift-Tab': isInsideTaskOrDecisionItem,
Tab: isInsideTaskOrDecisionItem
} : {};
var toggleTaskListShortcut = function toggleTaskListShortcut(state, dispatch) {
if (!state.schema.nodes.taskItem) {
return false;
}
if (dispatch) {
var _api$analytics3;
var command = toggleTaskList(api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions)();
var tr = command({
tr: state.tr
});
if (tr) {
dispatch(tr);
return true;
}
}
return false;
};
var keymaps = _objectSpread(_defineProperty(_defineProperty({
Backspace: backspace(api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions),
Delete: deleteForwards,
'Ctrl-d': deleteForwards,
Enter: enter(api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, getContextIdentifier)
}, toggleTaskItemCheckbox.common, cmdOptEnter), toggleTaskListKeymap.common, toggleTaskListShortcut), allowNestedTasks ? indentHandlers : defaultHandlers);
return keymap(keymaps);
}
export default keymapPlugin;