mirror of https://github.com/fantasticit/think.git
support paste page html
This commit is contained in:
parent
8453788b84
commit
edadc508e7
|
@ -1,52 +1,79 @@
|
|||
import { Editor as CoreEditor, Extension } from '@tiptap/core';
|
||||
import { Editor as CoreEditor, Extension, getSchema } from '@tiptap/core';
|
||||
import { Document } from '@tiptap/extension-document';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { toggleMark } from 'prosemirror-commands';
|
||||
import { DOMParser, Fragment, Node, Schema } from 'prosemirror-model';
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||
import { DOMParser as PMDOMParser, Fragment, Node, Schema } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
import {
|
||||
debug,
|
||||
fixHTML,
|
||||
handleFileEvent,
|
||||
isInCode,
|
||||
isInTitle,
|
||||
isMarkdown,
|
||||
isTitleNode,
|
||||
isValidURL,
|
||||
normalizeMarkdown,
|
||||
} from 'tiptap/prose-utils';
|
||||
|
||||
import { TitleExtensionName } from './title';
|
||||
const safePos = (state: EditorState, pos) => {
|
||||
if (pos < 0) return 0;
|
||||
|
||||
function insertText(view, text) {
|
||||
const texts = text.split('\n').filter(Boolean);
|
||||
event.preventDefault();
|
||||
view.dispatch(view.state.tr.insertText(texts[0]));
|
||||
return Math.min(state.doc.content.size, pos);
|
||||
};
|
||||
|
||||
const json = {
|
||||
const htmlToProsemirror = (editor: CoreEditor, html, isPasteMarkdown = false) => {
|
||||
const firstNode = editor.view.state.doc.content.firstChild;
|
||||
const shouldInsertTitleText = !!(firstNode?.textContent?.length <= 0 ?? true);
|
||||
|
||||
if (!shouldInsertTitleText && !isPasteMarkdown) return false;
|
||||
|
||||
const parser = new window.DOMParser();
|
||||
const { body } = parser.parseFromString(fixHTML(html), 'text/html');
|
||||
|
||||
const schema = getSchema(
|
||||
[].concat(
|
||||
Document,
|
||||
editor.extensionManager.extensions.filter(
|
||||
(ext) => ext.type === 'node' && !['title', 'doc', 'collaboration', 'collaborationCursor'].includes(ext.name)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const toPasteNode = PMDOMParser.fromSchema(schema).parse(body);
|
||||
const doc = {
|
||||
type: 'doc',
|
||||
content: [{ type: 'title', attrs: { cover: '' }, content: [{ type: 'text', text: texts[0] }] }].concat(
|
||||
// @ts-ignore
|
||||
texts.slice(1).map((t) => {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
attrs: { indent: 0, textAlign: 'left' },
|
||||
content: [{ type: 'text', text: t }],
|
||||
};
|
||||
})
|
||||
),
|
||||
content: toPasteNode.content.toJSON(),
|
||||
};
|
||||
|
||||
let tr = view.state.tr;
|
||||
const selection = tr.selection;
|
||||
view.state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
|
||||
const startPosition = Math.min(position, selection.from) || 0;
|
||||
const endPosition = Math.min(position + node.nodeSize, selection.to);
|
||||
tr = tr.replaceWith(startPosition, endPosition, view.state.schema.nodeFromJSON(json));
|
||||
});
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
let toInsertAtTitleNode = null;
|
||||
|
||||
if (shouldInsertTitleText) {
|
||||
toInsertAtTitleNode = doc.content.shift();
|
||||
}
|
||||
|
||||
let tr = editor.view.state.tr;
|
||||
|
||||
const insertAt = isInTitle(editor.state)
|
||||
? safePos(editor.state, firstNode.nodeSize)
|
||||
: safePos(editor.state, editor.state.selection.from - 1);
|
||||
|
||||
const node = editor.state.schema.nodeFromJSON(doc);
|
||||
tr = tr.insert(insertAt, node);
|
||||
|
||||
if (shouldInsertTitleText) {
|
||||
if (toInsertAtTitleNode) {
|
||||
if (['heading', 'paragraph'].includes(toInsertAtTitleNode.type)) {
|
||||
tr.insertText(toInsertAtTitleNode?.content?.[0]?.text, 1, 1);
|
||||
} else {
|
||||
tr.insert(insertAt, editor.state.schema.nodeFromJSON(toInsertAtTitleNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.view.dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
interface IPasteOptions {
|
||||
/**
|
||||
|
@ -116,7 +143,6 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
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');
|
||||
|
@ -134,53 +160,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);
|
||||
let tr = view.state.tr;
|
||||
tr = tr.replaceSelection(slice);
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
domNode = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO:各家 office 套件标准不一样,是否需要做成用户自行选择粘贴 html 或者 图片?
|
||||
if (html?.includes('urn:schemas-microsoft-com:office') || html?.includes('</table>')) {
|
||||
const doc = htmlToProsemirror({
|
||||
editor,
|
||||
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) {
|
||||
|
@ -209,43 +188,24 @@ export const Paste = Extension.create<IPasteOptions>({
|
|||
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
|
||||
const pasteCodeLanguage = vscodeMeta?.mode;
|
||||
|
||||
if (pasteCodeLanguage && pasteCodeLanguage !== 'markdown') {
|
||||
event.preventDefault();
|
||||
const { tr } = view.state;
|
||||
tr.replaceSelectionWith(view.state.schema.nodes.codeBlock.create({ language: pasteCodeLanguage }));
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, tr.selection.from - 1))));
|
||||
tr.insertText(text.replace(/\r\n?/g, '\n'));
|
||||
tr.setMeta('paste', true);
|
||||
view.dispatch(tr);
|
||||
return true;
|
||||
if (html.length > 0 || text.length === 0) {
|
||||
return htmlToProsemirror(editor, html);
|
||||
}
|
||||
|
||||
// 处理 markdown
|
||||
if (markdownText || isMarkdown(text)) {
|
||||
console.log(text);
|
||||
const { markdownToHTML } = extensionThis.options;
|
||||
|
||||
if ((markdownText || isMarkdown(text)) && markdownToHTML) {
|
||||
event.preventDefault();
|
||||
const schema = view.props.state.schema;
|
||||
const doc = markdownToProsemirror({
|
||||
editor,
|
||||
schema,
|
||||
content: normalizeMarkdown(markdownText || text),
|
||||
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;
|
||||
const html = markdownToHTML(normalizeMarkdown(markdownText || text));
|
||||
if (html && html.length) return htmlToProsemirror(editor, html, true);
|
||||
}
|
||||
|
||||
if (isInTitle(view.state)) {
|
||||
if (text.length) {
|
||||
return insertText(view, text);
|
||||
}
|
||||
if (files.length) {
|
||||
event.preventDefault();
|
||||
files.forEach((file) => {
|
||||
handleFileEvent({ editor, file });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
export function fixHTML(html) {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
|
||||
let el;
|
||||
|
||||
while ((el = container.querySelector('a > img'))) {
|
||||
unwrapLink(el.parentNode, el.getAttribute('alt') || 'Image link');
|
||||
}
|
||||
|
||||
while ((el = container.querySelector('p > img'))) {
|
||||
unwrap(el.parentNode);
|
||||
}
|
||||
|
||||
while ((el = container.querySelector('a:not(p a)'))) {
|
||||
wrap(el, document.createElement('p'));
|
||||
}
|
||||
|
||||
return container.innerHTML;
|
||||
}
|
||||
|
||||
function unwrap(el) {
|
||||
const parent = el.parentNode;
|
||||
|
||||
// Move all children to the parent element.
|
||||
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
||||
|
||||
parent.removeChild(el);
|
||||
}
|
||||
|
||||
function unwrapLink(el, replacementText) {
|
||||
const parent = el.parentNode;
|
||||
|
||||
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
||||
|
||||
el.textContent = replacementText;
|
||||
}
|
||||
|
||||
function wrap(el, wrapper) {
|
||||
el.parentNode.insertBefore(wrapper, el);
|
||||
wrapper.appendChild(el);
|
||||
}
|
|
@ -13,6 +13,7 @@ export * from './dom-dataset';
|
|||
export * from './download';
|
||||
export * from './editor-container-size';
|
||||
export * from './file';
|
||||
export * from './html';
|
||||
export * from './lowlight-plugin';
|
||||
export * from './mark';
|
||||
export * from './markdown';
|
||||
|
|
Loading…
Reference in New Issue