1.shift+enter 行内换行问题
问题:quill 不支持行内换行,需要自己实现。
解决:
2.剪切板复制内容到编辑
1.3.x 版本 用户复制粘贴到编辑器里面的内容是,先依赖于浏览器,然后对变化的dom在更新到quill 实例中去。
2.x 版本会 onCapturePaste 会从剪切板里取数据,代码执行quill 方法插入内容。
问题: 1.3.x版本 部分浏览复制内容到编辑器会丢失图片。
使用1.3.x版本可以自己 处理 past事件。
this.quill.root.addEventListener('paste', this.handlePaste, false);
// 处理复制粘贴的内容
handlePaste(e) {
if (e.defaultPrevented || !this.quill.isEnabled()) return;
const files = Array.from(e.clipboardData.files || []);
const text = e.clipboardData.getData('text/plain');
const html = e.clipboardData.getData('text/html');
const range = this.quill.getSelection(true);
const issafariBrowser = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
if (files.length > 0) {
const toHTML = editorUtil.cleanHtml(text || html); // fix: Safari 复制一张图片出现多个图
if (issafariBrowser) {
this.upload(range, [files[0]]);
} else {
this.upload(range, files);
}
if (toHTML) {
setTimeout(() => {
const rangeNow = this.quill.getSelection();
this.quill.clipboard.dangerouslyPasteHTML(rangeNow.index, toHTML, 'silent');
}, 10);
}
e.preventDefault();
} else {
if (e.defaultPrevented || !this.quill.isEnabled()) return;
let toHTML = editorUtil.cleanHtml(html || text);
toHTML = this.fixHtml(toHTML);
if (toHTML) {
setTimeout(() => {
const rangeNow = this.quill.getSelection();
this.quill.clipboard.dangerouslyPasteHTML(rangeNow.index, toHTML, 'user');
this.quill.focus();
}, 10);
}
e.preventDefault();
}
}
3.不支持ie问题
问题: 官方文档没写支持ie浏览
发现下面api 不支持ie。
const delta = this.quill.clipboard.convert(text);
this.quill.setContents(delta, 'silent');
备注(CleanHtml 方法)
class CleanHtml { constructor() { this.cleanReplacements = [ [new RegExp(/<strong/gi), '<b'], [new RegExp(/<\/strong>/gi), '</b>'], [new RegExp(/<h-char/gi), '<span'], [new RegExp(/<\/h-char>/gi), '</span>'], [new RegExp(/<h-inner/gi), '<span'], [new RegExp(/<\/h-inner>/gi), '</span>'], [new RegExp(/<em/gi), '<i'], [new RegExp(/<\/em>/gi), '</i>'], [new RegExp(/<h[1-6]/gi), '<h4'], [new RegExp(/<\/h[1-6]>/gi), '</h4>'], [new RegExp(/<nav/gi), '<p'], [new RegExp(/<\/nav>/gi), '</p>'], [new RegExp(/<div/gi), '<p'], [new RegExp(/<\/div>/gi), '</p>'], [new RegExp(/<(button|input|font|blockquote|code|ul|li|ol|dt|dl|dd|table|tr|td|thead|tbody|tfoot|th|u)[^>]*>/gi), ''], [new RegExp(/<\/(button|input|font|blockquote|code|ul|li|ol|dt|dl|dd|table|tr|td|thead|tbody|tfoot|th|u)>/gi), ''], ]; this.cleanAttrs = ['class', 'style', 'dir', 'color', 'face', 'size', 'align', 'border', 'bgcolor', 'id', 'data-offset-key']; this.cleanTags = ['meta', 'style', 'script', 'center', 'basefont', 'frame', 'iframe', 'frameset', 'noscript']; this.document = __isBrowser__ ? window.document : {}; } handle(html) { let text = html; let workEl; const multiline = /<p|<br|<div/.test(text); const replacements = [].concat( this.createReplacements(), this.cleanReplacements || [], ); if (!text) { return ''; } for (let i = 0; i < replacements.length; i += 1) { text = text.replace(replacements[i][0], replacements[i][1]); } if (!multiline) { return this.pasteHTML(text); } // create a temporary div to cleanup block elements const tmp = this.document.createElement('div'); // double br's aren't converted to p tags, but we want paragraphs. tmp.innerHTML = `<p>${text.split('<br><br>').join('</p><p>')}</p>`; // block element cleanup const elList = tmp.querySelectorAll('a,p,div,br'); for (let i = 0; i < elList.length; i += 1) { workEl = elList[i]; // Microsoft Word replaces some spaces with newlines. // While newlines between block elements are meaningless, newlines within // elements are sometimes actually spaces. workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' '); // eslint-disable-next-line default-case switch (workEl.nodeName.toLowerCase()) { case 'p': case 'div': this.filterCommonBlocks(workEl); break; case 'br': this.filterLineBreak(workEl); break; } } return this.pasteHTML(tmp.innerHTML); } filterCommonBlocks(el) { if (/^\s*$/.test(el.innerHTML) && el.parentNode) { el.parentNode.removeChild(el); } } filterLineBreak(el) { if (this.isCommonBlock(el.previousElementSibling)) { // remove stray br's following common block elements this.removeWithParent(el); } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) { // remove br's just inside open or close tags of a div/p this.removeWithParent(el); } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') { // and br's that are the only child of elements other than div/p this.removeWithParent(el); } } isCommonBlock(el) { return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div')); } removeWithParent(el) { if (el && el.parentNode) { if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) { el.parentNode.parentNode.removeChild(el.parentNode); } else { el.parentNode.removeChild(el); } } } createReplacements() { return [ // Remove anything but the contents within the BODY element [new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''], // cleanup comments added by Chrome when pasting html [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''], // Trailing BR elements [new RegExp(/<br>$/i), ''], // replace two bogus tags that begin pastes from google docs [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''], [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''], // un-html spaces and newlines inserted by OS X [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '], [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'], // replace google docs italics+bold with a span to be replaced once the html is inserted [new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'], // replace google docs italics with a span to be replaced once the html is inserted [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'], // [replace google docs bolds with a span to be replaced once the html is inserted [new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class="replace-with bold">'], // replace manually entered b/i/a tags with real ones [new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'], // replace manually a tags with real ones, converting smart-quotes from google docs [new RegExp(/<a(?:(?!href).)+href=(?:"|”|“|"|“|”)(((?!"|”|“|"|“|”).)*)(?:"|”|“|"|“|”)(?:(?!>).)*>/gi), '<a href="$1">'], // Newlines between paragraphs in html have no syntactic value, // but then have a tendency to accidentally become additional paragraphs down the line [new RegExp(/<\/p>\n+/gi), '</p>'], [new RegExp(/\n+<p/gi), '<p'], // Microsoft Word makes these odd tags, like <o:p></o:p> [new RegExp(/<\/?o:[a-z]*>/gi), ''], // Microsoft Word adds some special elements around list items [new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1'], ]; } pasteHTML(html, options) { options = { ...options, cleanAttrs: this.cleanAttrs, cleanTags: this.cleanTags, }; let elList; let workEl; let fragmentBody; const pasteBlock = this.document.createDocumentFragment(); pasteBlock.appendChild(this.document.createElement('body')); fragmentBody = pasteBlock.querySelector('body'); fragmentBody.innerHTML = html; this.cleanupSpans(fragmentBody); elList = fragmentBody.querySelectorAll('*'); for (let i = 0; i < elList.length; i += 1) { workEl = elList[i]; this.cleanupAttrs(workEl, options.cleanAttrs); this.cleanupTags(workEl, options.cleanTags); } return fragmentBody.innerHTML.replace(/ /g, ' '); } cleanupAttrs(el, attrs) { attrs.forEach((attr) => { el.removeAttribute(attr); }); } cleanupTags(el, tags) { if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) { el.parentNode.removeChild(el); } } setTargetBlank(el, anchorUrl) { let i; const url = anchorUrl || false; if (el.nodeName.toLowerCase() === 'a') { el.target = '_blank'; } else { el = el.getElementsByTagName('a'); for (i = 0; i < el.length; i += 1) { if (url === false || url === el[i].attributes.href.value) { el[i].target = '_blank'; } } } } cleanupSpans(containerEl) { let i; let el; let newEl; let spans = containerEl.querySelectorAll('.replace-with'); const isCEF = function (el) { return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false'); }; for (i = 0; i < spans.length; i += 1) { el = spans[i]; newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i'); if (el.classList.contains('bold') && el.classList.contains('italic')) { // add an i tag as well if this has both italics and bold newEl.innerHTML = `<i>${el.innerHTML}</i>`; } else { newEl.innerHTML = el.innerHTML; } el.parentNode.replaceChild(newEl, el); } spans = containerEl.querySelectorAll('span'); for (i = 0; i < spans.length; i += 1) { el = spans[i]; // bail if span is in contenteditable = false if (this.traverseUp(el, isCEF)) { return false; } // remove empty spans, replace others with their contents this.unwrap(el, this.document); } } traverseUp(current, testElementFunction) { if (!current) { return false; } do { if (current.nodeType === 1) { if (testElementFunction(current)) { return current; } // do not traverse upwards past the nearest containing editor if (this.isMediumEditorElement(current)) { return false; } } current = current.parentNode; } while (current); return false; } isMediumEditorElement(element) { return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element'); } unwrap(el, doc) { const fragment = doc.createDocumentFragment(); const nodes = Array.prototype.slice.call(el.childNodes); // cast nodeList to array since appending child // to a different node will alter length of el.childNodes for (let i = 0; i < nodes.length; i++) { fragment.appendChild(nodes[i]); } if (fragment.childNodes.length) { el.parentNode.replaceChild(fragment, el); } else { el.parentNode.removeChild(el); } }}export default new CleanHtml();