quill.js 编辑器 使用问题收集

3,128 阅读2分钟

1.shift+enter 行内换行问题

问题:quill 不支持行内换行,需要自己实现。

解决:

codepen.io/OrangeManLi…

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(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],      // replace manually a tags with real ones, converting smart-quotes from google docs      [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|"|“|”)(((?!&quot;|&rdquo;|&ldquo;|"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|"|“|”)(?:(?!&gt;).)*&gt;/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(/&nbsp;/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();