Bootstrap 可视化HTML编辑器之summernote,用其官网上的介绍就是“Super Simple WYSIWYG editor”,只是在我看来。与bootstrap中文官网上提供的“bootstrap-wysiwyg”要更simple,更美丽,更好用!javascript
尽管我以前尝试过使用bootstrap-wysiwyg,可參照Bootstrap wysiwyg富文本数据怎样保存到mysql,但过后诸葛亮的经验告诉我。summernote绝对是更佳的富文本编辑器,这里对其工做team点三十二个赞!css
!!!!html
通过一天时间的探索,对summernote有所掌握,那么为了更广大前端爱好者提供便利,我将费劲一番心血来介绍一下summernote。超级福利啊。前端
工欲善其事必先利其器。首先把summernote的源代码拿到以及相应官方API告诉你们是首个任务!html5
官网(demo和api)
github源代码下载,注意下载开发版java
效果图1
mysql
效果图2
jquery
效果图3
git
大的方向为下面三个内容:github
<!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的组件显示怪怪的,button的大小布局不一致,这里就再也不上图了,但是千万注意!<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表单封装。具体可百度,但其有用法很是easy,例如如下
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;
@Service
public 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());
}
}
}
后端文件保存方法也很是easy,懂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="http://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),用法很是easy。下载好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就完整了。
感谢您阅读【沉默王二的博客】,假设王二的博客给您带来一丝帮助或感动。我(也就是王二)将不甚荣幸。 假设您碰巧喜欢,可以留言或者私信我,这将是我鼓捣不少其它优秀文章的最强动力。