Browse Source

add froala (#945)

* update images upload

* modify js res.url

* add froala

* froala-editor.js
hotqin888 1 year ago
parent
commit
0ce6c5bbf9

+ 2 - 1
controllers/BookController.go

@@ -164,7 +164,8 @@ func (c *BookController) SaveBook() {
 	if !models.NewItemsets().Exist(itemId) {
 		c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_space_not_exist"))
 	}
-	if editor != EditorMarkdown && editor != EditorCherryMarkdown && editor != EditorHtml && editor != EditorNewHtml {
+	// if editor != EditorMarkdown && editor != EditorCherryMarkdown && editor != EditorHtml && editor != EditorNewHtml {
+	if editor != EditorMarkdown && editor != EditorCherryMarkdown && editor != EditorHtml && editor != EditorNewHtml && editor != EditorFroala {
 		editor = EditorMarkdown
 	}
 

+ 18 - 5
controllers/DocumentController.go

@@ -503,8 +503,15 @@ func (c *DocumentController) Upload() {
 		name = "editormd-image-file"
 		files, err = c.GetFiles(name)
 		if err == http.ErrMissingFile {
-			c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty"))
-			return
+			// c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty"))
+			// return
+			name = "file"
+			files, err = c.GetFiles(name)
+			// logs.Info(files)
+			if err == http.ErrMissingFile {
+				c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty"))
+				return
+			}
 		}
 	}
 
@@ -515,6 +522,7 @@ func (c *DocumentController) Upload() {
 	// jMap := make(map[string]interface{})
 	// s := []map[int]interface{}{}
 	result2 := []map[string]interface{}{}
+	var result map[string]interface{}
 	for i, _ := range files {
 		//for each fileheader, get a handle to the actual file
 		file, err := files[i].Open()
@@ -680,19 +688,24 @@ func (c *DocumentController) Upload() {
 				c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed"))
 			}
 		}
-		result := map[string]interface{}{
+		result = map[string]interface{}{
 			"errcode":   0,
 			"success":   1,
 			"message":   "ok",
 			"url":       attachment.HttpPath,
+			"link":      attachment.HttpPath,
 			"alt":       attachment.FileName,
 			"is_attach": isAttach,
 			"attach":    attachment,
 		}
 		result2 = append(result2, result)
 	}
-
-	c.Ctx.Output.JSON(result2, true, false)
+	if name == "file" {
+		// froala单图片上传
+		c.Ctx.Output.JSON(result, true, false)
+	} else {
+		c.Ctx.Output.JSON(result2, true, false)
+	}
 	c.StopRun()
 }
 

+ 1 - 0
controllers/const.go

@@ -6,4 +6,5 @@ const (
 	EditorCherryMarkdown = "cherry_markdown"
 	EditorHtml           = "html"
 	EditorNewHtml        = "new_html"
+	EditorFroala         = "froala"
 )

+ 496 - 0
static/js/froala-editor.js

@@ -0,0 +1,496 @@
+$(function () {
+    //超大屏幕
+    var toolbarButtons = ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', 'fontFamily', 'fontSize', '|', 'color', 'emoticons', 'inlineStyle', 'paragraphStyle', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', '-', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting', 'selectAll', 'html'];
+    //大屏幕
+    var toolbarButtonsMD = ['fullscreen', 'bold', 'italic', 'underline', 'fontFamily', 'fontSize', 'color', 'paragraphStyle', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting'];
+    //小屏幕
+    var toolbarButtonsSM = ['fullscreen', 'bold', 'italic', 'underline', 'fontFamily', 'fontSize', 'insertLink', 'insertImage', 'insertTable', 'undo', 'redo'];
+    //手机
+    var toolbarButtonsXS = ['bold', 'italic', 'fontFamily', 'fontSize', 'undo', 'redo'];
+
+    window.addDocumentModalFormHtml = $(this).find("form").html();
+    window.editor = new FroalaEditor("#froalaEditor", {
+      // enter: $.FroalaEditor.ENTER_P,
+      placeholderText: '请输入内容',
+      charCounterCount: true, //默认
+      // charCounterMax         : -1,//默认
+      saveInterval: 0, //不自动保存,默认10000
+      // theme                    : "red",
+      height: "100%",
+      toolbarBottom: false, //默认
+      toolbarButtonsMD: toolbarButtonsMD,
+      toolbarButtonsSM: toolbarButtonsSM,
+      toolbarButtonsXS: toolbarButtonsXS,
+      toolbarInline: false, //true选中设置样式,默认false
+      imageUploadMethod: 'POST',
+      // heightMin: 450,
+      charCounterMax: 3000,
+      // imageUploadURL: "uploadImgEditor",
+      // imageParams: { postId: "123" },
+      params: {
+        // acl: '01',
+        // AWSAccessKeyId: '02',
+        // policy: '03',
+        // signature: '04',
+        editor : "froalaEditor",
+      },
+      autosave: true,
+      autosaveInterval: 2500,
+      // saveURL: 'hander/FroalaHandler.ashx',
+      saveParams: { postId: '1' },
+      spellcheck: false,
+      imageUploadURL: window.imageUploadURL, //'/uploadimg', //上传到本地服务器
+      // imageUploadParams: { pid: '{{.product.ProjectId}}' },
+      // imageDeleteURL: 'lib/delete_image.php', //删除图片
+      // imagesLoadURL: 'lib/load_images.php', //管理图片
+      videoUploadURL: window.imageUploadURL,
+      // videoUploadParams: { pid: '{{.product.ProjectId}}' },
+      fileUploadURL: window.imageUploadURL,
+      // fileUploadParams: { pid: '{{.product.ProjectId}}' },
+      // enter: $.FroalaEditor.ENTER_BR,
+      language: 'zh_cn',
+        // Add the custom buttons in the toolbarButtons list, after the separator.
+      toolbarButtons: [toolbarButtons, ['saveIcon', 'insert','alert']]
+      // toolbarButtons: ['bold', 'italic', 'underline', 'paragraphFormat', 'align','color','fontSize','insertImage','insertTable','undo', 'redo']
+    });
+
+    FroalaEditor.DefineIcon('alert', {NAME: 'info', SVG_KEY: 'user'});
+    FroalaEditor.RegisterCommand('alert', {
+      title: 'Hello',
+      focus: false,
+      undo: false,
+      refreshAfterCallback: false,
+      callback: function () {
+        alert('Hello!');
+      }
+    });
+
+    // FroalaEditor.DefineIcon('magicIcon', {NAME: 'magic'});
+    FroalaEditor.DefineIcon('saveIcon', {NAME: 'plus', SVG_KEY: 'cogs'});
+    FroalaEditor.RegisterCommand('saveIcon', {
+      title: '保存',
+      focus: false,
+      undo: true,
+      refreshAfterCallback: true,
+      callback: function () {
+        saveDocument();
+      }
+    });
+    
+    FroalaEditor.DefineIcon('insert', {NAME: 'plus', SVG_KEY: 'add'});
+    FroalaEditor.RegisterCommand('insert', {
+      title: '发布',
+      focus: true,
+      undo: true,
+      refreshAfterCallback: true,
+      callback: function () {
+        // this.html.insert('My New HTML');
+        // saveDocument(true,callback);
+        releaseBook();
+      }
+    });
+    
+    // new FroalaEditor('div#froala-editor', {
+    //   // Add the custom buttons in the toolbarButtons list, after the separator.
+    //   toolbarButtons: [['undo', 'redo' , 'bold'], ['alert', 'clear', 'insert']]
+    // })
+
+    // editor.config.mapAk = window.baiduMapKey;
+    // editor.config.printLog = false;
+    // editor.config.showMenuTooltips = true
+    // editor.config.menuTooltipPosition = 'down'
+    // editor.config.uploadImgUrl = window.imageUploadURL;
+    // editor.config.uploadImgFileName = "editormd-image-file";
+    // editor.config.uploadParams = {
+    //     "editor" : "froalaEditor"
+    // };
+    // editor.config.uploadImgServer = window.imageUploadURL;
+    // editor.config.customUploadImg = function (resultFiles, insertImgFn) {
+    //     // resultFiles 是 input 中选中的文件列表
+    //     // insertImgFn 是获取图片 url 后,插入到编辑器的方法
+    //     var file = resultFiles[0];
+    //     // file type is only image.
+    //     if (/^image\//.test(file.type)) {
+    //         var form = new FormData();
+    //         form.append('editormd-image-file', file, file.name);
+
+    //         var layerIndex = 0;
+
+    //         $.ajax({
+    //             url: window.imageUploadURL,
+    //             type: "POST",
+    //             dataType: "json",
+    //             data: form,
+    //             processData: false,
+    //             contentType: false,
+    //             error: function() {
+    //                 layer.close(layerIndex);
+    //                 layer.msg("图片上传失败");
+    //             },
+    //             success: function(data) {
+    //                 layer.close(layerIndex);
+    //                 if(data.errcode !== 0){
+    //                     layer.msg(data.message);
+    //                 }else{
+    //                     insertImgFn(data.url);
+    //                 }
+    //             }
+    //         });
+    //     } else {
+    //         console.warn('You could only upload images.');
+    //     }
+    // };
+    // editor.config.lang = window.lang;
+    // editor.i18next = window.i18next;
+    // editor.config.languages['en']['froalaEditor']['menus']['title']['保存'] = 'save';
+    // editor.config.languages['en']['froalaEditor']['menus']['title']['发布'] = 'publish';
+    // editor.config.languages['en']['froalaEditor']['menus']['title']['附件'] = 'attachment';
+    // editor.config.languages['en']['froalaEditor']['menus']['title']['history'] = 'history';
+
+    window.editormdLocales = {
+        'zh-CN': {
+            placeholder: '本编辑器支持 Markdown 编辑,左边编写,右边预览。',
+            contentUnsaved: '编辑内容未保存,需要保存吗?',
+            noDocNeedPublish: '没有需要发布的文档',
+            loadDocFailed: '文档加载失败',
+            fetchDocFailed: '获取当前文档信息失败',
+            cannotAddToEmptyNode: '空节点不能添加内容',
+            overrideModified: '文档已被其他人修改确定覆盖已存在的文档吗?',
+            confirm: '确定',
+            cancel: '取消',
+            contentsNameEmpty: '目录名称不能为空',
+            addDoc: '添加文档',
+            edit: '编辑',
+            delete: '删除',
+            loadFailed: '加载失败请重试',
+            tplNameEmpty: '模板名称不能为空',
+            tplContentEmpty: '模板内容不能为空',
+            saveSucc: '保存成功',
+            serverExcept: '服务器异常',
+            paramName: '参数名称',
+            paramType: '参数类型',
+            example: '示例值',
+            remark: '备注',
+        },
+        'en': {
+            placeholder: 'This editor supports Markdown editing, writing on the left and previewing on the right.',
+            contentUnsaved: 'The edited content is not saved, need to save it?',
+            noDocNeedPublish: 'No Document need to be publish',
+            loadDocFailed: 'Load Document failed',
+            fetchDocFailed: 'Fetch Document info failed',
+            cannotAddToEmptyNode: 'Cannot add content to empty node',
+            overrideModified: 'The document has been modified by someone else, are you sure to overwrite the document?',
+            confirm: 'Confirm',
+            cancel: 'Cancel',
+            contentsNameEmpty: 'Document Name cannot be empty',
+            addDoc: 'Add Document',
+            edit: 'Edit',
+            delete: 'Delete',
+            loadFailed: 'Failed to load, please try again',
+            tplNameEmpty: 'Template name cannot be empty',
+            tplContentEmpty: 'Template content cannot be empty',
+            saveSucc: 'Save success',
+            serverExcept: 'Server Exception',
+            paramName: 'Parameter',
+            paramType: 'Type',
+            example: 'Example',
+            remark: 'Remark',
+        }
+    };
+
+    // window.editor.config.onchange = function (newHtml) {
+    //     var saveMenu = window.editor.menus.menuList.find((item) => item.key == 'save');
+    //     // 判断内容是否改变
+    //     if (window.source !== window.editor.txt.html()) {
+    //         saveMenu.$elem.addClass('selected');
+    //     } else {
+    //         saveMenu.$elem.removeClass('selected');
+    //     }
+    // };
+
+    // window.editor.create();
+
+    // $("#froalaEditor").css("height","100%");
+
+    if(window.documentCategory.length > 0){
+        var item =  window.documentCategory[0];
+        var $select_node = { node : {id : item.id}};
+        loadDocument($select_node);
+    }
+
+    /***
+     * 加载指定的文档到编辑器中
+     * @param $node
+     */
+    function loadDocument($node) {
+        var index = layer.load(1, {
+            shade: [0.1,'#fff'] //0.1透明度的白色背景
+        });
+
+        $.get(window.editURL + $node.node.id ).done(function (res) {
+            layer.close(index);
+
+            if(res.errcode === 0){
+                window.isLoad = true;
+
+                // window.editor.txt.clear();
+                // window.editor.txt.html(res.data.content);
+                window.editor.html.set('');
+                window.editor.events.focus();
+                window.editor.html.set(res.data.content);
+                // 将原始内容备份
+                window.source = res.data.content;
+                var node = { "id" : res.data.doc_id,'parent' : res.data.parent_id === 0 ? '#' : res.data.parent_id ,"text" : res.data.doc_name,"identify" : res.data.identify,"version" : res.data.version};
+                pushDocumentCategory(node);
+                window.selectNode = node;
+
+                pushVueLists(res.data.attach);
+
+            }else{
+                layer.msg("文档加载失败");
+            }
+        }).fail(function () {
+            layer.close(index);
+            layer.msg("文档加载失败");
+        });
+    }
+
+    /**
+     * 保存文档到服务器
+     * @param $is_cover 是否强制覆盖
+     */
+    function saveDocument($is_cover,callback) {
+        var index = null;
+        var node = window.selectNode;
+
+        // var html = window.editor.txt.html() ;
+        var html = window.editor.html.get();
+
+        var content = "";
+        if($.trim(html) !== ""){
+            content = toMarkdown(html, { gfm: true });
+        }
+        var version = "";
+
+        if (!node) {
+            layer.msg(editormdLocales[lang].fetchDocFailed);
+            return;
+        }
+        var doc_id = parseInt(node.id);
+
+        for(var i in window.documentCategory){
+            var item = window.documentCategory[i];
+
+            if(item.id === doc_id){
+                version = item.version;
+                break;
+            }
+        }
+        $.ajax({
+            beforeSend  : function () {
+                index = layer.load(1, {shade: [0.1,'#fff'] });
+            },
+            url :  window.editURL,
+            data : {"identify" : window.book.identify,"doc_id" : doc_id,"markdown" : content,"html" : html,"cover" : $is_cover ? "yes":"no","version": version},
+            type :"post",
+            dataType :"json",
+            success : function (res) {
+                layer.close(index);
+                if(res.errcode === 0){
+                    for(var i in window.documentCategory){
+                        var item = window.documentCategory[i];
+
+                        if(item.id === doc_id){
+                            window.documentCategory[i].version = res.data.version;
+                            break;
+                        }
+                    }
+                    // 更新内容备份
+                    window.source = res.data.content;
+                    // 触发编辑器 onchange 回调函数
+                    // window.editor.config.onchange();
+                    // window.editor.onchange();
+                    // window.editor.events: {
+                    //     'contentChanged': function () {
+                    //       // Do something here.
+                    //       // this is the editor instance.
+                    //       console.log(this);
+                    //     }
+                    // }
+                    if(typeof callback === "function"){
+                        callback();
+                    }
+                }else if(res.errcode === 6005){
+                    var confirmIndex = layer.confirm(editormdLocales[lang].overrideModified, {
+                        btn: [editormdLocales[lang].confirm, editormdLocales[lang].cancel] // 按钮
+                    }, function () {
+                        layer.close(confirmIndex);
+                        saveDocument(true,callback);
+                    });
+                }else{
+                    layer.msg(res.message);
+                }
+            }
+        });
+    }
+
+
+
+    /**
+     * 添加顶级文档
+     */
+    $("#addDocumentForm").ajaxForm({
+        beforeSubmit : function () {
+            var doc_name = $.trim($("#documentName").val());
+            if (doc_name === ""){
+                return showError("目录名称不能为空","#add-error-message")
+            }
+            window.addDocumentFormIndex = layer.load(1, { shade: [0.1,'#fff']  });
+            return true;
+        },
+        success : function (res) {
+            if(res.errcode === 0){
+
+                var data = { "id" : res.data.doc_id,'parent' : res.data.parent_id === 0 ? '#' : res.data.parent_id ,"text" : res.data.doc_name,"identify" : res.data.identify,"version" : res.data.version};
+
+                var node = window.treeCatalog.get_node(data.id);
+                if(node){
+                    window.treeCatalog.rename_node({"id":data.id},data.text);
+
+                }else {
+                    window.treeCatalog.create_node(data.parent, data);
+                    window.treeCatalog.deselect_all();
+                    window.treeCatalog.select_node(data);
+                }
+                pushDocumentCategory(data);
+                $("#markdown-save").removeClass('change').addClass('disabled');
+                $("#addDocumentModal").modal('hide');
+            }else{
+                showError(res.message,"#add-error-message")
+            }
+            layer.close(window.addDocumentFormIndex);
+        }
+    });
+
+    /**
+     * 文档目录树
+     */
+    $("#sidebar").jstree({
+        'plugins': ["wholerow", "types", 'dnd', 'contextmenu'],
+        "types": {
+            "default": {
+                "icon": false  // 删除默认图标
+            }
+        },
+        'core': {
+            'check_callback': true,
+            "multiple": false,
+            'animation': 0,
+            "data": window.documentCategory
+        },
+        "contextmenu": {
+            show_at_node: false,
+            select_node: false,
+            "items": {
+                "添加文档": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": window.editormdLocales[window.lang].addDoc,//"添加文档",
+                    "icon": "fa fa-plus",
+                    "action": function (data) {
+
+                        var inst = $.jstree.reference(data.reference),
+                            node = inst.get_node(data.reference);
+
+                        openCreateCatalogDialog(node);
+                    }
+                },
+                "编辑": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": window.editormdLocales[window.lang].edit,
+                    "icon": "fa fa-edit",
+                    "action": function (data) {
+                        var inst = $.jstree.reference(data.reference);
+                        var node = inst.get_node(data.reference);
+                        openEditCatalogDialog(node);
+                    }
+                },
+                "删除": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": window.editormdLocales[window.lang].delete,
+                    "icon": "fa fa-trash-o",
+                    "action": function (data) {
+                        var inst = $.jstree.reference(data.reference);
+                        var node = inst.get_node(data.reference);
+                        openDeleteDocumentDialog(node);
+                    }
+                }
+            }
+        }
+    }).on('loaded.jstree', function () {
+        window.treeCatalog = $(this).jstree();
+    }).on('select_node.jstree', function (node, selected, event) {
+        // if(window.editor.menus.menuList.find((item) => item.key == 'save').$elem.hasClass('selected')) {
+        //      if (confirm(window.editormdLocales[window.lang].contentUnsaved)) {
+        //         saveDocument(false,function () {
+        //             loadDocument(selected);
+        //         });
+        //         return true;
+        //     }
+        // }
+        loadDocument(selected);
+
+    }).on("move_node.jstree", jstree_save);
+
+    window.saveDocument = saveDocument;
+
+    window.releaseBook = function () {
+        // if(Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0){
+        //     if(window.editor.menus.menuList.find((item) => item.key == 'save').$elem.hasClass('selected')) {
+        //         if(confirm(editormdLocales[lang].contentUnsaved)) {
+        //             saveDocument();
+        //         }
+        //     }
+            locales = {
+                'zh-CN': {
+                    publishToQueue: '发布任务已推送到任务队列,稍后将在后台执行。',
+                },
+                'en': {
+                    publishToQueue: 'The publish task has been pushed to the queue</br> and will be executed soon.',
+                }
+            }
+            $.ajax({
+                url: window.releaseURL,
+                data: {"identify": window.book.identify},
+                type: "post",
+                dataType: "json",
+                success: function (res) {
+                    if (res.errcode === 0) {
+                        layer.msg(locales[lang].publishToQueue);
+                    } else {
+                        layer.msg(res.message);
+                    }
+                }
+            });
+        // }else{
+        //     layer.msg(editormdLocales[lang].noDocNeedPublish)
+        // }
+    };
+
+    // $(window).resize(function(e) {
+    //   var $container = $(editor.$textContainerElem.elems[0]);
+    //   var $manual = $container.closest('.manual-froalaEditor');
+    //   var maxHeight = $manual.closest('.manual-editor-container').innerHeight();
+    //   var statusHeight = $manual.siblings('.manual-editor-status').outerHeight(true);
+    //   var manualHeihgt = maxHeight - statusHeight;
+    //   $manual.height(manualHeihgt);
+    //   var toolbarHeight = $container.siblings('.w-e-toolbar').outerHeight(true);
+    //   $container.height($container.parent().innerHeight() - toolbarHeight);
+    // });
+    // $(window).trigger('resize');
+});

+ 4 - 0
views/book/setting.tpl

@@ -97,6 +97,10 @@
                                     <label class="radio-inline">
                                         <input type="radio"{{if eq .Model.Editor "html"}} checked{{end}} name="editor" value="html"> Html {{i18n $.Lang "blog.text_editor"}}(wangEditor)
                                     </label>
+                    <!-- 3xxx 20240603 -->
+                    <label class="radio-inline">
+                      <input type="radio" {{if eq .Model.Editor "froala"}} checked{{end}} name="editor" value="froala"> Froala {{i18n $.Lang "blog.text_editor"}}
+                    </label>
                                 </div>
                             </div>
                             <div class="form-group">

+ 316 - 0
views/document/froala_edit_template.tpl

@@ -0,0 +1,316 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>{{i18n .Lang "doc.edit_doc"}} - Powered by MinDoc</title>
+  <style type="text/css">
+  .w-e-menu.selected>i {
+    color: #44B036 !important;
+  }
+  </style>
+  <script type="text/javascript">
+  window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
+  window.BASE_URL = '{{urlfor "HomeController.Index" }}';
+  </script>
+    <script type="text/javascript">
+        window.editor = null;
+        window.imageUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
+        window.fileUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
+        window.documentCategory = {{.Result}};
+        window.book = {{.ModelResult}};
+        window.selectNode = null;
+        window.deleteURL = "{{urlfor "DocumentController.Delete" ":key" .Model.Identify}}";
+        window.editURL = "{{urlfor "DocumentController.Content" ":key" .Model.Identify ":id" ""}}";
+        window.releaseURL = "{{urlfor "BookController.Release" ":key" .Model.Identify}}";
+        window.sortURL = "{{urlfor "BookController.SaveSort" ":key" .Model.Identify}}";
+        window.baiduMapKey = "{{.BaiDuMapKey}}";
+        window.historyURL = "{{urlfor "DocumentController.History"}}";
+        window.removeAttachURL = "{{urlfor "DocumentController.RemoveAttachment"}}";
+        window.vueApp = null;
+        window.lang = {{i18n $.Lang "common.js_lang"}};
+    </script>
+  <!-- Bootstrap -->
+  <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
+  <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
+  <link href="{{cdncss "/static/jstree/3.3.4/themes/default/style.min.css"}}" rel="stylesheet">
+  <link href="{{cdncss (print "/static/editor.md/lib/highlight/styles/" .HighlightStyle ".css") "version"}}" rel="stylesheet">
+  <link href="{{cdncss "/static/webuploader/webuploader.css"}}" rel="stylesheet">
+  <link href="{{cdncss "/static/css/jstree.css"}}" rel="stylesheet">
+  <link href="{{cdncss "/static/css/markdown.css"}}" rel="stylesheet">
+
+  <link rel="stylesheet" href="/static/froala/css/codemirror.min.css">
+
+  <link rel="stylesheet" href="/static/froala/css/froala_editor.min.css">
+  <link rel="stylesheet" href="/static/froala/css/froala_style.min.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/code_view.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/draggable.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/colors.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/emoticons.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/image_manager.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/image.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/line_breaker.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/table.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/char_counter.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/video.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/fullscreen.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/file.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/quick_insert.css">
+  <link rel="stylesheet" href="/static/froala/css/plugins/help.css">
+  <!-- <link rel="stylesheet" href="/static/froala/css/third_party/spell_checker.css"> -->
+  <link rel="stylesheet" href="/static/froala/css/plugins/special_characters.css">
+
+</head>
+
+<body>
+  <div class="m-manual manual-editor">
+
+    <div class="manual-body">
+      <div class="manual-category" id="manualCategory" style="top: 0;">
+        <div class="manual-nav">
+          <div class="nav-item active"><i class="fa fa-bars" aria-hidden="true"></i> {{i18n .Lang "doc.document"}}</div>
+          <div class="nav-plus pull-right" data-toggle="tooltip" data-title="{{i18n .Lang "doc.backward"}}" data-direction="right">
+            <a style="color: #999999;" href="{{urlfor "BookController.Dashboard" ":key" .Model.Identify}}" target="_blank"><i class="fa fa-chevron-left" aria-hidden="true"></i></a>
+          </div>
+          <div class="nav-plus pull-right" id="btnAddDocument" data-toggle="tooltip" data-title="{{i18n .Lang "doc.create_doc"}}" data-direction="right"><i class="fa fa-plus" aria-hidden="true"></i></div>
+          <div class="clearfix"></div>
+        </div>
+        <div class="manual-tree" id="sidebar">
+        </div>
+      </div>
+      <div class="manual-editor-container" id="manualEditorContainer" style="top: 0;">
+          <div class="manual-wangEditor">
+              <div id="froalaEditor" class="manual-editormd-active" style="height: 100%"></div>
+          </div>
+        <div class="manual-editor-status">
+          <div id="attachInfo" class="item" style="display: inline-block; padding: 0 3em;">0 {{i18n .Lang "doc.attachments"}}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- Modal -->
+  <div class="modal fade" id="addDocumentModal" tabindex="-1" style="z-index: 10001 !important;" role="dialog" aria-labelledby="addDocumentModalLabel">
+    <div class="modal-dialog" role="document">
+      <form method="post" action="{{urlfor "DocumentController.Create" ":key" .Model.Identify}}" id="addDocumentForm" class="form-horizontal">
+        <input type="hidden" name="identify" value="{{.Model.Identify}}">
+        <input type="hidden" name="doc_id" value="0">
+        <input type="hidden" name="parent_id" value="0">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+            <h4 class="modal-title" id="myModalLabel">{{i18n .Lang "doc.create_doc"}}</h4>
+          </div>
+          <div class="modal-body">
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{i18n .Lang "doc.doc_name"}} <span class="error-message">*</span></label>
+              <div class="col-sm-10">
+                <input type="text" name="doc_name" id="documentName" placeholder="{{i18n .Lang "doc.doc_name"}}" class="form-control" maxlength="50">
+                <p style="color: #999;font-size: 12px;">{{i18n .Lang "doc.doc_name_tips"}}</p>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{i18n .Lang "doc.doc_id"}}</label>
+              <div class="col-sm-10">
+                <input type="text" name="doc_identify" id="documentIdentify" placeholder="{{i18n .Lang "doc.doc_id"}}" class="form-control" maxlength="50">
+                <p style="color: #999;font-size: 12px;">{{i18n .Lang "doc.doc_id_tips"}}</p>
+              </div>
+            </div>
+          </div>
+          <div class="modal-footer">
+            <span id="add-error-message" class="error-message"></span>
+            <button type="button" class="btn btn-default" data-dismiss="modal">{{i18n .Lang "common.cancel"}}</button>
+            <button type="submit" class="btn btn-primary" id="btnSaveDocument" data-loading-text="{{i18n .Lang "message.processing"}}">{{i18n .Lang "doc.save"}}</button>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+  <div class="modal fade" id="uploadAttachModal" tabindex="-1" style="z-index: 10001 !important;" role="dialog" aria-labelledby="uploadAttachModalLabel">
+    <div class="modal-dialog" role="document">
+      <form method="post" action="{{urlfor "DocumentController.Create" ":key" .Model.Identify}}" id="addDocumentForm" class="form-horizontal">
+        <input type="hidden" name="identify" value="{{.Model.Identify}}">
+        <input type="hidden" name="doc_id" value="0">
+        <input type="hidden" name="parent_id" value="0">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+            <h4 class="modal-title" id="myModalLabel">{{i18n .Lang "doc.upload_attachment"}}</h4>
+          </div>
+          <div class="modal-body">
+            <div class="attach-drop-panel">
+              <div class="upload-container" id="filePicker"><i class="fa fa-upload" aria-hidden="true"></i></div>
+            </div>
+            <div class="attach-list" id="attachList">
+              <template v-for="item in lists">
+                <div class="attach-item" :id="item.attachment_id">
+                  <template v-if="item.state == 'wait'">
+                    <div class="progress">
+                      <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100">
+                        <span class="sr-only">0% Complete (success)</span>
+                      </div>
+                    </div>
+                  </template>
+                  <template v-else-if="item.state == 'error'">
+                    <span class="error-message">${item.message}</span>
+                    <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
+                      <i class="fa fa-remove" aria-hidden="true"></i>
+                    </button>
+                  </template>
+                  <template v-else>
+                    <a :href="item.http_path" target="_blank" :title="item.file_name">${item.file_name}</a>
+                    <span class="text">(${(item.file_size/1024/1024).toFixed(4)}MB)</span>
+                    <span class="error-message">${item.message}</span>
+                    <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
+                      <i class="fa fa-remove" aria-hidden="true"></i>
+                    </button>
+                    <div class="clearfix"></div>
+                  </template>
+                </div>
+              </template>
+            </div>
+          </div>
+          <div class="modal-footer">
+            <span id="add-error-message" class="error-message"></span>
+            <button type="button" class="btn btn-default" data-dismiss="modal">{{i18n .Lang "common.cancel"}}</button>
+            <button type="button" class="btn btn-primary" id="btnUploadAttachFile" data-dismiss="modal">{{i18n .Lang "common.confirm"}}</button>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+  <!-- <script src="https://cdn.jsdelivr.net/npm/i18next/i18next.min.js"></script> -->
+  <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
+  <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
+  <script src="{{cdnjs "/static/vuejs/vue.min.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/jstree/3.3.4/jstree.min.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/webuploader/webuploader.min.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/class2browser.js"}}" type="text/javascript"></script>
+<!--   <script src="{{cdnjs "/static/wangEditor/wangEditor.min.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/wangEditor-plugins/save-menu.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/wangEditor-plugins/release-menu.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/wangEditor-plugins/attach-menu.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/wangEditor-plugins/history-menu.js"}}" type="text/javascript"></script> -->
+  <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/to-markdown/dist/to-markdown.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/editor.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/froala-editor.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
+  <script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
+
+  <script type="text/javascript" src="/static/froala/js/froala_editor.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/align.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/char_counter.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/code_beautifier.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/code_view.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/colors.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/draggable.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/emoticons.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/entities.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/file.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/font_size.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/font_family.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/fullscreen.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/image.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/image_manager.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/line_breaker.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/inline_style.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/link.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/lists.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/paragraph_format.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/paragraph_style.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/quick_insert.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/quote.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/table.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/save.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/url.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/video.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/help.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/print.min.js"></script>
+  <!-- <script type="text/javascript" src="/static/froala/js/third_party/spell_checker.min.js"></script> -->
+  <script type="text/javascript" src="/static/froala/js/plugins/special_characters.min.js"></script>
+  <script type="text/javascript" src="/static/froala/js/plugins/word_paste.min.js"></script>
+  <script src="/static/froala/js/languages/zh_cn.js"></script>
+
+  <script type="text/javascript">
+  $(function() {
+    lang = {{ i18n $.Lang "common.js_lang" }};
+    $("#attachInfo").on("click", function() {
+      $("#uploadAttachModal").modal("show");
+    });
+
+    window.uploader = null;
+
+    $("#uploadAttachModal").on("shown.bs.modal", function() {
+      if (window.uploader === null) {
+        try {
+          window.uploader = WebUploader.create({
+            auto: true,
+            dnd: true,
+            swf: '{{.BaseUrl}}/static/webuploader/Uploader.swf',
+            server: '{{urlfor "DocumentController.Upload"}}',
+            formData: { "identify": {{.Model.Identify }}, "doc_id": window.selectNode.id },
+            pick: "#filePicker",
+            fileVal: "editormd-file-file",
+            fileNumLimit: 1,
+            compress: false
+          }).on("beforeFileQueued", function(file) {
+            uploader.reset();
+          }).on('fileQueued', function(file) {
+            var item = {
+              state: "wait",
+              attachment_id: file.id,
+              file_size: file.size,
+              file_name: file.name,
+              message: "{{i18n .Lang "              doc.uploading "}}"
+            };
+            window.vueApp.lists.splice(0, 0, item);
+
+          }).on("uploadError", function(file, reason) {
+            for (var i in window.vueApp.lists) {
+              var item = window.vueApp.lists[i];
+              if (item.attachment_id == file.id) {
+                item.state = "error";
+                item.message = "{{i18n .Lang "                message.upload_failed "}}:" + reason;
+                break;
+              }
+            }
+
+          }).on("uploadSuccess", function(file, res) {
+
+            for (var index in window.vueApp.lists) {
+              var item = window.vueApp.lists[index];
+              if (item.attachment_id === file.id) {
+                if (res.errcode === 0) {
+                  window.vueApp.lists.splice(index, 1, res.attach);
+                } else {
+                  item.message = res.message;
+                  item.state = "error";
+                }
+                break;
+              }
+            }
+
+          }).on("beforeFileQueued", function(file) {
+
+          }).on("uploadComplete", function() {
+
+          }).on("uploadProgress", function(file, percentage) {
+            var $li = $('#' + file.id),
+              $percent = $li.find('.progress .progress-bar');
+
+            $percent.css('width', percentage * 100 + '%');
+          });
+        } catch (e) {
+          console.log(e);
+        }
+      }
+    });
+  });
+
+  </script>
+</body>
+
+</html>