in app/assets/javascripts/content_editor/extensions/copy_paste.js [110:230]
addProseMirrorPlugins() {
let pasteRaw = false;
const handleCutAndCopy = (view, event) => {
const slice = view.state.selection.content();
let gfmContent = this.options.serializer.serialize({ doc: slice.content });
const gfmContentWithoutSingleTableCell = gfmContent.replace(
/^<table>[\s\n]*<tr>[\s\n]*<t[hd]>|<\/t[hd]>[\s\n]*<\/tr>[\s\n]*<\/table>[\s\n]*$/gim,
'',
);
const containsSingleTableCell = !/<t[hd]>/.test(gfmContentWithoutSingleTableCell);
if (containsSingleTableCell) {
gfmContent = gfmContentWithoutSingleTableCell;
}
const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
slice.content,
);
const div = document.createElement('div');
div.appendChild(documentFragment);
event.clipboardData.setData(TEXT_FORMAT, div.innerText);
event.clipboardData.setData(HTML_FORMAT, div.innerHTML);
event.clipboardData.setData(GFM_FORMAT, gfmContent);
event.preventDefault();
event.stopPropagation();
};
return [
new Plugin({
key: new PluginKey('copyPaste'),
props: {
handleDOMEvents: {
copy: handleCutAndCopy,
cut: (view, event) => {
handleCutAndCopy(view, event);
this.editor.commands.deleteSelection();
},
},
handleKeyDown: (_, event) => {
pasteRaw = event.key === 'v' && (event.metaKey || event.ctrlKey) && event.shiftKey;
},
handlePaste: (view, event) => {
const { clipboardData } = event;
const gfmContent = clipboardData.getData(GFM_FORMAT);
const textContent = clipboardData.getData(TEXT_FORMAT);
const htmlContent = clipboardData.getData(HTML_FORMAT);
const { from, to } = view.state.selection;
const isCodeBlockActive = CODE_BLOCK_NODE_TYPES.some((type) =>
this.editor.isActive(type),
);
if (pasteRaw || isCodeBlockActive) {
const isMarkdownCodeBlockActive = this.editor.isActive(CodeBlockHighlight.name, {
language: 'markdown',
});
let contentToInsert;
if (isMarkdownCodeBlockActive) {
contentToInsert = gfmContent || textContent;
} else if (pasteRaw) {
contentToInsert = textContent.replace(/^\s+|\s+$/gm, '');
} else {
contentToInsert = textContent;
}
if (!contentToInsert) return false;
if (isCodeBlockActive) contentToInsert = { type: 'text', text: contentToInsert };
else {
contentToInsert = {
type: 'paragraph',
content: contentToInsert
.split('\n')
.map((text) => [{ type: 'text', text }, { type: 'hardBreak' }])
.flat(),
};
}
this.editor.commands.insertContentAt({ from, to }, contentToInsert);
return true;
}
if (!textContent) return false;
const hasHTML = clipboardData.types.some((type) => type === HTML_FORMAT);
const hasVsCode = clipboardData.types.some((type) => type === VS_CODE_FORMAT);
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
const language = vsCodeMeta.mode;
if (hasVsCode) {
return this.editor.commands.pasteContent(
language === 'markdown' ? textContent : `\`\`\`${language}\n${textContent}\n\`\`\``,
true,
);
}
if (gfmContent) {
return this.editor.commands.pasteContent(gfmContent, true);
}
const preStartRegex = /^<pre[^>]*lang="markdown"[^>]*>/;
const preEndRegex = /<\/pre>$/;
const htmlContentWithoutMeta = htmlContent?.replace(/^<meta[^>]*>/, '');
const pastingMarkdownBlock =
hasHTML &&
preStartRegex.test(htmlContentWithoutMeta) &&
preEndRegex.test(htmlContentWithoutMeta);
if (pastingMarkdownBlock) return this.editor.commands.pasteContent(textContent, true);
return this.editor.commands.pasteContent(hasHTML ? htmlContent : textContent, !hasHTML);
},
},
}),
];
},