feat: add outline list

This commit is contained in:
thecodrr
2022-06-07 21:49:03 +05:00
parent ae8dab7048
commit 7b2887c1ee
26 changed files with 801 additions and 2 deletions

View File

@@ -0,0 +1,4 @@
/// <reference types="react" />
import { ReactNodeViewProps } from "../react";
import { OutlineListAttributes } from "./outline-list";
export declare function OutlineListComponent(props: ReactNodeViewProps<OutlineListAttributes>): JSX.Element;

View File

@@ -0,0 +1,22 @@
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
import { Text } from "rebass";
import { useMemo } from "react";
import { OutlineListItem } from "../outline-list-item";
export function OutlineListComponent(props) {
var editor = props.editor, getPos = props.getPos, node = props.node, updateAttributes = props.updateAttributes, forwardRef = props.forwardRef;
var collapsed = node.attrs.collapsed;
var isNested = useMemo(function () {
var _a;
var pos = editor.state.doc.resolve(getPos());
return ((_a = pos.parent) === null || _a === void 0 ? void 0 : _a.type.name) === OutlineListItem.name;
}, []);
return (_jsx(_Fragment, { children: _jsx(Text, { className: "outline-list", as: "div", ref: forwardRef, sx: {
ul: {
display: collapsed ? "none" : "block",
paddingInlineStart: 0,
paddingLeft: isNested ? 1 : 0,
marginBlockStart: isNested ? 5 : 0,
marginBlockEnd: 0,
},
} }) }));
}

View File

@@ -0,0 +1 @@
export * from "./outline-list";

View File

@@ -0,0 +1 @@
export * from "./outline-list";

View File

@@ -0,0 +1,19 @@
import { Node } from "@tiptap/core";
export declare type OutlineListAttributes = {
collapsed: boolean;
};
export interface OutlineListOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
outlineList: {
/**
* Toggle a bullet list
*/
toggleOutlineList: () => ReturnType;
};
}
}
export declare const inputRegex: RegExp;
export declare const OutlineList: Node<OutlineListOptions, any>;

View File

@@ -0,0 +1,81 @@
import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core";
import { createNodeView } from "../react";
import { OutlineListComponent } from "./component";
export var inputRegex = /^\s*(-o)\s$/;
var outlineListItemName = "outlineListItem";
export var OutlineList = Node.create({
name: "outlineList",
addOptions: function () {
return {
HTMLAttributes: {},
};
},
addAttributes: function () {
return {
collapsed: {
default: false,
keepOnSplit: false,
parseHTML: function (element) { return element.dataset.collapsed === "true"; },
renderHTML: function (attributes) { return ({
"data-collapsed": attributes.collapsed === true,
}); },
},
};
},
group: "block list",
content: "".concat(outlineListItemName, "+"),
parseHTML: function () {
return [
{
tag: "ul[data-type=\"".concat(this.name, "\"]"),
priority: 52,
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"ul",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
0,
];
},
addCommands: function () {
var _this = this;
return {
toggleOutlineList: function () {
return function (_a) {
var commands = _a.commands;
return commands.toggleList(_this.name, outlineListItemName);
};
},
};
},
addKeyboardShortcuts: function () {
var _this = this;
return {
"Mod-Shift-O": function () { return _this.editor.commands.toggleOutlineList(); },
};
},
addInputRules: function () {
return [
wrappingInputRule({
find: inputRegex,
type: this.type,
}),
];
},
addNodeView: function () {
var _this = this;
return createNodeView(OutlineListComponent, {
contentDOMFactory: function () {
var content = document.createElement("ul");
content.classList.add("".concat(_this.name.toLowerCase(), "-content-wrapper"));
content.style.whiteSpace = "inherit";
return { dom: content };
},
});
},
});

View File

@@ -0,0 +1,5 @@
import { Node } from "@tiptap/core";
export interface ListItemOptions {
HTMLAttributes: Record<string, any>;
}
export declare const OutlineListItem: Node<ListItemOptions, any>;

View File

@@ -0,0 +1,39 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { onBackspacePressed } from "../list-item/commands";
export var OutlineListItem = Node.create({
name: "outlineListItem",
addOptions: function () {
return {
HTMLAttributes: {},
};
},
content: "paragraph block*",
defining: true,
parseHTML: function () {
return [
{
tag: "ul[data-list-type=\"outline\"] > li",
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addKeyboardShortcuts: function () {
var _this = this;
return {
Enter: function () { return _this.editor.commands.splitListItem(_this.name); },
Tab: function () { return _this.editor.commands.sinkListItem(_this.name); },
"Shift-Tab": function () { return _this.editor.commands.liftListItem(_this.name); },
Backspace: function (_a) {
var editor = _a.editor;
return onBackspacePressed(editor, _this.name, _this.type);
},
};
},
});

View File

@@ -0,0 +1,3 @@
/// <reference types="react" />
import { ReactNodeViewProps } from "../react";
export declare function OutlineListItemComponent(props: ReactNodeViewProps): JSX.Element;

View File

@@ -0,0 +1,78 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Box, Flex, Text } from "rebass";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import { findChildren, } from "@tiptap/core";
import { OutlineList } from "../outline-list/outline-list";
export function OutlineListItemComponent(props) {
var _a, _b;
var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node, getPos = props.getPos, forwardRef = props.forwardRef;
var isNested = ((_a = node.lastChild) === null || _a === void 0 ? void 0 : _a.type.name) === OutlineList.name;
var isCollapsed = isNested && ((_b = node.lastChild) === null || _b === void 0 ? void 0 : _b.attrs.collapsed);
return (_jsxs(Flex, { children: [_jsxs(Flex, __assign({ className: "outline", sx: {
flexDirection: "column",
alignItems: "center",
mt: "3px",
} }, { children: [isNested ? (_jsx(Icon, { path: isCollapsed ? Icons.chevronRight : Icons.chevronDown, title: isCollapsed
? "Click to uncollapse list"
: "Click to collapse list", sx: {
cursor: "pointer",
transition: "all .2s ease-in-out",
":hover": {
transform: "scale(1.3)",
},
".icon:hover path": {
fill: "var(--checked) !important",
},
}, size: 18, onMouseDown: function (e) { return e.preventDefault(); }, onClick: function () {
var _a = __read(findChildren(node, function (node) { return node.type.name === OutlineList.name; }), 1), subList = _a[0];
if (!subList)
return;
var pos = subList.pos;
editor.commands.toggleOutlineCollapse(pos + getPos() + 1, !isCollapsed);
} })) : (_jsx(Icon, { path: Icons.circle, size: 18, sx: { transform: "scale(0.4)" } })), isNested && !isCollapsed && (_jsx(Box, { sx: {
flex: 1,
width: 1,
mt: 2,
backgroundColor: "border",
borderRadius: 50,
flexShrink: 0,
cursor: "pointer",
transition: "all .2s ease-in-out",
":hover": {
backgroundColor: "fontTertiary",
width: 4,
},
}, contentEditable: false }))] })), _jsx(Text, { as: "li", ref: forwardRef, sx: {
pl: 2,
listStyleType: "none",
flex: 1,
} })] }));
}

View File

@@ -0,0 +1 @@
export * from "./outline-list-item";

View File

@@ -0,0 +1 @@
export * from "./outline-list-item";

View File

@@ -0,0 +1,12 @@
import { Node } from "@tiptap/core";
export interface ListItemOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
outlineListItem: {
toggleOutlineCollapse: (subListPos: number, state: boolean) => ReturnType;
};
}
}
export declare const OutlineListItem: Node<ListItemOptions, any>;

View File

@@ -0,0 +1,126 @@
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { Node, mergeAttributes, findChildren } from "@tiptap/core";
import { findParentNodeOfTypeClosestToPos } from "prosemirror-utils";
import { onBackspacePressed } from "../list-item/commands";
import { OutlineList } from "../outline-list/outline-list";
import { createNodeView } from "../react";
import { OutlineListItemComponent } from "./component";
export var OutlineListItem = Node.create({
name: "outlineListItem",
addOptions: function () {
return {
HTMLAttributes: {},
};
},
content: "heading* block*",
defining: true,
parseHTML: function () {
return [
{
tag: "li[data-type=\"".concat(this.name, "\"]"),
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
0,
];
},
addKeyboardShortcuts: function () {
var _this = this;
return {
"Mod-Space": function (_a) {
var editor = _a.editor;
var subList = findSublist(editor, _this.type);
if (!subList)
return false;
var isCollapsed = subList.isCollapsed, subListPos = subList.subListPos;
return _this.editor.commands.toggleOutlineCollapse(subListPos, !isCollapsed);
},
Enter: function (_a) {
var editor = _a.editor;
var subList = findSublist(editor, _this.type);
if (!subList)
return false;
var isCollapsed = subList.isCollapsed, subListPos = subList.subListPos;
if (isCollapsed) {
return _this.editor.commands.toggleOutlineCollapse(subListPos, false);
}
return _this.editor.commands.splitListItem(_this.name);
},
Tab: function () { return _this.editor.commands.sinkListItem(_this.name); },
"Shift-Tab": function () { return _this.editor.commands.liftListItem(_this.name); },
Backspace: function (_a) {
var editor = _a.editor;
return onBackspacePressed(editor, _this.name, _this.type);
},
};
},
addCommands: function () {
return {
toggleOutlineCollapse: function (pos, state) {
return function (_a) {
var tr = _a.tr;
tr.setNodeMarkup(pos, undefined, {
collapsed: state,
});
return true;
};
},
};
},
addNodeView: function () {
return createNodeView(OutlineListItemComponent, {
contentDOMFactory: true,
// wrapperFactory: () => document.createElement("li"),
});
},
});
function findSublist(editor, type) {
var _a, _b;
var selection = editor.state.selection;
var $from = selection.$from;
var listItem = findParentNodeOfTypeClosestToPos($from, type);
if (!listItem)
return false;
var _c = __read(findChildren(listItem.node, function (node) { return node.type.name === OutlineList.name; }), 1), subList = _c[0];
if (!subList)
return false;
var isNested = ((_a = subList === null || subList === void 0 ? void 0 : subList.node) === null || _a === void 0 ? void 0 : _a.type.name) === OutlineList.name;
var isCollapsed = (_b = subList === null || subList === void 0 ? void 0 : subList.node) === null || _b === void 0 ? void 0 : _b.attrs.collapsed;
var subListPos = listItem.pos + subList.pos + 1;
return { isCollapsed: isCollapsed, isNested: isNested, subListPos: subListPos };
// return (
// this.editor
// .chain()
// .command(({ tr }) => {
// tr.setNodeMarkup(listItem.pos + subList.pos + 1, undefined, {
// collapsed: !isCollapsed,
// });
// return true;
// })
// //.setTextSelection(listItem.pos + subList.pos + 1)
// //.splitListItem(this.name)
// .run()
// );
}

View File

@@ -23,7 +23,7 @@ export declare class PortalProvider extends React.Component<BasePortalProviderPr
static displayName: string;
portalProviderAPI: PortalProviderAPI;
constructor(props: BasePortalProviderProps);
render(): JSX.Element | React.ReactChild | null;
render(): React.ReactChild | JSX.Element | null;
componentDidUpdate(): void;
}
export declare class PortalRenderer extends React.Component<{

View File

@@ -54,6 +54,8 @@ import { CodeBlock } from "./extensions/code-block";
import { ListItem } from "./extensions/list-item";
import { Link } from "./extensions/link";
import { EventDispatcher } from "./extensions/react";
import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item";
EditorView.prototype.updateState = function updateState(state) {
if (!this.docView)
return; // This prevents the matchesNode error on hot reloads
@@ -115,6 +117,8 @@ var useTiptap = function (options, deps) {
onDownloadAttachment: onDownloadAttachment,
onOpenAttachmentPicker: onOpenAttachmentPicker,
}),
OutlineListItem,
OutlineList,
ListItem,
],
onCreate: function (_a) {

View File

@@ -70,6 +70,7 @@ export declare const Icons: {
chevronDown: string;
chevronUp: string;
chevronRight: string;
circle: string;
none: string;
};
export declare type IconNames = keyof typeof Icons;

View File

@@ -1,4 +1,4 @@
import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusAfter, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, mdiTableColumnWidth, mdiTableRowHeight, mdiMinus, mdiPaletteOutline, } from "@mdi/js";
import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusAfter, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, mdiTableColumnWidth, mdiTableRowHeight, mdiMinus, mdiPaletteOutline, mdiCircle, } from "@mdi/js";
export var Icons = {
bold: mdiFormatBold,
italic: mdiFormatItalic,
@@ -71,5 +71,6 @@ export var Icons = {
chevronDown: mdiChevronDown,
chevronUp: mdiChevronUp,
chevronRight: mdiChevronRight,
circle: mdiCircle,
none: "",
};

View File

@@ -0,0 +1,105 @@
import { Box, Flex, Text } from "rebass";
import { ReactNodeViewProps } from "../react";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import { Node } from "prosemirror-model";
import { Transaction } from "prosemirror-state";
import {
findChildren,
findParentNode,
getNodeType,
NodeWithPos,
} from "@tiptap/core";
import { useCallback, useEffect } from "react";
import { OutlineList } from "../outline-list/outline-list";
export function OutlineListItemComponent(props: ReactNodeViewProps) {
const { editor, updateAttributes, node, getPos, forwardRef } = props;
const isNested = node.lastChild?.type.name === OutlineList.name;
const isCollapsed = isNested && node.lastChild?.attrs.collapsed;
return (
<Flex>
<Flex
className="outline"
sx={{
flexDirection: "column",
alignItems: "center",
mt: "3px",
}}
>
{isNested ? (
<Icon
path={isCollapsed ? Icons.chevronRight : Icons.chevronDown}
title={
isCollapsed
? "Click to uncollapse list"
: "Click to collapse list"
}
sx={{
cursor: "pointer",
transition: `all .2s ease-in-out`,
":hover": {
transform: "scale(1.3)",
},
".icon:hover path": {
fill: "var(--checked) !important",
},
}}
size={18}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
const [subList] = findChildren(
node,
(node) => node.type.name === OutlineList.name
);
if (!subList) return;
const { pos } = subList;
editor.commands.toggleOutlineCollapse(
pos + getPos() + 1,
!isCollapsed
);
}}
/>
) : (
<Icon
path={Icons.circle}
size={18}
sx={{ transform: "scale(0.4)" }}
/>
)}
{isNested && !isCollapsed && (
<Box
sx={{
flex: 1,
width: 1,
mt: 2,
backgroundColor: "border",
borderRadius: 50,
flexShrink: 0,
cursor: "pointer",
transition: `all .2s ease-in-out`,
":hover": {
backgroundColor: "fontTertiary",
width: 4,
},
}}
contentEditable={false}
/>
)}
</Flex>
<Text
as="li"
ref={forwardRef}
sx={{
pl: 2,
listStyleType: "none",
flex: 1,
}}
/>
</Flex>
);
}

View File

@@ -0,0 +1 @@
export * from "./outline-list-item";

View File

@@ -0,0 +1,134 @@
import { Node, mergeAttributes, findChildren, Editor } from "@tiptap/core";
import { NodeType } from "prosemirror-model";
import { findParentNodeOfTypeClosestToPos } from "prosemirror-utils";
import { onBackspacePressed } from "../list-item/commands";
import { OutlineList } from "../outline-list/outline-list";
import { createNodeView } from "../react";
import { OutlineListItemComponent } from "./component";
export interface ListItemOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
outlineListItem: {
toggleOutlineCollapse: (subListPos: number, state: boolean) => ReturnType;
};
}
}
export const OutlineListItem = Node.create<ListItemOptions>({
name: "outlineListItem",
addOptions() {
return {
HTMLAttributes: {},
};
},
content: "heading* block*",
defining: true,
parseHTML() {
return [
{
tag: `li[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
0,
];
},
addKeyboardShortcuts() {
return {
"Mod-Space": ({ editor }) => {
const subList = findSublist(editor, this.type);
if (!subList) return false;
const { isCollapsed, subListPos } = subList;
return this.editor.commands.toggleOutlineCollapse(
subListPos,
!isCollapsed
);
},
Enter: ({ editor }) => {
const subList = findSublist(editor, this.type);
if (!subList) return false;
const { isCollapsed, subListPos } = subList;
if (isCollapsed) {
return this.editor.commands.toggleOutlineCollapse(subListPos, false);
}
return this.editor.commands.splitListItem(this.name);
},
Tab: () => this.editor.commands.sinkListItem(this.name),
"Shift-Tab": () => this.editor.commands.liftListItem(this.name),
Backspace: ({ editor }) =>
onBackspacePressed(editor, this.name, this.type),
};
},
addCommands() {
return {
toggleOutlineCollapse:
(pos, state) =>
({ tr }) => {
tr.setNodeMarkup(pos, undefined, {
collapsed: state,
});
return true;
},
};
},
addNodeView() {
return createNodeView(OutlineListItemComponent, {
contentDOMFactory: true,
// wrapperFactory: () => document.createElement("li"),
});
},
});
function findSublist(editor: Editor, type: NodeType) {
const { selection } = editor.state;
const { $from } = selection;
const listItem = findParentNodeOfTypeClosestToPos($from, type);
if (!listItem) return false;
const [subList] = findChildren(
listItem.node,
(node) => node.type.name === OutlineList.name
);
if (!subList) return false;
const isNested = subList?.node?.type.name === OutlineList.name;
const isCollapsed = subList?.node?.attrs.collapsed;
const subListPos = listItem.pos + subList.pos + 1;
return { isCollapsed, isNested, subListPos };
// return (
// this.editor
// .chain()
// .command(({ tr }) => {
// tr.setNodeMarkup(listItem.pos + subList.pos + 1, undefined, {
// collapsed: !isCollapsed,
// });
// return true;
// })
// //.setTextSelection(listItem.pos + subList.pos + 1)
// //.splitListItem(this.name)
// .run()
// );
}

View File

@@ -0,0 +1,47 @@
import { Box, Flex, Text } from "rebass";
import { ReactNodeViewProps } from "../react";
import { Node } from "prosemirror-model";
import {
findParentNodeClosestToPos,
findChildren,
getNodeType,
} from "@tiptap/core";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import { useEffect, useMemo, useState } from "react";
import { Input } from "@rebass/forms";
import { TaskItemNode } from "../task-item";
import { OutlineListAttributes } from "./outline-list";
import { findParentNodeOfTypeClosestToPos } from "prosemirror-utils";
import { OutlineListItem } from "../outline-list-item";
export function OutlineListComponent(
props: ReactNodeViewProps<OutlineListAttributes>
) {
const { editor, getPos, node, updateAttributes, forwardRef } = props;
const { collapsed } = node.attrs;
const isNested = useMemo(() => {
const pos = editor.state.doc.resolve(getPos());
return pos.parent?.type.name === OutlineListItem.name;
}, []);
return (
<>
<Text
className="outline-list"
as={"div"}
ref={forwardRef}
sx={{
ul: {
display: collapsed ? "none" : "block",
paddingInlineStart: 0,
paddingLeft: isNested ? 1 : 0,
marginBlockStart: isNested ? 5 : 0,
marginBlockEnd: 0,
},
}}
/>
</>
);
}

View File

@@ -0,0 +1 @@
export * from "./outline-list";

View File

@@ -0,0 +1,106 @@
import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core";
import { createNodeView } from "../react";
import { OutlineListComponent } from "./component";
export type OutlineListAttributes = {
collapsed: boolean;
};
export interface OutlineListOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
outlineList: {
/**
* Toggle a bullet list
*/
toggleOutlineList: () => ReturnType;
};
}
}
export const inputRegex = /^\s*(-o)\s$/;
const outlineListItemName = `outlineListItem`;
export const OutlineList = Node.create<OutlineListOptions>({
name: "outlineList",
addOptions() {
return {
HTMLAttributes: {},
};
},
addAttributes() {
return {
collapsed: {
default: false,
keepOnSplit: false,
parseHTML: (element) => element.dataset.collapsed === "true",
renderHTML: (attributes) => ({
"data-collapsed": attributes.collapsed === true,
}),
},
};
},
group: "block list",
content: `${outlineListItemName}+`,
parseHTML() {
return [
{
tag: `ul[data-type="${this.name}"]`,
priority: 52,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"ul",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
0,
];
},
addCommands() {
return {
toggleOutlineList:
() =>
({ commands }) => {
return commands.toggleList(this.name, outlineListItemName);
},
};
},
addKeyboardShortcuts() {
return {
"Mod-Shift-O": () => this.editor.commands.toggleOutlineList(),
};
},
addInputRules() {
return [
wrappingInputRule({
find: inputRegex,
type: this.type,
}),
];
},
addNodeView() {
return createNodeView(OutlineListComponent, {
contentDOMFactory: () => {
const content = document.createElement("ul");
content.classList.add(`${this.name.toLowerCase()}-content-wrapper`);
content.style.whiteSpace = "inherit";
return { dom: content };
},
});
},
});

View File

@@ -33,6 +33,8 @@ import { CodeBlock } from "./extensions/code-block";
import { ListItem } from "./extensions/list-item";
import { Link } from "./extensions/link";
import { PortalProviderAPI, EventDispatcher } from "./extensions/react";
import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item";
EditorView.prototype.updateState = function updateState(state) {
if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads
@@ -110,6 +112,8 @@ const useTiptap = (
onDownloadAttachment,
onOpenAttachmentPicker,
}),
OutlineListItem,
OutlineList,
ListItem,
],
onCreate: ({ editor }) => {

View File

@@ -84,6 +84,7 @@ import {
mdiTableRowHeight,
mdiMinus,
mdiPaletteOutline,
mdiCircle,
} from "@mdi/js";
export const Icons = {
@@ -161,6 +162,7 @@ export const Icons = {
chevronDown: mdiChevronDown,
chevronUp: mdiChevronUp,
chevronRight: mdiChevronRight,
circle: mdiCircle,
none: "",
};