Path: blob/master/src/packages/frontend/editors/slate/format/indent.ts
1698 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Editor, Element, Location, Path, Transforms } from "slate";6import { isListElement } from "../elements/list";7import { emptyParagraph } from "../padding";8import { moveCursorToBeginningOfBlock } from "../control";910export function unindentListItem(editor: Editor): boolean {11const [item, path] = getNode(editor, (node) => node.type == "list_item");12if (item == null || path == null) {13// no list item containing cursor...14return false;15}16if (!item.children) {17// this shouldn't happen since all list_item's should18// have children19return false;20}2122const [list, listPath] = getNode(editor, isListElement);23if (list == null || listPath == null) {24// shouldn't happen, since list_item should be inside of an actual list.25return false;26}2728// Now the parent of that list itself has to be a list item29// to be able to unindent.30const parentOfListPath = Path.parent(listPath);31const [parentOfList] = Editor.node(editor, parentOfListPath);3233if (!Element.isElement(parentOfList) || parentOfList.type != "list_item") {34// Top level list item. Remove bullet point and make it35// no longer a list item at all.36let to = Path.parent(path);37if (Path.hasPrevious(path)) {38to = Path.next(to);39}40try {41Editor.withoutNormalizing(editor, () => {42// First move the cursor, since cursor can't be in the middle43// of this list item, or we end up splitting44// the item itself, e.g.,45// - foo46// - bar[CURSOR]stuff47// Also, moving to beginning feels right for unindent. We do48// this in all cases for consistency.49moveCursorToBeginningOfBlock(editor);50if (path[path.length - 1] < list.children.length - 1) {51// not last child so split:52Transforms.splitNodes(editor, {53match: (node) => Element.isElement(node) && isListElement(node),54mode: "lowest",55});56}57Transforms.moveNodes(editor, {58match: (node) => node === item,59to,60});61Transforms.unwrapNodes(editor, {62match: (node) => node === item,63mode: "lowest",64at: to,65});66});67} catch (err) {68console.warn(`SLATE -- issue making list item ${err}`);69}70return true;71}7273const to = Path.next(parentOfListPath);7475try {76Editor.withoutNormalizing(editor, () => {77Transforms.moveNodes(editor, {78to,79match: (node) => node === list,80});81Transforms.unwrapNodes(editor, { at: to });82});83} catch (err) {84console.warn(`SLATE -- issue with unindentListItem ${err}`);85}8687// re-indent any extra siblings that we just unintentionally un-indented88// Yes, I wish there was a simpler way than this, but fortunately this89// is not a speed critical path for anything.90const numBefore = path[path.length - 1];91const numAfter = list.children.length - numBefore - 1;92for (let i = 0; i < numBefore; i++) {93indentListItem(editor, to);94}95const after = Path.next(to);96for (let i = 0; i < numAfter; i++) {97indentListItem(editor, after);98}99100return true;101}102103export function getNode(104editor,105match,106at: Location | undefined = undefined,107): [Element, number[]] | [undefined, undefined] {108if (at != null) {109// First try the node at *specific* given position.110// For some reason there seems to be no mode111// with Editor.nodes that does this, but we use112// this for re-indenting in the unindent code above.113try {114const [elt, path] = Editor.node(editor, at);115if (Element.isElement(elt) && match(elt, path)) {116return [elt as Element, path];117}118} catch (_err) {119// no such element, so try search below...120}121}122try {123for (const elt of Editor.nodes(editor, {124match: (node, path) => Element.isElement(node) && match(node, path),125mode: "lowest",126at,127})) {128return [elt[0] as Element, elt[1]];129}130} catch (_err) {131// no such element132}133return [undefined, undefined];134}135136export function indentListItem(137editor: Editor,138at: Location | undefined = undefined,139): boolean {140const [item, path] = getNode(editor, (node) => node.type == "list_item", at);141if (item == null || path == null) {142// console.log("no list item containing cursor...");143return false;144}145if (!item.children) {146// console.log("this shouldn't happen since all list_item's should have children");147return false;148}149150const [list] = getNode(editor, isListElement, at);151if (list == null) {152// console.log("shouldn't happen, since list_item should be inside of an actual list.");153return false;154}155156if (list.children[0] === item || path[path.length - 1] == 0) {157// console.log("can't indent the first item", { list, path, item });158return false;159}160161const prevPath = Path.previous(path);162const [prevItem] = Editor.node(editor, prevPath);163if (!Element.isElement(prevItem)) {164// console.log("type issue -- should not happen");165return false;166}167if (168prevItem.children.length > 0 &&169!Element.isElement(prevItem.children[prevItem.children.length - 1])170) {171// we can't stick our list item adjacent to a leaf node (e.g.,172// not next to a text node). This naturally happens, since an173// empty list item is parsed without a block child in it.174Transforms.wrapNodes(editor, emptyParagraph() as any, {175at: prevPath.concat(0),176});177}178const to = prevPath.concat([prevItem.children.length]);179180if (list.type != "bullet_list" && list.type != "ordered_list") {181// console.log("Type issue that should not happen.");182return false;183}184try {185Editor.withoutNormalizing(editor, () => {186Transforms.moveNodes(editor, {187to,188match: (node) => node === item,189at,190});191Transforms.wrapNodes(192editor,193{ type: list.type, tight: true, children: [] },194{ at: to },195);196});197} catch (err) {198console.warn(`SLATE -- issue with indentListItem ${err}`);199}200201return true;202}203204205