mirror of https://github.com/fantasticit/think.git
feat: improve editor
This commit is contained in:
parent
9ddab5a134
commit
3f981d44b0
|
@ -1,7 +1,7 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import cls from 'classnames';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { Layout, Nav, BackTop, Toast } from '@douyinfe/semi-ui';
|
||||
import { BackTop } from '@douyinfe/semi-ui';
|
||||
import { ILoginUser, IAuthority } from '@think/domains';
|
||||
import { useToggle } from 'hooks/useToggle';
|
||||
import {
|
||||
|
@ -18,8 +18,6 @@ import { DataRender } from 'components/data-render';
|
|||
import { joinUser } from 'components/document/collaboration';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
interface IProps {
|
||||
user: ILoginUser;
|
||||
documentId: string;
|
||||
|
@ -70,6 +68,8 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
toggleLoading(false);
|
||||
});
|
||||
|
||||
// provid
|
||||
|
||||
provider.on('status', async ({ status }) => {
|
||||
console.log('status', status);
|
||||
});
|
||||
|
|
|
@ -43,8 +43,6 @@ export const CreateUser: React.FC<{ document: IDocument; container: () => HTMLEl
|
|||
|
||||
const el = container && container();
|
||||
|
||||
console.log(el);
|
||||
|
||||
if (!el) return content;
|
||||
return createPortal(content, el);
|
||||
};
|
||||
|
|
|
@ -15,9 +15,6 @@ import { Emoji } from './extensions/emoji';
|
|||
import { EvokeMenu } from './extensions/evokeMenu';
|
||||
import { Focus } from './extensions/focus';
|
||||
import { FontSize } from './extensions/fontSize';
|
||||
import { FootnoteDefinition } from './extensions/footnoteDefinition';
|
||||
import { FootnoteReference } from './extensions/footnoteReference';
|
||||
import { FootnotesSection } from './extensions/footnotesSection';
|
||||
import { Gapcursor } from './extensions/gapCursor';
|
||||
import { HardBreak } from './extensions/hardBreak';
|
||||
import { Heading } from './extensions/heading';
|
||||
|
@ -34,7 +31,6 @@ import { Loading } from './extensions/loading';
|
|||
import { Mind } from './extensions/mind';
|
||||
import { OrderedList } from './extensions/orderedList';
|
||||
import { Paragraph } from './extensions/paragraph';
|
||||
import { Paste } from './extensions/paste';
|
||||
import { Placeholder } from './extensions/placeholder';
|
||||
import { SearchNReplace } from './extensions/search';
|
||||
import { Status } from './extensions/status';
|
||||
|
@ -51,6 +47,7 @@ import { TaskList } from './extensions/taskList';
|
|||
import { Title } from './extensions/title';
|
||||
import { TrailingNode } from './extensions/trailingNode';
|
||||
import { Underline } from './extensions/underline';
|
||||
import { Paste } from './extensions/paste';
|
||||
|
||||
export const BaseKit = [
|
||||
Attachment,
|
||||
|
@ -70,9 +67,6 @@ export const BaseKit = [
|
|||
EvokeMenu,
|
||||
Focus,
|
||||
FontSize,
|
||||
FootnoteDefinition,
|
||||
FootnoteReference,
|
||||
FootnotesSection,
|
||||
Gapcursor,
|
||||
HardBreak,
|
||||
Heading,
|
||||
|
@ -89,7 +83,6 @@ export const BaseKit = [
|
|||
Mind,
|
||||
OrderedList,
|
||||
Paragraph,
|
||||
Paste,
|
||||
Placeholder,
|
||||
SearchNReplace,
|
||||
Status,
|
||||
|
@ -106,4 +99,5 @@ export const BaseKit = [
|
|||
Title,
|
||||
TrailingNode,
|
||||
Underline,
|
||||
Paste,
|
||||
];
|
||||
|
|
|
@ -49,7 +49,7 @@ const getFileTypeIcon = (type: FileType) => {
|
|||
export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const $upload = useRef();
|
||||
const isEditable = editor.isEditable;
|
||||
const { autoTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs;
|
||||
const { hasTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs;
|
||||
const [loading, toggleLoading] = useToggle(false);
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
|
@ -81,11 +81,11 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
|||
const type = normalizeFileType(fileType);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url && !autoTrigger) {
|
||||
if (!url && !hasTrigger) {
|
||||
selectFile();
|
||||
updateAttributes({ autoTrigger: true });
|
||||
updateAttributes({ hasTrigger: true });
|
||||
}
|
||||
}, [url, autoTrigger]);
|
||||
}, [url, hasTrigger]);
|
||||
|
||||
const content = (() => {
|
||||
if (error) {
|
||||
|
|
|
@ -8,17 +8,31 @@ import { DataRender } from 'components/data-render';
|
|||
import { Empty } from 'components/empty';
|
||||
import { IconDocument } from 'components/icons';
|
||||
import styles from './index.module.scss';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentChildrenWrapper = ({ editor }) => {
|
||||
export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const isEditable = editor.isEditable;
|
||||
const { pathname, query } = useRouter();
|
||||
const wikiId = query?.wikiId;
|
||||
const documentId = query?.documentId;
|
||||
let { wikiId, documentId } = node.attrs;
|
||||
if (!wikiId) {
|
||||
query?.wikiId;
|
||||
}
|
||||
if (!documentId) {
|
||||
documentId = query?.documentId;
|
||||
}
|
||||
const isShare = pathname.includes('share');
|
||||
const { data: documents, loading, error } = useChildrenDocument({ wikiId, documentId, isShare });
|
||||
|
||||
useEffect(() => {
|
||||
const attrs = node.attrs;
|
||||
|
||||
if (attrs.wikiId !== wikiId || attrs.documentId !== documentId) {
|
||||
updateAttributes({ wikiId, documentId });
|
||||
}
|
||||
}, [node.attrs, wikiId, documentId]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}>
|
||||
<div>
|
||||
|
|
|
@ -7,6 +7,8 @@ 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 });
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ const { Text } = Typography;
|
|||
|
||||
export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const isEditable = editor.isEditable;
|
||||
const { autoTrigger, error, src, alt, title, width, height, textAlign } = node.attrs;
|
||||
const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs;
|
||||
const $upload = useRef();
|
||||
const [loading, toggleLoading] = useToggle(false);
|
||||
|
||||
|
@ -45,11 +45,11 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!src && !autoTrigger) {
|
||||
if (!src && !hasTrigger) {
|
||||
selectFile();
|
||||
updateAttributes({ autoTrigger: true });
|
||||
updateAttributes({ hasTrigger: true });
|
||||
}
|
||||
}, [src, autoTrigger]);
|
||||
}, [src, hasTrigger]);
|
||||
|
||||
const content = (() => {
|
||||
if (error) {
|
||||
|
|
|
@ -11,8 +11,6 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
|
|||
const isEditable = editor.isEditable;
|
||||
const { text } = node.attrs;
|
||||
|
||||
console.log(node.attrs);
|
||||
|
||||
const formatText = useMemo(() => {
|
||||
try {
|
||||
return katex.renderToString(`${text}`);
|
||||
|
@ -27,10 +25,6 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
|
|||
<span contentEditable={false}>点击输入公式</span>
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// updateAttributes(node.attrs);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="span" className={styles.wrap} contentEditable={false}>
|
||||
{isEditable ? (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { AttachmentWrapper } from '../components/attachment';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -38,27 +39,35 @@ export const Attachment = Node.create({
|
|||
return {
|
||||
fileName: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('filename'),
|
||||
},
|
||||
fileSize: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('filesize'),
|
||||
},
|
||||
fileType: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('filetype'),
|
||||
},
|
||||
fileExt: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('fileext'),
|
||||
},
|
||||
url: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('url'),
|
||||
},
|
||||
autoTrigger: {
|
||||
hasTrigger: {
|
||||
default: false,
|
||||
parseHTML: (element) => getDatasetAttribute('hastrigger')(element) === 'true',
|
||||
},
|
||||
error: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('error'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
addCommands() {
|
||||
return {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
|||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { BannerWrapper } from '../components/banner';
|
||||
import { typesAvailable } from '../services/markdown/markdownToHTML/markdownBanner';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -17,24 +18,15 @@ export const Banner = Node.create({
|
|||
group: 'block',
|
||||
defining: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: typesAvailable,
|
||||
HTMLAttributes: {
|
||||
class: 'banner',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: {
|
||||
default: 'info',
|
||||
rendered: false,
|
||||
parseHTML: (element) => element.getAttribute('data-banner'),
|
||||
parseHTML: getDatasetAttribute('info'),
|
||||
renderHTML: (attributes) => {
|
||||
return {
|
||||
'data-banner': attributes.type,
|
||||
'data-type': attributes.type,
|
||||
'class': `banner banner-${attributes.type}`,
|
||||
};
|
||||
},
|
||||
|
@ -42,6 +34,14 @@ export const Banner = Node.create({
|
|||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'banner',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
|
@ -50,16 +50,8 @@ export const Banner = Node.create({
|
|||
];
|
||||
},
|
||||
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
const { class: classy } = this.options.HTMLAttributes;
|
||||
|
||||
const attributes = {
|
||||
...this.options.HTMLAttributes,
|
||||
'data-callout': node.attrs.type,
|
||||
'class': `${classy} ${classy}-${node.attrs.type}`,
|
||||
};
|
||||
|
||||
return ['div', mergeAttributes(attributes, HTMLAttributes), 0];
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
||||
import { wrappingInputRule } from '@tiptap/core';
|
||||
import { getParents } from '../services/dom';
|
||||
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||
import { getMarkdownSource } from '../services/markdown';
|
||||
|
||||
export const Blockquote = BuiltInBlockquote.extend({
|
||||
addAttributes() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
||||
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||
import { getMarkdownSource } from '../services/markdown';
|
||||
import { listInputRule } from '../services/listInputRule';
|
||||
|
||||
export const BulletList = BuiltInBulletList.extend({
|
||||
|
|
|
@ -1,37 +1,324 @@
|
|||
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { lowlight } from 'lowlight/lib/all';
|
||||
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';
|
||||
|
||||
const extractLanguage = (element) => element.getAttribute('lang');
|
||||
export interface CodeBlockOptions {
|
||||
/**
|
||||
* Adds a prefix to language classes that are applied to code tags.
|
||||
* Defaults to `'language-'`.
|
||||
*/
|
||||
languageClassPrefix: string;
|
||||
/**
|
||||
* Define whether the node should be exited on triple enter.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
exitOnTripleEnter: boolean;
|
||||
/**
|
||||
* Define whether the node should be exited on arrow down if there is no node after it.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
exitOnArrowDown: boolean;
|
||||
/**
|
||||
* Custom HTML attributes that should be added to the rendered HTML tag.
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const CodeBlock = CodeBlockLowlight.extend({
|
||||
isolating: true,
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
codeBlock: {
|
||||
/**
|
||||
* Set a code block
|
||||
*/
|
||||
setCodeBlock: (attributes?: { language: string }) => ReturnType;
|
||||
/**
|
||||
* Toggle a code block
|
||||
*/
|
||||
toggleCodeBlock: (attributes?: { language: string }) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
|
||||
export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
|
||||
|
||||
export const BuiltInCodeBlock = Node.create<CodeBlockOptions>({
|
||||
name: 'codeBlock',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
languageClassPrefix: 'language-',
|
||||
exitOnTripleEnter: true,
|
||||
exitOnArrowDown: true,
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
content: 'text*',
|
||||
|
||||
marks: '',
|
||||
|
||||
group: 'block',
|
||||
|
||||
code: true,
|
||||
|
||||
defining: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
language: {
|
||||
default: null,
|
||||
parseHTML: (element) => extractLanguage(element),
|
||||
},
|
||||
class: {
|
||||
default: 'code highlight',
|
||||
parseHTML: (element) => {
|
||||
const { languageClassPrefix } = this.options;
|
||||
const classNames = Array.from(element.firstElementChild?.classList || element.classList || []);
|
||||
const languages = classNames
|
||||
.filter((className) => className.startsWith(languageClassPrefix))
|
||||
.map((className) => className.replace(languageClassPrefix, ''));
|
||||
const language = languages[0];
|
||||
|
||||
if (!language) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return language;
|
||||
},
|
||||
rendered: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
'pre',
|
||||
{
|
||||
...HTMLAttributes,
|
||||
class: `content-editor-code-block ${HTMLAttributes.class}`,
|
||||
tag: 'pre',
|
||||
preserveWhitespace: 'full',
|
||||
},
|
||||
['code', {}, 0],
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
return [
|
||||
'pre',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
[
|
||||
'code',
|
||||
{
|
||||
class: node.attrs.language ? this.options.languageClassPrefix + node.attrs.language : null,
|
||||
},
|
||||
0,
|
||||
],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name, attributes);
|
||||
},
|
||||
toggleCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, 'paragraph', attributes);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock(),
|
||||
|
||||
// remove code block when at start of document or code block is empty
|
||||
'Backspace': () => {
|
||||
const { empty, $anchor } = this.editor.state.selection;
|
||||
const isAtStart = $anchor.pos === 1;
|
||||
|
||||
if (!empty || $anchor.parent.type.name !== this.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAtStart || !$anchor.parent.textContent.length) {
|
||||
return this.editor.commands.clearNodes();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// exit node on triple enter
|
||||
'Enter': ({ editor }) => {
|
||||
if (!this.options.exitOnTripleEnter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
if (!empty || $from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||
const endsWithDoubleNewline = $from.parent.textContent.endsWith('\n\n');
|
||||
|
||||
if (!isAtEnd || !endsWithDoubleNewline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor
|
||||
.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.delete($from.pos - 2, $from.pos);
|
||||
|
||||
return true;
|
||||
})
|
||||
.exitCode()
|
||||
.run();
|
||||
},
|
||||
|
||||
// exit node on arrow down
|
||||
'ArrowDown': ({ editor }) => {
|
||||
if (!this.options.exitOnArrowDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state } = editor;
|
||||
const { selection, doc } = state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
if (!empty || $from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||
|
||||
if (!isAtEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const after = $from.after();
|
||||
|
||||
if (after === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeAfter = doc.nodeAt(after);
|
||||
|
||||
if (nodeAfter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.exitCode();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textblockTypeInputRule({
|
||||
find: backtickInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language: match[1],
|
||||
}),
|
||||
}),
|
||||
textblockTypeInputRule({
|
||||
find: tildeInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language: match[1],
|
||||
}),
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
// this plugin creates a code block for pasted content from VS Code
|
||||
// we can also detect the copied code language
|
||||
new Plugin({
|
||||
key: new PluginKey('codeBlockVSCodeHandler'),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (!event.clipboardData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// don’t create a new code block within code blocks
|
||||
if (this.editor.isActive(this.type.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||
const vscodeData = vscode ? JSON.parse(vscode) : undefined;
|
||||
const language = vscodeData?.mode;
|
||||
|
||||
if (!text || !language) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { tr } = view.state;
|
||||
|
||||
// create an empty code block
|
||||
tr.replaceSelectionWith(this.type.create({ language }));
|
||||
|
||||
// put cursor inside the newly created code block
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, tr.selection.from - 2))));
|
||||
|
||||
// add text to code block
|
||||
// strip carriage return chars from text pasted as code
|
||||
// see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd
|
||||
tr.insertText(text.replace(/\r\n?/g, '\n'));
|
||||
|
||||
// store meta information
|
||||
// this is useful for other plugins that depends on the paste event
|
||||
// like the paste rule plugin
|
||||
tr.setMeta('paste', true);
|
||||
|
||||
view.dispatch(tr);
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export interface CodeBlockLowlightOptions extends CodeBlockOptions {
|
||||
lowlight: any;
|
||||
defaultLanguage: string | null | undefined;
|
||||
}
|
||||
|
||||
export const CodeBlock = BuiltInCodeBlock.extend<CodeBlockLowlightOptions>({
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
lowlight,
|
||||
defaultLanguage: null,
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
...(this.parent?.() || []),
|
||||
LowlightPlugin({
|
||||
name: this.name,
|
||||
lowlight: this.options.lowlight,
|
||||
defaultLanguage: this.options.defaultLanguage,
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CodeBlockWrapper);
|
||||
},
|
||||
}).configure({
|
||||
lowlight,
|
||||
defaultLanguage: 'auto',
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { DocumentChildrenWrapper } from '../components/documentChildren';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -21,23 +22,35 @@ export const DocumentChildren = Node.create({
|
|||
|
||||
addAttributes() {
|
||||
return {
|
||||
color: {
|
||||
default: 'grey',
|
||||
},
|
||||
text: {
|
||||
wikiId: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('wikiId'),
|
||||
},
|
||||
documentId: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('documentId'),
|
||||
},
|
||||
};
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'documentChildren',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'div[data-type=documentChildren]' }];
|
||||
return [
|
||||
{
|
||||
tag: 'div',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
addCommands() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { DocumentReferenceWrapper } from '../components/documentReference';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -23,22 +24,37 @@ export const DocumentReference = Node.create({
|
|||
return {
|
||||
wikiId: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('wikiId'),
|
||||
},
|
||||
documentId: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('documentId'),
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('title'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'documentReference',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'div[data-type=documentReference]' }];
|
||||
return [
|
||||
{
|
||||
tag: 'div',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||
|
||||
export const FootnoteDefinition = Node.create({
|
||||
name: 'footnoteDefinition',
|
||||
|
||||
content: 'paragraph',
|
||||
|
||||
group: 'block',
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{ tag: 'section.footnotes li' },
|
||||
{ tag: '.footnote-backref', priority: PARSE_HTML_PRIORITY_HIGHEST, ignore: true },
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['li', mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||
|
||||
export const FootnoteReference = Node.create({
|
||||
name: 'footnoteReference',
|
||||
|
||||
inline: true,
|
||||
|
||||
group: 'inline',
|
||||
|
||||
atom: true,
|
||||
|
||||
draggable: true,
|
||||
|
||||
selectable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
footnoteId: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.querySelector('a').getAttribute('id'),
|
||||
},
|
||||
footnoteNumber: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.textContent,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
|
||||
return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
|
||||
},
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
|
||||
export const FootnotesSection = Node.create({
|
||||
name: 'footnotesSection',
|
||||
|
||||
content: 'footnoteDefinition+',
|
||||
|
||||
group: 'block',
|
||||
|
||||
isolating: true,
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'section.footnotes > ol' }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['ol', mergeAttributes(HTMLAttributes, { class: 'footnotes gl-font-sm' }), 0];
|
||||
},
|
||||
});
|
|
@ -2,24 +2,7 @@ import { Mark, mergeAttributes, markInputRule } from '@tiptap/core';
|
|||
import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
|
||||
import { markInputRegex, extractMarkAttributesFromMatch } from '../services/markUtils';
|
||||
|
||||
const marks = [
|
||||
'ins',
|
||||
'abbr',
|
||||
'bdo',
|
||||
'cite',
|
||||
'dfn',
|
||||
'mark',
|
||||
'small',
|
||||
'span',
|
||||
'time',
|
||||
'kbd',
|
||||
'q',
|
||||
'samp',
|
||||
'var',
|
||||
'ruby',
|
||||
'rp',
|
||||
'rt',
|
||||
];
|
||||
export const marks = [{ name: 'underline', tag: 'u' }];
|
||||
|
||||
const attrs = {
|
||||
time: ['datetime'],
|
||||
|
@ -28,9 +11,10 @@ const attrs = {
|
|||
bdo: ['dir'],
|
||||
};
|
||||
|
||||
export const HTMLMarks = marks.map((name) =>
|
||||
export const HTMLMarks = marks.map(({ name, tag }) =>
|
||||
Mark.create({
|
||||
name,
|
||||
tag,
|
||||
inclusive: false,
|
||||
addOptions() {
|
||||
return {
|
||||
|
@ -51,17 +35,17 @@ export const HTMLMarks = marks.map((name) =>
|
|||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
|
||||
return [{ tag: tag, priority: PARSE_HTML_PRIORITY_LOWEST }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
return [tag, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule({
|
||||
find: markInputRegex(name),
|
||||
find: markInputRegex(tag),
|
||||
type: this.type,
|
||||
getAttributes: extractMarkAttributesFromMatch,
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { IframeWrapper } from '../components/iframe';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -11,7 +12,7 @@ declare module '@tiptap/core' {
|
|||
}
|
||||
|
||||
export const Iframe = Node.create({
|
||||
name: 'external-iframe',
|
||||
name: 'iframe',
|
||||
content: '',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
|
@ -21,7 +22,7 @@ export const Iframe = Node.create({
|
|||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
'data-type': 'external-iframe',
|
||||
class: 'iframe',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -30,12 +31,15 @@ export const Iframe = Node.create({
|
|||
return {
|
||||
width: {
|
||||
default: '100%',
|
||||
parseHTML: getDatasetAttribute('width'),
|
||||
},
|
||||
height: {
|
||||
default: 54,
|
||||
default: 200,
|
||||
parseHTML: getDatasetAttribute('height'),
|
||||
},
|
||||
url: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('url'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -43,7 +47,7 @@ export const Iframe = Node.create({
|
|||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe[data-type="external-iframe"]',
|
||||
tag: 'iframe',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
|
@ -51,7 +51,7 @@ export const Image = BuiltInImage.extend({
|
|||
height: {
|
||||
default: 'auto',
|
||||
},
|
||||
autoTrigger: {
|
||||
hasTrigger: {
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { MindWrapper } from '../components/mind';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
const DEFAULT_MIND_DATA = {
|
||||
meta: {
|
||||
|
@ -21,31 +23,34 @@ declare module '@tiptap/core' {
|
|||
}
|
||||
|
||||
export const Mind = Node.create({
|
||||
name: 'jsmind',
|
||||
name: 'mind',
|
||||
content: '',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
'data-type': 'jsmind',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
width: {
|
||||
default: '100%',
|
||||
parseHTML: getDatasetAttribute('width'),
|
||||
},
|
||||
height: {
|
||||
default: 240,
|
||||
parseHTML: getDatasetAttribute('height'),
|
||||
},
|
||||
data: {
|
||||
default: DEFAULT_MIND_DATA,
|
||||
parseHTML: getDatasetAttribute('data', true),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'mind',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -53,7 +58,7 @@ export const Mind = Node.create({
|
|||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[data-type="jsmind"]',
|
||||
tag: 'div',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
||||
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||
import { getMarkdownSource } from '../services/markdown';
|
||||
|
||||
export const OrderedList = BuiltInOrderedList.extend({
|
||||
addAttributes() {
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { markdownSerializer } from '../services/markdown/serializer';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
||||
import { handleFileEvent } from '../services/upload';
|
||||
import { isInCode, LANGUAGES } from '../services/code';
|
||||
import { isMarkdown, normalizePastedMarkdown } from '../services/markdown/helpers';
|
||||
import {
|
||||
isMarkdown,
|
||||
normalizePastedMarkdown,
|
||||
markdownToProsemirror,
|
||||
prosemirrorToMarkdown,
|
||||
} from '../services/markdown';
|
||||
import { isTitleNode } from '../services/node';
|
||||
|
||||
export const Paste = Extension.create({
|
||||
|
@ -40,7 +44,7 @@ export const Paste = Extension.create({
|
|||
// 粘贴代码
|
||||
if (isInCode(view.state)) {
|
||||
event.preventDefault();
|
||||
view.dispatch(view.state.tr.insertText(text));
|
||||
view.dispatch(view.state.tr.insertText(text).scrollIntoView());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -56,7 +60,7 @@ export const Paste = Extension.create({
|
|||
})
|
||||
)
|
||||
);
|
||||
view.dispatch(view.state.tr.insertText(text));
|
||||
view.dispatch(view.state.tr.insertText(text).scrollIntoView());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -66,14 +70,19 @@ export const Paste = Extension.create({
|
|||
const firstNode = view.props.state.doc.content.firstChild;
|
||||
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
||||
const schema = view.props.state.schema;
|
||||
const doc = markdownSerializer.markdownToProsemirror({
|
||||
const doc = markdownToProsemirror({
|
||||
schema,
|
||||
content: normalizePastedMarkdown(text),
|
||||
hasTitle,
|
||||
});
|
||||
// @ts-ignore
|
||||
const transaction = view.state.tr.insert(view.state.selection.head, view.state.schema.nodeFromJSON(doc));
|
||||
view.dispatch(transaction);
|
||||
let tr = view.state.tr;
|
||||
const selection = tr.selection;
|
||||
view.state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
|
||||
const startPosition = hasTitle ? Math.min(position, selection.from) : 0;
|
||||
const endPosition = Math.min(position + node.nodeSize, selection.to);
|
||||
tr = tr.replaceWith(startPosition, endPosition, view.state.schema.nodeFromJSON(doc));
|
||||
});
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -111,8 +120,8 @@ export const Paste = Extension.create({
|
|||
if (!doc) {
|
||||
return '';
|
||||
}
|
||||
const content = markdownSerializer.proseMirrorToMarkdown({
|
||||
schema: this.editor.schema,
|
||||
|
||||
const content = prosemirrorToMarkdown({
|
||||
content: doc,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { StatusWrapper } from '../components/status';
|
||||
import { getDatasetAttribute } from '../services/dataset';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -20,19 +21,33 @@ export const Status = Node.create({
|
|||
return {
|
||||
color: {
|
||||
default: 'grey',
|
||||
parseHTML: getDatasetAttribute('color'),
|
||||
},
|
||||
text: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('text'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'status',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'span[data-type=status]' }];
|
||||
return [
|
||||
{
|
||||
tag: 'div',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['span', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,5 +1,56 @@
|
|||
import { mergeAttributes } from '@tiptap/core';
|
||||
import { Table as BuiltInTable } from '@tiptap/extension-table';
|
||||
|
||||
export const Table = BuiltInTable.configure({
|
||||
resizable: false,
|
||||
export const Table = BuiltInTable.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
style: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
|
||||
try {
|
||||
// use first row to determine width of table;
|
||||
// @ts-ignore
|
||||
const tr = node.content.content[0];
|
||||
tr.content.content.forEach((td) => {
|
||||
if (td.attrs.colwidth) {
|
||||
td.attrs.colwidth.forEach((col) => {
|
||||
if (!col) {
|
||||
fixedWidth = false;
|
||||
totalWidth += this.options.cellMinWidth;
|
||||
} else {
|
||||
totalWidth += col;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
const colspan = td.attrs.colspan ? td.attrs.colspan : 1;
|
||||
totalWidth += this.options.cellMinWidth * colspan;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
fixedWidth = false;
|
||||
}
|
||||
|
||||
if (fixedWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `width: ${totalWidth}px;`;
|
||||
} else if (totalWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `min-width: ${totalWidth}px`;
|
||||
} else {
|
||||
HTMLAttributes.style = null;
|
||||
}
|
||||
|
||||
return [
|
||||
'div',
|
||||
{ class: 'tableWrapper' },
|
||||
['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]],
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
resizable: true,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import ReactDOM from 'react-dom';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { mergeAttributes } from '@tiptap/core';
|
||||
import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
|
@ -16,6 +17,56 @@ import {
|
|||
import { FloatMenuView } from '../views/floatMenuView';
|
||||
|
||||
export const TableCell = BuiltInTableCell.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
colspan: {
|
||||
default: 1,
|
||||
},
|
||||
rowspan: {
|
||||
default: 1,
|
||||
},
|
||||
colwidth: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const colwidth = element.getAttribute('colwidth');
|
||||
const value = colwidth ? colwidth.split(',').map((item) => parseInt(item, 10)) : null;
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
style: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
|
||||
if (HTMLAttributes.colwidth) {
|
||||
HTMLAttributes.colwidth.forEach((col) => {
|
||||
if (!col) {
|
||||
fixedWidth = false;
|
||||
} else {
|
||||
totalWidth += col;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
}
|
||||
|
||||
if (fixedWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `width: ${totalWidth}px;`;
|
||||
} else if (totalWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `min-width: ${totalWidth}px`;
|
||||
} else {
|
||||
HTMLAttributes.style = null;
|
||||
}
|
||||
|
||||
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const extensionThis = this;
|
||||
let selectedRowIndex = -1;
|
||||
|
|
|
@ -8,7 +8,62 @@ import { Decoration, DecorationSet } from 'prosemirror-view';
|
|||
import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from '../services/table';
|
||||
import { FloatMenuView } from '../views/floatMenuView';
|
||||
|
||||
// @flow
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { mergeAttributes } from '@tiptap/core';
|
||||
// import TableHeader from "@tiptap/extension-table-header";
|
||||
|
||||
export const TableHeader = BuiltInTableHeader.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
colspan: {
|
||||
default: 1,
|
||||
},
|
||||
rowspan: {
|
||||
default: 1,
|
||||
},
|
||||
colwidth: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const colwidth = element.getAttribute('colwidth');
|
||||
const value = colwidth ? colwidth.split(',').map((item) => parseInt(item, 10)) : null;
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
style: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
|
||||
if (HTMLAttributes.colwidth) {
|
||||
HTMLAttributes.colwidth.forEach((col) => {
|
||||
if (!col) {
|
||||
fixedWidth = false;
|
||||
} else {
|
||||
totalWidth += col;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
}
|
||||
|
||||
if (fixedWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `width: ${totalWidth}px;`;
|
||||
} else if (totalWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `min-width: ${totalWidth}px`;
|
||||
} else {
|
||||
HTMLAttributes.style = null;
|
||||
}
|
||||
|
||||
return ['th', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const extensionThis = this;
|
||||
|
||||
|
@ -21,7 +76,7 @@ export const TableHeader = BuiltInTableHeader.extend({
|
|||
tippyOptions: {
|
||||
zIndex: 100,
|
||||
},
|
||||
shouldShow: ({ editor }) => {
|
||||
shouldShow: ({ editor }, floatMenuView) => {
|
||||
if (!editor.isEditable) {
|
||||
return false;
|
||||
}
|
||||
|
@ -30,6 +85,12 @@ export const TableHeader = BuiltInTableHeader.extend({
|
|||
return false;
|
||||
}
|
||||
const cells = getCellsInRow(0)(selection);
|
||||
|
||||
if (cells && cells[0]) {
|
||||
const node = editor.view.nodeDOM(cells[0].pos) as HTMLElement;
|
||||
floatMenuView.setConatiner(node.parentElement.parentElement.parentElement.parentElement);
|
||||
}
|
||||
|
||||
return !!cells?.some((cell, index) => isColumnSelected(index)(selection));
|
||||
},
|
||||
init: (dom, editor) => {
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
|
||||
export const Title = Node.create({
|
||||
export interface TitleOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
title: {
|
||||
setTitle: (attributes) => ReturnType;
|
||||
toggleTitle: (attributes) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Title = Node.create<TitleOptions>({
|
||||
name: 'title',
|
||||
content: 'text*',
|
||||
selectable: true,
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
inline: false,
|
||||
group: 'basic',
|
||||
allowGapCursor: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
@ -16,16 +26,15 @@ export const Title = Node.create({
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'h1[class=title]',
|
||||
tag: 'p[class=title]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['h1', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Link } from '../extensions/link';
|
|||
import { Attachment } from '../extensions/attachment';
|
||||
import { Image } from '../extensions/image';
|
||||
import { Banner } from '../extensions/banner';
|
||||
import { Status } from '../extensions/status';
|
||||
import { HorizontalRule } from '../extensions/horizontalRule';
|
||||
import { Iframe } from '../extensions/iframe';
|
||||
import { Mind } from '../extensions/mind';
|
||||
|
@ -21,6 +22,7 @@ const OTHER_BUBBLE_MENU_TYPES = [
|
|||
Attachment.name,
|
||||
Image.name,
|
||||
Banner.name,
|
||||
Status.name,
|
||||
Iframe.name,
|
||||
Mind.name,
|
||||
Table.name,
|
||||
|
|
|
@ -21,7 +21,7 @@ export const Paragraph = ({ editor }) => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
console.log(getCurrentCaretTitle(editor));
|
||||
// console.log(getCurrentCaretTitle(editor));
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
|
|
@ -25,7 +25,9 @@ export const TableBubbleMenu = ({ editor }) => {
|
|||
tippyOptions={{
|
||||
maxWidth: 456,
|
||||
}}
|
||||
matchRenderContainer={(node: HTMLElement) => node.classList && node.tagName === 'TABLE'}
|
||||
matchRenderContainer={(node: HTMLElement) =>
|
||||
node && node.classList && node.classList.contains('tableWrapper') && node.tagName === 'DIV'
|
||||
}
|
||||
>
|
||||
<Space>
|
||||
<Tooltip content="向前插入一列">
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { safeJSONParse } from 'helpers/json';
|
||||
|
||||
/**
|
||||
* 将 JSON 转为字符串
|
||||
* @param json
|
||||
*/
|
||||
export const jsonToStr = (json: Record<string, unknown>) => {
|
||||
try {
|
||||
return JSON.stringify(json);
|
||||
} catch (e) {
|
||||
return JSON.stringify({});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将字符串转为 JSON
|
||||
* @param str
|
||||
*/
|
||||
export const strToJSON = (str: string) => {
|
||||
return safeJSONParse(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 JSON 转为 DOM 节点的 dataset
|
||||
* @param element
|
||||
* @param json
|
||||
*/
|
||||
export const jsonToDOMDataset = (json: Record<string, unknown>) => {
|
||||
return Object.keys(json).map((key) => {
|
||||
let value = json[key];
|
||||
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
return {
|
||||
key: `data-${key}`,
|
||||
value: encodeURIComponent(value as string),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 element 上提取 dataset 数据
|
||||
* @param element
|
||||
* @param attribute
|
||||
* @param transformToJSON 是否要转为 JSON
|
||||
*/
|
||||
export const getDatasetAttribute =
|
||||
(attribute: string, transformToJSON: boolean = false) =>
|
||||
(element: HTMLElement) => {
|
||||
const dataKey = attribute.startsWith('data-') ? attribute : `data-${attribute}`;
|
||||
const value = decodeURIComponent(element.getAttribute(dataKey));
|
||||
|
||||
if (transformToJSON) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (value.includes('%') || value.includes('auto')) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const toNumber = parseInt(value);
|
||||
return toNumber !== toNumber ? value : toNumber; // 避免 NaN
|
||||
};
|
|
@ -0,0 +1,139 @@
|
|||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
||||
import { findChildren } from '@tiptap/core';
|
||||
|
||||
function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const classes = [...className, ...(node.properties ? node.properties.className : [])];
|
||||
|
||||
if (node.children) {
|
||||
return parseNodes(node.children, classes);
|
||||
}
|
||||
|
||||
return {
|
||||
text: node.value,
|
||||
classes,
|
||||
};
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function getHighlightNodes(result: any) {
|
||||
// `.value` for lowlight v1, `.children` for lowlight v2
|
||||
return result.value || result.children || [];
|
||||
}
|
||||
|
||||
function getDecorations({
|
||||
doc,
|
||||
name,
|
||||
lowlight,
|
||||
defaultLanguage,
|
||||
}: {
|
||||
doc: ProsemirrorNode;
|
||||
name: string;
|
||||
lowlight: any;
|
||||
defaultLanguage: string | null | undefined;
|
||||
}) {
|
||||
const decorations: Decoration[] = [];
|
||||
|
||||
findChildren(doc, (node) => node.type.name === name).forEach((block) => {
|
||||
let from = block.pos + 1;
|
||||
const language = block.node.attrs.language || defaultLanguage;
|
||||
const languages = lowlight.listLanguages();
|
||||
const nodes =
|
||||
language && languages.includes(language)
|
||||
? getHighlightNodes(lowlight.highlight(language, block.node.textContent))
|
||||
: getHighlightNodes(lowlight.highlightAuto(block.node.textContent));
|
||||
|
||||
parseNodes(nodes).forEach((node) => {
|
||||
const to = from + node.text.length;
|
||||
|
||||
if (node.classes.length) {
|
||||
const decoration = Decoration.inline(from, to, {
|
||||
class: node.classes.join(' '),
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
from = to;
|
||||
});
|
||||
});
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
}
|
||||
|
||||
export function LowlightPlugin({
|
||||
name,
|
||||
lowlight,
|
||||
defaultLanguage,
|
||||
}: {
|
||||
name: string;
|
||||
lowlight: any;
|
||||
defaultLanguage: string | null | undefined;
|
||||
}) {
|
||||
return new Plugin({
|
||||
key: new PluginKey('lowlight'),
|
||||
|
||||
state: {
|
||||
init: (_, { doc }) =>
|
||||
getDecorations({
|
||||
doc,
|
||||
name,
|
||||
lowlight,
|
||||
defaultLanguage,
|
||||
}),
|
||||
apply: (transaction, decorationSet, oldState, newState) => {
|
||||
const oldNodeName = oldState.selection.$head.parent.type.name;
|
||||
const newNodeName = newState.selection.$head.parent.type.name;
|
||||
const oldNodes = findChildren(oldState.doc, (node) => node.type.name === name);
|
||||
const newNodes = findChildren(newState.doc, (node) => node.type.name === name);
|
||||
|
||||
if (
|
||||
transaction.docChanged &&
|
||||
// Apply decorations if:
|
||||
// selection includes named node,
|
||||
([oldNodeName, newNodeName].includes(name) ||
|
||||
// OR transaction adds/removes named node,
|
||||
newNodes.length !== oldNodes.length ||
|
||||
// OR transaction has changes that completely encapsulte a node
|
||||
// (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 (
|
||||
step.from !== undefined &&
|
||||
// @ts-ignore
|
||||
step.to !== undefined &&
|
||||
oldNodes.some((node) => {
|
||||
// @ts-ignore
|
||||
return (
|
||||
node.pos >= step.from &&
|
||||
// @ts-ignore
|
||||
node.pos + node.node.nodeSize <= step.to
|
||||
);
|
||||
})
|
||||
);
|
||||
}))
|
||||
) {
|
||||
return getDecorations({
|
||||
doc: transaction.doc,
|
||||
name,
|
||||
lowlight,
|
||||
defaultLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
return decorationSet.map(transaction.mapping, transaction.doc);
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
export const isMarkdown = (text: string): boolean => {
|
||||
// html
|
||||
const html = text.match(/<\/?[a-z][\s\S]*>/i);
|
||||
if (html && html.length) return true;
|
||||
|
||||
// table
|
||||
const tables = text.match(/^\|(\S)*\|/gm);
|
||||
if (tables && tables.length) return true;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
> 将 HTML 转换成 prosemirror node
|
|
@ -2,6 +2,52 @@ import { Renderer } from './renderer';
|
|||
|
||||
const renderer = new Renderer();
|
||||
|
||||
export const htmlToPromsemirror = (body) => {
|
||||
return renderer.render(body);
|
||||
/**
|
||||
* 将 HTML 转换成 prosemirror node
|
||||
* @param body
|
||||
* @param forceATitle 是否需要一个标题
|
||||
* @returns
|
||||
*/
|
||||
export const htmlToPromsemirror = (body, forceATitle = false) => {
|
||||
const json = renderer.render(body);
|
||||
|
||||
// 设置标题
|
||||
if (forceATitle) {
|
||||
const firstNode = json.content[0];
|
||||
if (firstNode && firstNode.type !== 'title') {
|
||||
if (firstNode.type === 'heading' || firstNode.type === 'paragraph') {
|
||||
firstNode.type = 'title';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = json.content;
|
||||
const result = { type: 'doc', content: [] };
|
||||
|
||||
for (let i = 0; i < nodes.length; ) {
|
||||
const node = nodes[i];
|
||||
// 目的:合并成 promirror 需要的 table 格式
|
||||
if (node.type === 'tableRow') {
|
||||
const nextNode = nodes[i + 1];
|
||||
if (nextNode && nextNode.type === 'table') {
|
||||
nextNode.content.unshift(node);
|
||||
result.content.push(nextNode);
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
result.content.push(node);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// trailing node
|
||||
result.content.push({
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
indent: 0,
|
||||
textAlign: 'left',
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,6 @@ export class Mark {
|
|||
}
|
||||
|
||||
data() {
|
||||
return [];
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Mark } from './mark';
|
||||
|
||||
export class Underline extends Mark {
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'U';
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
type: 'underline',
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Attachment extends Node {
|
||||
type = 'attachment';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('attachment');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Banner extends Node {
|
||||
type = 'banner';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('banner');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class DocumentChildren extends Node {
|
||||
type = 'documentChildren';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('documentChildren');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class DocumentReference extends Node {
|
||||
type = 'documentReference';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('documentReference');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Iframe extends Node {
|
||||
type = 'iframe';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('iframe');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Mind extends Node {
|
||||
type = 'mind';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('mind');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Status extends Node {
|
||||
type = 'status';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('status');
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ export class Text extends Node {
|
|||
|
||||
return {
|
||||
type: 'text',
|
||||
text,
|
||||
text: text.trim() || '\n',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { Node } from './node';
|
||||
|
||||
export class Title extends Node {
|
||||
type = 'title';
|
||||
|
||||
matching() {
|
||||
return this.DOMNode.nodeName === 'P' && this.DOMNode.classList.contains('title');
|
||||
}
|
||||
}
|
|
@ -1,4 +1,12 @@
|
|||
// nodes
|
||||
// 自定义节点
|
||||
import { Iframe } from './nodes/iframe';
|
||||
import { Attachment } from './nodes/attachment';
|
||||
import { Banner } from './nodes/banner';
|
||||
import { Status } from './nodes/status';
|
||||
import { DocumentReference } from './nodes/documentReference';
|
||||
import { DocumentChildren } from './nodes/documentChildren';
|
||||
import { Mind } from './nodes/mind';
|
||||
// 通用
|
||||
import { CodeBlock } from './nodes/codeBlock';
|
||||
import { CodeBlockWrapper } from './nodes/codeBlockWrapper';
|
||||
import { HardBreak } from './nodes/hardBreak';
|
||||
|
@ -6,18 +14,16 @@ import { Heading } from './nodes/heading';
|
|||
import { Image } from './nodes/image';
|
||||
import { HorizontalRule } from './nodes/horizontalRule';
|
||||
import { Blockquote } from './nodes/blockQuote';
|
||||
|
||||
// 文本
|
||||
import { Title } from './nodes/title';
|
||||
import { Katex } from './nodes/katex';
|
||||
import { Paragraph } from './nodes/paragraph';
|
||||
import { Text } from './nodes/text';
|
||||
|
||||
// 表格
|
||||
import { Table } from './nodes/table';
|
||||
import { TableHeader } from './nodes/tableHeader';
|
||||
import { TableRow } from './nodes/tableRow';
|
||||
import { TableCell } from './nodes/tableCell';
|
||||
|
||||
// 列表
|
||||
import { TaskList } from './nodes/taskList';
|
||||
import { TaskListItem } from './nodes/taskListItem';
|
||||
|
@ -30,6 +36,7 @@ import { Bold } from './marks/bold';
|
|||
import { Code } from './marks/code';
|
||||
import { Italic } from './marks/italic';
|
||||
import { Link } from './marks/link';
|
||||
import { Underline } from './marks/underline';
|
||||
|
||||
export class Renderer {
|
||||
document: HTMLElement;
|
||||
|
@ -42,6 +49,14 @@ export class Renderer {
|
|||
this.storedMarks = [];
|
||||
|
||||
this.nodes = [
|
||||
Attachment,
|
||||
Banner,
|
||||
Iframe,
|
||||
Status,
|
||||
Mind,
|
||||
DocumentChildren,
|
||||
DocumentReference,
|
||||
|
||||
CodeBlock,
|
||||
CodeBlockWrapper,
|
||||
HardBreak,
|
||||
|
@ -49,6 +64,7 @@ export class Renderer {
|
|||
Image,
|
||||
HorizontalRule,
|
||||
|
||||
Title,
|
||||
Katex,
|
||||
Paragraph,
|
||||
|
||||
|
@ -68,7 +84,7 @@ export class Renderer {
|
|||
BulletList,
|
||||
];
|
||||
|
||||
this.marks = [Bold, Code, Italic, Link];
|
||||
this.marks = [Bold, Code, Italic, Link, Underline];
|
||||
}
|
||||
|
||||
setDocument(document) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { htmlToPromsemirror } from './htmlToProsemirror';
|
||||
import { markdownToHTML } from './markdownToHTML';
|
||||
export { prosemirrorToMarkdown } from './prosemirrorToMarkdown';
|
||||
export * from './helpers';
|
||||
export * from './markdownSourceMap';
|
||||
|
||||
// 将 markdown 字符串转换为 ProseMirror JSONDocument
|
||||
export const markdownToProsemirror = ({ schema, content, hasTitle }) => {
|
||||
const html = markdownToHTML(content);
|
||||
|
||||
if (!html) return null;
|
||||
|
||||
console.log(html);
|
||||
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(html, 'text/html');
|
||||
body.append(document.createComment(content));
|
||||
return htmlToPromsemirror(body, !hasTitle);
|
||||
};
|
|
@ -10,6 +10,14 @@ import splitMixedLists from './markedownSplitMixedList';
|
|||
import markdownUnderline from './markdownUnderline';
|
||||
import markdownBanner from './markdownBanner';
|
||||
import { markdownItTable } from './markdownTable';
|
||||
import { createMarkdownContainer } from './markdownItContainer';
|
||||
|
||||
const markdownAttachment = createMarkdownContainer('attachment');
|
||||
const markdownIframe = createMarkdownContainer('iframe');
|
||||
const markdownStatus = createMarkdownContainer('status');
|
||||
const markdownMind = createMarkdownContainer('mind');
|
||||
const markdownDocumentReference = createMarkdownContainer('documentReference');
|
||||
const markdownDocumentChildren = createMarkdownContainer('documentChildren');
|
||||
|
||||
const markdown = markdownit('commonmark')
|
||||
.enable('strikethrough')
|
||||
|
@ -19,10 +27,17 @@ const markdown = markdownit('commonmark')
|
|||
.use(tasklist)
|
||||
.use(splitMixedLists)
|
||||
.use(markdownUnderline)
|
||||
.use(markdownBanner)
|
||||
.use(markdownItTable)
|
||||
.use(emoji)
|
||||
.use(katex);
|
||||
.use(katex)
|
||||
// 以下为自定义节点
|
||||
.use(markdownBanner)
|
||||
.use(markdownAttachment)
|
||||
.use(markdownIframe)
|
||||
.use(markdownStatus)
|
||||
.use(markdownMind)
|
||||
.use(markdownDocumentReference)
|
||||
.use(markdownDocumentChildren);
|
||||
|
||||
export const markdownToHTML = (rawMarkdown) => {
|
||||
return sanitize(markdown.render(rawMarkdown), {});
|
||||
|
|
|
@ -5,9 +5,8 @@ export const typesAvailable = ['info', 'warning', 'danger', 'success'];
|
|||
const buildRender = (type) => (tokens, idx, options, env, slf) => {
|
||||
const tag = tokens[idx];
|
||||
|
||||
// add attributes to the opening tag
|
||||
if (tag.nesting === 1) {
|
||||
tag.attrSet('data-banner', type);
|
||||
tag.attrSet('data-type', type);
|
||||
tag.attrJoin('class', `banner banner-${type}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import container from 'markdown-it-container';
|
||||
import { strToJSON, jsonToDOMDataset } from '../../dataset';
|
||||
|
||||
export const createMarkdownContainer = (types: string | Array<string>) => (md) => {
|
||||
if (!Array.isArray(types)) {
|
||||
types = [types];
|
||||
}
|
||||
|
||||
types.forEach((type) => {
|
||||
const regexp = new RegExp(`^${type}\\s+(.*)$`);
|
||||
|
||||
md.use(container, type, {
|
||||
validate: function (params) {
|
||||
return params.trim().match(regexp);
|
||||
},
|
||||
|
||||
render: function (tokens, idx, options, env, slf) {
|
||||
const tag = tokens[idx];
|
||||
|
||||
if (tag.nesting === 1) {
|
||||
tag.attrSet('class', type);
|
||||
|
||||
var m = tag.info.trim().match(regexp);
|
||||
if (m[1]) {
|
||||
const data = strToJSON(m[1]);
|
||||
jsonToDOMDataset(data).forEach(({ key, value }) => {
|
||||
tag.attrJoin(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return slf.renderToken(tokens, idx, options, env, slf);
|
||||
},
|
||||
});
|
||||
});
|
||||
return md;
|
||||
};
|
|
@ -0,0 +1,160 @@
|
|||
import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
import { Attachment } from '../../../extensions/attachment';
|
||||
import { Banner } from '../../../extensions/banner';
|
||||
import { Bold } from '../../../extensions/bold';
|
||||
import { BulletList } from '../../../extensions/bulletList';
|
||||
import { Code } from '../../../extensions/code';
|
||||
import { CodeBlock } from '../../../extensions/codeBlock';
|
||||
import { DocumentChildren } from '../../../extensions/documentChildren';
|
||||
import { DocumentReference } from '../../../extensions/documentReference';
|
||||
import { HardBreak } from '../../../extensions/hardBreak';
|
||||
import { Heading } from '../../../extensions/heading';
|
||||
import { HorizontalRule } from '../../../extensions/horizontalRule';
|
||||
import { marks, HTMLMarks } from '../../../extensions/htmlMarks';
|
||||
import { Iframe } from '../../../extensions/iframe';
|
||||
import { Image } from '../../../extensions/image';
|
||||
import { Italic } from '../../../extensions/italic';
|
||||
import { Katex } from '../../../extensions/katex';
|
||||
import { Link } from '../../../extensions/link';
|
||||
import { ListItem } from '../../../extensions/listItem';
|
||||
import { Mind } from '../../../extensions/mind';
|
||||
import { OrderedList } from '../../../extensions/orderedList';
|
||||
import { Paragraph } from '../../../extensions/paragraph';
|
||||
import { Status } from '../../../extensions/status';
|
||||
import { Strike } from '../../../extensions/strike';
|
||||
import { Table } from '../../../extensions/table';
|
||||
import { TableCell } from '../../../extensions/tableCell';
|
||||
import { TableHeader } from '../../../extensions/tableHeader';
|
||||
import { TableRow } from '../../../extensions/tableRow';
|
||||
import { Text } from '../../../extensions/text';
|
||||
import { TaskItem } from '../../../extensions/taskItem';
|
||||
import { TaskList } from '../../../extensions/taskList';
|
||||
import { Title } from '../../../extensions/title';
|
||||
import {
|
||||
isPlainURL,
|
||||
renderHardBreak,
|
||||
renderTable,
|
||||
renderTableCell,
|
||||
renderTableRow,
|
||||
openTag,
|
||||
closeTag,
|
||||
renderOrderedList,
|
||||
renderImage,
|
||||
renderCustomContainer,
|
||||
renderHTMLNode,
|
||||
} from './serializerHelpers';
|
||||
|
||||
const SerializerConfig = {
|
||||
marks: {
|
||||
[Bold.name]: defaultMarkdownSerializer.marks.strong,
|
||||
[Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
|
||||
[Code.name]: defaultMarkdownSerializer.marks.code,
|
||||
[Link.name]: {
|
||||
open(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? '<' : '[';
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
const href = mark.attrs.canonicalSrc || mark.attrs.href;
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? '>'
|
||||
: `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
|
||||
},
|
||||
},
|
||||
[Strike.name]: {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
...marks.reduce(
|
||||
(acc, { name, tag }) => ({
|
||||
...acc,
|
||||
[name]: {
|
||||
mixable: true,
|
||||
open(state, node) {
|
||||
return openTag(tag, node.attrs);
|
||||
},
|
||||
close: closeTag(tag),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
},
|
||||
|
||||
nodes: {
|
||||
[Attachment.name]: renderCustomContainer('attachment'),
|
||||
[Banner.name]: (state, node) => {
|
||||
state.write(`:::${node.attrs.type || 'info'}\n`);
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.ensureNewLine();
|
||||
state.write(':::');
|
||||
state.closeBlock(node);
|
||||
},
|
||||
blockquote: (state, node) => {
|
||||
if (node.attrs.multiline) {
|
||||
state.write('>>>');
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.ensureNewLine();
|
||||
state.write('>>>');
|
||||
state.closeBlock(node);
|
||||
} else {
|
||||
state.wrapBlock('> ', null, node, () => state.renderContent(node));
|
||||
}
|
||||
},
|
||||
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
|
||||
[CodeBlock.name]: (state, node) => {
|
||||
state.write(`\`\`\`${node.attrs.language || ''}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write('```');
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[DocumentChildren.name]: renderCustomContainer('documentChildren'),
|
||||
[DocumentReference.name]: renderCustomContainer('documentReference'),
|
||||
[HardBreak.name]: renderHardBreak,
|
||||
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
|
||||
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
|
||||
[Iframe.name]: renderCustomContainer('iframe'),
|
||||
[Image.name]: renderImage,
|
||||
[Katex.name]: (state, node) => {
|
||||
state.ensureNewLine();
|
||||
state.write(`\$\$${node.attrs.text || ''}\$\$`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
|
||||
[Mind.name]: renderCustomContainer('mind'),
|
||||
[OrderedList.name]: renderOrderedList,
|
||||
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
|
||||
[Status.name]: renderCustomContainer('status'),
|
||||
[Table.name]: renderTable,
|
||||
[TableCell.name]: renderTableCell,
|
||||
[TableHeader.name]: renderTableCell,
|
||||
[TableRow.name]: renderTableRow,
|
||||
[TaskItem.name]: (state, node) => {
|
||||
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
|
||||
state.renderContent(node);
|
||||
},
|
||||
[TaskList.name]: (state, node) => {
|
||||
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ');
|
||||
},
|
||||
[Text.name]: defaultMarkdownSerializer.nodes.text,
|
||||
[Title.name]: renderHTMLNode('p', false, true, { class: 'title' }),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 ProseMirror Document Node JSON 转换为 markdown 字符串
|
||||
* @param param.content
|
||||
* @returns
|
||||
*/
|
||||
export const prosemirrorToMarkdown = ({ content }) => {
|
||||
const serializer = new ProseMirrorMarkdownSerializer(SerializerConfig.nodes, SerializerConfig.marks);
|
||||
|
||||
console.log(content);
|
||||
|
||||
return serializer.serialize(content, {
|
||||
tightLists: true,
|
||||
});
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { jsonToStr } from '../../dataset';
|
||||
|
||||
const uniq = (arr: string[]) => [...new Set(arr)];
|
||||
|
||||
function isString(value) {
|
||||
|
@ -271,7 +273,7 @@ export function renderHTMLNode(tagName, forceRenderInline = false, needNewLine =
|
|||
renderTagClose(state, tagName, false);
|
||||
if (needNewLine) {
|
||||
state.ensureNewLine();
|
||||
state.write('<br />');
|
||||
state.write('\n');
|
||||
state.ensureNewLine();
|
||||
}
|
||||
};
|
||||
|
@ -346,3 +348,11 @@ export function renderImage(state, node) {
|
|||
export function renderPlayable(state, node) {
|
||||
renderImage(state, node);
|
||||
}
|
||||
|
||||
export function renderCustomContainer(name) {
|
||||
return function (state, node) {
|
||||
state.ensureNewLine();
|
||||
state.write(`::: ${name} ${jsonToStr(node.attrs)}\n:::\n`);
|
||||
state.closeBlock(node);
|
||||
};
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
import { Attachment } from '../../extensions/attachment';
|
||||
import { Banner } from '../../extensions/banner';
|
||||
import { Blockquote } from '../../extensions/blockquote';
|
||||
import { Bold } from '../../extensions/bold';
|
||||
import { BulletList } from '../../extensions/bulletList';
|
||||
import { Code } from '../../extensions/code';
|
||||
import { CodeBlock } from '../../extensions/codeBlock';
|
||||
import { DocumentChildren } from '../../extensions/documentChildren';
|
||||
import { DocumentReference } from '../../extensions/documentReference';
|
||||
import { FootnoteDefinition } from '../../extensions/footnoteDefinition';
|
||||
import { FootnoteReference } from '../../extensions/footnoteReference';
|
||||
import { FootnotesSection } from '../../extensions/footnotesSection';
|
||||
import { HardBreak } from '../../extensions/hardBreak';
|
||||
import { Heading } from '../../extensions/heading';
|
||||
import { HorizontalRule } from '../../extensions/horizontalRule';
|
||||
import { HTMLMarks } from '../../extensions/htmlMarks';
|
||||
import { Iframe } from '../../extensions/iframe';
|
||||
import { Image } from '../../extensions/image';
|
||||
import { Italic } from '../../extensions/italic';
|
||||
import { Katex } from '../../extensions/katex';
|
||||
import { Link } from '../../extensions/link';
|
||||
import { ListItem } from '../../extensions/listItem';
|
||||
import { Mind } from '../../extensions/mind';
|
||||
import { OrderedList } from '../../extensions/orderedList';
|
||||
import { Paragraph } from '../../extensions/paragraph';
|
||||
import { Strike } from '../../extensions/strike';
|
||||
import { Table } from '../../extensions/table';
|
||||
import { TableCell } from '../../extensions/tableCell';
|
||||
import { TableHeader } from '../../extensions/tableHeader';
|
||||
import { TableRow } from '../../extensions/tableRow';
|
||||
import { Text } from '../../extensions/text';
|
||||
import { TaskItem } from '../../extensions/taskItem';
|
||||
import { TaskList } from '../../extensions/taskList';
|
||||
import { Title } from '../../extensions/title';
|
||||
import {
|
||||
isPlainURL,
|
||||
renderHardBreak,
|
||||
renderTable,
|
||||
renderTableCell,
|
||||
renderTableRow,
|
||||
openTag,
|
||||
closeTag,
|
||||
renderOrderedList,
|
||||
renderImage,
|
||||
renderHTMLNode,
|
||||
} from './serializerHelpers';
|
||||
import { htmlToPromsemirror } from './htmlToProsemirror';
|
||||
import { markdownToHTML } from './markdownToHTML';
|
||||
|
||||
const defaultSerializerConfig = {
|
||||
marks: {
|
||||
[Bold.name]: defaultMarkdownSerializer.marks.strong,
|
||||
[Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
|
||||
[Code.name]: defaultMarkdownSerializer.marks.code,
|
||||
[Link.name]: {
|
||||
open(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? '<' : '[';
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
const href = mark.attrs.canonicalSrc || mark.attrs.href;
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? '>'
|
||||
: `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
|
||||
},
|
||||
},
|
||||
[Strike.name]: {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
...HTMLMarks.reduce(
|
||||
(acc, { name }) => ({
|
||||
...acc,
|
||||
[name]: {
|
||||
mixable: true,
|
||||
open(state, node) {
|
||||
return openTag(name, node.attrs);
|
||||
},
|
||||
close: closeTag(name),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
},
|
||||
|
||||
nodes: {
|
||||
[Attachment.name]: (state, node) => {
|
||||
state.ensureNewLine();
|
||||
state.write(`attachment$`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[Banner.name]: (state, node) => {
|
||||
state.write(`:::${node.attrs.type || 'info'}\n`);
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.ensureNewLine();
|
||||
state.write(':::');
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[Blockquote.name]: (state, node) => {
|
||||
if (node.attrs.multiline) {
|
||||
state.write('>>>');
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.ensureNewLine();
|
||||
state.write('>>>');
|
||||
state.closeBlock(node);
|
||||
} else {
|
||||
state.wrapBlock('> ', null, node, () => state.renderContent(node));
|
||||
}
|
||||
},
|
||||
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
|
||||
[CodeBlock.name]: (state, node) => {
|
||||
state.write(`\`\`\`${node.attrs.language || ''}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write('```');
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[DocumentChildren.name]: (state, node) => {
|
||||
state.ensureNewLine();
|
||||
state.write(`documentChildren$`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[DocumentReference.name]: (state, node) => {
|
||||
state.ensureNewLine();
|
||||
state.write(`documentReference$`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[FootnoteDefinition.name]: (state, node) => {
|
||||
state.renderInline(node);
|
||||
},
|
||||
[FootnoteReference.name]: (state, node) => {
|
||||
state.write(`[^${node.attrs.footnoteNumber}]`);
|
||||
},
|
||||
[FootnotesSection.name]: (state, node) => {
|
||||
state.renderList(node, '', (index) => `[^${index + 1}]: `);
|
||||
},
|
||||
[HardBreak.name]: renderHardBreak,
|
||||
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
|
||||
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
|
||||
[Iframe.name]: renderImage,
|
||||
[Image.name]: renderImage,
|
||||
[Katex.name]: (state, node) => {
|
||||
state.ensureNewLine();
|
||||
state.write(`\$\$${node.attrs.text || ''}\$\$`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
|
||||
[Mind.name]: (state, node) => {
|
||||
state.write(`$mind\n`);
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.ensureNewLine();
|
||||
state.closeBlock(node);
|
||||
},
|
||||
[OrderedList.name]: renderOrderedList,
|
||||
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
|
||||
[Table.name]: renderTable,
|
||||
[TableCell.name]: renderTableCell,
|
||||
[TableHeader.name]: renderTableCell,
|
||||
[TableRow.name]: renderTableRow,
|
||||
[TaskItem.name]: (state, node) => {
|
||||
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
|
||||
state.renderContent(node);
|
||||
},
|
||||
[TaskList.name]: (state, node) => {
|
||||
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ');
|
||||
},
|
||||
[Text.name]: defaultMarkdownSerializer.nodes.text,
|
||||
[Title.name]: (state, node) => {
|
||||
if (!node.textContent) return;
|
||||
|
||||
state.write(`# `);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createMarkdownSerializer = () => ({
|
||||
// 将 markdown 字符串转换为 ProseMirror JSONDocument
|
||||
markdownToProsemirror: ({ schema, content, hasTitle }) => {
|
||||
const html = markdownToHTML(content);
|
||||
if (!html) return null;
|
||||
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(html, 'text/html');
|
||||
body.append(document.createComment(content));
|
||||
const json = htmlToPromsemirror(body);
|
||||
|
||||
console.log({ hasTitle, json, body });
|
||||
|
||||
// 设置标题
|
||||
if (!hasTitle) {
|
||||
const firstNode = json.content[0];
|
||||
if (firstNode) {
|
||||
if (firstNode.type === 'heading' || firstNode.type === 'paragraph') {
|
||||
firstNode.type = 'title';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = json.content;
|
||||
const result = { type: 'doc', content: [] };
|
||||
|
||||
for (let i = 0; i < nodes.length; ) {
|
||||
const node = nodes[i];
|
||||
// 目的:合并成 promirror 需要的 table 格式
|
||||
if (node.type === 'tableRow') {
|
||||
const nextNode = nodes[i + 1];
|
||||
if (nextNode && nextNode.type === 'table') {
|
||||
nextNode.content.unshift(node);
|
||||
result.content.push(nextNode);
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
result.content.push(node);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 将 ProseMirror JSONDocument 转换为 markdown 字符串
|
||||
proseMirrorToMarkdown: ({ schema, content }) => {
|
||||
const serializer = new ProseMirrorMarkdownSerializer(
|
||||
{
|
||||
...defaultSerializerConfig.nodes,
|
||||
},
|
||||
{
|
||||
...defaultSerializerConfig.marks,
|
||||
}
|
||||
);
|
||||
return serializer.serialize(content, {
|
||||
tightLists: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownSerializer = createMarkdownSerializer();
|
|
@ -24,6 +24,7 @@ export type FloatMenuViewOptions = {
|
|||
export class FloatMenuView {
|
||||
public editor: Editor;
|
||||
public parentNode: null | HTMLElement;
|
||||
public container: null | HTMLElement;
|
||||
private dom: HTMLElement;
|
||||
private popup: Instance;
|
||||
private _update: FloatMenuViewOptions['update'];
|
||||
|
@ -36,12 +37,24 @@ export class FloatMenuView {
|
|||
}
|
||||
if (isNodeSelection(state.selection)) {
|
||||
const node = view.nodeDOM(range.from) as HTMLElement;
|
||||
console.log(node);
|
||||
|
||||
if (node) {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
return posToDOMRect(view, range.from, range.to);
|
||||
|
||||
const rangeRect = posToDOMRect(view, range.from, range.to);
|
||||
|
||||
if (this.container) {
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
|
||||
if (rangeRect.width > containerRect.width) {
|
||||
return containerRect;
|
||||
}
|
||||
}
|
||||
|
||||
return rangeRect;
|
||||
};
|
||||
|
||||
constructor(props: FloatMenuViewOptions) {
|
||||
|
@ -61,6 +74,14 @@ export class FloatMenuView {
|
|||
this.createPopup();
|
||||
}
|
||||
|
||||
setConatiner(el) {
|
||||
this.container = el;
|
||||
// this.popup?.setProps({
|
||||
// appendTo: el,
|
||||
// });
|
||||
// this.popup?.
|
||||
}
|
||||
|
||||
createPopup() {
|
||||
const { element: editorElement } = this.editor.options;
|
||||
const editorIsAttached = !!editorElement.parentElement;
|
||||
|
|
|
@ -14,15 +14,15 @@ export const useTheme = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// const body = document.body;
|
||||
// if (theme === 'dark') {
|
||||
// body.setAttribute('theme-mode', 'dark');
|
||||
// return;
|
||||
// }
|
||||
// if (theme === 'light') {
|
||||
// body.setAttribute('theme-mode', 'light');
|
||||
// return;
|
||||
// }
|
||||
const body = document.body;
|
||||
if (theme === 'dark') {
|
||||
body.setAttribute('theme-mode', 'dark');
|
||||
return;
|
||||
}
|
||||
if (theme === 'light') {
|
||||
body.setAttribute('theme-mode', 'light');
|
||||
return;
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -240,19 +240,27 @@
|
|||
padding: 0;
|
||||
white-space: pre;
|
||||
background-color: transparent;
|
||||
|
||||
width: 100%;
|
||||
max-height: 370px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
|
||||
&.has-focus {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 1em 0;
|
||||
|
||||
td,
|
||||
|
|
|
@ -8,11 +8,11 @@ export class TemplateDto {
|
|||
readonly title: string;
|
||||
|
||||
@IsOptional()
|
||||
content: string;
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
state: Uint8Array;
|
||||
state?: Uint8Array;
|
||||
|
||||
@IsOptional()
|
||||
isPublic: boolean;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export class UpdateDocumentDto {
|
|||
readonly title: string;
|
||||
|
||||
@IsOptional()
|
||||
content: string;
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
state?: Uint8Array;
|
||||
|
|
|
@ -65,6 +65,7 @@ export class CollaborationService {
|
|||
onAuthenticate: this.onAuthenticate.bind(this),
|
||||
onLoadDocument: this.onLoadDocument.bind(this),
|
||||
onChange: this.onChange.bind(this),
|
||||
onDisconnect: this.onDisconnect.bind(this),
|
||||
});
|
||||
this.server = server;
|
||||
this.server.listen(lodash.get(getConfig(), 'server.collaborationPort', 5003));
|
||||
|
@ -213,4 +214,40 @@ export class CollaborationService {
|
|||
state,
|
||||
});
|
||||
}
|
||||
|
||||
async onDisconnect(data) {
|
||||
const { requestParameters, document } = data;
|
||||
const targetId = requestParameters.get('targetId');
|
||||
const docType = requestParameters.get('docType');
|
||||
const userId = requestParameters.get('userId');
|
||||
|
||||
switch (docType) {
|
||||
case 'document': {
|
||||
const documentId = targetId;
|
||||
const { title } = await this.documentService.findById(documentId);
|
||||
|
||||
if (!title) {
|
||||
await this.documentService.updateDocument({ id: userId } as OutUser, targetId, {
|
||||
title: '未命名文档',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'template': {
|
||||
const templateId = targetId;
|
||||
const { title } = await this.templateService.findById(templateId);
|
||||
|
||||
if (!title) {
|
||||
await this.templateService.updateTemplate({ id: userId } as OutUser, targetId, {
|
||||
title: '未命名模板',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('未知类型');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export class TemplateService {
|
|||
* @param id
|
||||
* @param tag
|
||||
*/
|
||||
async updateTemplate(user, id, dto: TemplateDto & { id: string }) {
|
||||
async updateTemplate(user, id, dto: TemplateDto) {
|
||||
const old = await this.templateRepo.findOne(id);
|
||||
|
||||
if (user.id !== old.createUserId) {
|
||||
|
|
Loading…
Reference in New Issue