mirror of https://github.com/fantasticit/think.git
feat: improve tiptap editor
This commit is contained in:
parent
2ad6518a51
commit
cd22f259fa
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -16,6 +16,7 @@ declare module '@tiptap/core' {
|
|||
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||
name: 'horizontalRule',
|
||||
group: 'block',
|
||||
selectable: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 }) => {
|
|
@ -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: (
|
||||
|
|
|
@ -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];
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
|
@ -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>
|
||||
);
|
|
@ -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
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
|
@ -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
|
|
@ -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>
|
|
@ -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} />
|
|
@ -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
|
|
@ -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 {
|
|
@ -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}>
|
|
@ -1,6 +1,6 @@
|
|||
.wrap {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
font-size: 0;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue