chore: overhauls lists, prepares for left indent

This commit is contained in:
James
2022-12-22 15:55:07 -05:00
parent 087eeb01a2
commit 4516c3670e
9 changed files with 187 additions and 101 deletions

View File

@@ -1,10 +1,10 @@
import React, { useCallback } from 'react';
import { useSlate } from 'slate-react';
import isListActive from './isListActive';
import toggleList from './toggleList';
import { ButtonProps } from './types';
import '../buttons.scss';
import isListActive from './isListActive';
export const baseClass = 'rich-text__button';

View File

@@ -0,0 +1,19 @@
import { Editor, Node, NodeEntry } from 'slate';
export const getCommonBlock = (editor: Editor): NodeEntry<Node> => {
const range = Editor.unhangRange(editor, editor.selection, { voids: true });
const [common, path] = Node.common(
editor,
range.anchor.path,
range.focus.path,
);
if (Editor.isBlock(editor, common) || Editor.isEditor(common)) {
return [common, path];
}
return Editor.above(editor, {
at: path,
match: (n) => Editor.isBlock(editor, n) || Editor.isEditor(n),
});
};

View File

@@ -1,11 +1,10 @@
import React, { useCallback } from 'react';
import { useSlate, ReactEditor } from 'slate-react';
import { Editor, Element, Transforms } from 'slate';
import { Editor, Element, Text, Transforms } from 'slate';
import IndentLeft from '../../../../../icons/IndentLeft';
import IndentRight from '../../../../../icons/IndentRight';
import { baseClass } from '../Button';
import isElementActive from '../isActive';
import listTypes from '../listTypes';
const indentType = 'indent';
@@ -25,26 +24,30 @@ const indent = {
e.preventDefault();
if (dir === 'left') {
Transforms.unwrapNodes(editor, {
match: (n) => Element.isElement(n) && [indentType, ...listTypes].includes(n.type),
split: true,
mode: 'lowest',
});
// Transforms.unwrapNodes(editor, {
// match: (n) => Element.isElement(n) && [indentType, ...listTypes].includes(n.type),
// split: true,
// mode: 'lowest',
// });
if (isElementActive(editor, 'li')) {
const [, parentLocation] = Editor.parent(editor, editor.selection);
const [, parentDepth] = parentLocation;
// if (isElementActive(editor, 'li')) {
// const [, parentLocation] = Editor.parent(editor, editor.selection);
// const [, parentDepth] = parentLocation;
if (parentDepth > 0 || parentDepth === 0) {
Transforms.unwrapNodes(editor, {
match: (n) => Element.isElement(n) && n.type === 'li',
split: true,
mode: 'lowest',
});
} else {
Transforms.unsetNodes(editor, ['type']);
}
}
// if (parentDepth > 0 || parentDepth === 0) {
// Transforms.unwrapNodes(editor, {
// match: (n) => Element.isElement(n) && n.type === 'li',
// split: true,
// mode: 'lowest',
// });
// } else {
// Transforms.unsetNodes(editor, ['type']);
// }
// }
const [previousNode, previousNodePath] = Editor.parent(editor, editor.selection, { depth: 4 });
console.log({ previousNode, previousNodePath });
}
if (dir === 'right') {
@@ -52,55 +55,59 @@ const indent = {
const isCurrentlyUL = isElementActive(editor, 'ul');
if (isCurrentlyOL || isCurrentlyUL) {
let hasText = false;
// Get the path of the first selected li -
// Multiple lis could be selected
// and the selection may start in the middle of the first li
const [[, firstSelectedLIPath]] = Array.from(Editor.nodes(editor, {
mode: 'lowest',
match: (node) => Element.isElement(node) && node.type === 'li',
}));
if (editor.selection) {
const leafNode = Editor.leaf(editor, editor.selection.focus);
if (leafNode) {
const [leaf] = leafNode;
hasText = leaf.text.length > 0;
// Is the first selected li the first in its list?
const hasPrecedingLI = firstSelectedLIPath[firstSelectedLIPath.length - 1] > 0;
// If the first selected li is NOT the first in its list,
// we need to inject it into the prior li
if (hasPrecedingLI) {
const [, precedingLIPath] = Editor.previous(editor, {
at: firstSelectedLIPath,
});
const [precedingLIChildren] = Editor.node(editor, [...precedingLIPath, 0]);
const precedingLIChildrenIsText = Text.isText(precedingLIChildren);
if (precedingLIChildrenIsText) {
// Wrap the prior li text content so that it can be nested next to a list
Transforms.wrapNodes(editor, { children: [] }, { at: [...precedingLIPath, 0] });
}
}
if (hasText) {
Transforms.wrapNodes(editor, {
type: 'li',
children: [],
});
Transforms.wrapNodes(editor, { type: isCurrentlyOL ? 'ol' : 'ul', children: [{ text: ' ' }] });
Transforms.setNodes(editor, { type: 'li' });
} else {
const [previousNode, previousNodePath] = Editor.previous(editor, {
at: editor.selection.focus,
// Move the selected lis after the prior li contents
Transforms.moveNodes(editor, {
to: [...precedingLIPath, 1],
match: (node) => Element.isElement(node) && node.type === 'li',
mode: 'lowest',
});
Transforms.removeNodes(editor);
Transforms.insertNodes(
// Wrap the selected lis in a new list
Transforms.wrapNodes(
editor,
[
{
children: [
previousNode,
],
},
{
type: isCurrentlyOL ? 'ol' : 'ul',
children: [
{
type: 'li',
children: [
{
text: '',
},
],
},
],
},
],
{
at: previousNodePath,
select: true,
type: isCurrentlyOL ? 'ol' : 'ul', children: [],
},
{
match: (node) => Element.isElement(node) && node.type === 'li',
mode: 'lowest',
},
);
} else {
Transforms.wrapNodes(
editor,
{
type: isCurrentlyOL ? 'ol' : 'ul', children: [{ type: 'li', children: [] }],
},
{
match: (node) => Element.isElement(node) && node.type === 'li',
mode: 'lowest',
},
);
}

View File

@@ -1,6 +1,6 @@
import { Editor, Element } from 'slate';
const isElementActive = (editor, format) => {
const isElementActive = (editor: Editor, format: string): boolean => {
if (!editor.selection) return false;
const [match] = Array.from(Editor.nodes(editor, {

View File

@@ -1,23 +1,22 @@
import { Ancestor, Editor, Element, NodeEntry } from 'slate';
import { Editor, Element } from 'slate';
import { getCommonBlock } from './getCommonBlock';
const isListActive = (editor: Editor, format: string): boolean => {
let parentLI: NodeEntry<Ancestor>;
if (!editor.selection) return false;
const [, topmostSelectedNodePath] = getCommonBlock(editor);
try {
parentLI = Editor.parent(editor, editor.selection);
} catch (e) {
// swallow error, Slate
}
const [match] = Array.from(Editor.nodes(editor, {
at: topmostSelectedNodePath,
mode: 'lowest',
match: (node, path) => {
return !Editor.isEditor(node)
&& Element.isElement(node)
&& node.type === format
&& path.length >= topmostSelectedNodePath.length - 1;
},
}));
if (parentLI?.[1]?.length > 0) {
const ancestor = Editor.above(editor, {
at: parentLI[1],
});
return Element.isElement(ancestor[0]) && ancestor[0].type === format;
}
return false;
return !!match;
};
export default isListActive;

View File

@@ -0,0 +1,17 @@
import { Editor, Element, Ancestor, NodeEntry } from 'slate';
export const isWithinListItem = (editor: Editor): boolean => {
let parentLI: NodeEntry<Ancestor>;
try {
parentLI = Editor.parent(editor, editor.selection);
} catch (e) {
// swallow error, Slate
}
if (Element.isElement(parentLI?.[0]) && parentLI?.[0]?.type === 'li') {
return true;
}
return false;
};

View File

@@ -1,15 +1,23 @@
import { Transforms } from 'slate';
import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import isElementActive from './isActive';
import { isWithinListItem } from './isWithinListItem';
const toggleElement = (editor, format) => {
const toggleElement = (editor: Editor, format: string): void => {
const isActive = isElementActive(editor, format);
let type = format;
const isWithinLI = isWithinListItem(editor);
if (isActive) {
type = undefined;
}
if (!isActive && isWithinLI) {
const block = { type: 'li', children: [] };
Transforms.wrapNodes(editor, block);
}
Transforms.setNodes(editor, { type });
ReactEditor.focus(editor);

View File

@@ -1,29 +1,40 @@
import { Element, Transforms } from 'slate';
import { Editor, Element, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import isElementActive from './isActive';
import isListActive from './isListActive';
import listTypes from './listTypes';
const toggleList = (editor, format) => {
const isActive = isElementActive(editor, format);
const isList = listTypes.includes(format);
const toggleList = (editor: Editor, format: string): void => {
let currentListFormat: string;
let type = format;
if (isListActive(editor, 'ol')) currentListFormat = 'ol';
if (isListActive(editor, 'ul')) currentListFormat = 'ul';
if (isActive) {
type = undefined;
} else if (isList) {
type = 'li';
}
// If the format is currently active,
// unwrap the list and set li type to undefined
if (currentListFormat === format) {
Transforms.unwrapNodes(editor, {
match: (n) => Element.isElement(n) && listTypes.includes(n.type),
mode: 'lowest',
});
Transforms.unwrapNodes(editor, {
match: (n) => Element.isElement(n) && listTypes.includes(n.type as string),
split: true,
mode: 'lowest',
});
Transforms.setNodes(editor, { type: undefined });
Transforms.setNodes(editor, { type });
if (!isActive && isList) {
// Otherwise, if a list is active and we are changing it,
// change it
} else if (currentListFormat !== format) {
Transforms.setNodes(
editor,
{
type: format,
},
{
match: (node) => Element.isElement(node) && listTypes.includes(node.type),
mode: 'lowest',
},
);
// Otherwise we can assume that we should just activate the list
} else {
Transforms.setNodes(editor, { type: 'li' });
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}

View File

@@ -271,6 +271,31 @@ export const richTextBulletsDoc = {
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
children: [
{
text: 'I am semantically connected to my sub-bullets',
},
],
},
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
text: 'I am sub-bullets that are semantically connected to the parent bullet',
},
],
},
],
},
],
},
{
children: [
{