feat: improve tiptap editor

This commit is contained in:
fantasticit 2022-03-24 21:37:32 +08:00
parent 2ad6518a51
commit cd22f259fa
70 changed files with 303 additions and 128 deletions

View File

@ -33,6 +33,7 @@ import { OrderedList } from './extensions/orderedList';
import { Paragraph } from './extensions/paragraph';
import { Placeholder } from './extensions/placeholder';
import { SearchNReplace } from './extensions/search';
import { SelectionExtension } from './extensions/selection';
import { Status } from './extensions/status';
import { Strike } from './extensions/strike';
import { Table } from './extensions/table';
@ -85,6 +86,7 @@ export const BaseKit = [
Paragraph,
Placeholder,
SearchNReplace,
SelectionExtension,
Status,
Strike,
Table,

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { AttachmentWrapper } from '../components/attachment';
import { AttachmentWrapper } from '../wrappers/attachment';
import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' {
@ -68,7 +68,6 @@ export const Attachment = Node.create({
};
},
// @ts-ignore
addCommands() {
return {
setAttachment:
@ -78,6 +77,7 @@ export const Attachment = Node.create({
},
};
},
addNodeView() {
return ReactNodeViewRenderer(AttachmentWrapper);
},

View File

@ -1,6 +1,6 @@
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { BannerWrapper } from '../components/banner';
import { BannerWrapper } from '../wrappers/banner';
import { typesAvailable } from '../services/markdown/markdownToHTML/markdownBanner';
import { getDatasetAttribute } from '../services/dataset';
@ -54,7 +54,6 @@ export const Banner = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setBanner:

View File

@ -3,7 +3,7 @@ import { Node, textblockTypeInputRule, mergeAttributes } from '@tiptap/core';
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { LowlightPlugin } from '../services/lowlightPlugin';
import { CodeBlockWrapper } from '../components/codeBlock';
import { CodeBlockWrapper } from '../wrappers/codeBlock';
export interface CodeBlockOptions {
/**

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { DocumentChildrenWrapper } from '../components/documentChildren';
import { DocumentChildrenWrapper } from '../wrappers/documentChildren';
import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' {
@ -16,10 +16,16 @@ export const DocumentChildrenInputRegex = /^documentChildren\$$/;
export const DocumentChildren = Node.create({
name: 'documentChildren',
group: 'block',
draggable: true,
selectable: true,
atom: true,
addOptions() {
return {
HTMLAttributes: {
class: 'documentChildren',
},
};
},
addAttributes() {
return {
wikiId: {
@ -32,18 +38,11 @@ export const DocumentChildren = Node.create({
},
};
},
addOptions() {
return {
HTMLAttributes: {
class: 'documentChildren',
},
};
},
parseHTML() {
return [
{
tag: 'div',
tag: 'div.documentChildren',
},
];
},
@ -51,7 +50,7 @@ export const DocumentChildren = Node.create({
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setDocumentChildren:

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { DocumentReferenceWrapper } from '../components/documentReference';
import { DocumentReferenceWrapper } from '../wrappers/documentReference';
import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' {
@ -16,9 +16,7 @@ export const DocumentReferenceInputRegex = /^documentReference\$$/;
export const DocumentReference = Node.create({
name: 'documentReference',
group: 'block',
draggable: true,
atom: true,
selectable: true,
addAttributes() {
return {
@ -48,7 +46,7 @@ export const DocumentReference = Node.create({
parseHTML() {
return [
{
tag: 'div',
tag: 'div.documentReference',
},
];
},
@ -57,7 +55,6 @@ export const DocumentReference = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setDocumentReference:

View File

@ -4,8 +4,8 @@ import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import Suggestion from '@tiptap/suggestion';
import tippy from 'tippy.js';
import { EmojiList } from '../components/emojiList';
import { emojiSearch, emojisToName } from '../components/emojiList/emojis';
import { EmojiList } from '../wrappers/emojiList';
import { emojiSearch, emojisToName } from '../wrappers/emojiList/emojis';
declare module '@tiptap/core' {
interface Commands<ReturnType> {

View File

@ -4,7 +4,7 @@ import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import Suggestion from '@tiptap/suggestion';
import tippy from 'tippy.js';
import { MenuList } from '../components/menuList';
import { MenuList } from '../wrappers/menuList';
import { EVOKE_MENU_ITEMS } from '../menus/evokeMenu';
export const EvokeMenuPluginKey = new PluginKey('evokeMenu');

View File

@ -16,6 +16,7 @@ declare module '@tiptap/core' {
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
name: 'horizontalRule',
group: 'block',
selectable: true,
addOptions() {
return {

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { IframeWrapper } from '../components/iframe';
import { IframeWrapper } from '../wrappers/iframe';
import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' {
@ -16,7 +16,7 @@ export const Iframe = Node.create({
content: '',
marks: '',
group: 'block',
draggable: true,
selectable: true,
atom: true,
addOptions() {
@ -56,7 +56,6 @@ export const Iframe = Node.create({
return ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setIframe:

View File

@ -1,6 +1,6 @@
import { Image as BuiltInImage } from '@tiptap/extension-image';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { ImageWrapper } from '../components/image';
import { ImageWrapper } from '../wrappers/image';
const resolveImageEl = (element) => (element.nodeName === 'IMG' ? element : element.querySelector('img'));
@ -20,10 +20,12 @@ export const Image = BuiltInImage.extend({
content: '',
marks: '',
group: 'block',
draggable: true,
draggable: false,
selectable: true,
atom: true,
};
},
addAttributes() {
return {
...this.parent?.(),
@ -59,6 +61,7 @@ export const Image = BuiltInImage.extend({
},
};
},
addCommands() {
return {
...this.parent?.(),
@ -69,6 +72,7 @@ export const Image = BuiltInImage.extend({
},
};
},
addNodeView() {
return ReactNodeViewRenderer(ImageWrapper);
},

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { KatexWrapper } from '../components/katex';
import { KatexWrapper } from '../wrappers/katex';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
@ -46,11 +46,10 @@ export const Katex = Node.create({
return ['span', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setKatex:
(options) =>
(options = {}) =>
({ commands }) => {
return commands.insertContent({
type: this.name,

View File

@ -1,6 +1,6 @@
import { Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { LoadingWrapper } from '../components/loading';
import { LoadingWrapper } from '../wrappers/loading';
export const Loading = Node.create({
name: 'loading',

View File

@ -1,7 +1,6 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { safeJSONParse } from 'helpers/json';
import { MindWrapper } from '../components/mind';
import { MindWrapper } from '../wrappers/mind';
import { getDatasetAttribute } from '../services/dataset';
const DEFAULT_MIND_DATA = {
@ -27,7 +26,7 @@ export const Mind = Node.create({
content: '',
marks: '',
group: 'block',
draggable: true,
selectable: true,
atom: true,
addAttributes() {
@ -67,7 +66,6 @@ export const Mind = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setMind:

View File

@ -0,0 +1,60 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey, NodeSelection, TextSelection, Selection, AllSelection } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
export const selectionPluginKey = new PluginKey('selection');
export const getTopLevelNodesFromSelection = (selection: Selection, doc) => {
const nodes: { node; pos: number }[] = [];
if (selection.from !== selection.to) {
const { from, to } = selection;
doc.nodesBetween(from, to, (node, pos) => {
const withinSelection = from <= pos && pos + node.nodeSize <= to;
if (node && node.type.name !== 'paragraph' && !node.isText && withinSelection) {
nodes.push({ node, pos });
return false;
}
return true;
});
}
return nodes;
};
export const getDecorations = (doc, selection: Selection): DecorationSet => {
if (selection instanceof NodeSelection) {
return DecorationSet.create(doc, [
Decoration.node(selection.from, selection.to, {
class: 'selected-node',
}),
]);
}
if (selection instanceof TextSelection || selection instanceof AllSelection) {
const decorations = getTopLevelNodesFromSelection(selection, doc).map(({ node, pos }) => {
return Decoration.node(pos, pos + node.nodeSize, {
class: 'selected-node',
});
});
return DecorationSet.create(doc, decorations);
}
return DecorationSet.empty;
};
export const SelectionExtension = Extension.create({
name: 'selection',
priority: EXTENSION_PRIORITY_HIGHEST,
addProseMirrorPlugins() {
return [
new Plugin({
key: selectionPluginKey,
props: {
decorations(state) {
const { doc, selection } = state;
const decorationSet = getDecorations(doc, selection);
return decorationSet;
},
},
}),
];
},
});

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { StatusWrapper } from '../components/status';
import { StatusWrapper } from '../wrappers/status';
import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' {
@ -41,20 +41,19 @@ export const Status = Node.create({
parseHTML() {
return [
{
tag: 'div',
tag: 'span.status',
},
];
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
// @ts-ignore
addCommands() {
return {
setStatus:
(options) =>
(options = {}) =>
({ commands }) => {
return commands.insertContent({
type: this.name,

View File

@ -40,17 +40,18 @@ export const Table = BuiltInTable.extend({
if (fixedWidth && totalWidth > 0) {
HTMLAttributes.style = `width: ${totalWidth}px;`;
} else if (totalWidth && totalWidth > 0) {
HTMLAttributes.style = `min-width: 100%`;
HTMLAttributes.style = `min-width: ${totalWidth}px`;
} else {
HTMLAttributes.style = null;
}
return [
'div',
{ class: 'tableWrapper' },
{ class: 'tableWrapper adas' },
['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]],
];
},
}).configure({
resizable: true,
cellMinWidth: 50,
});

View File

@ -4,7 +4,7 @@ import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
import { Plugin } from 'prosemirror-state';
import { findParentNodeClosestToPos } from 'prosemirror-utils';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import { TaskItemWrapper } from '../components/taskItem';
import { TaskItemWrapper } from '../wrappers/taskItem';
const CustomTaskItem = BuiltInTaskItem.extend({
parseHTML() {

View File

@ -3,10 +3,10 @@ import { Space, Button } from '@douyinfe/semi-ui';
import { IconUndo, IconRedo } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { IconClear } from 'components/icons';
import { Divider } from './components/divider';
import { Divider } from './wrappers/divider';
import { MediaInsertMenu } from './menus/mediaInsert';
import { Paragraph } from './menus/components/paragraph';
import { FontSize } from './menus/components/fontSize';
import { Paragraph } from './menus/paragraph';
import { FontSize } from './menus/fontSize';
import { BaseMenu } from './menus/baseMenu';
import { AlignMenu } from './menus/align';
import { ListMenu } from './menus/list';

View File

@ -29,12 +29,15 @@ export const AlignMenu = ({ editor }) => {
<Tooltip content="左对齐">
<Button onClick={toggle('left')} icon={<IconAlignLeft />} type="tertiary" theme="borderless" />
</Tooltip>
<Tooltip content="居中">
<Button onClick={toggle('center')} icon={<IconAlignCenter />} type="tertiary" theme="borderless" />
</Tooltip>
<Tooltip content="右对齐">
<Button onClick={toggle('right')} icon={<IconAlignRight />} type="tertiary" theme="borderless" />
</Tooltip>
<Tooltip content="两端对齐">
<Button onClick={toggle('justify')} icon={<IconAlignJustify />} type="tertiary" theme="borderless" />
</Tooltip>

View File

@ -1,8 +1,8 @@
import { Space, Button } from '@douyinfe/semi-ui';
import { IconDelete, IconTickCircle, IconAlertTriangle, IconClear, IconInfoCircle } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from './components/bubbleMenu';
import { Divider } from '../components/divider';
import { BubbleMenu } from '../views/bubbleMenu';
import { Divider } from '../wrappers/divider';
import { Banner } from '../extensions/banner';
import { deleteNode } from '../services/deleteNode';

View File

@ -3,7 +3,7 @@ import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip';
import { IconQuote, IconLink, IconHorizontalRule } from 'components/icons';
import { isTitleActive } from '../services/isActive';
import { Emoji } from './components/emoji';
import { Emoji } from './emoji';
import { Search } from './search';
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {

View File

@ -3,7 +3,7 @@ import { Button } from '@douyinfe/semi-ui';
import { IconFont, IconMark } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { isTitleActive } from '../services/isActive';
import { Color } from './components/color';
import { ColorPicker } from './colorPicker';
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
const { color, backgroundColor } = editor.getAttributes('textStyle');
@ -14,7 +14,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
return (
<>
<Color
<ColorPicker
onSetColor={(color) => {
editor.chain().focus().setColor(color).run();
}}
@ -45,8 +45,9 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
disabled={isTitleActive(editor)}
/>
</Tooltip>
</Color>
<Color
</ColorPicker>
<ColorPicker
onSetColor={(color) => {
editor.chain().focus().setBackgroundColor(color).run();
}}
@ -71,7 +72,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
disabled={isTitleActive(editor)}
/>
</Tooltip>
</Color>
</ColorPicker>
</>
);
};

View File

@ -26,7 +26,7 @@ const colors = [
'rgb(234, 230, 255)',
];
export const Color: React.FC<{
export const ColorPicker: React.FC<{
onSetColor;
disabled?: boolean;
}> = ({ children, onSetColor, disabled = false }) => {

View File

@ -31,6 +31,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
},
{
key: '标题2',
label: (
@ -41,6 +42,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
},
{
key: '标题1',
label: (
@ -51,6 +53,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
},
{
key: '无序列表',
label: (
@ -61,6 +64,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleBulletList().run(),
},
{
key: '有序列表',
label: (
@ -71,6 +75,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleOrderedList().run(),
},
{
key: '任务列表',
label: (
@ -81,6 +86,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleTaskList().run(),
},
{
key: '链接',
label: (
@ -91,6 +97,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleLink({ href: '' }).run(),
},
{
key: '引用',
label: (
@ -101,6 +108,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleBlockquote().run(),
},
{
key: '分割线',
label: (
@ -111,6 +119,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setHorizontalRule().run(),
},
{
key: '表格',
label: (
@ -121,6 +130,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
},
{
key: '代码块',
label: (
@ -131,6 +141,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().toggleCodeBlock().run(),
},
{
key: '图片',
label: () => (
@ -141,6 +152,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setEmptyImage().run(),
},
{
key: '附件',
label: () => (
@ -151,6 +163,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setAttachment().run(),
},
{
key: '外链',
label: (
@ -161,6 +174,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(),
},
{
key: '思维导图',
label: (
@ -171,6 +185,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setMind().run(),
},
{
key: '数学公式',
label: (
@ -181,6 +196,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setKatex().run(),
},
{
key: '状态',
label: (
@ -191,6 +207,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setStatus().run(),
},
{
key: '信息框',
label: (
@ -201,6 +218,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setBanner({ type: 'info' }).run(),
},
{
key: '文档',
label: (
@ -211,6 +229,7 @@ export const EVOKE_MENU_ITEMS = [
),
command: (editor: Editor) => editor.chain().focus().setDocumentReference().run(),
},
{
key: '子文档',
label: (

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from '../../services/isActive';
import { isTitleActive } from '../services/isActive';
export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48];

View File

@ -3,8 +3,8 @@ import { Space, Button, InputNumber, Typography } from '@douyinfe/semi-ui';
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconUpload, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { Upload } from 'components/upload';
import { BubbleMenu } from './components/bubbleMenu';
import { Divider } from '../components/divider';
import { BubbleMenu } from '../views/bubbleMenu';
import { Divider } from '../wrappers/divider';
import { Image } from '../extensions/image';
import { getImageOriginSize } from '../services/image';
@ -51,6 +51,7 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small"
/>
</Tooltip>
<Tooltip content="居中">
<Button
onClick={() => {
@ -69,6 +70,7 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small"
/>
</Tooltip>
<Tooltip content="右对齐">
<Button
onClick={() => {
@ -87,7 +89,9 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small"
/>
</Tooltip>
<Divider />
<Text></Text>
<InputNumber
size="small"
@ -106,6 +110,7 @@ export const ImageBubbleMenu = ({ editor }) => {
.run();
}}
/>
<Text></Text>
<InputNumber
size="small"
@ -124,30 +129,9 @@ export const ImageBubbleMenu = ({ editor }) => {
.run();
}}
/>
<Divider />
<Upload
accept="image/*"
onOK={async (url, fileName) => {
const { width, height } = await getImageOriginSize(url);
editor
.chain()
.updateAttributes(Image.name, {
src: url,
alt: fileName,
width,
height,
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
>
{() => (
<Tooltip content="上传图片">
<Button size="small" type="tertiary" theme="borderless" icon={<IconUpload />} />
</Tooltip>
)}
</Upload>
<Tooltip content="删除" hideOnClick>
<Button
size="small"

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Space, Button, Input } from '@douyinfe/semi-ui';
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from './components/bubbleMenu';
import { BubbleMenu } from '../views/bubbleMenu';
import { Link } from '../extensions/link';
export const LinkBubbleMenu = ({ editor }) => {
@ -28,11 +28,11 @@ export const LinkBubbleMenu = ({ editor }) => {
onChange={(v) => setUrl(v)}
placeholder={'输入链接'}
onEnterPress={(e) => {
// @ts-ignore
const url = e.target.value;
const url = (e.target as HTMLInputElement).value;
setUrl(url);
}}
/>
<Tooltip content="设置链接">
<Button
size="small"
@ -51,6 +51,7 @@ export const LinkBubbleMenu = ({ editor }) => {
}}
/>
</Tooltip>
<Tooltip content="去除链接">
<Button
onClick={() => {
@ -62,6 +63,7 @@ export const LinkBubbleMenu = ({ editor }) => {
size="small"
/>
</Tooltip>
<Tooltip content="访问链接">
<Button
size="small"

View File

@ -50,7 +50,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
</div>
}
>
<Dropdown.Item onClick={() => editor.chain().focus().toggleCodeBlock().run()}>
<Dropdown.Item>
<IconTable />
</Dropdown.Item>
</Popover>
@ -63,6 +63,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<IconImage />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setAttachment().run()}>
<IconAttachment />
@ -71,6 +72,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}>
<IconLink />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setMind().run()}>
<IconMind />
</Dropdown.Item>
@ -81,18 +83,22 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<Dropdown.Divider />
<Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setStatus().run()}>
<IconStatus />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setBanner({ type: 'info' }).run()}>
<IconInfo />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentReference().run()}>
<IconDocument />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentChildren().run()}>
<IconDocument />
</Dropdown.Item>

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from '../../services/isActive';
import { isTitleActive } from '../services/isActive';
const getCurrentCaretTitle = (editor) => {
if (editor.isActive('heading', { level: 1 })) return 1;

View File

@ -53,12 +53,15 @@ export const Search = ({ editor }) => {
<Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.replace()}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToPrevSearchResult()}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToNextSearchResult()}>
</Button>

View File

@ -11,8 +11,8 @@ import {
IconDeleteTable,
} from 'components/icons';
import { Tooltip } from 'components/tooltip';
import { Divider } from '../components/divider';
import { BubbleMenu } from './components/bubbleMenu';
import { Divider } from '../wrappers/divider';
import { BubbleMenu } from '../views/bubbleMenu';
import { Table } from '../extensions/table';
export const TableBubbleMenu = ({ editor }) => {

View File

@ -102,14 +102,14 @@ export function LowlightPlugin({
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some((step) => {
// @ts-ignore
return (
// @ts-ignore
step.from !== undefined &&
// @ts-ignore
step.to !== undefined &&
oldNodes.some((node) => {
// @ts-ignore
return (
// @ts-ignore
node.pos >= step.from &&
// @ts-ignore
node.pos + node.node.nodeSize <= step.to

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import cls from 'classnames';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Button, Typography, Spin, Collapsible, Space } from '@douyinfe/semi-ui';
import {
@ -47,7 +48,7 @@ const getFileTypeIcon = (type: FileType) => {
};
export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const $upload = useRef();
const $upload = useRef<HTMLInputElement>();
const isEditable = editor.isEditable;
const { hasTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs;
const [loading, toggleLoading] = useToggle(false);
@ -55,7 +56,6 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const selectFile = () => {
if (!isEditable || error || url) return;
// @ts-ignore
isEditable && $upload.current.click();
};
@ -90,7 +90,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const content = (() => {
if (error) {
return (
<div className={styles.wrap} onClick={selectFile}>
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Text>{error}</Text>
</div>
);
@ -99,7 +99,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
if (url) {
return (
<>
<div className={styles.wrap} onClick={selectFile}>
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Space>
{getFileTypeIcon(type)}
{fileName}.{fileExt}
@ -139,7 +139,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
if (isEditable && !url) {
return (
<div className={styles.wrap} onClick={selectFile}>
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Spin spinning={loading}>
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择文件'}</Text>
<input ref={$upload} type="file" hidden onChange={handleFile} />

View File

@ -1,10 +1,11 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
import cls from 'classnames';
import styles from './index.module.scss';
export const BannerWrapper = ({ node }) => {
return (
<NodeViewWrapper id="js-bannber-container" className={styles.wrap}>
<NodeViewWrapper id="js-bannber-container" className={cls(styles.wrap, 'render-wrapper')}>
<SemiBanner type={node.attrs.type} description={<NodeViewContent />} closeIcon={null} fullMode={false} />
</NodeViewWrapper>
);

View File

@ -1,4 +1,5 @@
import React, { useRef } from 'react';
import cls from 'classnames';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Button, Select, Tooltip } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons';
@ -17,7 +18,7 @@ export const CodeBlockWrapper = ({
const $container = useRef<HTMLPreElement>();
return (
<NodeViewWrapper className={styles.wrap}>
<NodeViewWrapper className={cls(styles.wrap, 'render-wrapper')}>
<div className={styles.handleWrap}>
{isEditable && (
<Select

View File

@ -1,14 +1,21 @@
.wrap {
margin: 28px 0 16px;
padding-top: 12px;
border-top: 1px solid var(--semi-color-border);
margin-top: 12px;
padding: 12px;
border: 1px solid var(--semi-color-border);
border-left: 0;
border-right: 0;
&.isEditable {
border: 0;
background-color: var(--semi-color-fill-0);
&:hover {
outline: 1px solid var(--semi-color-link);
background-color: var(--semi-color-fill-1);
}
.itemWrap {
pointer-events: none;
&:hover {
color: var(--semi-color-text-1);
border-color: var(--semi-color-border);
@ -23,6 +30,8 @@
text-decoration: none;
color: var(--semi-color-text-1);
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
cursor: pointer;
&:hover {
color: var(--semi-color-link);

View File

@ -34,7 +34,10 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
}, [node.attrs, wikiId, documentId]);
return (
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}>
<NodeViewWrapper
as="div"
className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}
>
<div>
<div>
<Text type="tertiary"></Text>

View File

@ -1,12 +1,17 @@
.wrap {
margin: 8px 0;
margin-top: 12px;
&.isEditable {
padding: 12px;
background-color: var(--semi-color-fill-0);
&:hover {
outline: 1px solid var(--semi-color-link);
background-color: var(--semi-color-fill-1);
}
.itemWrap {
pointer-events: none;
&:hover {
color: var(--semi-color-text-1);
border-color: var(--semi-color-border);

View File

@ -22,7 +22,7 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
};
return (
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}>
<NodeViewWrapper as="div" className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable)}>
<div>
{isEditable && (
<DataRender

View File

@ -1,4 +1,5 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import cls from 'classnames';
import { Input } from '@douyinfe/semi-ui';
import { Resizeable } from 'components/resizeable';
import styles from './index.module.scss';
@ -7,13 +8,11 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable;
const { url, width, height } = node.attrs;
console.log('render iframe', node.attrs);
const onResize = (size) => {
updateAttributes({ width: size.width, height: size.height });
};
const content = (
<NodeViewContent as="div" className={styles.wrap}>
<NodeViewContent as="div" className={cls(styles.wrap, 'render-wrapper')}>
{isEditable && (
<div className={styles.handlerWrap}>
<Input placeholder={'输入外链地址'} value={url} onChange={(url) => updateAttributes({ url })}></Input>

View File

@ -1,6 +1,7 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Resizeable } from 'components/resizeable';
import { useEffect, useRef } from 'react';
import cls from 'classnames';
import { Typography, Spin } from '@douyinfe/semi-ui';
import { useToggle } from 'hooks/useToggle';
import { uploadFile } from 'services/file';
@ -12,7 +13,7 @@ const { Text } = Typography;
export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable;
const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs;
const $upload = useRef();
const $upload = useRef<HTMLInputElement>();
const [loading, toggleLoading] = useToggle(false);
const onResize = (size) => {
@ -21,7 +22,6 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const selectFile = () => {
if (!isEditable || error || src) return;
// @ts-ignore
isEditable && $upload.current.click();
};
@ -54,7 +54,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const content = (() => {
if (error) {
return (
<div className={styles.wrap}>
<div className={cls(styles.wrap, 'render-wrapper')}>
<Text>{error}</Text>
</div>
);
@ -62,7 +62,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
if (!src) {
return (
<div className={styles.wrap} onClick={selectFile}>
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Spin spinning={loading}>
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text>
<input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} />

View File

@ -1,5 +1,6 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import cls from 'classnames';
import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons';
import katex from 'katex';
@ -26,7 +27,7 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
);
return (
<NodeViewWrapper as="span" className={styles.wrap} contentEditable={false}>
<NodeViewWrapper as="span" className={cls(styles.wrap, 'render-wrapper')} contentEditable={false}>
{isEditable ? (
<Popover
showArrow

View File

@ -5,6 +5,8 @@
line-height: 0;
overflow: visible;
outline: none;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
.jsmindWrap {
position: absolute;
@ -29,8 +31,7 @@
position: relative;
min-height: 50px;
overflow: hidden;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
outline: none;
> input {

View File

@ -1,4 +1,5 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import cls from 'classnames';
import { useCallback, useEffect, useRef } from 'react';
import { Button } from '@douyinfe/semi-ui';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
@ -120,7 +121,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
);
return (
<NodeViewWrapper className={styles.wrap}>
<NodeViewWrapper className={cls(styles.wrap, 'render-wrapper')}>
<NodeViewContent as="div">
{isEditable ? (
<Resizeable width={width} height={height} onChange={onResize}>

View File

@ -1,6 +1,6 @@
.wrap {
display: inline-block;
vertical-align: middle;
margin-right: 4px;
font-size: 0;
cursor: pointer;
}

View File

@ -1,14 +1,19 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
import cls from 'classnames';
import styles from './index.module.scss';
export const StatusWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable;
const { color, text } = node.attrs;
const content = <Tag color={color}>{text || '点击设置状态'}</Tag>;
const content = (
<Tag className="render-wrapper" color={color}>
{text || '点击设置状态'}
</Tag>
);
return (
<NodeViewWrapper as="span" className={styles.wrap}>
<NodeViewWrapper as="span" className={cls(styles.wrap, 'status')}>
{isEditable ? (
<Popover
showArrow
@ -24,8 +29,7 @@ export const StatusWrapper = ({ editor, node, updateAttributes }) => {
key={color}
style={{ width: 24, height: 24, cursor: 'pointer' }}
type="solid"
// @ts-ignore
color={color}
color={color as unknown as any}
onClick={() => updateAttributes({ color })}
></Tag>
);

View File

@ -255,8 +255,7 @@
pre {
position: relative;
border-radius: var(--border-radius);
margin: 0.75rem 0px;
margin: 0;
counter-reset: line 0;
display: flex;
min-width: 48px;
@ -264,7 +263,6 @@
line-height: 1.3;
background-color: #0d0d0d;
background-color: var(--semi-color-fill-0);
border: 1px solid var(--semi-color-border);
code {
color: inherit;
@ -397,6 +395,82 @@
.search-result-current {
background: rgb(255, 0, 0);
}
/******* 选中样式 *******/
hr.selected-node {
background-color: rgb(0, 101, 255);
}
.node-status {
.semi-tag-default {
border: 1px solid var(--semi-color-border);
}
&.selected-node {
.semi-tag-default {
border: 1px solid rgb(0 101 255);
}
}
}
.node-codeBlock,
.node-documentChildren,
.node-documentReference,
.node-katex {
.render-wrapper {
border: 1px solid transparent;
}
}
.node-katex {
.render-wrapper {
border-radius: var(--border-radius);
}
}
.node-attachment,
.node-banner,
.node-iframe,
.node-image,
.node-katex,
.node-mind,
.node-codeBlock,
.node-documentChildren,
.node-documentReference {
&:not(.has-focus) {
::selection {
background-color: transparent;
}
}
// #e0ebfa
.render-wrapper {
position: relative;
user-select: text;
}
&.selected-node {
.render-wrapper {
border: 1px solid rgb(0 101 255);
background-color: var(--semi-color-info-light-hover);
}
}
}
.tableWrapper {
::selection {
background-color: transparent;
}
&.selected-node {
td,
th {
border-color: rgb(0 101 255);
background-color: var(--semi-color-info-light-hover);
}
}
}
/******* 选中样式 *******/
}
.resize-cursor {