mirror of https://github.com/fantasticit/think.git
tiptap: paste from excel sheet
This commit is contained in:
parent
3ee0c5e095
commit
4bed78aa64
|
@ -1,7 +1,7 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { safeJSONParse } from 'helpers/json';
|
import { safeJSONParse } from 'helpers/json';
|
||||||
import { toggleMark } from 'prosemirror-commands';
|
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 { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||||
import {
|
import {
|
||||||
|
@ -11,7 +11,6 @@ import {
|
||||||
isMarkdown,
|
isMarkdown,
|
||||||
isTitleNode,
|
isTitleNode,
|
||||||
isValidURL,
|
isValidURL,
|
||||||
LANGUAGES,
|
|
||||||
normalizeMarkdown,
|
normalizeMarkdown,
|
||||||
} from 'tiptap/prose-utils';
|
} from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
@ -20,15 +19,15 @@ import { TitleExtensionName } from './title';
|
||||||
interface IPasteOptions {
|
interface IPasteOptions {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* 将 markdown 转换为 html
|
* 将 html 转换为 prosemirror
|
||||||
*/
|
*/
|
||||||
markdownToHTML: (arg: string) => string;
|
htmlToProsemirror: (arg: { schema: Schema; html: string; needTitle: boolean; defaultTitle?: string }) => Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 markdown 转换为 prosemirror 节点
|
* 将 markdown 转换为 prosemirror 节点
|
||||||
* FIXME: prosemirror 节点的类型是什么?
|
* FIXME: prosemirror 节点的类型是什么?
|
||||||
*/
|
*/
|
||||||
markdownToProsemirror: (arg: { schema: Schema; content: string; needTitle: boolean }) => unknown;
|
markdownToProsemirror: (arg: { schema: Schema; content: string; needTitle: boolean }) => Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 prosemirror 转换为 markdown
|
* 将 prosemirror 转换为 markdown
|
||||||
|
@ -42,7 +41,7 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
markdownToHTML: (arg) => arg,
|
htmlToProsemirror: (arg) => '',
|
||||||
markdownToProsemirror: (arg) => arg.content,
|
markdownToProsemirror: (arg) => arg.content,
|
||||||
prosemirrorToMarkdown: (arg) => String(arg.content),
|
prosemirrorToMarkdown: (arg) => String(arg.content),
|
||||||
};
|
};
|
||||||
|
@ -67,32 +66,22 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
|
|
||||||
if (!event.clipboardData) return false;
|
if (!event.clipboardData) return false;
|
||||||
|
|
||||||
// 文件
|
|
||||||
const files = Array.from(event.clipboardData.files);
|
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 text = event.clipboardData.getData('text/plain');
|
||||||
const html = event.clipboardData.getData('text/html');
|
const html = event.clipboardData.getData('text/html');
|
||||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||||
const node = event.clipboardData.getData('text/node');
|
const node = event.clipboardData.getData('text/node');
|
||||||
const markdownText = event.clipboardData.getData('text/markdown');
|
const markdownText = event.clipboardData.getData('text/markdown');
|
||||||
const { state, dispatch } = view;
|
const { state, dispatch } = view;
|
||||||
|
const { htmlToProsemirror, markdownToProsemirror } = extensionThis.options;
|
||||||
|
|
||||||
debug(() => {
|
debug(() => {
|
||||||
console.group('paste');
|
console.group('paste');
|
||||||
console.log({ text, vscode, node, markdownText });
|
console.log({ text, vscode, node, markdownText, files });
|
||||||
console.log(html);
|
console.log(html);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { markdownToProsemirror } = extensionThis.options;
|
|
||||||
|
|
||||||
// 直接复制节点
|
// 直接复制节点
|
||||||
if (node) {
|
if (node) {
|
||||||
const json = safeJSONParse(node);
|
const json = safeJSONParse(node);
|
||||||
|
@ -102,6 +91,52 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
return true;
|
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 (isValidURL(text)) {
|
||||||
if (!state.selection.empty) {
|
if (!state.selection.empty) {
|
||||||
|
@ -141,31 +176,6 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
return true;
|
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
|
// 处理 markdown
|
||||||
if (markdownText || isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
if (markdownText || isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
||||||
event.preventDefault();
|
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 { Plugin } from 'prosemirror-state';
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
import { getCellsInColumn, isRowSelected, isTableSelected, selectRow, selectTable } from 'tiptap/prose-utils';
|
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() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
colspan: {
|
colspan: {
|
||||||
default: 1,
|
default: 1,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const colspan = element.getAttribute('colspan');
|
||||||
|
const value = colspan ? parseInt(colspan, 10) : 1;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rowspan: {
|
rowspan: {
|
||||||
default: 1,
|
default: 1,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const rowspan = element.getAttribute('rowspan');
|
||||||
|
const value = rowspan ? parseInt(rowspan, 10) : 1;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colwidth: {
|
colwidth: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => {
|
parseHTML: (element) => {
|
||||||
const colwidth = element.getAttribute('colwidth');
|
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;
|
return value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,8 +64,8 @@ import { Title } from 'tiptap/core/extensions/title';
|
||||||
import { TrailingNode } from 'tiptap/core/extensions/trailing-node';
|
import { TrailingNode } from 'tiptap/core/extensions/trailing-node';
|
||||||
import { Underline } from 'tiptap/core/extensions/underline';
|
import { Underline } from 'tiptap/core/extensions/underline';
|
||||||
// markdown 支持
|
// markdown 支持
|
||||||
|
import { htmlToProsemirror } from 'tiptap/markdown/html-to-prosemirror';
|
||||||
import { markdownToProsemirror } from 'tiptap/markdown/markdown-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';
|
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
|
||||||
|
|
||||||
const DocumentWithTitle = Document.extend({
|
const DocumentWithTitle = Document.extend({
|
||||||
|
@ -133,7 +133,7 @@ export const CollaborationKit = [
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
Underline,
|
Underline,
|
||||||
Paste.configure({
|
Paste.configure({
|
||||||
markdownToHTML,
|
htmlToProsemirror,
|
||||||
markdownToProsemirror,
|
markdownToProsemirror,
|
||||||
prosemirrorToMarkdown,
|
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();
|
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
|
* 将 HTML 转换成 prosemirror node
|
||||||
* @param body
|
* @param body
|
||||||
|
@ -9,7 +52,7 @@ const renderer = new Renderer();
|
||||||
* @param defaultTitle 优先作为文档标题,否则默认读取一个 heading 或者 paragraph 的文字内容
|
* @param defaultTitle 优先作为文档标题,否则默认读取一个 heading 或者 paragraph 的文字内容
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const htmlToPromsemirror = (body, needTitle = false, defaultTitle = '') => {
|
export const htmlToProsemirror = (body, needTitle = false, defaultTitle = '') => {
|
||||||
const json = renderer.render(body);
|
const json = renderer.render(body);
|
||||||
|
|
||||||
// 设置标题
|
// 设置标题
|
||||||
|
@ -55,5 +98,6 @@ export const htmlToPromsemirror = (body, needTitle = false, defaultTitle = '') =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fixNode(result);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,12 +14,16 @@ const getAttribute = (
|
||||||
) => {
|
) => {
|
||||||
return Object.keys(config).reduce((accu, key) => {
|
return Object.keys(config).reduce((accu, key) => {
|
||||||
const conf = config[key];
|
const conf = config[key];
|
||||||
accu[key] = conf.default;
|
accu[key] = null;
|
||||||
|
|
||||||
if (conf.parseHTML) {
|
if (conf.parseHTML) {
|
||||||
accu[key] = conf.parseHTML(element);
|
accu[key] = conf.parseHTML(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accu[key]) {
|
||||||
|
accu[key] = conf.default;
|
||||||
|
}
|
||||||
|
|
||||||
return accu;
|
return accu;
|
||||||
}, ret);
|
}, ret);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { htmlToPromsemirror } from './html-to-prosemirror';
|
import { htmlToProsemirror } from './html-to-prosemirror';
|
||||||
import { markdownToHTML } from './markdown-to-html';
|
import { markdownToHTML } from './markdown-to-html';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +12,7 @@ import { markdownToHTML } from './markdown-to-html';
|
||||||
* @param html
|
* @param html
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const extractImage = (html) => {
|
export const extractImage = (html) => {
|
||||||
let matches = [];
|
let matches = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
@ -32,9 +32,8 @@ export const markdownToProsemirror = ({ schema, content, needTitle, defaultTitle
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const { body } = parser.parseFromString(extractImage(html), 'text/html');
|
const { body } = parser.parseFromString(extractImage(html), 'text/html');
|
||||||
|
|
||||||
body.append(document.createComment(content));
|
body.append(document.createComment(content));
|
||||||
const node = htmlToPromsemirror(body, needTitle, defaultTitle);
|
const node = htmlToProsemirror(body, needTitle, defaultTitle);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue