mirror of https://github.com/fantasticit/think.git
tiptap: paste from excel sheet
parent
3ee0c5e095
commit
4bed78aa64
|
@ -1,7 +1,7 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { toggleMark } from 'prosemirror-commands';
|
||||
import { DOMParser, Fragment, Schema } from 'prosemirror-model';
|
||||
import { DOMParser, Fragment, Node, Schema } from 'prosemirror-model';
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
import {
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
isMarkdown,
|
||||
isTitleNode,
|
||||
isValidURL,
|
||||
LANGUAGES,
|
||||
normalizeMarkdown,
|
||||
} from 'tiptap/prose-utils';
|
||||
|
||||
|
@ -20,15 +19,15 @@ import { TitleExtensionName } from './title';
|
|||
interface IPasteOptions {
|
||||
/**
|
||||
*
|
||||
* 将 markdown 转换为 html
|
||||
* 将 html 转换为 prosemirror
|
||||
*/
|
||||
markdownToHTML: (arg: string) => string;
|
||||
htmlToProsemirror: (arg: { schema: Schema; html: string; needTitle: boolean; defaultTitle?: string }) => Node;
|
||||
|
||||
/**
|
||||
* 将 markdown 转换为 prosemirror 节点
|
||||
* FIXME: prosemirror 节点的类型是什么?
|
||||
*/
|
||||
markdownToProsemirror: (arg: { schema: Schema; content: string; needTitle: boolean }) => unknown;
|
||||
markdownToProsemirror: (arg: { schema: Schema; content: string; needTitle: boolean }) => Node;
|
||||
|
||||
/**
|
||||
* 将 prosemirror 转换为 markdown
|
||||
|
@ -42,7 +41,7 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
|
||||
addOptions() {
|
||||
return {
|
||||
markdownToHTML: (arg) => arg,
|
||||
htmlToProsemirror: (arg) => '',
|
||||
markdownToProsemirror: (arg) => arg.content,
|
||||
prosemirrorToMarkdown: (arg) => String(arg.content),
|
||||
};
|
||||
|
@ -67,32 +66,22 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
|
||||
if (!event.clipboardData) return false;
|
||||
|
||||
// 文件
|
||||
const files = Array.from(event.clipboardData.files);
|
||||
if (files.length) {
|
||||
event.preventDefault();
|
||||
files.forEach((file) => {
|
||||
handleFileEvent({ editor, file });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const html = event.clipboardData.getData('text/html');
|
||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||
const node = event.clipboardData.getData('text/node');
|
||||
const markdownText = event.clipboardData.getData('text/markdown');
|
||||
const { state, dispatch } = view;
|
||||
const { htmlToProsemirror, markdownToProsemirror } = extensionThis.options;
|
||||
|
||||
debug(() => {
|
||||
console.group('paste');
|
||||
console.log({ text, vscode, node, markdownText });
|
||||
console.log({ text, vscode, node, markdownText, files });
|
||||
console.log(html);
|
||||
console.groupEnd();
|
||||
});
|
||||
|
||||
const { markdownToProsemirror } = extensionThis.options;
|
||||
|
||||
// 直接复制节点
|
||||
if (node) {
|
||||
const json = safeJSONParse(node);
|
||||
|
@ -102,6 +91,52 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
return true;
|
||||
}
|
||||
|
||||
const firstNode = view.props.state.doc.content.firstChild;
|
||||
const hasTitleExtension = !!editor.extensionManager.extensions.find(
|
||||
(extension) => extension.name === TitleExtensionName
|
||||
);
|
||||
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
||||
|
||||
// If the HTML on the clipboard is from Prosemirror then the best
|
||||
// compatability is to just use the HTML parser, regardless of
|
||||
// whether it "looks" like Markdown, see: outline/outline#2416
|
||||
if (html?.includes('data-pm-slice')) {
|
||||
let domNode = document.createElement('div');
|
||||
domNode.innerHTML = html;
|
||||
const slice = DOMParser.fromSchema(editor.schema).parseSlice(domNode);
|
||||
let tr = view.state.tr;
|
||||
tr = tr.replaceSelection(slice);
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
domNode = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 新增:office 套件内容处理
|
||||
if (html?.includes('urn:schemas-microsoft-com:office')) {
|
||||
const doc = htmlToProsemirror({
|
||||
schema: editor.schema,
|
||||
html,
|
||||
needTitle: hasTitleExtension && !hasTitle,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
event.preventDefault();
|
||||
files.forEach((file) => {
|
||||
handleFileEvent({ editor, file });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 链接
|
||||
if (isValidURL(text)) {
|
||||
if (!state.selection.empty) {
|
||||
|
@ -141,31 +176,6 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
return true;
|
||||
}
|
||||
|
||||
const firstNode = view.props.state.doc.content.firstChild;
|
||||
const hasTitleExtension = !!editor.extensionManager.extensions.find(
|
||||
(extension) => extension.name === TitleExtensionName
|
||||
);
|
||||
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
||||
|
||||
// If the HTML on the clipboard is from Prosemirror then the best
|
||||
// compatability is to just use the HTML parser, regardless of
|
||||
// whether it "looks" like Markdown, see: outline/outline#2416
|
||||
if (html?.includes('data-pm-slice')) {
|
||||
let domNode = document.createElement('div');
|
||||
domNode.innerHTML = html;
|
||||
const slice = DOMParser.fromSchema(editor.schema).parseSlice(domNode);
|
||||
|
||||
debug(() => {
|
||||
console.log('html', domNode, html, slice);
|
||||
});
|
||||
|
||||
let tr = view.state.tr;
|
||||
tr = tr.replaceSelection(slice);
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
domNode = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理 markdown
|
||||
if (markdownText || isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1,23 +1,55 @@
|
|||
import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell';
|
||||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
import { Plugin } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { getCellsInColumn, isRowSelected, isTableSelected, selectRow, selectTable } from 'tiptap/prose-utils';
|
||||
|
||||
export const TableCell = BuiltInTableCell.extend({
|
||||
export interface TableCellOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const TableCell = Node.create<TableCellOptions>({
|
||||
name: 'tableCell',
|
||||
content: 'block+',
|
||||
tableRole: 'cell',
|
||||
isolating: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'td' }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
colspan: {
|
||||
default: 1,
|
||||
parseHTML: (element) => {
|
||||
const colspan = element.getAttribute('colspan');
|
||||
const value = colspan ? parseInt(colspan, 10) : 1;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
rowspan: {
|
||||
default: 1,
|
||||
parseHTML: (element) => {
|
||||
const rowspan = element.getAttribute('rowspan');
|
||||
const value = rowspan ? parseInt(rowspan, 10) : 1;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
colwidth: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const colwidth = element.getAttribute('colwidth');
|
||||
const value = colwidth ? colwidth.split(',').map((item) => parseInt(item, 10)) : null;
|
||||
|
||||
const value = colwidth ? [parseInt(colwidth, 10)] : null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -64,8 +64,8 @@ import { Title } from 'tiptap/core/extensions/title';
|
|||
import { TrailingNode } from 'tiptap/core/extensions/trailing-node';
|
||||
import { Underline } from 'tiptap/core/extensions/underline';
|
||||
// markdown 支持
|
||||
import { htmlToProsemirror } from 'tiptap/markdown/html-to-prosemirror';
|
||||
import { markdownToProsemirror } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
import { markdownToHTML } from 'tiptap/markdown/markdown-to-prosemirror/markdown-to-html';
|
||||
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
|
||||
|
||||
const DocumentWithTitle = Document.extend({
|
||||
|
@ -133,7 +133,7 @@ export const CollaborationKit = [
|
|||
TrailingNode,
|
||||
Underline,
|
||||
Paste.configure({
|
||||
markdownToHTML,
|
||||
htmlToProsemirror,
|
||||
markdownToProsemirror,
|
||||
prosemirrorToMarkdown,
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { extractImage } from '../markdown-to-prosemirror';
|
||||
import { htmlToProsemirror as mdHTMLToProsemirror } from '../markdown-to-prosemirror/html-to-prosemirror';
|
||||
|
||||
/**
|
||||
* 将 HTML 转换成 prosemirror node
|
||||
* @param schema
|
||||
* @param html
|
||||
* @param needTitle 是否需要一个标题
|
||||
* @param defaultTitle 优先作为文档标题,否则默认读取一个 heading 或者 paragraph 的文字内容
|
||||
* @returns
|
||||
*/
|
||||
export const htmlToProsemirror = ({ schema, html, needTitle, defaultTitle = '' }) => {
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(extractImage(html), 'text/html');
|
||||
body.append(document.createComment(html));
|
||||
const doc = mdHTMLToProsemirror(body, needTitle, defaultTitle);
|
||||
return doc;
|
||||
};
|
|
@ -2,6 +2,49 @@ import { Renderer } from './renderer';
|
|||
|
||||
const renderer = new Renderer();
|
||||
|
||||
/**
|
||||
* 表格的内容格式不正确,需要进行过滤修复
|
||||
* @param doc
|
||||
* @returns
|
||||
*/
|
||||
function fixNode(doc) {
|
||||
if (!doc) return;
|
||||
|
||||
const queue = [doc];
|
||||
|
||||
while (queue.length) {
|
||||
const node = queue.shift();
|
||||
|
||||
if (node.content) {
|
||||
node.content = node.content.filter((subNode) => !(subNode.type === 'text' && subNode.text === '\n'));
|
||||
}
|
||||
|
||||
if (node.type === 'table') {
|
||||
node.content = (node.content || []).filter((subNode) => subNode.type.includes('table'));
|
||||
}
|
||||
|
||||
if (node.type === 'tableRow') {
|
||||
node.content = (node.content || []).filter((subNode) => subNode.type === 'tableCell');
|
||||
}
|
||||
|
||||
if (node.type === 'tableCell') {
|
||||
(node.content || []).forEach((subNode, i) => {
|
||||
if (subNode && subNode.type === 'text') {
|
||||
node.content[i] = {
|
||||
attrs: subNode.attrs || {},
|
||||
content: [subNode],
|
||||
type: 'paragraph',
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
queue.push(...(node.content || []).filter((subNode) => subNode.type.includes('table')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 HTML 转换成 prosemirror node
|
||||
* @param body
|
||||
|
@ -9,7 +52,7 @@ const renderer = new Renderer();
|
|||
* @param defaultTitle 优先作为文档标题,否则默认读取一个 heading 或者 paragraph 的文字内容
|
||||
* @returns
|
||||
*/
|
||||
export const htmlToPromsemirror = (body, needTitle = false, defaultTitle = '') => {
|
||||
export const htmlToProsemirror = (body, needTitle = false, defaultTitle = '') => {
|
||||
const json = renderer.render(body);
|
||||
|
||||
// 设置标题
|
||||
|
@ -55,5 +98,6 @@ export const htmlToPromsemirror = (body, needTitle = false, defaultTitle = '') =
|
|||
}
|
||||
}
|
||||
|
||||
fixNode(result);
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -14,12 +14,16 @@ const getAttribute = (
|
|||
) => {
|
||||
return Object.keys(config).reduce((accu, key) => {
|
||||
const conf = config[key];
|
||||
accu[key] = conf.default;
|
||||
accu[key] = null;
|
||||
|
||||
if (conf.parseHTML) {
|
||||
accu[key] = conf.parseHTML(element);
|
||||
}
|
||||
|
||||
if (!accu[key]) {
|
||||
accu[key] = conf.default;
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, ret);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { htmlToPromsemirror } from './html-to-prosemirror';
|
||||
import { htmlToProsemirror } from './html-to-prosemirror';
|
||||
import { markdownToHTML } from './markdown-to-html';
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,7 @@ import { markdownToHTML } from './markdown-to-html';
|
|||
* @param html
|
||||
* @returns
|
||||
*/
|
||||
const extractImage = (html) => {
|
||||
export const extractImage = (html) => {
|
||||
let matches = [];
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
|
@ -32,9 +32,8 @@ export const markdownToProsemirror = ({ schema, content, needTitle, defaultTitle
|
|||
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(extractImage(html), 'text/html');
|
||||
|
||||
body.append(document.createComment(content));
|
||||
const node = htmlToPromsemirror(body, needTitle, defaultTitle);
|
||||
const node = htmlToProsemirror(body, needTitle, defaultTitle);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue