Bootstrap summernote,用其官网上的介绍就是“Super Simple WYSIWYG editor”,不过在我看来,与bootstrap中文官网上提供的“bootstrap-wysiwyg”要更simple,更漂亮,更好用!
虽然我之前尝试过使用bootstrap-wysiwyg,可参照Bootstrap wysiwyg富文本数据如何保存到mysql,但事后诸葛亮的经验告诉我,summernote绝对是更佳的富文本编辑器,这里对其工作team点三十二个赞!!!!!
经过一天时间的探索,对summernote有所掌握,那么为了更广大前端爱好者提供便利,我将费劲一番心血来介绍一下summernote,超级福利啊。
工欲善其事必先利其器,首先把summernote的源码拿到以及对应官方API告诉大家是首个任务!
官网(demo和api)
github源码下载,注意下载开发版
效果图1
效果图2
效果图3
大的方向为以下三个内容:
<!DOCTYPE html><html lang="zh-CN"><%@ include file="/components/common/taglib.jsp"%><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>summernote - bs3fa4</title> <!-- include jquery --><script type="text/javascript" src="${ctx}/components/jquery/jquery.js"></script> <!-- include libs stylesheets --> <link type="text/css" rel="stylesheet" href="${ctx}/components/bootstrap/css/bootstrap.css" /><script type="text/javascript" src="${ctx}/components/bootstrap/js/bootstrap.min.js"></script> <!-- include summernote --><link type="text/css" rel="stylesheet" href="${ctx}/components/summernote/summernote.css" /><script type="text/javascript" src="${ctx}/components/summernote/summernote.js"></script><script type="text/javascript" src="${ctx}/components/summernote/lang/summernote-zh-CN.js"></script> <script type="text/javascript"> $('div.summernote').each(function() { var $this = $(this); var placeholder = $this.attr("placeholder") || ''; var url = $this.attr("action") || ''; $this.summernote({ lang : 'zh-CN', placeholder : placeholder, minHeight : 300, dialogsFade : true,// Add fade effect on dialogs dialogsInBody : true,// Dialogs can be placed in body, not in // summernote. disableDragAndDrop : false,// default false You can disable drag // and drop callbacks : { onImageUpload : function(files) { var $files = $(files); $files.each(function() { var file = this; var data = new FormData(); data.append("file", file); $.ajax({ data : data, type : "POST", url : url, cache : false, contentType : false, processData : false, success : function(response) { var json = YUNM.jsonEval(response); YUNM.debug(json); YUNM.ajaxDone(json); if (json[YUNM.keys.statusCode] == YUNM.statusCode.ok) { // 文件不为空 if (json[YUNM.keys.result]) { var imageUrl = json[YUNM.keys.result].completeSavePath; $this.summernote('insertImage', imageUrl, function($image) { }); } } }, error : YUNM.ajaxError }); }); } } }); }); </script></head><body><div class="container"> <form class="form-horizontal required-validate" action="#" enctype="multipart/form-data" method="post" onsubmit="return iframeCallback(this, pageAjaxDone)"> <div class="form-group"> <label for="" class="col-md-2 control-label">项目封面</label> <div class="col-md-8 tl th"> <input type="file" name="image" class="projectfile" value="${deal.image}"/> <p class="help-block">支持jpg、jpeg、png、gif格式,大小不超过2.0M</p> </div> </div> <div class="form-group"> <label for="" class="col-md-2 control-label">项目详情</label> <div class="col-md-8"> <div class="summernote" name="description" placeholder="请对项目进行详细的描述,使更多的人了解你的" action="${ctx}/file">${deal.description}</div> </div> </div> </form></div></body></html>
<!DOCTYPE html>
html5的标记是必须的,注意千万不能是<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
这种doctype,否则summernote的组件显示怪怪的,按钮的大小布局不一致,这里就不再上图了,但是千万注意!<div class="summernote" name="description" placeholder="请对项目进行详细的描述,使更多的人了解你的" action="${ctx}/file">${deal.description}</div>
相信你也看到了我为div加上的三个属性name、placeholder、action,那么我们来详细介绍一下三个属性的作用:
另外${deal.description}其实你不需要太多关注,和textarea的赋值的用法一致,就是单纯的显示保存后的内容。
$('div.summernote').each(function() { var $this = $(this); var placeholder = $this.attr("placeholder") || ''; var url = $this.attr("action") || ''; $this.summernote({ lang : 'zh-CN', placeholder : placeholder, minHeight : 300, dialogsFade : true,// Add fade effect on dialogs dialogsInBody : true,// Dialogs can be placed in body, not in // summernote. disableDragAndDrop : false,// default false You can disable drag // and drop }); });
使用jQuery获取到页面上的summernote,对其进行初始化,我们来详细介绍列出参数的用法(先不介绍图片上传的onImageUpload 方法)。
假如问度娘如下的话:“onImageUpload方法怎么写?”,度娘大多会为你找到如下回答:
$(\'.summernote\').summernote({ height:300, onImageUpload: function(files, editor, welEditable) { sendFile(files[0],editor,welEditable); } }); });function sendFile(file, editor, welEditable) { data = new FormData(); data.append("file", file); url = "http://localhost/spichlerz/uploads"; $.ajax({ data: data, type: "POST", url: url, cache: false, contentType: false, processData: false, success: function (url) { editor.insertImage(welEditable, url); } });}</script>
以上资源来自于stackoverflow。
但其实呢,summernote-develop版本的summernote已经不支持这种onImageUpload写法,那么如今的写法是什么样子呢?参照summernote的官网例子。
onImageUpload
Override image upload handler(default: base64 dataURL on IMG tag). You can upload image to server or AWS S3: more…
// onImageUpload callback$('#summernote').summernote({ callbacks: { onImageUpload: function(files) { // upload image to server and create imgNode... $summernote.summernote('insertNode', imgNode); } }});// summernote.image.upload$('#summernote').on('summernote.image.upload', function(we, files) { // upload image to server and create imgNode... $summernote.summernote('insertNode', imgNode);});
那么此时onImageUpload的具体写法呢?(后端为springMVC):
callbacks : { // onImageUpload的参数为files,summernote支持选择多张图片 onImageUpload : function(files) { var $files = $(files); // 通过each方法遍历每一个file $files.each(function() { var file = this; // FormData,新的form表单封装,具体可百度,但其实用法很简单,如下 var data = new FormData(); // 将文件加入到file中,后端可获得到参数名为“file” data.append("file", file); // ajax上传 $.ajax({ data : data, type : "POST", url : url,// div上的action cache : false, contentType : false, processData : false, // 成功时调用方法,后端返回json数据 success : function(response) { // 封装的eval方法,可百度 var json = YUNM.jsonEval(response); // 控制台输出返回数据 YUNM.debug(json); // 封装方法,主要是显示错误提示信息 YUNM.ajaxDone(json); // 状态ok时 if (json[YUNM.keys.statusCode] == YUNM.statusCode.ok) { // 文件不为空 if (json[YUNM.keys.result]) { // 获取后台数据保存的图片完整路径 var imageUrl = json[YUNM.keys.result].completeSavePath; // 插入到summernote $this.summernote('insertImage', imageUrl, function($image) { // todo,后续可以对image对象增加新的css式样等等,这里默认 }); } } }, // ajax请求失败时处理 error : YUNM.ajaxError }); }); }}
注释当中加的很详细,这里把其他关联的代码一并贴出,仅供参照。
debug : function(msg) { if (this._set.debug) { if (typeof (console) != "undefined") console.log(msg); else alert(msg); } },jsonEval : function(data) { try { if ($.type(data) == 'string') return eval('(' + data + ')'); else return data; } catch (e) { return {}; } }, ajaxError : function(xhr, ajaxOptions, thrownError) { if (xhr.responseText) { $.showErr("<div>" + xhr.responseText + "</div>"); } else { $.showErr("<div>Http status: " + xhr.status + " " + xhr.statusText + "</div>" + "<div>ajaxOptions: " + ajaxOptions + "</div>" + "<div>thrownError: " + thrownError + "</div>"); } }, ajaxDone : function(json) { if (json[YUNM.keys.statusCode] == YUNM.statusCode.error) { if (json[YUNM.keys.message]) { YUNM.debug(json[YUNM.keys.message]); $.showErr(json[YUNM.keys.message]); } } else if (json[YUNM.keys.statusCode] == YUNM.statusCode.timeout) { YUNM.debug(json[YUNM.keys.message]); $.showErr(json[YUNM.keys.message] || YUNM.msg("sessionTimout"), YUNM.loadLogin); } },
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="UTF-8"> <property name="maxUploadSize" value="1024000000"></property> </bean><mvc:annotation-driven conversion-service="conversionService" /> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <!-- 这里使用string to date可以将dao在jsp到controller转换的时候直接将string格式的日期转换为date类型 --> <bean class="com.honzh.common.plugin.StringToDateConverter" /><!-- 为type为file类型的数据模型增加转换器 --> <bean class="com.honzh.common.plugin.CommonsMultipartFileToString" /> </list> </property> </bean>
这里就不做过多介绍了,可参照我之前写的SpringMVC之context-dispatcher.xml,了解基本的控制器
package com.honzh.spring.controller;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import com.honzh.common.base.UploadFile;import com.honzh.spring.service.FileService;@Controller@RequestMapping(value = "/file")public class FileController extends BaseController { private static Logger logger = Logger.getLogger(FileController.class); @Autowired private FileService fileService; @RequestMapping("") public void index(HttpServletRequest request, HttpServletResponse response) { logger.debug("获取上传文件..."); try { UploadFile uploadFiles = fileService.saveFile(request); renderJsonDone(response, uploadFiles); } catch (Exception e) { logger.error(e.getMessage()); logger.error(e.getMessage(), e); renderJsonError(response, "文件上传失败"); } }}
package com.honzh.spring.service;import java.io.IOException;import java.util.Iterator;import java.util.Map;import java.util.Random;import javax.servlet.http.HttpServletRequest;import org.apache.commons.io.FileUtils;import org.apache.log4j.Logger;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.multipart.MultipartHttpServletRequest;import com.honzh.common.Variables;import com.honzh.common.base.UploadFile;import com.honzh.common.util.DateUtil;@Servicepublic class FileService { private static Logger logger = Logger.getLogger(FileService.class); public UploadFile saveFile(HttpServletRequest request) throws IOException { logger.debug("获取上传文件..."); // 转换为文件类型的request MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; // 获取对应file对象 Map<String, MultipartFile> fileMap = multipartRequest.getFileMap(); Iterator<String> fileIterator = multipartRequest.getFileNames(); // 获取项目的相对路径(http://localhost:8080/file) String requestURL = request.getRequestURL().toString(); String prePath = requestURL.substring(0, requestURL.indexOf(Variables.ctx)); while (fileIterator.hasNext()) { String fileKey = fileIterator.next(); logger.debug("文件名为:" + fileKey); // 获取对应文件 MultipartFile multipartFile = fileMap.get(fileKey); if (multipartFile.getSize() != 0L) { validateImage(multipartFile); // 调用saveImage方法保存 UploadFile file = saveImage(multipartFile); file.setPrePath(prePath); return file; } } return null; } private UploadFile saveImage(MultipartFile image) throws IOException { String originalFilename = image.getOriginalFilename(); logger.debug("文件原始名称为:" + originalFilename); String contentType = image.getContentType(); String type = contentType.substring(contentType.indexOf("/") + 1); String fileName = DateUtil.getCurrentMillStr() + new Random().nextInt(100) + "." + type; // 封装了一个简单的file对象,增加了几个属性 UploadFile file = new UploadFile(Variables.save_directory, fileName); file.setContentType(contentType); logger.debug("文件保存路径:" + file.getSaveDirectory()); // 通过org.apache.commons.io.FileUtils的writeByteArrayToFile对图片进行保存 FileUtils.writeByteArrayToFile(file.getFile(), image.getBytes()); return file; } private void validateImage(MultipartFile image) { }}
package com.honzh.common.base;import java.io.File;import com.honzh.common.Variables;public class UploadFile { private String saveDirectory; private String fileName; private String contentType; private String prePath; private String completeSavePath; private String relativeSavePath; public UploadFile(String saveDirectory, String filesystemName) { this.saveDirectory = saveDirectory; this.fileName = filesystemName; } public String getFileName() { return fileName; } public String getSaveDirectory() { return saveDirectory; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } public String getPrePath() { if (prePath == null) { return ""; } return prePath; } public void setPrePath(String prePath) { this.prePath = prePath; setCompleteSavePath(prePath + getRelativeSavePath()); } public String getCompleteSavePath() { return completeSavePath; } public void setCompleteSavePath(String completeSavePath) { this.completeSavePath = completeSavePath; } public String getRelativeSavePath() { return relativeSavePath; } public void setRelativeSavePath(String relativeSavePath) { this.relativeSavePath = relativeSavePath; } public void setSaveDirectory(String saveDirectory) { this.saveDirectory = saveDirectory; } public void setFileName(String fileName) { this.fileName = fileName; } public File getFile() { if (getSaveDirectory() == null || getFileName() == null) { return null; } else { setRelativeSavePath(Variables.ctx + "/" + Variables.upload + "/" + getFileName()); return new File(getSaveDirectory() + "/" + getFileName()); } }}
后端文件保存方法也非常简单,懂Java的同学都可以看得懂,那么对于后端不使用springmvc的同学,你可以再找找方法。
辛苦的介绍完前两节后,我们来一个动态图看一下效果吧!
这里,我们再回顾一下summernote所在的form表单,其中还包含了一个普通file的input标签,也就是说,该form还需要上传一张项目封面。
<form class="form-horizontal required-validate" action="#" enctype="multipart/form-data" method="post" onsubmit="return iframeCallback(this, pageAjaxDone)">
先看一下form的属性:
function iframeCallback(form, callback) { YUNM.debug("带文件上传处理"); var $form = $(form), $iframe = $("#callbackframe"); var data = $form.data('bootstrapValidator'); if (data) { if (!data.isValid()) { return false; } } // 富文本编辑器 $("div.summernote", $form).each(function() { var $this = $(this); if (!$this.summernote('isEmpty')) { var editor = "<input type='hidden' name='" + $this.attr("name") + "' value='" + $this.summernote('code') + "' />"; $form.append(editor); } else { $.showErr("请填写项目详情"); return false; } }); if ($iframe.size() == 0) { $iframe = $("<iframe id='callbackframe' name='callbackframe' src='about:blank' style='display:none'></iframe>").appendTo("body"); } if (!form.ajax) { $form.append('<input type="hidden" name="ajax" value="1" />'); } form.target = "callbackframe"; _iframeResponse($iframe[0], callback || YUNM.ajaxDone);}function _iframeResponse(iframe, callback) { var $iframe = $(iframe), $document = $(document); $document.trigger("ajaxStart"); $iframe.bind("load", function(event) { $iframe.unbind("load"); $document.trigger("ajaxStop"); if (iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" || // For // Safari iframe.src == "javascript:'<html></html>';") { // For FF, IE return; } var doc = iframe.contentDocument || iframe.document; // fixing Opera 9.26,10.00 if (doc.readyState && doc.readyState != 'complete') return; // fixing Opera 9.64 if (doc.body && doc.body.innerHTML == "false") return; var response; if (doc.XMLDocument) { // response is a xml document Internet Explorer property response = doc.XMLDocument; } else if (doc.body) { try { response = $iframe.contents().find("body").text(); response = jQuery.parseJSON(response); } catch (e) { // response is html document or plain text response = doc.body.innerHTML; } } else { // response is a xml document response = doc; } callback(response); });}
贴上全部代码以供参考,但是这里我们只讲以下部分:
// 富文本编辑器 $("div.summernote", $form).each(function() { var $this = $(this); if (!$this.summernote('isEmpty')) { var editor = "<input type='hidden' name='" + $this.attr("name") + "' value='" + $this.summernote('code') + "' />"; $form.append(editor); } else { $.showErr("请填写项目详情"); return false; } });
!$this.summernote('isEmpty')
来判断用户是否对富文本编辑器有内容上的填写,保证不为空,为空时,就弹出提示信息。$this.summernote('code')
可获得summernote编辑器的html内容,将其封装到input对象中,name为前文中div提供的name,供后端使用。这里其他地方就不做多解释了,详细可参照Bootstrap wysiwyg富文本数据如何保存到mysql。
保存到数据库中是什么样子呢?
<p><img src="//localhost:8080/ymeng/upload/2016033117093076.jpeg" style=""></p><p><br></p><p>你好,有兴趣可以加入到沉默王二的群啊<br></p>
页面效果为:
var $form = $(form), $iframe = $("#callbackframe");YUNM.debug("验证其他简单组件");var data = $form.data('bootstrapValidator');if (data) { if (!data.isValid()) { return false; }}// 富文本编辑器$("div.summernote", $form).each(function() { var $this = $(this); if ($this.summernote('isEmpty')) { } else { YUNM.debug($this.summernote('code')); // 使用base64对内容进行编码 // 1.解决复制不闭合的html文档,保存后显示错乱的bug // 2.解决文本中特殊字符导致的bug var editor = "<input type='hidden' name='" + $this.attr("name") + "' value='" + $.base64.btoa($this.summernote('code')) + "' />"; $form.append(editor); }});YUNM.debug("验证通过");
比对之前的代码,可以发现代码有两处发生了变化:
js端我在Bootstrap wysiwyg富文本数据如何保存到mysql这篇文章中做了说明,此处不再说明。
可能会有同学需要JavaScript端的base64编码,而需要在springMVC后端使用base64的解码,那么此处介绍一个jar包(Java Base64.jar),使用方法很简单,下载好jar包后,就可以使用如下方法解码:
import it.sauronsoftware.base64.Base64;deal.setDescription(StringEscapeUtils.escapeHtml(Base64.decode(description, "utf-8")));
<div class="form-group"> <label for="" class="col-md-1 control-label">项目详情</label> <div class="col-md-10"> <div class="summernote" name="description" data-bv-excluded="false" data-bv-notempty placeholder="请对项目进行详细的描述,使更多的人了解你的云梦" action="${ctx}/file">${deal.description}</div> </div></div>
onChange : function(contents, $editable) { if ($this.parents().length > 0) { var $form = $this.parents().find("form.required-validate", $p); if ($form.length > 0) { var data = $form.data('bootstrapValidator'); YUNM.debug($this.summernote('isEmpty')); if ($this.summernote('isEmpty')) { data.updateStatus($this.attr("name"), 'INVALID'); } else { data.updateStatus($this.attr("name"), 'VALID'); } } }},onInit : function() { if ($this.parents().length > 0) { var $form = $this.parents().find("form.required-validate", $p); if ($form.length > 0) { var data = $form.data('bootstrapValidator'); if (!$this.summernote('isEmpty')) { data.updateStatus($this.attr("name"), 'VALID'); } } }},
在summernote的callbacks中加入onChange 、onInit,当文本域发生变化、初始化时,对summernote在form中的验证字段进行状态的更新,validator中使用updateStatus方法。
/** * Update all validating results of field * * @param {String|jQuery} field The field name or field element * @param {String} status The status. Can be 'NOT_VALIDATED', 'VALIDATING', 'INVALID' or 'VALID' * @param {String} [validatorName] The validator name. If null, the method updates validity result for all validators * @returns {BootstrapValidator} */ updateStatus: function(field, status, validatorName) {
OK,等补上以上两个内容后,整个summernote就完整了。
感谢您阅读【沉默王二的博客】,如果王二的博客给您带来一丝帮助或感动,我(也就是王二)将不甚荣幸。
如果您碰巧喜欢,可以留言或者私信我,这将是我鼓捣更多优秀文章的最强动力。
联系客服