mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support list item style (#4934)
* fix: wrong offset of mention panel feat: support list item style * feat: support backward selection when the end selection is empty
This commit is contained in:
parent
b2fb631500
commit
73df51f35f
@ -74,6 +74,9 @@ export interface QuoteNode extends Element {
|
||||
export interface NumberedListNode extends Element {
|
||||
type: EditorNodeType.NumberedListBlock;
|
||||
blockId: string;
|
||||
data: {
|
||||
number?: number;
|
||||
} & BlockData;
|
||||
}
|
||||
|
||||
export interface BulletedListNode extends Element {
|
||||
|
@ -76,13 +76,20 @@ export const CustomEditor = {
|
||||
return CustomEditor.isInlineNode(editor, afterPoint);
|
||||
},
|
||||
|
||||
isMultipleBlockSelected: (editor: ReactEditor, filterEmpty = false) => {
|
||||
/**
|
||||
* judge if the selection is multiple block
|
||||
* @param editor
|
||||
* @param filterEmptyEndSelection if the filterEmptyEndSelection is true, the function will filter the empty end selection
|
||||
*/
|
||||
isMultipleBlockSelected: (editor: ReactEditor, filterEmptyEndSelection?: boolean): boolean => {
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return false;
|
||||
|
||||
const start = selection.anchor;
|
||||
const end = selection.focus;
|
||||
if (Range.isCollapsed(selection)) return false;
|
||||
const start = Range.start(selection);
|
||||
const end = Range.end(selection);
|
||||
const isBackward = Range.isBackward(selection);
|
||||
const startBlock = CustomEditor.getBlock(editor, start);
|
||||
const endBlock = CustomEditor.getBlock(editor, end);
|
||||
|
||||
@ -90,30 +97,44 @@ export const CustomEditor = {
|
||||
|
||||
const [, startPath] = startBlock;
|
||||
const [, endPath] = endBlock;
|
||||
const pathIsEqual = Path.equals(startPath, endPath);
|
||||
|
||||
if (pathIsEqual) {
|
||||
const isSomePath = Path.equals(startPath, endPath);
|
||||
|
||||
// if the start and end path is the same, return false
|
||||
if (isSomePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filterEmpty) {
|
||||
if (!filterEmptyEndSelection) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const notEmptyBlocks = Array.from(
|
||||
editor.nodes({
|
||||
match: (n) => {
|
||||
return (
|
||||
!Editor.isEditor(n) &&
|
||||
Element.isElement(n) &&
|
||||
n.blockId !== undefined &&
|
||||
!CustomEditor.isEmptyText(editor, n)
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
// The end point is at the start of the end block
|
||||
const focusEndStart = Point.equals(end, editor.start(endPath));
|
||||
|
||||
return notEmptyBlocks.length > 1;
|
||||
if (!focusEndStart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// find the previous block
|
||||
const previous = editor.previous({
|
||||
at: endPath,
|
||||
match: (n) => Element.isElement(n) && n.blockId !== undefined,
|
||||
});
|
||||
|
||||
if (!previous) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// backward selection
|
||||
const newEnd = editor.end(editor.range(previous[1]));
|
||||
|
||||
editor.select({
|
||||
anchor: isBackward ? newEnd : start,
|
||||
focus: isBackward ? start : newEnd,
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -625,4 +646,28 @@ export const CustomEditor = {
|
||||
isEmbedNode(node: Element): boolean {
|
||||
return EmbedTypes.includes(node.type);
|
||||
},
|
||||
|
||||
getListLevel(editor: ReactEditor, type: EditorNodeType, path: Path) {
|
||||
let level = 0;
|
||||
let currentPath = path;
|
||||
|
||||
while (currentPath.length > 0) {
|
||||
const parent = editor.parent(currentPath);
|
||||
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [parentNode, parentPath] = parent as NodeEntry<Element>;
|
||||
|
||||
if (parentNode.type !== type) {
|
||||
break;
|
||||
}
|
||||
|
||||
level += 1;
|
||||
currentPath = parentPath;
|
||||
}
|
||||
|
||||
return level;
|
||||
},
|
||||
};
|
||||
|
@ -1,14 +1,49 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BulletedListNode } from '$app/application/document/document.types';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
|
||||
enum Letter {
|
||||
Disc,
|
||||
Circle,
|
||||
Square,
|
||||
}
|
||||
|
||||
function BulletedListIcon({ block, className }: { block: BulletedListNode; className: string }) {
|
||||
const staticEditor = useSlateStatic();
|
||||
const path = ReactEditor.findPath(staticEditor, block);
|
||||
|
||||
const letter = useMemo(() => {
|
||||
const level = CustomEditor.getListLevel(staticEditor, block.type, path);
|
||||
|
||||
if (level % 3 === 0) {
|
||||
return Letter.Disc;
|
||||
} else if (level % 3 === 1) {
|
||||
return Letter.Circle;
|
||||
} else {
|
||||
return Letter.Square;
|
||||
}
|
||||
}, [block.type, staticEditor, path]);
|
||||
|
||||
const dataLetter = useMemo(() => {
|
||||
switch (letter) {
|
||||
case Letter.Disc:
|
||||
return '•';
|
||||
case Letter.Circle:
|
||||
return '◦';
|
||||
case Letter.Square:
|
||||
return '▪';
|
||||
}
|
||||
}, [letter]);
|
||||
|
||||
function BulletedListIcon({ block: _, className }: { block: BulletedListNode; className: string }) {
|
||||
return (
|
||||
<span
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
data-letter={dataLetter}
|
||||
contentEditable={false}
|
||||
className={`${className} bulleted-icon flex min-w-[23px] justify-center pr-1 font-medium`}
|
||||
className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,35 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { ReactEditor, useSlate, useSlateStatic } from 'slate-react';
|
||||
import { Element, Path } from 'slate';
|
||||
import { NumberedListNode } from '$app/application/document/document.types';
|
||||
import { letterize, romanize } from '$app/utils/list';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
|
||||
enum Letter {
|
||||
Number = 'number',
|
||||
Letter = 'letter',
|
||||
Roman = 'roman',
|
||||
}
|
||||
|
||||
function getLetterNumber(index: number, letter: Letter) {
|
||||
if (letter === Letter.Number) {
|
||||
return index;
|
||||
} else if (letter === Letter.Letter) {
|
||||
return letterize(index);
|
||||
} else {
|
||||
return romanize(index);
|
||||
}
|
||||
}
|
||||
|
||||
function NumberListIcon({ block, className }: { block: NumberedListNode; className: string }) {
|
||||
const editor = useSlate();
|
||||
const staticEditor = useSlateStatic();
|
||||
|
||||
const path = ReactEditor.findPath(editor, block);
|
||||
const index = useMemo(() => {
|
||||
let index = 1;
|
||||
|
||||
let topNode;
|
||||
let prevPath = Path.previous(path);
|
||||
|
||||
while (prevPath) {
|
||||
@ -19,6 +39,7 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa
|
||||
|
||||
if (prevNode.type === block.type) {
|
||||
index += 1;
|
||||
topNode = prevNode;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -26,17 +47,39 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa
|
||||
prevPath = Path.previous(prevPath);
|
||||
}
|
||||
|
||||
return index;
|
||||
if (!topNode) {
|
||||
return Number(block.data?.number ?? 1);
|
||||
}
|
||||
|
||||
const startIndex = (topNode as NumberedListNode).data?.number ?? 1;
|
||||
|
||||
return index + Number(startIndex) - 1;
|
||||
}, [editor, block, path]);
|
||||
|
||||
const letter = useMemo(() => {
|
||||
const level = CustomEditor.getListLevel(staticEditor, block.type, path);
|
||||
|
||||
if (level % 3 === 0) {
|
||||
return Letter.Number;
|
||||
} else if (level % 3 === 1) {
|
||||
return Letter.Letter;
|
||||
} else {
|
||||
return Letter.Roman;
|
||||
}
|
||||
}, [block.type, staticEditor, path]);
|
||||
|
||||
const dataNumber = useMemo(() => {
|
||||
return getLetterNumber(index, letter);
|
||||
}, [index, letter]);
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
contentEditable={false}
|
||||
data-number={index}
|
||||
className={`${className} numbered-icon flex w-[23px] min-w-[23px] justify-center pr-1 font-medium`}
|
||||
data-number={dataNumber}
|
||||
className={`${className} numbered-icon flex w-[24px] min-w-[24px] justify-center pr-1 font-medium`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -14,14 +14,14 @@ export const Text = memo(
|
||||
<span
|
||||
ref={ref}
|
||||
{...attributes}
|
||||
className={`text-element relative my-1 flex w-full whitespace-pre-wrap px-1 ${isEmpty ? 'select-none' : ''} ${
|
||||
className ?? ''
|
||||
} ${hasStartIcon ? 'has-start-icon' : ''}`}
|
||||
className={`text-element relative my-1 flex w-full whitespace-pre-wrap break-words px-1 ${className ?? ''} ${
|
||||
hasStartIcon ? 'has-start-icon' : ''
|
||||
}`}
|
||||
>
|
||||
{renderIcon()}
|
||||
<Placeholder isEmpty={isEmpty} node={node} />
|
||||
|
||||
<span className={`text-content ${isEmpty ? 'empty-content' : ''}`}>{children}</span>
|
||||
<span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
})
|
||||
|
@ -1,51 +1,26 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlate } from 'slate-react';
|
||||
import { MentionPage, MentionType } from '$app/application/document/document.types';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
// enum DateKey {
|
||||
// Today = 'today',
|
||||
// Tomorrow = 'tomorrow',
|
||||
// }
|
||||
export function useMentionPanel({
|
||||
closePanel,
|
||||
searchText,
|
||||
pages,
|
||||
}: {
|
||||
searchText: string;
|
||||
pages: MentionPage[];
|
||||
closePanel: (deleteText?: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlate();
|
||||
|
||||
const pagesMap = useAppSelector((state) => state.pages.pageMap);
|
||||
|
||||
const pagesRef = useRef<MentionPage[]>([]);
|
||||
const [recentPages, setPages] = useState<MentionPage[]>([]);
|
||||
|
||||
const loadPages = useCallback(async () => {
|
||||
const pages = Object.values(pagesMap);
|
||||
|
||||
pagesRef.current = pages;
|
||||
setPages(pages);
|
||||
}, [pagesMap]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPages();
|
||||
}, [loadPages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchText) {
|
||||
setPages(pagesRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredPages = pagesRef.current.filter((page) => {
|
||||
return page.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
|
||||
setPages(filteredPages);
|
||||
}, [searchText]);
|
||||
|
||||
const onConfirm = useCallback(
|
||||
(key: string) => {
|
||||
const [, id] = key.split(',');
|
||||
@ -75,15 +50,62 @@ export function useMentionPanel({
|
||||
[t]
|
||||
);
|
||||
|
||||
// const renderDate = useCallback(() => {
|
||||
// return [
|
||||
// {
|
||||
// key: DateKey.Today,
|
||||
// content: (
|
||||
// <div className={'px-3 pb-1 pt-2 text-xs '}>
|
||||
// <span className={'text-text-title'}>{t('relativeDates.today')}</span> -{' '}
|
||||
// <span className={'text-xs text-text-caption'}>{dayjs().format('MMM D, YYYY')}</span>
|
||||
// </div>
|
||||
// ),
|
||||
//
|
||||
// children: [],
|
||||
// },
|
||||
// {
|
||||
// key: DateKey.Tomorrow,
|
||||
// content: (
|
||||
// <div className={'px-3 pb-1 pt-2 text-xs '}>
|
||||
// <span className={'text-text-title'}>{t('relativeDates.tomorrow')}</span>
|
||||
// </div>
|
||||
// ),
|
||||
// children: [],
|
||||
// },
|
||||
// ];
|
||||
// }, [t]);
|
||||
|
||||
const options: KeyboardNavigationOption<MentionType | string>[] = useMemo(() => {
|
||||
return [
|
||||
// {
|
||||
// key: MentionType.Date,
|
||||
// content: <div className={'px-3 pb-1 pt-2 text-sm'}>{t('editor.date')}</div>,
|
||||
// children: renderDate(),
|
||||
// },
|
||||
{
|
||||
key: 'divider',
|
||||
content: <div className={'border-t border-line-divider'} />,
|
||||
children: [],
|
||||
},
|
||||
|
||||
{
|
||||
key: MentionType.PageRef,
|
||||
content: <div className={'px-3 pb-1 pt-2 text-sm'}>{t('document.mention.page.label')}</div>,
|
||||
children: recentPages.map(renderPage),
|
||||
children:
|
||||
pages.length > 0
|
||||
? pages.map(renderPage)
|
||||
: [
|
||||
{
|
||||
key: 'noPage',
|
||||
content: (
|
||||
<div className={'px-3 pb-3 pt-2 text-xs text-text-caption'}>{t('findAndReplace.noResult')}</div>
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter((option) => option.children.length > 0);
|
||||
}, [recentPages, renderPage, t]);
|
||||
];
|
||||
}, [pages, renderPage, t]);
|
||||
|
||||
return {
|
||||
options,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
initialAnchorOrigin,
|
||||
initialTransformOrigin,
|
||||
@ -9,10 +9,39 @@ import Popover from '@mui/material/Popover';
|
||||
|
||||
import MentionPanelContent from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent';
|
||||
import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { MentionPage } from '$app/application/document/document.types';
|
||||
|
||||
export function MentionPanel({ anchorPosition, closePanel, searchText }: PanelProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const pagesMap = useAppSelector((state) => state.pages.pageMap);
|
||||
|
||||
const pagesRef = useRef<MentionPage[]>([]);
|
||||
const [recentPages, setPages] = useState<MentionPage[]>([]);
|
||||
|
||||
const loadPages = useCallback(async () => {
|
||||
const pages = Object.values(pagesMap);
|
||||
|
||||
pagesRef.current = pages;
|
||||
setPages(pages);
|
||||
}, [pagesMap]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPages();
|
||||
}, [loadPages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchText) {
|
||||
setPages(pagesRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredPages = pagesRef.current.filter((page) => {
|
||||
return page.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
|
||||
setPages(filteredPages);
|
||||
}, [searchText]);
|
||||
const open = Boolean(anchorPosition);
|
||||
|
||||
const {
|
||||
@ -42,12 +71,7 @@ export function MentionPanel({ anchorPosition, closePanel, searchText }: PanelPr
|
||||
transformOrigin={transformOrigin}
|
||||
onClose={() => closePanel(false)}
|
||||
>
|
||||
<MentionPanelContent
|
||||
width={paperWidth}
|
||||
maxHeight={paperHeight}
|
||||
closePanel={closePanel}
|
||||
searchText={searchText}
|
||||
/>
|
||||
<MentionPanelContent width={paperWidth} maxHeight={paperHeight} closePanel={closePanel} pages={recentPages} />
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
|
@ -2,15 +2,16 @@ import React, { useRef } from 'react';
|
||||
import { useMentionPanel } from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks';
|
||||
|
||||
import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||
import { MentionPage } from '$app/application/document/document.types';
|
||||
|
||||
function MentionPanelContent({
|
||||
closePanel,
|
||||
searchText,
|
||||
pages,
|
||||
maxHeight,
|
||||
width,
|
||||
}: {
|
||||
closePanel: (deleteText?: boolean) => void;
|
||||
searchText: string;
|
||||
pages: MentionPage[];
|
||||
maxHeight: number;
|
||||
width: number;
|
||||
}) {
|
||||
@ -18,7 +19,7 @@ function MentionPanelContent({
|
||||
|
||||
const { options, onConfirm } = useMentionPanel({
|
||||
closePanel,
|
||||
searchText,
|
||||
pages,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -157,7 +157,7 @@ export function useSlashCommandPanel({
|
||||
|
||||
if (!newNode || !path) return;
|
||||
|
||||
const isEmpty = CustomEditor.isEmptyText(editor, newNode) && node.type === EditorNodeType.Paragraph;
|
||||
const isEmpty = CustomEditor.isEmptyText(editor, newNode);
|
||||
|
||||
if (!isEmpty) {
|
||||
const nextPath = Path.next(path);
|
||||
|
@ -169,6 +169,7 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
|
||||
};
|
||||
}, [visible, editor, ref]);
|
||||
|
||||
// Close toolbar when press ESC
|
||||
useEffect(() => {
|
||||
const slateEditorDom = ReactEditor.toDOMNode(editor, editor);
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
@ -195,6 +196,39 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
|
||||
};
|
||||
}, [closeToolbar, debounceRecalculatePosition, editor, visible]);
|
||||
|
||||
// Recalculate position when the scroll container is scrolled
|
||||
useEffect(() => {
|
||||
const slateEditorDom = ReactEditor.toDOMNode(editor, editor);
|
||||
const scrollContainer = slateEditorDom.closest('.appflowy-scroll-container');
|
||||
|
||||
if (!visible) return;
|
||||
if (!scrollContainer) return;
|
||||
const handleScroll = () => {
|
||||
if (isDraggingRef.current) return;
|
||||
|
||||
const domSelection = window.getSelection();
|
||||
const rangeCount = domSelection?.rangeCount;
|
||||
|
||||
if (!rangeCount) return null;
|
||||
|
||||
const domRange = rangeCount > 0 ? domSelection.getRangeAt(0) : undefined;
|
||||
|
||||
const rangeRect = domRange?.getBoundingClientRect();
|
||||
|
||||
// Stop calculating when the range is out of the window
|
||||
if (!rangeRect?.bottom || rangeRect.bottom < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
recalculatePosition();
|
||||
};
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [visible, editor, recalculatePosition]);
|
||||
|
||||
return {
|
||||
visible,
|
||||
restoreSelection,
|
||||
|
@ -9,8 +9,6 @@
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.block-element.block-align-left {
|
||||
> div > .text-element {
|
||||
text-align: left;
|
||||
@ -50,6 +48,8 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[role="textbox"] {
|
||||
::selection {
|
||||
@apply bg-content-blue-100;
|
||||
@ -58,6 +58,9 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
&::selection {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
&.selected {
|
||||
@apply bg-content-blue-100;
|
||||
}
|
||||
span {
|
||||
&::selection {
|
||||
@apply bg-content-blue-100;
|
||||
@ -67,7 +70,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
[data-dark-mode="true"] [role="textbox"]{
|
||||
::selection {
|
||||
background-color: #1e79a2;
|
||||
@ -77,6 +79,9 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
&::selection {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
&.selected {
|
||||
background-color: #1e79a2;
|
||||
}
|
||||
span {
|
||||
&::selection {
|
||||
background-color: #1e79a2;
|
||||
@ -85,10 +90,9 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.text-content, [data-dark-mode="true"] .text-content {
|
||||
@apply min-w-[1px];
|
||||
&.empty-content {
|
||||
&.empty-text {
|
||||
span {
|
||||
&::selection {
|
||||
@apply bg-transparent;
|
||||
@ -113,7 +117,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
|
||||
.has-start-icon > .text-placeholder {
|
||||
&:after {
|
||||
@apply left-[30px];
|
||||
@apply left-[29px];
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +129,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
|
||||
.bulleted-icon {
|
||||
&:after {
|
||||
content: "•";
|
||||
content: attr(data-letter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Range, Element, Editor, NodeEntry } from 'slate';
|
||||
import { Range, Element, Editor, NodeEntry, Path } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import {
|
||||
getRegex,
|
||||
@ -29,6 +29,10 @@ export const withMarkdown = (editor: ReactEditor) => {
|
||||
|
||||
const match = CustomEditor.getBlock(editor);
|
||||
const [node, path] = match as NodeEntry<Element>;
|
||||
const prevPath = Path.previous(path);
|
||||
const prev = editor.node(prevPath) as NodeEntry<Element>;
|
||||
const prevIsNumberedList = prev && prev[0].type === EditorNodeType.NumberedListBlock;
|
||||
|
||||
const start = Editor.start(editor, path);
|
||||
const beforeRange = { anchor: start, focus: selection.anchor };
|
||||
const beforeText = Editor.string(editor, beforeRange);
|
||||
@ -59,6 +63,11 @@ export const withMarkdown = (editor: ReactEditor) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. If the block is number list, and the previous block is also number list
|
||||
if (block.type === EditorNodeType.NumberedListBlock && prevIsNumberedList) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeBeforeText(beforeRange);
|
||||
CustomEditor.turnToBlock(editor, block);
|
||||
|
||||
@ -145,7 +154,9 @@ function whichBlock(shortcut: MarkdownShortcuts, beforeText: string) {
|
||||
case MarkdownShortcuts.NumberedList:
|
||||
return {
|
||||
type: EditorNodeType.NumberedListBlock,
|
||||
data: {},
|
||||
data: {
|
||||
number: Number(beforeText.split('.')[0]) ?? 1,
|
||||
},
|
||||
};
|
||||
case MarkdownShortcuts.TodoList:
|
||||
return {
|
||||
|
45
frontend/appflowy_tauri/src/appflowy_app/utils/list.ts
Normal file
45
frontend/appflowy_tauri/src/appflowy_app/utils/list.ts
Normal file
@ -0,0 +1,45 @@
|
||||
const romanMap: [number, string][] = [
|
||||
[1000, 'M'],
|
||||
[900, 'CM'],
|
||||
[500, 'D'],
|
||||
[400, 'CD'],
|
||||
[100, 'C'],
|
||||
[90, 'XC'],
|
||||
[50, 'L'],
|
||||
[40, 'XL'],
|
||||
[10, 'X'],
|
||||
[9, 'IX'],
|
||||
[5, 'V'],
|
||||
[4, 'IV'],
|
||||
[1, 'I'],
|
||||
];
|
||||
|
||||
export function romanize(num: number): string {
|
||||
let result = '';
|
||||
let nextNum = num;
|
||||
|
||||
for (const [value, symbol] of romanMap) {
|
||||
const count = Math.floor(nextNum / value);
|
||||
|
||||
nextNum -= value * count;
|
||||
result += symbol.repeat(count);
|
||||
if (nextNum === 0) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function letterize(num: number): string {
|
||||
let nextNum = num;
|
||||
let letters = '';
|
||||
|
||||
while (nextNum > 0) {
|
||||
nextNum--;
|
||||
const letter = String.fromCharCode((nextNum % 26) + 'a'.charCodeAt(0));
|
||||
|
||||
letters = letter + letters;
|
||||
nextNum = Math.floor(nextNum / 26);
|
||||
}
|
||||
|
||||
return letters;
|
||||
}
|
Loading…
Reference in New Issue
Block a user