之前demo相关博客查看的,只有3.x多的版本才有这些图标显示出来。应该是某些属性是用的老的样式,新版的没研究先用这3.x版本的吧。
https://www.bootcdn.cn/font-awesome/
https://plugins.jquery.com/hotkeys/
需要引入css:
<link th:href="@{/css/bootstrap-combined.no-icons.min.css}" rel="stylesheet"/> <link th:href="@{/bootstrap-4.4.1-dist/css/bootstrap.min.css}" rel="stylesheet" type="text/css" /> <link th:href="@{/css/font-awesome.css}" rel="stylesheet"/> <link th:href="@{/css/index.css}" rel="stylesheet"/>
js:
<script th:src="@{/js/jquery-3.4.1.min.js}">script> <script th:src="@{/js/jquery.hotkeys.js}">script> <script th:src="@{/js/popper.min.js}">script> <script th:src="@{/bootstrap-4.4.1-dist/js/bootstrap.min.js}">script> <script th:src="@{/js/bootstrap-wysiwyg-define.js}">script>
整体demo页面html:
1 DOCTYPE html> 2 <html lang="en" xmlns:th=http://www.thymeleaf.org> 3 <head> 4 <meta charset="UTF-8"> 5 <title>编辑新闻详情页面title> 6 <link th:href="@{/css/bootstrap-combined.no-icons.min.css}" rel="stylesheet"/> 7 <link th:href="@{/bootstrap-4.4.1-dist/css/bootstrap.min.css}" rel="stylesheet" type="text/css" /> 8 <link th:href="@{/css/font-awesome.css}" rel="stylesheet"/> 9 <link th:href="@{/css/index.css}" rel="stylesheet"/> 10 <style> 11 12 .editor-container { /* 编辑器容器样式 */ 13 14 padding: 25px 20px 25px; 15 16 margin-bottom: 10px; 17 18 background-color: #eeeeee; 19 20 -webkit-border-radius: 6px; 21 22 -moz-border-radius: 6px; 23 24 border-radius: 6px; 25 26 } 27 28 29 30 .btn-toolbar { 31 32 font-size: 0; 33 34 margin-top: 10px; 35 36 margin-bottom: 10px; 37 38 } 39 40 41 42 #editor { /* 编辑框样式 */ 43 44 max-height: 400px; 45 46 height: 400px; 47 48 width: 100%; 49 50 background-color: white; 51 52 border-collapse: separate; 53 54 border: 1px solid rgb(204, 204, 204); 55 56 padding: 4px; 57 58 box-sizing: content-box; 59 60 -webkit-box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset; 61 62 box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset; 63 64 border-top-right-radius: 3px; 65 66 border-bottom-right-radius: 3px; 67 68 border-bottom-left-radius: 3px; 69 70 border-top-left-radius: 3px; 71 72 overflow: scroll; 73 74 outline: none; 75 76 } 77 78 79 80 /* 工具条里按钮样式,这个样式其实是.btn-default的样式, 81 82 Button按钮之所以没有直接加上.btn-default样式是因为按钮选中后wysiwyg会给选中的按钮加.btn-info样式, 83 84 .btn-default和.btn-info同时存在样式会冲突 */ 85 .btn-toolbar .btn { 86 87 88 color: #333; 89 90 background-color: #fff; 91 92 border-color: #ccc; 93 94 } 95 96 97 98 /* 这个样式其实是.btn-info的样式,重写一遍是为了提高优先级, 99 100 否则.btn-info的样式会被.btn-toolbar .btn覆盖,这个样式要写在.btn-toolbar .btn之下 */ 101 .btn-toolbar .btn-info { 102 103 color: #fff; 104 105 background-color: #5bc0de; 106 107 border-color: #46b8da; 108 109 } 110 111 style> 112 head> 113 <body> 114 <div class="btn-toolbar" data-role="editor-toolbar" data-target="#editor"> 115 <div class="btn-group"> 116 <a class="btn dropdown-toggle" data-toggle="dropdown" title="Font"><i class="icon-font">i><b class="caret">b>a> 117 <ul class="dropdown-menu"> ul> 118 div> 119 <div class="btn-group"> 120 <a class="btn dropdown-toggle" data-toggle="dropdown" title="Font Size"><i class="icon-text-height">i> <b class="caret">b>a> 121 <ul class="dropdown-menu"> 122 <li><a data-edit="fontSize 5"><font size="5">Hugefont>a>li> 123 <li><a data-edit="fontSize 3"><font size="3">Normalfont>a>li> 124 <li><a data-edit="fontSize 1"><font size="1">Smallfont>a>li> 125 ul> 126 div> 127 <div class="btn-group"> 128 <a class="btn" data-edit="bold" title="Bold (Ctrl/Cmd+B)"><i class="icon-bold">i>a> 129 <a class="btn" data-edit="italic" title="Italic (Ctrl/Cmd+I)"><i class="icon-italic">i>a> 130 <a class="btn" data-edit="strikethrough" title="Strikethrough"><i class="icon-strikethrough">i>a> 131 <a class="btn" data-edit="underline" title="Underline (Ctrl/Cmd+U)"><i class="icon-underline">i>a> 132 div> 133 <div class="btn-group"> 134 <a class="btn" data-edit="insertunorderedlist" title="Bullet list"><i class="icon-list-ul">i>a> 135 <a class="btn" data-edit="insertorderedlist" title="Number list"><i class="icon-list-ol">i>a> 136 <a class="btn" data-edit="outdent" title="Reduce indent (Shift+Tab)"><i class="icon-indent-left">i>a> 137 <a class="btn" data-edit="indent" title="Indent (Tab)"><i class="icon-indent-right">i>a> 138 div> 139 <div class="btn-group"> 140 <a class="btn" data-edit="justifyleft" title="Align Left (Ctrl/Cmd+L)"><i class="icon-align-left">i>a> 141 <a class="btn" data-edit="justifycenter" title="Center (Ctrl/Cmd+E)"><i class="icon-align-center">i>a> 142 <a class="btn" data-edit="justifyright" title="Align Right (Ctrl/Cmd+R)"><i class="icon-align-right">i>a> 143 <a class="btn" data-edit="justifyfull" title="Justify (Ctrl/Cmd+J)"><i class="icon-align-justify">i>a> 144 div> 145 <div class="btn-group"> 146 <a class="btn dropdown-toggle" data-toggle="dropdown" title="Hyperlink"><i class="icon-link">i>a> 147 <div class="dropdown-menu input-append"> 148 <input class="span2" placeholder="URL" type="text" data-edit="createLink"/> 149 <button class="btn" type="button">Addbutton> 150 div> 151 <a class="btn" data-edit="unlink" title="Remove Hyperlink"><i class="icon-cut">i>a> 152 div> 153 <div class="btn-group"> 154 <a class="btn" title="Insert picture (or just drag & drop)" id="pictureBtn"><i class="icon-picture">i>a> 155 <input type="file" id="pictureInput" name="picture" data-role="magic-overlay" data-target="#pictureBtn" data-edit="insertImage" action="/ylxtWeb/news/upload"/> 156 div> 157 <div class="btn-group"> 158 <a class="btn" data-edit="undo" title="Undo (Ctrl/Cmd+Z)"><i class="icon-undo">i>a> 159 <a class="btn" data-edit="redo" title="Redo (Ctrl/Cmd+Y)"><i class="icon-repeat">i>a> 160 div> 161 <input type="text" data-edit="inserttext" id="voiceBtn" x-webkit-speech=""> 162 div> 163 164 <div id="editor" contenteditable="true" th:text="${news?.content}">div> 165 <div style="text-align: center;margin-top: 10px;"> 166 <button class="btn-info" onclick="cancel()" >返回button> 167 <button class="btn-info" onclick="clea()" style="margin: 0 36px;">清空button> 168 <button class="btn-info" id="confirmBtn" onclick="confir()" th:attr="news_id=${news?.id}" >确定button> 169 div> 170 <form action="" id="myForm" style="display: none;"> 171 <input type="text" name="news" th:value="${news}"> 172 form> 173 body> 174 <script th:src="@{/js/jquery-3.4.1.min.js}">script> 175 176 <script th:src="@{/js/jquery.hotkeys.js}">script> 177 178 179 180 <script th:src="@{/js/popper.min.js}">script> 181 <script th:src="@{/bootstrap-4.4.1-dist/js/bootstrap.min.js}">script> 182 183 <script th:src="@{/js/bootstrap-wysiwyg-define.js}">script> 184 185 <script th:inline="javascript"> 186 function cancel(){ 187 window.location.href = baseUrl+"/news/toPageNews"; 188 } 189 function clea(){ 190 console.log(222); 191 //获取清除HTML标签后的内容: 192 $("#editor").empty(); 193 } 194 195 function confir(){ 196 //获取富文本编辑器的内容,和获取普通div内容一样。 197 var cont = $("#editor").html(); 198 console.log(cont); 199 200 var news_id = $("#confirmBtn").attr("news_id"); 201 var data_news=[[${news}]] 202 data_news.content = cont; 203 var formEle = document.getElementById("myForm"); 204 formEle.action = baseUrl+"/news/updateNews"; 205 var formData = new FormData(formEle); 206 /*Object.keys(data_news).forEach((key) => { 207 formData.append(key, data_news[key]); 208 }); 209 console.log(formData.get("title")+"5555"+formData.get("isDelete"));*/ 210 //formEle.submit(); 211 //提交表单 212 var xhr = new XMLHttpRequest(); 213 xhr.open('PUT', baseUrl+"/news", true); 214 xhr.onload = function () { 215 if (xhr.status !== 200) { 216 console.log('An error occurred!'); 217 } 218 }; 219 xhr.send(formData); 220 } 221 var baseUrl = ""; 222 $(function(){ 223 var pathName = window.location.pathname.substring(1); 224 var webName = pathName == '' ? '' : pathName.substring(0, pathName.indexOf('/')); 225 baseUrl = window.location.protocol + '//' + window.location.host+'/' + webName + '/'; 226 // 初始化工具条 227 initToolbarBootstrapBindings(); 228 $('#editor').wysiwyg(); 229 }); 230 231 // 初始化工具条 232 function initToolbarBootstrapBindings() { 233 // 字体样式 234 var fonts = [ 'Serif', 'Sans', 'Arial', 'Arial Black', 'Courier', 235 'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 236 'Lucida Grande', 'Lucida Sans', 'Tahoma', 'Times', 237 'Times New Roman', 'Verdana' ], 238 fontTarget = $('[title=Font]').siblings('.dropdown-menu'); 239 $.each(fonts,function(idx, fontName) { 240 fontTarget.append($('' + fontName 241 +'" style="font-family:\''+ fontName +'\'">' + fontName + '')); 242 }); 243 244 $('button[title]').tooltip({ 245 container : 'body' 246 }); 247 248 // .dropdown-menu下的input事件 249 $('.dropdown-menu input').click(function() { 250 return false; 251 }) 252 .change(function() { 253 $(this).parent('.dropdown-menu').siblings('.dropdown-toggle').dropdown('toggle'); 254 }) 255 .keydown('esc', function() { 256 this.value = ''; 257 $(this).change(); 258 }); 259 260 // [data-role=magic-overlay]的样式 261 $('[data-role=magic-overlay]').each(function() { 262 var overlay = $(this), target = $(overlay.data('target')); 263 overlay.css('opacity', 0).css('position', 'absolute') 264 .offset(target.offset()).width(target.outerWidth()) 265 .height(target.outerHeight()); 266 }); 267 }; 268 269 script> 270 html>
查了好多博客里边的demo的图片都是没有上传到服务器的,需要修改bootstrap-wysiwyg.js文件,原理为:选中图片后就上传到服务器,然后服务端返回图片访问链接,前端把链接写入img标签的src属性内,并放到编辑框内,
修改后的bootstrap-wysiwyg.js文件,更名为bootstrap-wysiwyg-define.js引入了thymeleaf模板页面。
/* http://github.com/mindmup/bootstrap-wysiwyg */ /*global jQuery, $, FileReader*/ /*jslint browser:true*/ (function ($) { 'use strict'; /*转码图片*/ var readFileIntoDataUrl = function (fileInfo) { var loader = $.Deferred(), //jq延迟对象 fReader = new FileReader(); fReader.onload = function (e) { loader.resolve(e.target.result); }; fReader.onerror = loader.reject; //拒绝 fReader.onprogress = loader.notify; fReader.readAsDataURL(fileInfo); //转码图片 return loader.promise(); //返回promise对象,观察某种类型被绑定到集合的所有行动,是否已被加入到队列中。 }; //一、修改原readFileIntoDataUrl方法 /*var readFileIntoDataUrl = function (fileInfo) { var loader = $.Deferred(), fReader = new FileReader(), img = ''; fReader.onload = function (e) { img = e.target.result; $.ajax({ url: ctxPath+'news/upload', type: 'post', async: false, dataType: 'json', data: {"file":fileInfo}, success: function(data){ if(data.responseCode == 1){ loader.resolve(服务器返回的图片的地址); }else if(data.responseCode == 0){ alert('上传失败'); } } }); }; fReader.onerror = loader.reject; fReader.onprogress = loader.notify; fReader.readAsDataURL(fileInfo); return loader.promise(); };*/ var uploadFileToServer = function(id, action, callback) { var fileObj = document.getElementById("pictureInput").files[0]; // js 获取文件对象 var tokenv="ssssssss"; //var data = {"id":id,"picture":fileObj}; var formData = new FormData();//document.getElementById("myForm") //formData.append("picture",$("#pictureInput")[0].files[0]); formData.append("file",fileObj); formData.append("token",tokenv); console.log(formData); $.ajax({ type: 'post', url : action, cache: false, data: formData, processData: false,//如果processData不設置為false,jquery會把formData轉換為字符串。 contentType : false,//不设置是:application/x-www-form-urlencoded; dataType : 'json', success : function(obj) { if (obj.status) { callback(obj.imgsrc); } else options.fileUploadError("server-internal-exception", obj.message); }, error : function(e) { console.log(e); //options.fileUploadErroe("upload-failure", ""); } });} /*清空内容*/ $.fn.cleanHtml = function () { var html = $(this).html(); return html && html.replace(/(
|\s|
<\/div>| )*$/, ''); }; $.fn.wysiwyg = function (userOptions) { var editor = this, //设置ui-jq='设置的插件别名的dom元素'(此句注释可忽略,是针对我的项目结构写的) selectedRange, options, toolbarBtnSelector, //更新工具栏 updateToolbar = function () { if (options.activeToolbarClass) { $(options.toolbarSelector).find(toolbarBtnSelector).each(function () { var command = $(this).data(options.commandRole); //判断光标所在位置以确定命令的状态,为真则显示为激活 if (document.queryCommandState(command)) { $(this).addClass(options.activeToolbarClass); } else { $(this).removeClass(options.activeToolbarClass); } }); } }, //插入内容 execCommand = function (commandWithArgs, valueArg) { var commandArr = commandWithArgs.split(' '), command = commandArr.shift(), args = commandArr.join(' ') + (valueArg || ''); document.execCommand(command, 0, args); updateToolbar(); }, //用jquery.hotkeys.js插件监听键盘 bindHotkeys = function (hotKeys) { $.each(hotKeys, function (hotkey, command) { editor.keydown(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); execCommand(command); } }).keyup(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); } }); }); }, //获取当前range对象 getCurrentRange = function () { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); //从当前selection对象中获得一个range对象。 } }, //保存 saveSelection = function () { selectedRange = getCurrentRange(); }, //恢复 restoreSelection = function () { var selection = window.getSelection(); //获取当前既获区,selection是对当前激活选中区(即高亮文本)进行操作 if (selectedRange) { try { //移除selection中所有的range对象,执行后anchorNode、focusNode被设置为null,不存在任何被选中的内容。 selection.removeAllRanges(); } catch (ex) { document.body.createTextRange().select(); document.selection.empty(); } //将range添加到selection当中,所以一个selection中可以有多个range。 //注意Chrome不允许同时存在多个range,它的处理方式和Firefox有些不同。 selection.addRange(selectedRange); } }, //插入文件(这里指图片) //将insertFiles方法作改写如下: insertFiles = function (files, id, action) { editor.focus(); //遍历插入(应为可以多文件插入) $.each(files, function (idx, fileInfo) { //只可插入图片文件 if (/^image\//.test(fileInfo.type)) { //转码图片 /* * $.when(readFileIntoDataUrl(fileInfo)).done(function(dataUrl) { * execCommand('insertimage', dataUrl); }).fail(function(e) { * options.fileUploadError("file-reader", e); }); */ uploadFileToServer(id, action, function(src) { execCommand('insertimage', src); }); } else { //非图片文件会调用config的错误函数 options.fileUploadError("unsupported-file-type", fileInfo.type); } }); }, //TODO 暂不了解用意 markSelection = function (input, color) { restoreSelection(); //确定命令是否被支持,返回true或false if (document.queryCommandSupported('hiliteColor')) { document.execCommand('hiliteColor', 0, color || 'transparent'); } saveSelection(); input.data(options.selectionMarker, color); }, //绑定工具栏相应工具事件 bindToolbar = function (toolbar, options) { //给所有工具栏上的控件绑定点击事件 toolbar.find(toolbarBtnSelector).click(function () { restoreSelection(); editor.focus(); //获取焦点 //设置相应配置的工具execCommand execCommand($(this).data(options.commandRole)); //保存 saveSelection(); }); //对[data-toggle=dropdown]进行单独绑定点击事件处理 字体大小 toolbar.find('[data-toggle=dropdown]').click(restoreSelection); //对input控件进行单独处理,webkitspeechchange为语音事件 toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () { var newValue = this.value; //获取input 的value this.value = ''; //清空value防止冲突 restoreSelection(); if (newValue) { editor.focus();//获取焦点 //设置相应配置的工具execCommand execCommand($(this).data(options.commandRole), newValue); } saveSelection(); }).on('focus', function () { //获取焦点 var input = $(this); if (!input.data(options.selectionMarker)) { markSelection(input, options.selectionColor); input.focus(); } }).on('blur', function () { //失去焦点 var input = $(this); if (input.data(options.selectionMarker)) { markSelection(input, false); } }); //图片按钮添加了监听器事件 toolbar.find('input[type=file][data-' + options.commandRole + ']') .change( function() { restoreSelection(); if (this.type === 'file' && this.files && this.files.length > 0) { insertFiles(this.files, $(this).attr('id'), $(this).attr('action')); } saveSelection(); this.value = ''; }); }, //初始化拖放事件 initFileDrops = function () { editor.on('dragenter dragover', false) .on('drop', function (e) { var dataTransfer = e.originalEvent.dataTransfer; e.stopPropagation(); e.preventDefault(); if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { insertFiles(dataTransfer.files); } }); }; //合并传入的配置对象userOptions和默认的配置对象config options = $.extend({}, $.fn.wysiwyg.defaults, userOptions); //设置查找字符串:a[data-edit] button[data-edit] input[type=button][data-edit] toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + ']'; //设置热键 容器有[data-role=editor-toolbar]属性的dom元素 bindHotkeys(options.hotKeys); //是否允许拖放 允许则配置拖放 if (options.dragAndDropImages) {initFileDrops();} //配置工具栏 bindToolbar($(options.toolbarSelector), options); //设置编辑区域为可编辑状态并绑定事件mouseup keyup mouseout editor.attr('contenteditable', true) .on('mouseup keyup mouseout', function () { saveSelection(); updateToolbar(); }); //编辑区域绑定图片点击事件 //TODO 这是我自己添加的,因为有时要对图片进行一些操作 editor.on('mousedown','img', function (e) { e.preventDefault(); }).on('click', 'img', function (e) { var $img = $(e.currentTarget); console.log($img); e.preventDefault(); e.stopPropagation(); }); //window绑定touchend事件 $(window).bind('touchend', function (e) { var isInside = (editor.is(e.target) || editor.has(e.target).length > 0), currentRange = getCurrentRange(), clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); if (!clear || isInside) { saveSelection(); updateToolbar(); } }); return this; }; //配置参数 $.fn.wysiwyg.defaults = { hotKeys: { //热键 应用hotkeys.js jquery插件 'ctrl+b meta+b': 'bold', 'ctrl+i meta+i': 'italic', 'ctrl+u meta+u': 'underline', 'ctrl+z meta+z': 'undo', 'ctrl+y meta+y meta+shift+z': 'redo', 'ctrl+l meta+l': 'justifyleft', 'ctrl+r meta+r': 'justifyright', 'ctrl+e meta+e': 'justifycenter', 'ctrl+j meta+j': 'justifyfull', 'shift+tab': 'outdent', 'tab': 'indent' }, toolbarSelector: '[data-role=editor-toolbar]', commandRole: 'edit', activeToolbarClass: 'btn-info', selectionMarker: 'edit-focus-marker', selectionColor: 'darkgrey', dragAndDropImages: true, //是否支持拖放,默认为支持 fileUploadError: function (reason, detail) { console.log("File upload error", reason, detail); } }; }(window.jQuery));测试图片上传成功:
后台Java接收图片方法:
/** * * @param file MultipartFile 这个类一般是用来接受前台传过来的文件 * @param request * @return */ @ResponseBody @RequestMapping(value = "/upload",method = RequestMethod.POST) public MapmultiUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request) { Map res = new HashMap (); try { String scheme = request.getScheme();//http String serverName = request.getServerName();//localhost int serverPort = request.getServerPort();//8080 String contextPath = request.getContextPath();//项目名 String url = scheme+"://"+serverName+":"+serverPort+contextPath;//http://127.0.0.1:8080/test LOGGER.info("url:"+url); String imgUrl = ""; //String imgUrl=imgPath.substring(0,imgPath.length()-2);//截取到的是"/static/",如果配置访问路径的话 String path = request.getContextPath(); String intactPath =null; String filename=file.getOriginalFilename(); String imgDir = imgServerPath.substring(imgServerPath.indexOf("file:") + 5, imgServerPath.length()); File file2 = new File(imgDir, filename); LOGGER.info("imgDir:"+imgDir); //得到原始文件的输入流 InputStream inputStream=file.getInputStream(); FileOutputStream fileOutputStream=new FileOutputStream(file2); //写出文件 IOUtils.copy(inputStream,fileOutputStream); inputStream.close(); fileOutputStream.close(); if(!file.isEmpty()){ imgUrl = url+File.separator + file.getOriginalFilename(); System.out.println(intactPath); int pre = (int) System.currentTimeMillis(); } res.put("status","succ"); res.put("imgsrc",imgUrl); } catch (Exception e) { res.put("status","error"); res.put("msg",e.getMessage()); e.printStackTrace(); LOGGER.info("multiUpload方法异常!",e); System.out.println(e.getMessage()); } return res; } 参考链接:
https://www.jb51.net/article/85252.htm