chore: overhauls lists, prepares for left indent
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user