tiptap: paste from excel sheet

pull/122/head
fantasticit 2022-07-13 11:57:39 +08:00
parent 3ee0c5e095
commit 4bed78aa64
7 changed files with 162 additions and 55 deletions

View File

@ -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();

View File

@ -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;
},
},

View File

@ -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,
}),

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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);
};

View File

@ -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;
};