UNPKG

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

Version:

Tasks and decisions plugin for @atlaskit/editor-core

739 lines (706 loc) 38.7 kB
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;