Bootstrap 可視化HTML編輯器,summernote

Bootstrap 可視化HTML編輯器之summernote,用其官網上的介紹就是「Super Simple WYSIWYG editor」,不過在我看來,與bootstrap中文官網上提供的「bootstrap-wysiwyg」要更simple,更漂亮,更好用!javascript

雖然我以前嘗試過使用bootstrap-wysiwyg,可參照Bootstrap wysiwyg富文本數據如何保存到mysql,但過後諸葛亮的經驗告訴我,summernote絕對是更佳的富文本編輯器,這裏對其工做team點三十二個贊!!!!!css

通過一天時間的探索,對summernote有所掌握,那麼爲了更廣大前端愛好者提供便利,我將費勁一番心血來介紹一下summernote,超級福利啊。html

1、官方API和源碼下載

工欲善其事必先利其器,首先把summernote的源碼拿到以及對應官方API告訴你們是首個任務!前端

官網(demo和api) 
github源碼下載,注意下載開發版html5

2、效果圖

效果圖1 
效果圖3java

效果圖2 
效果圖2mysql

效果圖3 
效果圖1jquery

3、開講內容

大的方向爲如下三個內容:git

  1. summernote的頁面佈局(資源引入、初始參數)
  2. summernote從本地上傳圖片方法(前端onImageUpload方法、後端springMVC文件保存)
  3. summernote所在form表單的數據提交

①、summernote的頁面佈局

<!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的組件顯示怪怪的,按鈕的大小布局不一致,這裏就再也不上圖了,可是千萬注意!
  • bootstrap 的版本號最好爲v3.3.5

一、佈局div

<div class="summernote" name="description" placeholder="請對項目進行詳細的描述,使更多的人瞭解你的" action="${ctx}/file">${deal.description}</div>

相信你也看到了我爲div加上的三個屬性name、placeholder、action,那麼咱們來詳細介紹一下三個屬性的做用:github

  1. name,爲外層form表單提供summernote數據保存時的數據模型的屬性名,和input標籤的name屬性做用一致,稍候在form提交的時候具體介紹。
  2. placeholder,很直白,爲summernote提供初始狀態的文本描述,固然還須要後續加工,div顯然是不支持placeholder屬性的。
  3. action,爲圖片上傳提供後端接收地址,稍候在介紹圖片上傳onImageUpload會再次用到。

另外${deal.description}其實你不須要太多關注,和textarea的賦值的用法一致,就是單純的顯示保存後的內容。

二、summernote初始化

$('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 方法)。

  1. lang ,指定語言爲中文簡體
  2. placeholder ,summernote初始化顯示的內容。
  3. minHeight,最小高度爲300,注意這裏沒有使用height,是有緣由的,這裏稍做解釋,就不上圖了。當使用height指定高度後,假如上傳比height高的圖片,summernote就不會自動調整高度,而且前文中「效果圖3」中標出的紅色區域會不貼着圖片,而溢出到summernote外部。
  4. dialogsFade,增長summernote上彈出窗口滑進滑出的動態效果。
  5. dialogsInBody,這個屬性也很關鍵,默認爲false,字面上的意思是summernote的彈出框是否在body中(in嘛),設置爲false時,dialog的式樣會繼承其上一級外部(如上文中的form-horizontal)容器式樣,那麼顯示的效果就很彆扭,這裏也再也不上圖;那麼設置爲true時,就不會繼承上一級外部div的屬性啦,從屬於body嘛。
  6. disableDragAndDrop,設置爲false吧,有的時候拖拽會出點問題,你可實踐。

②、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); } },

二、後端springMVC文件保存

2.一、爲springMVC增長文件的配置
<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,瞭解基本的控制器

2.二、FileController.java
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, "文件上傳失敗"); } } } 
2.三、FileService.java
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) { } } 
2.四、UploadFile.java
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表單的數據提交

這裏,咱們再回顧一下summernote所在的form表單,其中還包含了一個普通file的input標籤,也就是說,該form還須要上傳一張項目封面。

<form class="form-horizontal required-validate" action="#" enctype="multipart/form-data" method="post" onsubmit="return iframeCallback(this, pageAjaxDone)">

先看一下form的屬性:

  1. enctype:」multipart/form-data」,代表爲文件類型的form保存
  2. iframeCallback方法,稍候詳細介紹,主要是對有文件上傳的form表單進行封裝。

一、iframeCallback

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; } });
  • 經過form獲取到summernote對象$this 後,經過!$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>

頁面效果爲:

這裏寫圖片描述


二、新版iframeCallback方法

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("驗證經過");

比對以前的代碼,能夠發現代碼有兩處發生了變化:

  1. 當summernote爲空時,以前沒有作在bootstrap的validator中,是由於尚未搞清楚summernote這種非input標籤在validator中的使用,下面會作詳細說明。
  2. 對summernote的內容加上了base64編碼處理,這會有不少好處,稍候介紹。

三、base64的使用方法

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")));
  1. 首先,base64的import如上,來自於javabase64.jar包。
  2. decode的編碼前端js使用的utf-8,此處天然也用utf-8。
  3. 至於StringEscapeUtils類,也是一個很是實用的工具類,有興趣的可詳細關注一下(主要能夠對html等等特殊標籤進行轉義)。

四、summernote加入到bootstrap validator中

<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>
  1. 注意data-bv-excluded=」false」(因爲summernote使用了div做爲form表單的呈現形式,非通常的input標籤,因此此處要將該name=」description」的field標識爲非excluded,默認的validator是不對「[‘:disabled’, ‘:hidden’, ‘:not(:visible)’]」三種標籤作處理的,而summernote會默認做爲disabled的一種,那麼設置上data-bv-excluded=」false」 後,validator將會對summernote作非空的判斷)、data-bv-notempty屬性。
  2. 固然有了上述兩個屬性後,並不能保證validator的有效性,那麼接下來,請繼續看。
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就完整了。

相關文章
相關標籤/搜索