日前项目需要在移动端增加富文本编辑,上网找了下,大多数都是针对pc版的,不太兼容手机,当然由于手机屏幕小等原因也限制富文本编辑器的众多强大功能,所以要找的编辑器功能必须是精简的。
找了好久,发现qeditor比较精简,操作简单,唯一缺点是上传图片时只能填写url,不能直接从手机上传。
针对这点,自己决定动手修改。
修改思路:
1、创建文件上传输入框
2、点击编辑器上传图片按钮时,触发文件输入框点击事件
3、选择图片后异步上传至服务器,返回图片路径
4、编辑器插入img标签,显示图片
以下是修改过程:
上图了解下原来是怎么样的,这个是qeditor的界面,qeditor的样式可以自己修改:
点击上传图片按钮后是弹框要求输入图片url的 :
以下是改造后的效果,点击图片上传按钮显示的是现在手机相册图片 :
选择后上传图片:
qeditor的代码只有200多行,相当简洁,以下是原始代码:
// Generated by CoffeeScript 1.6.3 /* jquery.qeditor ============== This is a simple WYSIWYG editor with jQuery. ## Author: Jason Lee <huacnlee@gmail.com> ## Requirements: [jQuery](http://jquery.com) (Font-Awesome)[http://fortawesome.github.io/Font-Awesome/] - Toolbar icons ## Usage: $("textarea").qeditor(); and then you need filt the html tags,attributes in you content page. In Rails application, you can use like this: <%= sanitize(@post.body,:tags => %w(strong b i u strike ol ul li address blockquote pre code br div p), :attributes => %w(src)) %> */ var QEDITOR_ALLOW_TAGS_ON_PASTE, QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE, QEDITOR_TOOLBAR_HTML; QEDITOR_TOOLBAR_HTML = "<div class="qeditor_toolbar"> <a href="#" data-action="bold" class="qe-bold"><span class="fa fa-bold" title="Bold"></span></a> <a href="#" data-action="italic" class="qe-italic"><span class="fa fa-italic" title="Italic"></span></a> <a href="#" data-action="underline" class="qe-underline"><span class="fa fa-underline" title="Underline"></span></a> <a href="#" data-action="strikethrough" class="qe-strikethrough"><span class="fa fa-strikethrough" title="Strike-through"></span></a> <span class="vline"></span> <span class="qe-icon qe-heading"> <ul class="qe-menu"> <li><a href="#" data-name="h1" class="qe-h1">Heading 1</a></li> <li><a href="#" data-name="h2" class="qe-h2">Heading 2</a></li> <li><a href="#" data-name="h3" class="qe-h3">Heading 3</a></li> <li><a href="#" data-name="h4" class="qe-h4">Heading 4</a></li> <li><a href="#" data-name="h5" class="qe-h5">Heading 5</a></li> <li><a href="#" data-name="h6" class="qe-h6">Heading 6</a></li> <li class="qe-hline"></li> <li><a href="#" data-name="p" class="qe-p">Paragraph</a></li> </ul> <span class="icon fa fa-font"></span> </span> <span class="vline"></span> <a href="#" data-action="insertorderedlist" class="qe-ol"><span class="fa fa-list-ol" title="Insert Ordered-list"></span></a> <a href="#" data-action="insertunorderedlist" class="qe-ul"><span class="fa fa-list-ul" title="Insert Unordered-list"></span></a> <a href="#" data-action="indent" class="qe-indent"><span class="fa fa-indent" title="Indent"></span></a> <a href="#" data-action="outdent" class="qe-outdent"><span class="fa fa-outdent" title="Outdent"></span></a> <span class="vline"></span> <a href="#" data-action="insertHorizontalRule" class="qe-hr"><span class="fa fa-minus" title="Insert Horizontal Rule"></span></a> <a href="#" data-action="blockquote" class="qe-blockquote"><span class="fa fa-quote-left" title="Blockquote"></span></a> <a href="#" data-action="pre" class="qe-pre"><span class="fa fa-code" title="Pre"></span></a> <a href="#" data-action="createLink" class="qe-link"><span class="fa fa-link" title="Create Link" title="Create Link"></span></a> <a href="#" data-action="insertimage" class="qe-image"><span class="fa fa-picture-o" title="Insert Image"></span></a> <a href="#" onclick="return QEditor.toggleFullScreen(this);" class="qe-fullscreen pull-right"><span class="fa fa-arrows-alt" title="Toggle Fullscreen"></span></a> </div>"; QEDITOR_ALLOW_TAGS_ON_PASTE = "div,p,ul,ol,li,hr,br,b,strong,i,em,img,h2,h3,h4,h5,h6,h7"; QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE = ["style", "class", "id", "name", "width", "height"]; window.QEditor = { actions: ['bold', 'italic', 'underline', 'strikethrough', 'insertunorderedlist', 'insertorderedlist', 'blockquote', 'pre'], action: function(el, a, p) { var editor; editor = $(".qeditor_preview", $(el).parent().parent()); editor.find(".qeditor_placeholder").remove(); editor.focus(); if (p === null) { p = false; } if (a === "blockquote" || a === "pre") { p = a; a = "formatBlock"; } if (a === "createLink") { p = prompt("Type URL:"); if (p.trim().length === 0) { return false; } } else if (a === "insertimage") { p = prompt("Image URL:"); if (p.trim().length === 0) { return false; } } if (QEditor.state(a)) { document.execCommand(a, false, null); } else { document.execCommand(a, false, p); } QEditor.checkSectionState(editor); editor.change(); return false; }, state: function(action) { return document.queryCommandState(action) === true; }, prompt: function(title) { var val; val = prompt(title); if (val) { return val; } else { return false; } }, toggleFullScreen: function(el) { var border; border = $(el).parent().parent(); if (border.data("qe-fullscreen") === "1") { QEditor.exitFullScreen(); } else { QEditor.enterFullScreen(border); } return false; }, enterFullScreen: function(border) { border.data("qe-fullscreen", "1").addClass("qeditor_fullscreen"); border.find(".qeditor_preview").focus(); return border.find(".qe-fullscreen span").attr("class", "fa fa-compress"); }, exitFullScreen: function() { return $(".qeditor_border").removeClass("qeditor_fullscreen").data("qe-fullscreen", "0").find(".qe-fullscreen span").attr("class", "fa fa-arrows-alt"); }, getCurrentContainerNode: function() { var containerNode, node; if (window.getSelection) { node = window.getSelection().anchorNode; containerNode = node.nodeType === 3 ? node.parentNode : node; } return containerNode; }, checkSectionState: function(editor) { var a, link, _i, _len, _ref, _results; _ref = QEditor.actions; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; link = editor.parent().find(".qeditor_toolbar a[data-action=" + a + "]"); if (QEditor.state(a)) { _results.push(link.addClass("qe-state-on")); } else { _results.push(link.removeClass("qe-state-on")); } } return _results; }, version: function() { return "0.2.0"; } }; (function($) { return $.fn.qeditor = function(options) { return this.each(function() { var currentVal, editor, obj, placeholder, qe_heading, toolbar; obj = $(this); obj.addClass("qeditor"); editor = $('<div class="qeditor_preview clearfix" contentEditable="true"></div>'); placeholder = $('<div class="qeditor_placeholder"></div>'); $(document).keyup(function(e) { if (e.keyCode === 27) { return QEditor.exitFullScreen(); } }); document.execCommand('defaultParagraphSeparator', false, 'p'); currentVal = obj.val(); editor.html(currentVal); editor.addClass(obj.attr("class")); obj.after(editor); placeholder.text(obj.attr("placeholder")); editor.attr("placeholder", obj.attr("placeholder") || ""); editor.append(placeholder); editor.focusin(function() { QEditor.checkSectionState(editor); return $(this).find(".qeditor_placeholder").remove(); }); editor.blur(function() { var t; t = $(this); QEditor.checkSectionState(editor); if (t.html().length === 0 || t.html() === "<br>" || t.html() === "<p></p>") { return $(this).html('<div class="qeditor_placeholder">' + $(this).attr("placeholder") + '</div>'); } }); editor.change(function() { var pobj, t; pobj = $(this); t = pobj.parent().find('.qeditor'); return t.val(pobj.html()); }); editor.on("paste", function() { var txt; txt = $(this); return setTimeout(function() { var attrName, els, _i, _len; els = txt.find("*"); for (_i = 0, _len = QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE.length; _i < _len; _i++) { attrName = QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE[_i]; els.removeAttr(attrName); } els.find(":not(" + QEDITOR_ALLOW_TAGS_ON_PASTE + ")").contents().unwrap(); txt.change(); return true; }, 100); }); editor.keyup(function(e) { QEditor.checkSectionState(editor); return $(this).change(); }); editor.on("click", function(e) { QEditor.checkSectionState(editor); return e.stopPropagation(); }); editor.keydown(function(e) { var node, nodeName; node = QEditor.getCurrentContainerNode(); nodeName = ""; if (node && node.nodeName) { nodeName = node.nodeName.toLowerCase(); } if (e.keyCode === 13 && !(e.shiftKey || e.ctrlKey)) { if (nodeName === "blockquote" || nodeName === "pre") { e.stopPropagation(); document.execCommand('InsertParagraph', false); document.execCommand("formatBlock", false, "p"); document.execCommand('outdent', false); return false; } } }); obj.hide(); obj.wrap('<div class="qeditor_border"></div>'); obj.after(editor); toolbar = $(QEDITOR_TOOLBAR_HTML); qe_heading = toolbar.find(".qe-heading"); qe_heading.mouseenter(function() { $(this).addClass("hover"); return $(this).find(".qe-menu").show(); }); qe_heading.mouseleave(function() { $(this).removeClass("hover"); return $(this).find(".qe-menu").hide(); }); toolbar.find(".qe-heading .qe-menu a").click(function() { var link; link = $(this); link.parent().parent().hide(); QEditor.action(this, "formatBlock", link.data("name")); return false; }); toolbar.find("a[data-action]").click(function() { return QEditor.action(this, $(this).attr("data-action")); }); return editor.before(toolbar); }); }; })(jQuery);
修改完成后的代码:
// Generated by CoffeeScript 1.6.3 /* jquery.qeditor ============== This is a simple WYSIWYG editor with jQuery. ## Author: Jason Lee <huacnlee@gmail.com> ## Requirements: [jQuery](http://jquery.com) (Font-Awesome)[http://fortawesome.github.io/Font-Awesome/] - Toolbar icons ## Usage: $("textarea").qeditor(); and then you need filt the html tags,attributes in you content page. In Rails application, you can use like this: <%= sanitize(@post.body,:tags => %w(strong b i u strike ol ul li address blockquote pre code br div p), :attributes => %w(src)) %> */ var QEDITOR_ALLOW_TAGS_ON_PASTE, QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE, QEDITOR_TOOLBAR_HTML; QEDITOR_TOOLBAR_HTML = "<div class="qeditor_toolbar"> <a href="#" data-action="bold" class="qe-bold"><span class="fa fa-bold" title="Bold"></span></a> <a href="#" data-action="italic" class="qe-italic"><span class="fa fa-italic" title="Italic"></span></a> <a href="#" data-action="underline" class="qe-underline"><span class="fa fa-underline" title="Underline"></span></a> <a href="#" data-action="strikethrough" class="qe-strikethrough"><span class="fa fa-strikethrough" title="Strike-through"></span></a> <span class="vline"></span> <span class="qe-icon qe-heading"> <ul class="qe-menu"> <li><a href="#" data-name="h1" class="qe-h1">Heading 1</a></li> <li><a href="#" data-name="h2" class="qe-h2">Heading 2</a></li> <li><a href="#" data-name="h3" class="qe-h3">Heading 3</a></li> <li><a href="#" data-name="h4" class="qe-h4">Heading 4</a></li> <li><a href="#" data-name="h5" class="qe-h5">Heading 5</a></li> <li><a href="#" data-name="h6" class="qe-h6">Heading 6</a></li> <li class="qe-hline"></li> <li><a href="#" data-name="p" class="qe-p">Paragraph</a></li> </ul> <span class="icon fa fa-font"></span> </span> <span class="vline"></span> <a href="#" data-action="insertorderedlist" class="qe-ol"><span class="fa fa-list-ol" title="Insert Ordered-list"></span></a> <a href="#" data-action="insertunorderedlist" class="qe-ul"><span class="fa fa-list-ul" title="Insert Unordered-list"></span></a> <a href="#" data-action="indent" class="qe-indent"><span class="fa fa-indent" title="Indent"></span></a> <a href="#" data-action="outdent" class="qe-outdent"><span class="fa fa-outdent" title="Outdent"></span></a> <span class="vline"></span> <a href="#" data-action="insertHorizontalRule" class="qe-hr"><span class="fa fa-minus" title="Insert Horizontal Rule"></span></a> <a href="#" data-action="blockquote" class="qe-blockquote"><span class="fa fa-quote-left" title="Blockquote"></span></a> <a href="#" data-action="pre" class="qe-pre"><span class="fa fa-code" title="Pre"></span></a> <a href="#" data-action="createLink" class="qe-link"><span class="fa fa-link" title="Create Link" title="Create Link"></span></a> <a href="#" data-action="insertimage" class="qe-image"><span class="fa fa-picture-o" title="Insert Image"></span></a> <a href="#" onclick="return QEditor.toggleFullScreen(this);" class="qe-fullscreen pull-right"><span class="fa fa-arrows-alt" title="Toggle Fullscreen"></span></a> </div>"; QEDITOR_ALLOW_TAGS_ON_PASTE = "div,p,ul,ol,li,hr,br,b,strong,i,em,img,h2,h3,h4,h5,h6,h7"; QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE = ["style", "class", "id", "name", "width", "height"]; window.QEditor = { actions: ['bold', 'italic', 'underline', 'strikethrough', 'insertunorderedlist', 'insertorderedlist', 'blockquote', 'pre'], action: function(el, a, p) { var editor; editor = $(".qeditor_preview", $(el).parent().parent()); editor.find(".qeditor_placeholder").remove(); editor.focus(); if (p === null) { p = false; } if (a === "blockquote" || a === "pre") { p = a; a = "formatBlock"; } if (a === "createLink") { p = prompt("Type URL:"); if (p.trim().length === 0) { return false; } } else if (a === "insertimage") { //p = prompt("Image URL:"); //TODO var input; if(document.getElementById('inImgId')){ input = document.getElementById('inImgId'); }else{ input = document.createElement('input'); input.setAttribute('id', 'inImgId'); input.setAttribute('type', 'file'); input.setAttribute('name', 'file'); input.setAttribute('accept', 'image/gif, image/jpeg, image/jpg, image/png'); document.body.appendChild(input); input.style.display = 'none'; } input.click(); input.onchange = function(){ if(!input.value){return;} var fd = new FormData(); var file; file = input.files[0]; fd.append('file', file); $.ajax({ url : window.location.protocol + '//' + window.location.host + '/weixin/uploadArticlePic', data : fd, processData : false, contentType : false, enctype : 'multipart/form-data', type : 'POST', success : function(data) { var json = JSON.parse(data); if (json.success) { QEditor.imageChange(json.data); } else { alert(json.message); } } }); } if (p == null || p.trim().length === 0) { return false; } } if (QEditor.state(a)) { document.execCommand(a, false, null); } else { document.execCommand(a, false, p); } QEditor.checkSectionState(editor); editor.change(); return false; }, state: function(action) { return document.queryCommandState(action) === true; }, prompt: function(title) { var val; val = prompt(title); if (val) { return val; } else { return false; } }, toggleFullScreen: function(el) { var border; border = $(el).parent().parent(); if (border.data("qe-fullscreen") === "1") { QEditor.exitFullScreen(); } else { QEditor.enterFullScreen(border); } return false; }, enterFullScreen: function(border) { border.data("qe-fullscreen", "1").addClass("qeditor_fullscreen"); border.find(".qeditor_preview").focus(); return border.find(".qe-fullscreen span").attr("class", "fa fa-compress"); }, exitFullScreen: function() { return $(".qeditor_border").removeClass("qeditor_fullscreen").data("qe-fullscreen", "0").find(".qe-fullscreen span").attr("class", "fa fa-arrows-alt"); }, getCurrentContainerNode: function() { var containerNode, node; if (window.getSelection) { node = window.getSelection().anchorNode; containerNode = node.nodeType === 3 ? node.parentNode : node; } return containerNode; }, checkSectionState: function(editor) { var a, link, _i, _len, _ref, _results; _ref = QEditor.actions; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; link = editor.parent().find(".qeditor_toolbar a[data-action=" + a + "]"); if (QEditor.state(a)) { _results.push(link.addClass("qe-state-on")); } else { _results.push(link.removeClass("qe-state-on")); } } return _results; }, imageChange: function(imgUrl) { var editor = $(".qeditor_preview", $(".qeditor_border")); editor.focus(); document.execCommand("insertimage", false, imgUrl); QEditor.checkSectionState(editor); editor.change(); }, version: function() { return "0.2.0"; } }; (function($) { return $.fn.qeditor = function(params) { // 控制上传图片按钮显示与否 Add by C.Q 20160803 var defaults = { showImage: true // true:显示上传图片按钮;false:不显示 }; params = $.extend({}, defaults, params); return this.each(function() { var currentVal, editor, obj, placeholder, qe_heading, toolbar; obj = $(this); obj.addClass("qeditor"); editor = $('<div class="qeditor_preview clearfix" contentEditable="true"></div>'); placeholder = $('<div class="qeditor_placeholder"></div>'); $(document).keyup(function(e) { if (e.keyCode === 27) { return QEditor.exitFullScreen(); } }); document.execCommand('defaultParagraphSeparator', false, 'p'); currentVal = obj.val(); editor.html(currentVal); editor.addClass(obj.attr("class")); obj.after(editor); placeholder.text(obj.attr("placeholder")); editor.attr("placeholder", obj.attr("placeholder") || ""); editor.append(placeholder); editor.focusin(function() { QEditor.checkSectionState(editor); return $(this).find(".qeditor_placeholder").remove(); }); editor.blur(function() { var t; t = $(this); QEditor.checkSectionState(editor); if (t.html().length === 0 || t.html() === "<br>" || t.html() === "<p></p>") { return $(this).html('<div class="qeditor_placeholder">' + $(this).attr("placeholder") + '</div>'); } }); editor.change(function() { var pobj, t; pobj = $(this); t = pobj.parent().find('.qeditor'); return t.val(pobj.html()); }); editor.on("paste", function() { var txt; txt = $(this); return setTimeout(function() { var attrName, els, _i, _len; els = txt.find("*"); for (_i = 0, _len = QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE.length; _i < _len; _i++) { attrName = QEDITOR_DISABLE_ATTRIBUTES_ON_PASTE[_i]; els.removeAttr(attrName); } els.find(":not(" + QEDITOR_ALLOW_TAGS_ON_PASTE + ")").contents().unwrap(); txt.change(); return true; }, 100); }); editor.keyup(function(e) { QEditor.checkSectionState(editor); return $(this).change(); }); editor.on("click", function(e) { QEditor.checkSectionState(editor); return e.stopPropagation(); }); editor.keydown(function(e) { var node, nodeName; node = QEditor.getCurrentContainerNode(); nodeName = ""; if (node && node.nodeName) { nodeName = node.nodeName.toLowerCase(); } if (e.keyCode === 13 && !(e.shiftKey || e.ctrlKey)) { if (nodeName === "blockquote" || nodeName === "pre") { e.stopPropagation(); document.execCommand('InsertParagraph', false); document.execCommand("formatBlock", false, "p"); document.execCommand('outdent', false); return false; } } }); obj.hide(); obj.wrap('<div class="qeditor_border"></div>'); obj.after(editor); // 控制图片显示与否 Add by C.Q 20160803 if (params.showImage == false) { QEDITOR_TOOLBAR_HTML = QEDITOR_TOOLBAR_HTML.replace('<a href="#" data-action="insertimage" class="qe-image"><span class="fa fa-picture-o" title="Insert Image"></span></a>', ""); } toolbar = $(QEDITOR_TOOLBAR_HTML); qe_heading = toolbar.find(".qe-heading"); qe_heading.mouseenter(function() { $(this).addClass("hover"); return $(this).find(".qe-menu").show(); }); qe_heading.mouseleave(function() { $(this).removeClass("hover"); return $(this).find(".qe-menu").hide(); }); toolbar.find(".qe-heading .qe-menu a").click(function() { var link; link = $(this); link.parent().parent().hide(); QEditor.action(this, "formatBlock", link.data("name")); return false; }); toolbar.find("a[data-action]").click(function() { return QEditor.action(this, $(this).attr("data-action")); }); return editor.before(toolbar); }); }; })(jQuery);
在这里我就不解读其它的代码功能了,主要讲解下修改部分:
1、在window.QEditor的action方法中有一处判断是否点击图片上传按钮的
else if (a === "insertimage") { p = prompt("Image URL:"); if (p.trim().length === 0) { return false; } }
从这里入手,根据思路进行相应改造
else if (a === "insertimage") { //p = prompt("Image URL:"); var input; if(document.getElementById('inImgId')){ input = document.getElementById('inImgId'); }else{ input = document.createElement('input'); input.setAttribute('id', 'inImgId'); input.setAttribute('type', 'file'); input.setAttribute('name', 'file'); input.setAttribute('accept', 'image/gif, image/jpeg, image/jpg, image/png'); document.body.appendChild(input); input.style.display = 'none'; } input.click(); input.onchange = function(){ if(!input.value){return;} var fd = new FormData(); var file; file = input.files[0]; fd.append('file', file); $.ajax({ url : window.location.protocol + '//' + window.location.host + '/weixin/uploadArticlePic', data : fd, processData : false, contentType : false, enctype : 'multipart/form-data', type : 'POST', success : function(data) { var json = JSON.parse(data); if (json.success) { QEditor.imageChange(json.data); } else { alert(json.message); } } }); } if (p == null || p.trim().length === 0) { return false; } }
注意到代码中上传图片成功后执行的 QEditor.imageChange(json.data)方法。
这是我加上去的,目的是使编辑器插入图片,并改变编辑器的值(注意qeditor是由textarea和预览div组成,插入图片是插入到预览div中,并不存在textarea中,而取值却是从textarea中取,所以原作者以增加change()方法解决此问题,本人加入的QEditor.imageChange(json.data)同样是为解决这个问题)
imageChange: function(imgUrl) { var editor = $(".qeditor_preview", $(".qeditor_border")); editor.focus(); document.execCommand("insertimage", false, imgUrl); QEditor.checkSectionState(editor); editor.change(); }
至此修改完毕。
经测试。。。。
出现各种各样问题。。。。。图片旋转的、ip4拍照闪退、图片过大等。。。
后续优化:
1、加入图片压缩,减少服务器带宽压力
2、解决图片旋转问题
3、加入进度条
4、等