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 { safeJSONParse } from 'helpers/json';
|
||||||
import { toggleMark } from 'prosemirror-commands';
|
import { toggleMark } from 'prosemirror-commands';
|
||||||
import { DOMParser, Fragment, Node, Schema } from 'prosemirror-model';
|
import { DOMParser as PMDOMParser, Fragment, Node, Schema } from 'prosemirror-model';
|
||||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||||
import {
|
import {
|
||||||
debug,
|
debug,
|
||||||
|
fixHTML,
|
||||||
handleFileEvent,
|
handleFileEvent,
|
||||||
isInCode,
|
isInCode,
|
||||||
isInTitle,
|
isInTitle,
|
||||||
isMarkdown,
|
isMarkdown,
|
||||||
isTitleNode,
|
|
||||||
isValidURL,
|
isValidURL,
|
||||||
normalizeMarkdown,
|
normalizeMarkdown,
|
||||||
} from 'tiptap/prose-utils';
|
} from 'tiptap/prose-utils';
|
||||||
|
|
||||||
import { TitleExtensionName } from './title';
|
const safePos = (state: EditorState, pos) => {
|
||||||
|
if (pos < 0) return 0;
|
||||||
|
|
||||||
function insertText(view, text) {
|
return Math.min(state.doc.content.size, pos);
|
||||||
const texts = text.split('\n').filter(Boolean);
|
};
|
||||||
event.preventDefault();
|
|
||||||
view.dispatch(view.state.tr.insertText(texts[0]));
|
|
||||||
|
|
||||||
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',
|
type: 'doc',
|
||||||
content: [{ type: 'title', attrs: { cover: '' }, content: [{ type: 'text', text: texts[0] }] }].concat(
|
content: toPasteNode.content.toJSON(),
|
||||||
// @ts-ignore
|
|
||||||
texts.slice(1).map((t) => {
|
|
||||||
return {
|
|
||||||
type: 'paragraph',
|
|
||||||
attrs: { indent: 0, textAlign: 'left' },
|
|
||||||
content: [{ type: 'text', text: t }],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let tr = view.state.tr;
|
let toInsertAtTitleNode = null;
|
||||||
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());
|
|
||||||
|
|
||||||
|
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;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface IPasteOptions {
|
interface IPasteOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -116,7 +143,6 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
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');
|
||||||
|
@ -134,53 +160,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);
|
|
||||||
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 (isValidURL(text)) {
|
||||||
if (!state.selection.empty) {
|
if (!state.selection.empty) {
|
||||||
|
@ -209,43 +188,24 @@ export const Paste = Extension.create<IPasteOptions>({
|
||||||
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
|
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
|
||||||
const pasteCodeLanguage = vscodeMeta?.mode;
|
const pasteCodeLanguage = vscodeMeta?.mode;
|
||||||
|
|
||||||
if (pasteCodeLanguage && pasteCodeLanguage !== 'markdown') {
|
if (html.length > 0 || text.length === 0) {
|
||||||
event.preventDefault();
|
return htmlToProsemirror(editor, html);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 markdown
|
const { markdownToHTML } = extensionThis.options;
|
||||||
if (markdownText || isMarkdown(text)) {
|
|
||||||
console.log(text);
|
if ((markdownText || isMarkdown(text)) && markdownToHTML) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const schema = view.props.state.schema;
|
const html = markdownToHTML(normalizeMarkdown(markdownText || text));
|
||||||
const doc = markdownToProsemirror({
|
if (html && html.length) return htmlToProsemirror(editor, html, true);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInTitle(view.state)) {
|
if (files.length) {
|
||||||
if (text.length) {
|
event.preventDefault();
|
||||||
return insertText(view, text);
|
files.forEach((file) => {
|
||||||
}
|
handleFileEvent({ editor, file });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 './download';
|
||||||
export * from './editor-container-size';
|
export * from './editor-container-size';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
|
export * from './html';
|
||||||
export * from './lowlight-plugin';
|
export * from './lowlight-plugin';
|
||||||
export * from './mark';
|
export * from './mark';
|
||||||
export * from './markdown';
|
export * from './markdown';
|
||||||
|
|
Loading…
Reference in New Issue