UNPKG

@atlaskit/editor-plugin-list

Version:

List plugin for @atlaskit/editor-core

292 lines (281 loc) 11.7 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, DELETE_DIRECTION, EVENT_TYPE, INPUT_METHOD, LIST_TEXT_SCENARIOS } from '@atlaskit/editor-common/analytics'; import { getBlockMarkAttrs, reconcileBlockMarkForParagraphAtPos } from '@atlaskit/editor-common/lists'; import { insertContentDeleteRange, isEmptySelectionAtStart, isListNode, isParagraphNode, walkPrevNode } from '@atlaskit/editor-common/utils'; import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isPosInsideList, isPosInsideParagraph } from '../utils/selection'; //Cases below refer to the cases found in this document: https://product-fabric.atlassian.net/wiki/spaces/E/pages/1146954996/List+Backspace+and+Delete+Behaviour //Case for two adjacent list items of the same indentation var listBackspaceCase2 = function listBackspaceCase2(tr, dispatch, $prev, $head) { /* CASE 2 * Initial Structure: * * List A { * ListItem B { * ...Children C * Paragraph D { text1 |textInsertPos| } //Cant have children since that would be Case 4 * |$prev||childrenGInsertPos| } * ListItem E { * Paragraph F { |$head| text2 } * ...Children G * } * } * * Converts to: * * List A { * ListItem B { * ...Children C * Paragraph C { text1text2 } * ...Children G * } * } * */ var listItemE = $head.node(-1); //Head is inside listItem E so it must have a first and last child var fontSize = tr.doc.type.schema.marks.fontSize; if (!listItemE.firstChild) { return false; } var beforeListItemE = $head.before(-1); var afterListItemE = $head.after(-1); var textInsertPos = $prev.pos - 1; //Paragraph D must be directly behind $prev otherwise it would be case 4 var childrenGInsertPos = $prev.pos; var textContent = $head.parent.content; var childrenGContent = listItemE.content.cut(listItemE.firstChild.nodeSize); insertContentDeleteRange(tr, function (tr) { return tr.doc.resolve(textInsertPos); }, [[textContent, textInsertPos], [childrenGContent, childrenGInsertPos]], [[beforeListItemE, afterListItemE]]); if (fontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true)) { var targetParagraphFontSizeAttrs = getBlockMarkAttrs($prev.parent.lastChild, fontSize); reconcileBlockMarkForParagraphAtPos(tr, tr.mapping.map(textInsertPos), fontSize, targetParagraphFontSizeAttrs); } if (dispatch) { dispatch(tr); } return true; }; //Case for two adjacent list items with the first being of lower indentation var listBackspaceCase3 = function listBackspaceCase3(tr, dispatch, $prev, $head) { /* CASE 3 * Initial Structure: * * List A { * ListItem B { * ...Children C * Paragraph D { text1 |$prev||textInsertPos| } |childrenHInsertPos| * List E { |childrenJInsertPos| * ListItem F { * Paragraph G { |$head| text2 } * ...Children H * List? I { * ...Children J * } * } * ...Children K * } * } * } * * Converts to: * * List A { * ListItem B { * ...Children C * Paragraph D { text1text2 } * ...Children H * List E { * ...Children J * ...Children K * } * } * } * */ var listE = $head.node(-2); var listItemF = $head.node(-1); //Head is inside listItem F so it must have a first and last child var fontSize = tr.doc.type.schema.marks.fontSize; if (!listItemF.firstChild || !listItemF.lastChild) { return false; } var beforeListE = $head.before(-2); var beforeListItemF = $head.before(-1); var afterParagraphD = $prev.after(); var afterListE = $head.after(-2); var afterListItemF = $head.after(-1); var startListE = $head.start(-2); var containsChildrenJ = isListNode(listItemF.lastChild); var shouldRemoveListE = listE.childCount === 1 && !containsChildrenJ; //Assures no Children J and K var textInsertPos = $prev.pos; var childrenHInsertPos = afterParagraphD; var childrenJInsertPos = startListE; var textContent = $head.parent.content; var childrenHContent = containsChildrenJ ? listItemF.content.cut(listItemF.firstChild.nodeSize, listItemF.nodeSize - listItemF.lastChild.nodeSize - 2) : listItemF.content.cut(listItemF.firstChild.nodeSize); //If Children J doesn't exist then Children H will include the last node var childrenJContent = listItemF.lastChild.content; //Will be invalid if there are no Children J but it will be unused insertContentDeleteRange(tr, function (tr) { return tr.doc.resolve(textInsertPos); }, containsChildrenJ ? [[textContent, textInsertPos], [childrenHContent, childrenHInsertPos], [childrenJContent, childrenJInsertPos]] : [[textContent, textInsertPos], [childrenHContent, childrenHInsertPos]], [shouldRemoveListE ? [beforeListE, afterListE] : [beforeListItemF, afterListItemF]]); if (fontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true)) { var targetParagraphFontSizeAttrs = getBlockMarkAttrs($prev.parent, fontSize); reconcileBlockMarkForParagraphAtPos(tr, tr.mapping.map(textInsertPos), fontSize, targetParagraphFontSizeAttrs); } if (dispatch) { dispatch(tr); } return true; }; //Case for two adjacent list items with the first being of greater indentation var listBackspaceCase4 = function listBackspaceCase4(tr, dispatch, $prev, $head, $last) { /* CASE 4 * Initial Structure: * * List A { * ListItem B { * Paragraph C { text1 } * ...Children D * List E { * ... * List F { //May be multiple levels of lists * ...Children G * ListItem H { //Last node of the block * ...Children I * Paragraph J { text2 |$last||textInsertPos| } |childrenMInsertPos| //Cant have children since this ListItem is the last of the block * } * } * ... * |childrenOInsertPosition| } * |$prev| } * ListItem K { * Paragraph L { |$head| text3 } * ...Children M * List? N { * ...Children O * } * } * } * * Converts to: * * List A { * ListItem B { * Paragraph C { text1 } * ...Children D * List E { * ... * List F { * ...Children G * ListItem H { * ...Children I * Paragraph J { text2text3 } * ...Children M * } * } * ... * ...Children O * } * } * } * */ if (!$last) { //Exit if an invalid last was given as a parameter return false; } var listItemK = $head.node(-1); //Head is inside listItem K so it must have a first and last child var fontSize = tr.doc.type.schema.marks.fontSize; if (!listItemK.firstChild || !listItemK.lastChild) { return false; } var paragraphL = $head.parent; var beforeListItemK = $head.before(-1); var afterParagraphJ = $last.after(); var afterListItemK = $head.after(-1); var containsChildrenO = isListNode(listItemK.lastChild); var textInsertPos = $last.pos; var childrenMInsertPos = afterParagraphJ; var childrenOInsertPos = $prev.pos - 1; //Last item of listItem B must be a list therefore we can simply decrement $prev to get there var textContent = paragraphL.content; var childrenMContent = containsChildrenO ? listItemK.content.cut(listItemK.firstChild.nodeSize, listItemK.nodeSize - listItemK.lastChild.nodeSize - 2) : listItemK.content.cut(listItemK.firstChild.nodeSize); var childrenOContent = listItemK.lastChild.content; insertContentDeleteRange(tr, function (tr) { return tr.doc.resolve(textInsertPos); }, containsChildrenO ? [[textContent, textInsertPos], [childrenMContent, childrenMInsertPos], [childrenOContent, childrenOInsertPos]] : [[textContent, textInsertPos], [childrenMContent, childrenMInsertPos]], [[beforeListItemK, afterListItemK]]); if (fontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true)) { var targetParagraphFontSizeAttrs = getBlockMarkAttrs($last.parent, fontSize); reconcileBlockMarkForParagraphAtPos(tr, tr.mapping.map(textInsertPos), fontSize, targetParagraphFontSizeAttrs); } if (dispatch) { dispatch(tr); } return true; }; var BACKSPACE_COMMANDS = _defineProperty(_defineProperty(_defineProperty({}, LIST_TEXT_SCENARIOS.JOIN_SIBLINGS, listBackspaceCase2), LIST_TEXT_SCENARIOS.JOIN_DESCENDANT_TO_PARENT, listBackspaceCase3), LIST_TEXT_SCENARIOS.JOIN_TO_SIBLING_DESCENDANT, listBackspaceCase4); export var calcJoinListScenario = function calcJoinListScenario(walkNode, $head, tr) { var $prev = walkNode.$pos, prevFoundNode = walkNode.foundNode; var prevInList = isPosInsideList($prev); var headInParagraph = isPosInsideParagraph($head); var headInFirstChild = $head.index(-1) === 0; var headInList = isPosInsideList($head); //Must be at the start of the selection of the first child in the listItem if (!prevFoundNode || !prevInList || !headInParagraph || !headInFirstChild || !headInList) { return false; } var prevInParagraph = isPosInsideParagraph($prev); if (prevInParagraph) { return [LIST_TEXT_SCENARIOS.JOIN_DESCENDANT_TO_PARENT, null]; } var prevParentLastChildIsList = $prev.parent.lastChild && isListNode($prev.parent.lastChild); var prevParentLastChildIsParagraph = isParagraphNode($prev.parent.lastChild); // Will search for the possible last node for case 4 (where the list could be indented multiple times) // $last is required to determine whether we are in case 2 or 4 var $last = tr.doc.resolve($prev.pos); var lastFoundNode; do { var _walkNode = walkPrevNode($last); $last = _walkNode.$pos; lastFoundNode = _walkNode.foundNode; } while (lastFoundNode && !$last.parent.isTextblock); var lastInParagraph = isPosInsideParagraph($last); if (lastFoundNode && prevParentLastChildIsList && lastInParagraph) { return [LIST_TEXT_SCENARIOS.JOIN_TO_SIBLING_DESCENDANT, $last]; } else if (prevParentLastChildIsParagraph) { return [LIST_TEXT_SCENARIOS.JOIN_SIBLINGS, null]; } return false; }; export var listBackspace = function listBackspace(editorAnalyticsAPI) { return function (state, dispatch) { var tr = state.tr, $head = state.selection.$head; var walkNode = walkPrevNode($head); if (!isEmptySelectionAtStart(state)) { return false; } var scenario = calcJoinListScenario(walkNode, $head, tr); if (!scenario) { return false; } var _state$schema$nodes = state.schema.nodes, bulletList = _state$schema$nodes.bulletList, orderedList = _state$schema$nodes.orderedList; var listParent = findParentNodeOfType([bulletList, orderedList])(tr.selection); var actionSubjectId = ACTION_SUBJECT_ID.FORMAT_LIST_BULLET; if (listParent && listParent.node.type === orderedList) { actionSubjectId = ACTION_SUBJECT_ID.FORMAT_LIST_NUMBER; } editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({ action: ACTION.LIST_ITEM_JOINED, actionSubject: ACTION_SUBJECT.LIST, actionSubjectId: actionSubjectId, eventType: EVENT_TYPE.TRACK, attributes: { inputMethod: INPUT_METHOD.KEYBOARD, direction: DELETE_DIRECTION.BACKWARD, scenario: scenario[0] } })(tr); return BACKSPACE_COMMANDS[scenario[0]](tr, dispatch, walkNode.$pos, $head, scenario[1]); }; };