Bootstrap summernote,超级漂亮的富文本编辑器

Posted 沉默王二

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Bootstrap summernote,超级漂亮的富文本编辑器相关的知识,希望对你有一定的参考价值。

Bootstrap 可视化HTML编辑器之summernote,用其官网上的介绍就是“Super Simple WYSIWYG editor”,不过在我看来,与bootstrap中文官网上提供的“bootstrap-wysiwyg”要更simple,更漂亮,更好用!

虽然我之前尝试过使用bootstrap-wysiwyg,可参照Bootstrap wysiwyg富文本数据如何保存到mysql,但事后诸葛亮的经验告诉我,summernote绝对是更佳的富文本编辑器,这里对其工作team点三十二个赞!!!!!

经过一天时间的探索,对summernote有所掌握,那么为了更广大前端爱好者提供便利,我将费劲一番心血来介绍一下summernote,超级福利啊。

##一、官方API和源码下载
工欲善其事必先利其器,首先把summernote的源码拿到以及对应官方API告诉大家是首个任务!

官网(demo和api)
github源码下载,注意下载开发版

##二、效果图
效果图1
效果图3

效果图2
效果图2

效果图3
效果图1

##三、开讲内容
大的方向为以下三个内容:

  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" οnsubmit="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

####1、布局div

<div class="summernote" name="description" placeholder="请对项目进行详细的描述,使更多的人了解你的" action="${ctx}/file">${deal.description}</div>

相信你也看到了我为div加上的三个属性name、placeholder、action,那么我们来详细介绍一下三个属性的作用:

  1. name,为外层form表单提供summernote数据保存时的数据模型的属性名,和input标签的name属性作用一致,稍候在form提交的时候具体介绍。
  2. placeholder,很直白,为summernote提供初始状态的文本描述,当然还需要后续加工,div显然是不支持placeholder属性的。
  3. action,为图片上传提供后端接收地址,稍候在介绍图片上传onImageUpload会再次用到。

另外${deal.description}其实你不需要太多关注,和textarea的赋值的用法一致,就是单纯的显示保存后的内容。

####2、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从本地上传图片方法
####1、前端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);
		}
	},

####2、后端springMVC文件保存

#####2.1、为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.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.3、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.4、UploadFile.java

package com.honzh.common.base;

import java.io以上是关于Bootstrap summernote,超级漂亮的富文本编辑器的主要内容,如果未能解决你的问题,请参考以下文章

如何将 Bootstrap 4 添加到库 django-summernote?

Summernote 未在 Bootstrap 模式上发布

Summernote 模态锁定在纯 Bootstrap 模态中

从 SummerNote WYSIWYG Bootstrap 生成的图像损坏

基于Metronic的Bootstrap开发框架经验总结(17)-- 使用 summernote插件实现HTML文档的编辑和图片插入操作

将summernote在vue项目中封装成组件