Bladeren bron

first commit

lsiten 6 jaren geleden
commit
e2f406c6d4
79 gewijzigde bestanden met toevoegingen van 11460 en 0 verwijderingen
  1. 5 0
      .bablerc
  2. 4 0
      .gitignore
  3. 0 0
      README.md
  4. 52 0
      package.json
  5. 5 0
      postcss.config.js
  6. 262 0
      src/config.js
  7. 3 0
      src/css/main.css
  8. 0 0
      src/css/style.css
  9. 176 0
      src/data.json
  10. BIN
      src/fonts/lsiten-e-icon.woff
  11. BIN
      src/image/icon.png
  12. 29 0
      src/index.html
  13. 175 0
      src/index.js
  14. 105 0
      src/js/command/index.js
  15. 46 0
      src/js/menus/backColor/index.js
  16. 52 0
      src/js/menus/bold/index.js
  17. 153 0
      src/js/menus/code/index.js
  18. 57 0
      src/js/menus/column/index.js
  19. 134 0
      src/js/menus/droplist.js
  20. 115 0
      src/js/menus/emoticon/index.js
  21. 46 0
      src/js/menus/fontName/index.js
  22. 48 0
      src/js/menus/fontSize/index.js
  23. 46 0
      src/js/menus/foreColor/index.js
  24. 70 0
      src/js/menus/head/index.js
  25. 110 0
      src/js/menus/img/index.js
  26. 120 0
      src/js/menus/index.js
  27. 60 0
      src/js/menus/italic/index.js
  28. 62 0
      src/js/menus/justify/index.js
  29. 168 0
      src/js/menus/link/index.js
  30. 82 0
      src/js/menus/list/index.js
  31. 81 0
      src/js/menus/menu-list.js
  32. 195 0
      src/js/menus/panel.js
  33. 54 0
      src/js/menus/questionBorder/index.js
  34. 31 0
      src/js/menus/questionData/index.js
  35. 31 0
      src/js/menus/questionHead/index.js
  36. 123 0
      src/js/menus/questiontype/index.js
  37. 75 0
      src/js/menus/quote/index.js
  38. 37 0
      src/js/menus/redo/index.js
  39. 60 0
      src/js/menus/strikethrough/index.js
  40. 376 0
      src/js/menus/table/index.js
  41. 79 0
      src/js/menus/underline/index.js
  42. 36 0
      src/js/menus/undo/index.js
  43. 86 0
      src/js/menus/video/index.js
  44. 22 0
      src/js/plugin/commands/answerHeader.js
  45. 157 0
      src/js/plugin/commands/border.js
  46. 121 0
      src/js/plugin/commands/getBorderData.js
  47. 36 0
      src/js/plugin/commands/questiontype.js
  48. 7 0
      src/js/plugin/index.js
  49. 149 0
      src/js/question/choice.js
  50. 11 0
      src/js/question/index.js
  51. 146 0
      src/js/question/judge.js
  52. 447 0
      src/js/question/questionhead.js
  53. 49 0
      src/js/question/subject.js
  54. 131 0
      src/js/question/writing.js
  55. 281 0
      src/js/selection/index.js
  56. 46 0
      src/js/util/ajax.js
  57. 538 0
      src/js/util/dom-core.js
  58. 55 0
      src/js/util/imageUtil.js
  59. 247 0
      src/js/util/message.js
  60. 92 0
      src/js/util/paste-handle.js
  61. 48 0
      src/js/util/poly-fill.js
  62. 499 0
      src/js/util/question.js
  63. 932 0
      src/js/util/questionPrev.js
  64. 21 0
      src/js/util/replace-lang.js
  65. 257 0
      src/js/util/util.js
  66. 1035 0
      src/js/yzPage.js
  67. 1644 0
      src/js/yzPageManager.js
  68. 130 0
      src/js/yzWebeditor.js
  69. 19 0
      src/less/common.less
  70. 78 0
      src/less/droplist.less
  71. 108 0
      src/less/icon.less
  72. 145 0
      src/less/main.less
  73. 36 0
      src/less/menus.less
  74. 84 0
      src/less/message.less
  75. 164 0
      src/less/panel.less
  76. 25 0
      src/less/scroller.less
  77. 9 0
      static/js/serverWorker.js
  78. 144 0
      static/js/sw-3.js
  79. 98 0
      webpack.config.js

+ 5 - 0
.bablerc

@@ -0,0 +1,5 @@
+{
+  "presets": ["es2015", "stage-2"],
+  "plugins": ["transform-runtime"],
+  "comments": false
+}

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.babelrc
+.idea/*
+dist/*
+node_modules/*

+ 0 - 0
README.md


+ 52 - 0
package.json

@@ -0,0 +1,52 @@
+{
+  "name": "lsiten-editor-new",
+  "version": "2.0.0",
+  "description": "answer sheet",
+  "main": "src/js/yzWebeditor.js",
+  "scripts": {
+    "dev": "webpack-dev-server --mode development --open",
+    "build": "webpack --mode production"
+  },
+  "keywords": [
+    "lsiten-editor"
+  ],
+  "author": "lsc",
+  "license": "ISC",
+  "devDependencies": {
+    "autoprefixer": "^8.2.0",
+    "babel-cli": "^6.26.0",
+    "babel-core": "^6.26.3",
+    "babel-loader": "^7.1.5",
+    "babel-plugin-transform-runtime": "^6.23.0",
+    "babel-preset-env": "^1.7.0",
+    "babel-preset-es2015": "^6.24.1",
+    "babel-preset-es2015-loose": "^8.0.0",
+    "babel-preset-react": "^6.24.1",
+    "css-loader": "^0.28.11",
+    "es3ify-loader": "^0.2.0",
+    "extract-text-webpack-plugin": "^4.0.0-beta.0",
+    "file-loader": "^1.1.11",
+    "html-webpack-plugin": "^3.1.0",
+    "less": "^3.0.4",
+    "less-loader": "^4.1.0",
+    "postcss-loader": "^2.1.3",
+    "purify-css": "^1.2.5",
+    "purifycss-webpack": "^0.7.0",
+    "style-loader": "^0.20.3",
+    "uglifyjs-webpack-plugin": "^1.2.4",
+    "url-loader": "^1.0.1",
+    "webpack": "^4.4.1",
+    "webpack-cli": "^2.0.13",
+    "webpack-dev-server": "^3.1.1"
+  },
+  "dependencies": {
+    "html-withimg-loader": "^0.1.16",
+    "jquery": "^3.3.1",
+    "jquery.caret": "^0.3.1",
+    "jr-qrcode": "^1.1.4"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://git.proginn.com/u153505/lsiten-editor.git"
+  }
+}

+ 5 - 0
postcss.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+    plugins: [
+        require('autoprefixer')
+    ]
+}

+ 262 - 0
src/config.js

@@ -0,0 +1,262 @@
+/*
+    配置信息
+*/
+
+const config = {
+
+    // 默认菜单配置
+    menus: [
+        // 'head',
+        'bold',
+        // 'fontSize',
+        // 'fontName',
+        // 'italic',
+        'underline',
+        // 'strikeThrough',
+        // 'foreColor',
+        // 'backColor',
+        // 'link',
+        // 'list',
+        'justify',
+        // 'quote',
+        // 'emoticon',
+        'image',
+        // 'table',
+        // 'video',
+        // 'code',
+        // 'undo',
+        // 'redo',
+        // 'questiontype',
+        // 'questionBorder',
+        // 'questionHead',
+        // 'questionData',
+
+        // 'questiontypes',
+        // 'questionborder',
+        // 'wordtool',
+        // 'column'
+    ],
+
+    fontNames: [
+        '宋体',
+        '微软雅黑',
+        'Arial',
+        'Tahoma',
+        'Verdana'
+    ],
+
+    colors: [
+        '#000000',
+        '#eeece0',
+        '#1c487f',
+        '#4d80bf',
+        '#c24f4a',
+        '#8baa4a',
+        '#7b5ba1',
+        '#46acc8',
+        '#f9963b',
+        '#ffffff'
+    ],
+
+    // // 语言配置
+    // lang: {
+    //     '设置标题': 'title',
+    //     '正文': 'p',
+    //     '链接文字': 'link text',
+    //     '链接': 'link',
+    //     '插入': 'insert',
+    //     '创建': 'init'
+    // },
+
+    // 表情
+    emotions: [
+        {
+            // tab 的标题
+            title: '默认',
+            // type -> 'emoji' / 'image'
+            type: 'image',
+            // content -> 数组
+            content: [
+                {
+                    alt: '[坏笑]',
+                    src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/50/pcmoren_huaixiao_org.png'
+                },
+                {
+                    alt: '[舔屏]',
+                    src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/pcmoren_tian_org.png'
+                },
+                {
+                    alt: '[污]',
+                    src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/3c/pcmoren_wu_org.png'
+                }
+            ]
+        },
+        {
+            // tab 的标题
+            title: '新浪',
+            // type -> 'emoji' / 'image'
+            type: 'image',
+            // content -> 数组
+            content: [
+                {
+                    src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/7a/shenshou_thumb.gif',
+                    alt: '[草泥马]'
+                },
+                {
+                    src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/60/horse2_thumb.gif',
+                    alt: '[神马]'
+                },
+                {
+                    src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/fuyun_thumb.gif',
+                    alt: '[浮云]'
+                }
+            ]
+        },
+        {
+            // tab 的标题
+            title: 'emoji',
+            // type -> 'emoji' / 'image'
+            type: 'emoji',
+            // content -> 数组
+            content: '😀 😃 😄 😁 😆 😅 😂 😊 😇 🙂 🙃 😉 😓 😪 😴 🙄 🤔 😬 🤐'.split(/\s/)
+        },
+        // {
+        //     // tab 的标题
+        //     title: '手势',
+        //     // type -> 'emoji' / 'image'
+        //     type: 'emoji',
+        //     // content -> 数组
+        //     content: ['🙌', '👏', '👋', '👍', '👎', '👊', '✊', '️👌', '✋', '👐', '💪', '🙏', '️👆', '👇', '👈', '👉', '🖕', '🖐', '🤘']
+        // }
+    ],
+
+    // 编辑区域的 z-index
+    zIndex: 89,
+
+    // 是否开启 debug 模式(debug 模式下错误会 throw error 形式抛出)
+    debug: false,
+
+    // 插入链接时候的格式校验
+    linkCheck: function (text, link) {
+        // text 是插入的文字
+        // link 是插入的链接
+        return true // 返回 true 即表示成功
+        // return '校验失败' // 返回字符串即表示失败的提示信息
+    },
+
+    // 插入网络图片的校验
+    linkImgCheck: function (src) {
+        // src 即图片的地址
+        return true // 返回 true 即表示成功
+        // return '校验失败'  // 返回字符串即表示失败的提示信息
+    },
+
+    // 粘贴过滤样式,默认开启
+    pasteFilterStyle: false,
+
+    // 粘贴内容时,忽略图片。默认关闭
+    pasteIgnoreImg: false,
+
+    // 对粘贴的文字进行自定义处理,返回处理后的结果。编辑器会将处理后的结果粘贴到编辑区域中。
+    // IE 暂时不支持
+    pasteTextHandle: function (content) {
+        // content 即粘贴过来的内容(html 或 纯文本),可进行自定义处理然后返回
+        return content
+    },
+
+    // onchange 事件
+    // onchange: function (html) {
+    //     // html 即变化之后的内容
+    //     console.log(html)
+    // },
+
+    // 是否显示添加网络图片的 tab
+    showLinkImg: true,
+
+    // 插入网络图片的回调
+    linkImgCallback: function (url) {
+        // console.log(url)  // url 即插入图片的地址
+    },
+
+    // 默认上传图片 max size: 5M
+    uploadImgMaxSize: 5 * 1024 * 1024,
+
+    // 配置一次最多上传几个图片
+    // uploadImgMaxLength: 5,
+
+    // 上传图片,是否显示 base64 格式
+    uploadImgShowBase64: false,
+
+    // 上传图片,server 地址(如果有值,则 base64 格式的配置则失效)
+    // uploadImgServer: '/upload',
+
+    // 自定义配置 filename
+    uploadFileName: '',
+
+    // 上传图片的自定义参数
+    uploadImgParams: {
+        // token: 'abcdef12345'
+    },
+
+    // 上传图片的自定义header
+    uploadImgHeaders: {
+        // 'Accept': 'text/x-json'
+    },
+
+    // 配置 XHR withCredentials
+    withCredentials: false,
+
+    // 自定义上传图片超时时间 ms
+    uploadImgTimeout: 10000,
+
+    // 上传图片 hook 
+    uploadImgHooks: {
+        // customInsert: function (insertLinkImg, result, editor) {
+        //     console.log('customInsert')
+        //     // 图片上传并返回结果,自定义插入图片的事件,而不是编辑器自动插入图片
+        //     const data = result.data1 || []
+        //     data.forEach(link => {
+        //         insertLinkImg(link)
+        //     })
+        // },
+        before: function (xhr, editor, files) {
+            // 图片上传之前触发
+
+            // 如果返回的结果是 {prevent: true, msg: 'xxxx'} 则表示用户放弃上传
+            // return {
+            //     prevent: true,
+            //     msg: '放弃上传'
+            // }
+        },
+        success: function (xhr, editor, result) {
+            // 图片上传并返回结果,图片插入成功之后触发
+        },
+        fail: function (xhr, editor, result) {
+            // 图片上传并返回结果,但图片插入错误时触发
+        },
+        error: function (xhr, editor) {
+            // 图片上传出错时触发
+        },
+        timeout: function (xhr, editor) {
+            // 图片上传超时时触发
+        }
+    },
+
+    // 是否上传七牛云,默认为 false
+    qiniu: false,
+
+    // 上传图片自定义提示方法
+    // customAlert: function (info) {
+    //     // 自定义上传提示
+    // },
+    
+    // // 自定义上传图片
+    // customUploadImg: function (files, insert) {
+    //     // files 是 input 中选中的文件列表
+    //     // insert 是获取图片 url 后,插入到编辑器的方法
+    //     insert(imgUrl)
+    // }
+    policyUrl: 'http://api.hostdev.ennjoy.cn/YinKe/Api/public/UploadFile/Policy'
+}
+
+export default config

+ 3 - 0
src/css/main.css

@@ -0,0 +1,3 @@
+/* page的样式开始 */
+
+/* page的样式结束 */

+ 0 - 0
src/css/style.css


+ 176 - 0
src/data.json

@@ -0,0 +1,176 @@
+{
+	"schoolId": "108",
+	"id": "459",
+	"subjectId": "1635",
+	"paperSize": "A4",
+	"layoutType": "1",
+	"sourceType": "2",
+	"prohibitType": "1",
+	"noMode": "1",
+	"noCount": "9",
+	"noBoth": "0",
+	"qrCode": "cm",
+	"alias": "test-lsiten 高二 语文",
+	"cardJson": null,
+	"cardHtml": "",
+		"attention": null,
+	"examPageCnt": "102",
+	"pageQus": [{
+		"sort": "1",
+		"attribute": null,
+		"content": null,
+		"annexable": "1",
+		"group": "0",
+		"pIndex": "1",
+		"pros": [{
+			"proId": "852",
+			"score": 70.0,
+			"pureObjective": "1",
+			"content": "",
+			"sort": "1",
+			"pnum": "5",
+			"group": "0",
+			"qus": [{
+				"quId": "1402",
+				"score": 20.0,
+				"quType": "单选题",
+				"nums": "4",
+				"content": "",
+				"pnum": "(1)",
+				"visible": true,
+				"rIndex": "1-1"
+			}, {
+				"quId": "1403",
+				"score": 20.0,
+				"quType": "单选题",
+				"nums": "4",
+				"content": "",
+				"pnum": "(2)",
+				"visible": true,
+				"rIndex": "1-1"
+			}, {
+				"quId": "1404",
+				"score": 30.0,
+				"quType": "单选题",
+				"nums": "4",
+				"content": "",
+				"pnum": "(3)",
+				"visible": true,
+				"rIndex": "1-1"
+			}]
+		}]
+	}, {
+		"sort": "2",
+		"attribute": null,
+		"content": null,
+		"annexable": "1",
+		"group": "0",
+		"pIndex": "1",
+		"pros": [{
+			"proId": "1087",
+			"score": 20.0,
+			"pureObjective": "2",
+			"content": "",
+			"sort": "1",
+			"pnum": "",
+			"group": "0",
+			"qus": [{
+				"quId": "1848",
+				"score": 20.0,
+				"quType": "填空题",
+				"nums": "1",
+				"content": "",
+				"pnum": "1",
+				"visible": true,
+				"rIndex": "0"
+			}]
+		}]
+	}, {
+		"sort": "3",
+		"attribute": null,
+		"content": null,
+		"annexable": "1",
+		"group": "0",
+		"pIndex": "1",
+		"pros": [{
+			"proId": "1088",
+			"score": 20.0,
+			"pureObjective": "2",
+			"content": "",
+			"sort": "2",
+			"pnum": "",
+			"group": "0",
+			"qus": [{
+				"quId": "1849",
+				"score": 20.0,
+				"quType": "填空题",
+				"nums": "1",
+				"content": "",
+				"pnum": "2",
+				"visible": true,
+				"rIndex": "0"
+			}]
+		}]
+	}, {
+		"sort": "4",
+		"attribute": null,
+		"content": null,
+		"annexable": "1",
+		"group": "0",
+		"pIndex": "1",
+		"pros": [{
+			"proId": "934",
+			"score": 40.0,
+			"pureObjective": "2",
+			"content": "",
+			"sort": "3",
+			"pnum": "",
+			"group": "0",
+			"qus": [{
+				"quId": "1523",
+				"score": 20.0,
+				"quType": "解答题",
+				"nums": null,
+				"content": "",
+				"pnum": "3",
+				"visible": true,
+				"rIndex": "0"
+			}, {
+				"quId": "1524",
+				"score": 20.0,
+				"quType": "解答题",
+				"nums": null,
+				"content": "",
+				"pnum": "4",
+				"visible": true,
+				"rIndex": "0"
+			}]
+		}]
+	}, {
+		"sort": "5",
+		"attribute": null,
+		"content": null,
+		"annexable": "1",
+		"group": "0",
+		"pIndex": "1-2",
+		"pros": [{
+			"proId": "857",
+			"score": 60.0,
+			"pureObjective": "2",
+			"content": "",
+			"sort": "5",
+			"pnum": "",
+			"group": "0",
+			"qus": [{
+				"quId": "1413",
+				"score": 60.0,
+				"quType": "作文题",
+				"nums": null,
+				"content": "",
+				"pnum": "6",
+				"visible": true,
+				"rIndex": "0"
+			}]
+		}]
+	}]
+}

BIN
src/fonts/lsiten-e-icon.woff


BIN
src/image/icon.png


+ 29 - 0
src/index.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta http-equiv="X-UA-Compatible" content="ie=edge">
+  <link rel="stylesheet" href="http://apps.bdimg.com/libs/jqueryui/1.9.2/themes/base/jquery-ui.css">
+  <link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.6.3/css/font-awesome.min.css">
+  <title>web版Word编辑器</title>
+  <style>
+    body, html, * {
+      margin: 0;
+      padding: 0;
+      width: 100%;
+      height: 100%;
+    }
+    #pages-box {
+      width: 100%;
+      height: 100%;
+      background: #f8f2f2;
+    }
+  </style>
+</head>
+<body>
+  <div id='pages-box'>
+
+  </div>
+</body>
+</html>

+ 175 - 0
src/index.js

@@ -0,0 +1,175 @@
+import './less/main.less';
+import yzWebeditor from './js/yzWebeditor';
+import $ from './js/util/dom-core.js';
+import data from './data.json';
+let editor = new yzWebeditor({dom:document.getElementById('pages-box'), data: data});
+
+let conmands = [];
+
+const selection = editor.selection;
+
+let cardHtml = data.cardHtml || '';
+if (cardHtml.length > 0) {
+  cardHtml = JSON.parse(data.cardHtml);
+  editor.page.parseCardHtml(cardHtml);
+} else {
+  let borders = data.pageQus;
+
+  let length = borders.length;
+  for (let i = 0; i < length; i++) {
+    let borderItem = borders[i];
+    let pros = borderItem.pros;
+    let plength = pros.length;
+    borderItem.pureObject = true;
+    for (let j = 0; j < plength; j++) {
+      let prosItem = pros[j];
+      if (parseInt(prosItem.pureObjective) ===2) {
+        borderItem.pureObject = false;
+      }
+    }
+  }
+  
+  
+  for (let i = 0; i < length; i ++) {
+    let borderItem = borders[i];
+    let borderData = {
+      annexable: borderItem.annexable,
+      attribute: borderItem.attribute,
+      content: borderItem.content,
+      group: borderItem.group,
+      pureObject: borderItem.pureObject,
+      pros: borderItem.pros,
+      sort: borderItem.sort
+    }
+    if (borderItem.pureObject) {
+      let conmandItem = {
+        exc: 'border',
+        params: {
+          type: 1,
+          data: borderData
+        }
+      }
+      conmands.push(conmandItem);
+    } else {
+      let conmandItem = {
+        exc: 'border',
+        params: {
+          type: 2,
+          data: borderData
+        }
+      }
+      conmands.push(conmandItem);
+    }
+  
+    let pros = borderItem.pros;
+    let plength = pros.length;
+    let maxNum = 0;
+    let questcount = 0;
+    for (let j = 0; j < plength; j++) {
+      let prosItem = pros[j];
+      let qus = prosItem.qus;
+      let klength = qus.length;
+      for (let k = 0; k < klength; k++) {
+        let qusItem = JSON.parse(JSON.stringify(qus[k]));
+        qusItem.proId = prosItem.proId;
+        qusItem.proData = {
+          content: prosItem.content,
+          proId: prosItem.proId,
+          pnum: prosItem.pnum,
+          pureObjective: prosItem.pureObjective,
+          score: prosItem.score,
+          sort: prosItem.sort
+        };
+        let qNums = parseInt(qusItem.nums);
+        qNums > maxNum && (maxNum = qNums);
+        parseQuestion(qusItem);
+        questcount++;
+      }
+    }
+  
+    if (borderItem.pureObject) {
+      let columns = 1;
+      if (maxNum > 4) {
+        columns = 2;
+      } else {
+        columns = 3;
+      }
+  
+      let rows = Math.ceil(questcount / columns / 5);
+      borderData.row = rows;
+      borderData.column = columns;
+    }
+  }
+  let commondsLength = conmands.length;
+  asynExecCommand(0, commondsLength);
+}
+
+function checkoutCententChange() {
+  return new Promise((resolve, reject) => {
+    while (editor.page._checkCrossing || editor.page._addParagraphStatus || editor.page._contentChangeStatus) {}    
+    resolve(true);
+  });
+}
+async function asynExecCommand (i, length) {
+  if (i < length) {
+    let result = await checkoutCententChange();
+    if (result) {
+      editor.cmd.do(conmands[i].exc, conmands[i].params);
+      if (conmands[i].exc === 'border' && conmands[i].params.type === 1) {
+        editor.isDoObject = true;
+      } else if (conmands[i].exc === 'questiontype' && conmands[i].params.type === 1){
+        editor.isDoObject = true;
+      } else if (conmands[i].exc === 'questiontype' && conmands[i].params.type === 2) {
+        editor.isDoObject = true;
+      } else {
+        editor.isDoObject = false;
+      }
+      i++;
+    }
+    asynExecCommand(i, length);
+  } else {
+    editor.page._checkContentOutThrottle();
+  }
+}
+
+function parseQuestion (qitem) {
+  let type = qitem.quType;
+  switch(type) {
+    case '单选题':
+    case '多选题':
+    let conmandItem1 = {
+      exc: 'questiontype',
+      params: {type: 1, data: qitem}
+    }
+    conmands.push(conmandItem1);
+    break;
+    case '判断题':
+
+    let conmandItem2 = {
+      exc: 'questiontype',
+      params: {type: 2, data: qitem}
+    }
+    conmands.push(conmandItem2);
+    break;
+
+    case '填空题':
+    case '解答题':
+    let conmandItem3 = {
+      exc: 'questiontype',
+      params: {type: 3, data: qitem}
+    }
+    conmands.push(conmandItem3);
+    break;
+
+    case '作文题':
+    let conmandItem4 = {
+      exc: 'questiontype',
+      params: {type: 4, data: qitem}
+    }
+    conmands.push(conmandItem4);
+    break;
+  }
+}
+
+
+console.log(editor);

+ 105 - 0
src/js/command/index.js

@@ -0,0 +1,105 @@
+/*
+    命令,封装 document.execCommand
+*/
+
+import $ from '../util/dom-core.js';
+import { UA, getParentByClassname } from '../util/util.js';
+import commands  from '../plugin/index.js';
+// 构造函数
+function Command(editor) {
+    this.editor = editor
+    this.commands = commands
+}
+
+// 修改原型
+Command.prototype = {
+    constructor: Command,
+
+    // 执行命令
+    do: function (name, value) {
+        const editor = this.editor
+        let $selectionElem = editor.selection.getSelectionContainerElem();
+        let $paragraph = getParentByClassname($selectionElem, 'js-paragraph-view');
+        if (!$paragraph) {
+          editor.message.showMessage('必须在段落中编辑!');
+          return false;
+        }
+        // 使用 styleWithCSS
+        if (!editor._useStyleWithCSS) {
+            document.execCommand('styleWithCSS', null, true)
+            editor._useStyleWithCSS = true
+        }
+        // 如果无选区,忽略
+        if (!editor.selection.getRange()) {
+            return
+        }
+
+        // 执行
+        const _name = '_' + name
+        if (this[_name]) {
+            // 有自定义事件
+            this[_name](value)
+        } else if (this.commands[_name]) {
+            // 修改菜单状态
+            editor.menus.changeActive()          
+            return this.commands[_name].call(this, value)
+        } else {
+            // 默认 command
+            // 修改菜单状态
+            // 恢复选取
+            editor.selection.restoreSelection()
+            editor.menus.changeActive() 
+            return this._execCommand(name, value)
+        }
+    },
+
+    // 自定义 insertHTML 事件
+    _insertHTML: function (html) {
+        const editor = this.editor
+        const range = editor.selection.getRange()
+        
+        if (this.queryCommandSupported('insertHTML')) {
+            // W3C
+            this._execCommand('insertHTML', html)
+        } else if (range.insertNode) {
+            // IE
+            range.deleteContents()
+            range.insertNode($(html)[0])
+        } else if (range.pasteHTML) {
+            // IE <= 10
+            range.pasteHTML(html)
+        }
+    },
+
+    // 插入 elem
+    _insertElem: function ($elem) {
+        const editor = this.editor
+        const range = editor.selection.getRange()
+        if (range.insertNode) {
+            range.deleteContents()
+            range.insertNode($elem[0])
+        }
+    },
+
+    // 封装 execCommand
+    _execCommand: function (name, value) {
+      document.execCommand(name, false, value)
+    },
+
+    // 封装 document.queryCommandValue
+    queryCommandValue: function (name) {
+        return document.queryCommandValue(name)
+    },
+
+    // 封装 document.queryCommandState
+    queryCommandState: function (name) {
+        return document.queryCommandState(name)
+    },
+
+    // 封装 document.queryCommandSupported
+    queryCommandSupported: function (name) {
+        return document.queryCommandSupported(name)
+    }
+}
+
+export default Command

+ 46 - 0
src/js/menus/backColor/index.js

@@ -0,0 +1,46 @@
+/*
+    menu - BackColor
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function BackColor(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-paint-brush"></i></div>')
+    this.type = 'droplist'
+
+    // 获取配置的颜色
+    const config = editor.config
+    const colors = config.colors || []
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 120,
+        $title: $('<p>背景色</p>'),
+        type: 'inline-block', // droplist 内容以 block 形式展示
+        list: colors.map(color => {
+            return { $elem: $(`<i style="color:${color};" class="lsiten-e-icon-paint-brush"></i>`), value: color }
+        }),
+        onClick: (value) => {
+            // 注意 this 是指向当前的 BackColor 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+BackColor.prototype = {
+    constructor: BackColor,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+        editor.cmd.do('backColor', value)
+    }
+}
+
+export default BackColor

+ 52 - 0
src/js/menus/bold/index.js

@@ -0,0 +1,52 @@
+/*
+    bold-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function Bold(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-bold"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Bold.prototype = {
+    constructor: Bold,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+        
+        const editor = this.editor
+        const isSeleEmpty = editor.selection.isSelectionEmpty()
+        if (isSeleEmpty) {
+            // 选区是空的,插入并选中一个“空白”
+            editor.selection.createEmptyRange()
+        }
+        // 执行 bold 命令
+        editor.cmd.do('bold')
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor.cmd.queryCommandState('bold')) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Bold

+ 153 - 0
src/js/menus/code/index.js

@@ -0,0 +1,153 @@
+/*
+    menu - code
+*/
+import $ from '../../util/dom-core.js'
+import { getRandom, replaceHtmlSymbol } from '../../util/util.js'
+import Panel from '../panel.js'
+import { UA } from '../../util/util.js'
+
+// 构造函数
+function Code(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-terminal"></i>
+        </div>`
+    )
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Code.prototype = {
+    constructor: Code,
+
+    onClick: function (e) {
+        const editor = this.editor
+        const $startElem = editor.selection.getSelectionStartElem()
+        const $endElem = editor.selection.getSelectionEndElem()
+        const isSeleEmpty = editor.selection.isSelectionEmpty()
+        const selectionText = editor.selection.getSelectionText()
+        let $code
+        if (!$startElem) {
+          // 未在编辑区域,不做处理
+          return 
+        }
+        if (!$startElem.equal($endElem)) {
+            // 跨元素选择,不做处理
+            editor.selection.restoreSelection()
+            return
+        }
+        if (!isSeleEmpty) {
+            // 选取不是空,用 <code> 包裹即可
+            $code = $(`<code>${selectionText}</code>`)
+            editor.cmd.do('insertElem', $code)
+            editor.selection.createRangeByElem($code, false)
+            editor.selection.restoreSelection()
+            return
+        }
+
+        // 选取是空,且没有夸元素选择,则插入 <pre><code></code></prev>
+        if (this._active) {
+            // 选中状态,将编辑内容
+            this._createPanel($startElem.html())
+        } else {
+            // 未选中状态,将创建内容
+            this._createPanel()
+        }
+    },
+
+    _createPanel: function (value) {
+        // value - 要编辑的内容
+        value = value || ''
+        const type = !value ? 'new' : 'edit'
+        const textId = getRandom('texxt')
+        const btnId = getRandom('btn')
+
+        const panel = new Panel(this, {
+            width: 500,
+            // 一个 Panel 包含多个 tab
+            tabs: [
+                {
+                    // 标题
+                    title: '插入代码',
+                    // 模板
+                    tpl: `<div>
+                        <textarea id="${textId}" style="height:145px;;">${value}</textarea>
+                        <div class="lsiten-e-button-container">
+                            <button id="${btnId}" class="right">插入</button>
+                        </div>
+                    <div>`,
+                    // 事件绑定
+                    events: [
+                        // 插入代码
+                        {
+                            selector: '#' + btnId,
+                            type: 'click',
+                            fn: () => {
+                                const $text = $('#' + textId)
+                                let text = $text.val() || $text.html()
+                                text = replaceHtmlSymbol(text)
+                                if (type === 'new') {
+                                    // 新插入
+                                    this._insertCode(text)
+                                } else {
+                                    // 编辑更新
+                                    this._updateCode(text)
+                                }
+
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        }
+                    ]
+                } // first tab end
+            ] // tabs end
+        }) // new Panel end
+
+        // 显示 panel
+        panel.show()
+
+        // 记录属性
+        this.panel = panel
+    },
+
+    // 插入代码
+    _insertCode: function (value) {
+        const editor = this.editor
+        editor.cmd.do('insertHTML', `<pre><code>${value}</code></pre><p><br></p>`)
+    },
+
+    // 更新代码
+    _updateCode: function (value) {
+        const editor = this.editor
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        $selectionELem.html(value)
+        editor.selection.restoreSelection()
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        const $parentElem = $selectionELem.parent()
+        if ($selectionELem.getNodeName() === 'CODE' && $parentElem.getNodeName() === 'PRE') {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Code

+ 57 - 0
src/js/menus/column/index.js

@@ -0,0 +1,57 @@
+/*
+    menu - header
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function Column(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu">分栏</div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>栏数</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<p>一栏</p>'), value: 'one' },
+            { $elem: $('<p>二栏</p>'), value: 'two' },
+            { $elem: $('<p>三栏</p>'), value: 'three' },
+        ],
+        onClick: (value) => {
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+Column.prototype = {
+    constructor: Column,
+
+    // 执行命令
+    _command: function (value) {
+        let funName = 'lsiten_' + value;
+        if (this[funName]) {
+          this[funName]();
+        }
+    },
+    lsiten_one: function () {
+      const editor = this.editor;
+      editor.cmd.do('column', 1);
+    },
+    lsiten_two: function () {
+      const editor = this.editor;
+      editor.cmd.do('column', 2);
+    },
+    lsiten_three: function () {
+      const editor = this.editor;
+      editor.cmd.do('column', 3);
+    }
+}
+
+export default Column

+ 134 - 0
src/js/menus/droplist.js

@@ -0,0 +1,134 @@
+/*
+    droplist
+*/
+import '../../less/droplist.less';
+import $ from '../util/dom-core.js'
+import replaceLang from '../util/replace-lang.js'
+
+const _emptyFn = () => {}
+
+// 构造函数
+function DropList(menu, opt) {
+    // droplist 所依附的菜单
+    const editor = menu.editor
+    this.menu = menu
+    this.opt = opt
+    // 容器
+    const $container = $('<div class="lsiten-e-droplist"></div>')
+
+    // 标题
+    const $title = opt.$title
+    let titleHtml
+    if ($title) {
+        // 替换多语言
+        titleHtml = $title.html()
+        titleHtml = replaceLang(editor, titleHtml)
+        $title.html(titleHtml)
+
+        $title.addClass('lsiten-e-dp-title')
+        $container.append($title)
+    }
+
+    const list = opt.list || []
+    const type = opt.type || 'list'  // 'list' 列表形式(如“标题”菜单) / 'inline-block' 块状形式(如“颜色”菜单)
+    const onClick = opt.onClick || _emptyFn
+
+    // 加入 DOM 并绑定事件
+    const $list = $('<ul class="' + (type === 'list' ? 'lsiten-e-list' : 'lsiten-e-block') + '"></ul>')
+    $container.append($list)
+    list.forEach(item => {
+        const $elem = item.$elem
+
+        // 替换多语言
+        let elemHtml = $elem.html()
+        elemHtml = replaceLang(editor, elemHtml)
+        $elem.html(elemHtml)
+
+        const value = item.value
+        const $li = $('<li class="lsiten-e-item"></li>')
+        if ($elem) {
+            $li.append($elem)
+            $list.append($li)
+            $li.on('click', e => {
+                if (editor.selection.getRange() == null) {
+                  return
+                }
+                onClick(value)
+
+                // 隐藏
+                this.hideTimeoutId = setTimeout(() => {
+                    this.hide()
+                }, 0)
+            })
+        }
+    })
+
+    // 绑定隐藏事件
+    $container.on('mouseleave', e => {
+        this.hideTimeoutId = setTimeout(() => {
+            this.hide()
+        }, 0)
+    })
+
+    // 记录属性
+    this.$container = $container
+
+    // 基本属性
+    this._rendered = false
+    this._show = false
+}
+
+// 原型
+DropList.prototype = {
+    constructor: DropList,
+
+    // 显示(插入DOM)
+    show: function () {
+        if (this.hideTimeoutId) {
+            // 清除之前的定时隐藏
+            clearTimeout(this.hideTimeoutId)
+        }
+
+        const menu = this.menu
+        const $menuELem = menu.$elem
+        const $container = this.$container
+        if (this._show) {
+            return
+        }
+        if (this._rendered) {
+            // 显示
+            $container.show()
+        } else {
+            // 加入 DOM 之前先定位位置
+            const menuHeight = $menuELem.getSizeData().height || 0
+            const width = this.opt.width || 100  // 默认为 100
+            $container.css('margin-top', menuHeight + 'px')
+                    .css('width', width + 'px')
+
+            // 加入到 DOM
+            $menuELem.append($container)
+            this._rendered = true
+        }
+
+        // 修改属性
+        this._show = true
+    },
+
+    // 隐藏(移除DOM)
+    hide: function () {
+        if (this.showTimeoutId) {
+            // 清除之前的定时显示
+            clearTimeout(this.showTimeoutId)
+        }
+
+        const $container = this.$container
+        if (!this._show) {
+            return
+        }
+        // 隐藏并需改属性
+        $container.hide()
+        this._show = false
+    }
+}
+
+export default DropList

+ 115 - 0
src/js/menus/emoticon/index.js

@@ -0,0 +1,115 @@
+/*
+    menu - emoticon
+*/
+import $ from '../../util/dom-core.js'
+import Panel from '../panel.js'
+
+// 构造函数
+function Emoticon(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-happy"></i>
+        </div>`
+    )
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Emoticon.prototype = {
+    constructor: Emoticon,
+
+    onClick: function () {
+        this._createPanel()
+    },
+
+    _createPanel: function () {
+        const editor = this.editor
+        const config = editor.config
+        // 获取表情配置
+        const emotions = config.emotions || []
+
+        // 创建表情 dropPanel 的配置
+        const tabConfig = []
+        emotions.forEach(emotData => {
+            const emotType = emotData.type
+            const content = emotData.content || []
+
+            // 这一组表情最终拼接出来的 html
+            let faceHtml = ''
+
+            // emoji 表情
+            if (emotType === 'emoji') {
+                content.forEach(item => {
+                    if (item) {
+                        faceHtml += '<span class="lsiten-e-item">' + item + '</span>'
+                    }
+                })
+            }
+            // 图片表情
+            if (emotType === 'image') {
+                content.forEach(item => {
+                    const src = item.src
+                    const alt = item.alt
+                    if (src) {
+                        // 加一个 data-lsiten-e 属性,点击图片的时候不再提示编辑图片
+                        faceHtml += '<span class="lsiten-e-item"><img src="' + src + '" alt="' + alt + '" data-lsiten-e="1"/></span>'
+                    }
+                })
+            }
+
+            tabConfig.push({
+                title: emotData.title,
+                tpl: `<div class="lsiten-e-emoticon-container">${faceHtml}</div>`,
+                events: [
+                    {
+                        selector: 'span.lsiten-e-item',
+                        type: 'click',
+                        fn: (e) => {
+                            const target = e.target
+                            const $target = $(target)
+                            const nodeName = $target.getNodeName()
+
+                            let insertHtml
+                            if (nodeName === 'IMG') {
+                                // 插入图片
+                                insertHtml = $target.parent().html()
+                            } else {
+                                // 插入 emoji
+                                insertHtml = '<span>' + $target.html() + '</span>'
+                            }
+
+                            this._insert(insertHtml)
+                            // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                            return true
+                        }
+                    }
+                ]
+            })
+        })
+
+        const panel = new Panel(this, {
+            width: 300,
+            height: 200,
+            // 一个 Panel 包含多个 tab
+            tabs: tabConfig
+        })
+
+        // 显示 panel
+        panel.show()
+
+        // 记录属性
+        this.panel = panel
+    },
+
+    // 插入表情
+    _insert: function (emotHtml) {
+        const editor = this.editor
+        editor.cmd.do('insertHTML', emotHtml)
+    }
+}
+
+export default Emoticon

+ 46 - 0
src/js/menus/fontName/index.js

@@ -0,0 +1,46 @@
+/*
+    menu - fontName
+*/
+
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function FontName(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-font"></i></div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 获取配置的字体
+    const config = editor.config
+    const fontNames = config.fontNames || []
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>字体</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: fontNames.map(fontName => {
+            return { $elem: $(`<span style="font-family: ${fontName};">${fontName}</span>`), value: fontName }
+        }),
+        onClick: (value) => {
+            // 注意 this 是指向当前的 FontName 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+FontName.prototype = {
+    constructor: FontName,
+
+    _command: function (value) {
+        const editor = this.editor
+        editor.cmd.do('fontName', value)
+    }
+}
+
+export default FontName

+ 48 - 0
src/js/menus/fontSize/index.js

@@ -0,0 +1,48 @@
+/*
+    menu - fontSize
+*/
+
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function FontSize(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-text-heigh"></i></div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 160,
+        $title: $('<p>字号</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<span style="font-size: x-small;">x-small</span>'), value: '1' },
+            { $elem: $('<span style="font-size: small;">small</span>'), value: '2' },
+            { $elem: $('<span>normal</span>'), value: '3' },
+            { $elem: $('<span style="font-size: large;">large</span>'), value: '4' },
+            { $elem: $('<span style="font-size: x-large;">x-large</span>'), value: '5' },
+            { $elem: $('<span style="font-size: xx-large;">xx-large</span>'), value: '6' }
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 FontSize 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+FontSize.prototype = {
+    constructor: FontSize,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+        editor.cmd.do('fontSize', value)
+    }
+}
+
+export default FontSize

+ 46 - 0
src/js/menus/foreColor/index.js

@@ -0,0 +1,46 @@
+/*
+    menu - Forecolor
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function ForeColor(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-pencil2"></i></div>')
+    this.type = 'droplist'
+
+    // 获取配置的颜色
+    const config = editor.config
+    const colors = config.colors || []
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 120,
+        $title: $('<p>文字颜色</p>'),
+        type: 'inline-block', // droplist 内容以 block 形式展示
+        list: colors.map(color => {
+            return { $elem: $(`<i style="color:${color};" class="lsiten-e-icon-pencil2"></i>`), value: color }
+        }),
+        onClick: (value) => {
+            // 注意 this 是指向当前的 ForeColor 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+ForeColor.prototype = {
+    constructor: ForeColor,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+        editor.cmd.do('foreColor', value)
+    }
+}
+
+export default ForeColor

+ 70 - 0
src/js/menus/head/index.js

@@ -0,0 +1,70 @@
+/*
+    menu - header
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function Head(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-header"></i></div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>设置标题</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<h1>标题</h1>'), value: '<h1>' },
+            { $elem: $('<h4>副标题</h4>'), value: '<h4>' },
+            { $elem: $('<h2>标题1</h2>'), value: '<h2>' },
+            { $elem: $('<h3>标题2</h3>'), value: '<h3>' },
+            { $elem: $('<h5>标题3</h5>'), value: '<h5>' },
+            { $elem: $('<p>正文</p>'), value: '<div class="para-text">' }
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 Head 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+Head.prototype = {
+    constructor: Head,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+
+        const $selectionElem = editor.selection.getSelectionContainerElem()
+        if (editor.$textElem.equal($selectionElem)) {
+            // 不能选中多行来设置标题,否则会出现问题
+            // 例如选中的是 <p>xxx</p><p>yyy</p> 来设置标题,设置之后会成为 <h1>xxx<br>yyy</h1> 不符合预期
+            return
+        }
+
+        editor.cmd.do('formatBlock', value)
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        const reg = /^h/i
+        const cmdValue = editor.cmd.queryCommandValue('formatBlock')
+        if (reg.test(cmdValue)) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Head

+ 110 - 0
src/js/menus/img/index.js

@@ -0,0 +1,110 @@
+/*
+    menu - img
+*/
+import $ from '../../util/dom-core.js'
+import imgUtl from '../../util/imageUtil.js'
+import { getRandom, getParentByClassname, arrForEach, uploadToAliyun } from '../../util/util.js'
+import Panel from '../panel.js'
+
+// 构造函数
+function Image(editor) {
+    this.editor = editor
+    const imgMenuId = getRandom('lsiten-e-img')
+    this.$elem = $('<div class="lsiten-e-menu" id="' + imgMenuId + '"><i class="lsiten-e-icon-image"></i></div>')
+    editor.imgMenuId = imgMenuId
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Image.prototype = {
+    constructor: Image,
+
+    onClick: function () {
+        let $selectionElem = this.editor.selection.getSelectionContainerElem();
+        let borderText = getParentByClassname($selectionElem, 'js-lsiten-border');
+        if (borderText) { 
+          let size = borderText.getSizeData();
+          this._createFileForm(size);
+          this.$input[0].click();
+        } else {
+          this.editor.message.showMessage('请在框内进行图片编辑');
+        }
+    },
+    // 创建上传Form
+    _createFileForm: function (size) {
+      if (this.$form) {
+        return '';
+      }
+      let $form = $('<form class="lsiten-image-file-upload"></form>');
+      $form.css('width', 0);
+      $form.css('height', 0);
+      let $input = $('<input type = "file" accept="image/gif, image/png, image/jpg, image/jpeg"/>');
+      $form.append($input);
+      this.$form = $form;
+      this.$input = $input;
+      this.$input.on('change', (e) => {
+        this._fileUpload(size);
+      })
+      $(document.body).append($form);
+    },
+    _fileUpload: function(size) {
+      let file = this.$input[0].files[0];
+      let message = this.editor.message;
+      const editor = this.editor;
+      let policyUrl = editor.config.policyUrl;
+      uploadToAliyun(file, policyUrl).then(data => {
+         if (data.result === '00') {
+           let src = data.data.url;
+           let srcStore = data.data.urlStore;
+           this._doCmd({
+             src: src,
+             store: srcStore,
+             bsize: size
+           });         
+         } else {
+            message.showMessage(data.msg || '上传出错,请重试!');
+         }
+      })
+    },
+    _doCmd: function (img) {
+      let imageTool = new imgUtl(img.src);
+      let _this = this;
+      imageTool.getSize(complete);
+      function complete() {
+        let owidth = imageTool.owidth;
+        let bsize = img.bsize;
+        let bwidth = bsize.width - 30;
+        if (owidth >= bwidth) {
+          let ratio = bwidth / owidth;
+          imageTool.resizeByRatio(ratio);
+        }
+        let oheight = imageTool.size.h;
+        let bheight = bsize.height - 22;
+        if (oheight > bheight) {
+          let ratio = bheight / oheight;
+          imageTool.resizeByRatio(ratio);
+        }
+        let html = $('<img class="lsiten-js-image-class" style="width: ' + imageTool.size.w + 'px; height: ' + imageTool.size.h + 'px" src="' + img.src + '" data-store = "' +img.store+ '"/>');
+        _this.editor.cmd.do('insertElem', html);
+        _this.editor.selection.getRange().setStart(html[0], 0);
+        _this.editor.page._checkoutContentChangeByFun();
+      }
+    },
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor._selectedImg) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Image

+ 120 - 0
src/js/menus/index.js

@@ -0,0 +1,120 @@
+/*
+    菜单集合
+*/
+import '../../less/menus.less';
+import { objForEach } from '../util/util.js'
+import MenuConstructors from './menu-list.js'
+
+// 构造函数
+function Menus(editor) {
+    this.editor = editor
+    this.menus = {}
+}
+
+// 修改原型
+Menus.prototype = {
+    constructor: Menus,
+
+    // 初始化菜单
+    init: function () {
+        const editor = this.editor
+        const config = editor.config || {}
+        const configMenus = config.menus || []  // 获取配置中的菜单
+
+        // 根据配置信息,创建菜单
+        configMenus.forEach(menuKey => {
+            const MenuConstructor = MenuConstructors[menuKey]
+            if (MenuConstructor && typeof MenuConstructor === 'function') {
+                // 创建单个菜单
+                this.menus[menuKey] = new MenuConstructor(editor)
+            }
+        })
+
+        // 添加到菜单栏
+        this._addToToolbar()
+
+        // 绑定事件
+        this._bindEvent()
+    },
+
+    // 添加到菜单栏
+    _addToToolbar: function () {
+        const editor = this.editor
+        const $toolbarElem = editor.$toolbar
+        const menus = this.menus
+        const config = editor.config
+        // config.zIndex 是配置的编辑区域的 z-index,菜单的 z-index 得在其基础上 +1
+        const zIndex = config.zIndex + 1
+        objForEach(menus, (key, menu) => {
+            const $elem = menu.$elem
+            if ($elem) {
+                // 设置 z-index
+                $elem.css('z-index', zIndex)
+                $toolbarElem.append($elem)
+            }
+        })
+    },
+
+    // 绑定菜单 click mouseenter 事件
+    _bindEvent: function () {
+        const menus = this.menus
+        const editor = this.editor
+        objForEach(menus, (key, menu) => {
+            const type = menu.type
+            if (!type) {
+                return
+            }
+            const $elem = menu.$elem
+            const droplist = menu.droplist
+            const panel = menu.panel
+
+            // 点击类型,例如 bold
+            if (type === 'click' && menu.onClick) {
+                $elem.on('click', e => {
+                    if (editor.selection.getRange() == null) {
+                        return
+                    }
+                    menu.onClick(e)
+                })
+            }
+
+            // 下拉框,例如 head
+            if (type === 'droplist' && droplist) {
+                $elem.on('mouseenter', e => {
+                    // 显示
+                    droplist.showTimeoutId = setTimeout(() => {
+                        droplist.show()
+                    }, 200)
+                }).on('mouseleave', e => {
+                    // 隐藏
+                    droplist.hideTimeoutId = setTimeout(() => {
+                        droplist.hide()
+                    }, 0)
+                })
+            }
+
+            // 弹框类型,例如 link
+            if (type === 'panel' && menu.onClick) {
+                $elem.on('click', e => {
+                    e.stopPropagation()
+                    // 在自定义事件中显示 panel
+                    menu.onClick(e)
+                })
+            }
+        })
+    },
+
+    // 尝试修改菜单状态
+    changeActive: function () {
+        const menus = this.menus
+        objForEach(menus, (key, menu) => {
+            if (menu.tryChangeActive) {
+                setTimeout(() => {
+                    menu.tryChangeActive()
+                }, 100)
+            }
+        })
+    }
+}
+
+export default Menus

+ 60 - 0
src/js/menus/italic/index.js

@@ -0,0 +1,60 @@
+/*
+    italic-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function Italic(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-italic"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Italic.prototype = {
+    constructor: Italic,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+        
+        const editor = this.editor
+        const isSeleEmpty = editor.selection.isSelectionEmpty()
+
+        if (isSeleEmpty) {
+            // 选区是空的,插入并选中一个“空白”
+            editor.selection.createEmptyRange()
+        }
+
+        // 执行 italic 命令
+        editor.cmd.do('italic')
+
+        if (isSeleEmpty) {
+            // 需要将选取折叠起来
+            editor.selection.collapseRange()
+            editor.selection.restoreSelection()
+        }
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor.cmd.queryCommandState('italic')) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Italic

+ 62 - 0
src/js/menus/justify/index.js

@@ -0,0 +1,62 @@
+/*
+    menu - justify
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function Justify(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-paragraph-left"></i></div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>对齐方式</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<span><i class="lsiten-e-icon-paragraph-left"></i> 靠左</span>'), value: 'justifyLeft' },
+            { $elem: $('<span><i class="lsiten-e-icon-paragraph-center"></i> 居中</span>'), value: 'justifyCenter' },
+            { $elem: $('<span><i class="lsiten-e-icon-paragraph-right"></i> 靠右</span>'), value: 'justifyRight' }
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 List 对象
+            // 图片处理
+            let $img = this.editor.page._$handleImg;
+            if ($img) {         
+                switch (value) {
+                  case 'justifyLeft':
+                  $img.parent().css('text-align', 'left');
+                  break;
+                  case 'justifyCenter':
+                  $img.parent().css('text-align', 'center');           
+                  break;
+                  case 'justifyRight':
+                  $img.parent().css('text-align', 'right');                             
+                  break;
+                }
+                this.editor.page._$handleImg = null;
+                this.editor.page._$drageDiv.hide();
+            } else {
+              this._command(value)
+            }
+        }
+    })
+}
+
+// 原型
+Justify.prototype = {
+    constructor: Justify,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+        editor.cmd.do(value)
+    }
+}
+
+export default Justify

+ 168 - 0
src/js/menus/link/index.js

@@ -0,0 +1,168 @@
+/*
+    menu - link
+*/
+import $ from '../../util/dom-core.js'
+import { getRandom } from '../../util/util.js'
+import Panel from '../panel.js'
+
+// 构造函数
+function Link(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-link"></i></div>')
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Link.prototype = {
+    constructor: Link,
+
+    // 点击事件
+    onClick: function (e) {
+        const editor = this.editor
+        let $linkelem
+
+        if (this._active) {
+            // 当前选区在链接里面
+            $linkelem = editor.selection.getSelectionContainerElem()
+            if (!$linkelem) {
+                return
+            }
+            // 将该元素都包含在选取之内,以便后面整体替换
+            editor.selection.createRangeByElem($linkelem)
+            editor.selection.restoreSelection()
+            // 显示 panel
+            this._createPanel($linkelem.text(), $linkelem.attr('href'))
+        } else {
+            // 当前选区不在链接里面
+            if (editor.selection.isSelectionEmpty()) {
+                // 选区是空的,未选中内容
+                this._createPanel('', '')
+            } else {
+                // 选中内容了
+                this._createPanel(editor.selection.getSelectionText(), '')
+            }
+        }
+    },
+
+    // 创建 panel
+    _createPanel: function (text, link) {
+        // panel 中需要用到的id
+        const inputLinkId = getRandom('input-link')
+        const inputTextId = getRandom('input-text')
+        const btnOkId = getRandom('btn-ok')
+        const btnDelId = getRandom('btn-del')
+
+        // 是否显示“删除链接”
+        const delBtnDisplay = this._active ? 'inline-block' : 'none'
+
+        // 初始化并显示 panel
+        const panel = new Panel(this, {
+            width: 300,
+            // panel 中可包含多个 tab
+            tabs: [
+                {
+                    // tab 的标题
+                    title: '链接',
+                    // 模板
+                    tpl: `<div>
+                            <input id="${inputTextId}" type="text" class="block" value="${text}" placeholder="链接文字"/></td>
+                            <input id="${inputLinkId}" type="text" class="block" value="${link}" placeholder="http://..."/></td>
+                            <div class="lsiten-e-button-container">
+                                <button id="${btnOkId}" class="right">插入</button>
+                                <button id="${btnDelId}" class="gray right" style="display:${delBtnDisplay}">删除链接</button>
+                            </div>
+                        </div>`,
+                    // 事件绑定
+                    events: [
+                        // 插入链接
+                        {
+                            selector: '#' + btnOkId,
+                            type: 'click',
+                            fn: () => {
+                                // 执行插入链接
+                                const $link = $('#' + inputLinkId)
+                                const $text = $('#' + inputTextId)
+                                const link = $link.val()
+                                const text = $text.val()
+                                this._insertLink(text, link)
+
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        },
+                        // 删除链接
+                        {
+                            selector: '#' + btnDelId,
+                            type: 'click',
+                            fn: () => {
+                                // 执行删除链接
+                                this._delLink()
+
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        }
+                    ]
+                } // tab end
+            ] // tabs end
+        })
+
+        // 显示 panel
+        panel.show()
+
+        // 记录属性
+        this.panel = panel
+    },
+
+    // 删除当前链接
+    _delLink: function () {
+        if (!this._active) {
+            return
+        }
+        const editor = this.editor
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        const selectionText = editor.selection.getSelectionText()
+        editor.cmd.do('insertHTML', '<span>' + selectionText + '</span>')
+    },
+
+    // 插入链接
+    _insertLink: function (text, link) {
+        const editor = this.editor
+        const config = editor.config
+        const linkCheck = config.linkCheck
+        let checkResult = true // 默认为 true
+        if (linkCheck && typeof linkCheck === 'function') {
+            checkResult = linkCheck(text, link)
+        }
+        if (checkResult === true) {
+            editor.cmd.do('insertHTML', `<a href="${link}" target="_blank">${text}</a>`)
+        } else {
+            alert(checkResult)
+        }
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        if ($selectionELem.getNodeName() === 'A') {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Link

+ 82 - 0
src/js/menus/list/index.js

@@ -0,0 +1,82 @@
+/*
+    menu - list
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function List(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-list2"></i></div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 120,
+        $title: $('<p>设置列表</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<span><i class="lsiten-e-icon-list-numbered"></i> 有序列表</span>'), value: 'insertOrderedList' },
+            { $elem: $('<span><i class="lsiten-e-icon-list2"></i> 无序列表</span>'), value: 'insertUnorderedList' }
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 List 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+List.prototype = {
+    constructor: List,
+
+    // 执行命令
+    _command: function (value) {
+        const editor = this.editor
+        const $textElem = editor.$textElem
+        editor.selection.restoreSelection()
+        if (editor.cmd.queryCommandState(value)) {
+            return
+        }
+        editor.cmd.do(value)
+
+        // 验证列表是否被包裹在 <p> 之内
+        let $selectionElem = editor.selection.getSelectionContainerElem()
+        if ($selectionElem.getNodeName() === 'LI') {
+            $selectionElem = $selectionElem.parent()
+        }
+        if (/^ol|ul$/i.test($selectionElem.getNodeName()) === false) {
+            return
+        }
+        if ($selectionElem.equal($textElem)) {
+            // 证明是顶级标签,没有被 <p> 包裹
+            return
+        }
+        const $parent = $selectionElem.parent()
+        if ($parent.equal($textElem)) {
+            // $parent 是顶级标签,不能删除
+            return
+        }
+
+        $selectionElem.insertAfter($parent)
+        $parent.remove()
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor.cmd.queryCommandState('insertUnOrderedList') || editor.cmd.queryCommandState('insertOrderedList')) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default List

+ 81 - 0
src/js/menus/menu-list.js

@@ -0,0 +1,81 @@
+/*
+    所有菜单的汇总
+*/
+
+// 存储菜单的构造函数
+const MenuConstructors = {}
+
+import Bold from './bold/index.js'
+MenuConstructors.bold = Bold
+
+import Head from './head/index.js'
+MenuConstructors.head = Head
+
+import FontSize from './fontSize/index.js'
+MenuConstructors.fontSize = FontSize
+
+import FontName from './fontName/index.js'
+MenuConstructors.fontName = FontName
+
+import Link from './link/index.js'
+MenuConstructors.link = Link
+
+import Italic from './italic/index.js'
+MenuConstructors.italic = Italic
+
+import Redo from './redo/index.js'
+MenuConstructors.redo = Redo
+
+import StrikeThrough from './strikethrough/index.js'
+MenuConstructors.strikeThrough = StrikeThrough
+
+import Underline from './underline/index.js'
+MenuConstructors.underline = Underline
+
+import Undo from './undo/index.js'
+MenuConstructors.undo = Undo
+
+import List from './list/index.js'
+MenuConstructors.list = List
+
+import Justify from './justify/index.js'
+MenuConstructors.justify = Justify
+
+import ForeColor from './foreColor/index.js'
+MenuConstructors.foreColor = ForeColor
+
+import BackColor from './backColor/index.js'
+MenuConstructors.backColor = BackColor
+
+import Quote from './quote/index.js'
+MenuConstructors.quote = Quote
+
+import Code from './code/index.js'
+MenuConstructors.code = Code
+
+import Emoticon from './emoticon/index.js'
+MenuConstructors.emoticon = Emoticon
+
+import Table from './table/index.js'
+MenuConstructors.table = Table
+
+import Video from './video/index.js'
+MenuConstructors.video = Video
+
+import Image from './img/index.js'
+MenuConstructors.image = Image
+
+import Questiontype from './questiontype/index.js'
+MenuConstructors.questiontype = Questiontype
+
+import QuestionBorder from './questionBorder/index.js'
+MenuConstructors.questionBorder = QuestionBorder
+
+import QuestionHead from './questionHead/index.js'
+MenuConstructors.questionHead = QuestionHead
+
+import QuestionData from './questionData/index.js'
+MenuConstructors.questionData = QuestionData
+
+// 吐出所有菜单集合
+export default MenuConstructors

+ 195 - 0
src/js/menus/panel.js

@@ -0,0 +1,195 @@
+/*
+    panel
+*/
+import '../../less/panel.less';
+import $ from '../util/dom-core.js'
+import replaceLang from '../util/replace-lang.js'
+const emptyFn = () => {}
+
+// 记录已经显示 panel 的菜单
+let _isCreatedPanelMenus = []
+
+// 构造函数
+function Panel(menu, opt) {
+    this.menu = menu
+    this.opt = opt
+}
+
+// 原型
+Panel.prototype = {
+    constructor: Panel,
+
+    // 显示(插入DOM)
+    show: function () {
+        const menu = this.menu
+        if (_isCreatedPanelMenus.indexOf(menu) >= 0) {
+            // 该菜单已经创建了 panel 不能再创建
+            return
+        }
+
+        const editor = menu.editor
+        const $body = $('body')
+        const $textContainerElem = editor.$editorArea
+        const opt = this.opt
+
+        // panel 的容器
+        const $container = $('<div class="lsiten-e-panel-container"></div>')
+        const width = opt.width || 300 // 默认 300px
+        $container.css('width', width + 'px')
+                .css('margin-left', (0 - width)/2 + 'px')
+
+        // 添加关闭按钮
+        const $closeBtn = $('<i class="lsiten-e-icon-close lsiten-e-panel-close"></i>')
+        $container.append($closeBtn)
+        $closeBtn.on('click', () => {
+            this.hide()
+        })
+
+        // 准备 tabs 容器
+        const $tabTitleContainer = $('<ul class="lsiten-e-panel-tab-title"></ul>')
+        const $tabContentContainer = $('<div class="lsiten-e-panel-tab-content"></div>')
+        $container.append($tabTitleContainer).append($tabContentContainer)
+
+        // 设置高度
+        const height = opt.height
+        if (height) {
+            $tabContentContainer.css('height', height + 'px').css('overflow-y', 'auto')
+        }
+        
+        // tabs
+        const tabs = opt.tabs || []
+        const tabTitleArr = []
+        const tabContentArr = []
+        tabs.forEach((tab, tabIndex) => {
+            if (!tab) {
+                return
+            }
+            let title = tab.title || ''
+            let tpl = tab.tpl || ''
+
+            // 替换多语言
+            title = replaceLang(editor, title)
+            tpl = replaceLang(editor, tpl)
+
+            // 添加到 DOM
+            const $title = $(`<li class="lsiten-e-item">${title}</li>`)
+            $tabTitleContainer.append($title)
+            const $content = $(tpl)
+            $tabContentContainer.append($content)
+
+            // 记录到内存
+            $title._index = tabIndex
+            tabTitleArr.push($title)
+            tabContentArr.push($content)
+
+            // 设置 active 项
+            if (tabIndex === 0) {
+                $title._active = true
+                $title.addClass('lsiten-e-active')
+            } else {
+                $content.hide()
+            }
+
+            // 绑定 tab 的事件
+            $title.on('click', e => {
+                if ($title._active) {
+                    return
+                }
+                // 隐藏所有的 tab
+                tabTitleArr.forEach($title => {
+                    $title._active = false
+                    $title.removeClass('lsiten-e-active')
+                })
+                tabContentArr.forEach($content => {
+                    $content.hide()
+                })
+
+                // 显示当前的 tab
+                $title._active = true
+                $title.addClass('lsiten-e-active')
+                $content.show()
+            })
+        })
+
+        // 绑定关闭事件
+        $container.on('click', e => {
+            // 点击时阻止冒泡
+            e.stopPropagation()
+        })
+        $body.on('click', e => {
+            this.hide()
+        })
+
+        // 添加到 DOM
+        $textContainerElem.append($container)
+
+        // 绑定 opt 的事件,只有添加到 DOM 之后才能绑定成功
+        tabs.forEach((tab, index) => {
+            if (!tab) {
+                return
+            }
+            const events = tab.events || []
+            events.forEach(event => {
+                const selector = event.selector
+                const type = event.type
+                const fn = event.fn || emptyFn
+                const $content = tabContentArr[index]
+                $content.find(selector).on(type, (e) => {
+                    e.stopPropagation()
+                    const needToHide = fn(e)
+                    // 执行完事件之后,是否要关闭 panel
+                    if (needToHide) {
+                        this.hide()
+                    }
+                })
+            })
+        })
+
+        // focus 第一个 elem
+        let $inputs = $container.find('input[type=text],textarea')
+        if ($inputs.length) {
+            $inputs.get(0).focus()
+        }
+
+        // 添加到属性
+        this.$container = $container
+
+        // 隐藏其他 panel
+        this._hideOtherPanels()
+        // 记录该 menu 已经创建了 panel
+        _isCreatedPanelMenus.push(menu)
+    },
+
+    // 隐藏(移除DOM)
+    hide: function () {
+        const menu = this.menu
+        const $container = this.$container
+        if ($container) {
+            $container.remove()
+        }
+
+        // 将该 menu 记录中移除
+        _isCreatedPanelMenus = _isCreatedPanelMenus.filter(item => {
+            if (item === menu) {
+                return false
+            } else {
+                return true
+            }
+        })
+    },
+
+    // 一个 panel 展示时,隐藏其他 panel
+    _hideOtherPanels: function () {
+        if (!_isCreatedPanelMenus.length) {
+            return
+        }
+        _isCreatedPanelMenus.forEach(menu => {
+            const panel = menu.panel || {}
+            if (panel.hide) {
+                panel.hide()
+            }
+        })
+    }
+}
+
+export default Panel

+ 54 - 0
src/js/menus/questionBorder/index.js

@@ -0,0 +1,54 @@
+/*
+    menu - header
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function Questionborder(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu">题框</div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>题框</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<p>客观题框</p>'), value: 'question_1' },
+            { $elem: $('<p>主观题框</p>'), value: 'question_2' }
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 Head 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+Questionborder.prototype = {
+    constructor: Questionborder,
+
+    // 执行命令
+    _command: function (value) {
+        let funName = 'lsiten_' + value;
+        if (this[funName]) {
+          this[funName]();
+        }
+    },
+    lsiten_question_1: function () {
+      this.editor.cmd.do('border', {type: 1, data: {
+          row: 2,
+          column: 3
+      }});
+    },
+    lsiten_question_2: function () {
+      this.editor.cmd.do('border', {type: 2, data: {}});
+    }
+}
+
+export default Questionborder

+ 31 - 0
src/js/menus/questionData/index.js

@@ -0,0 +1,31 @@
+/*
+    redo-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function QuestionData(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            答题数据获取
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+QuestionData.prototype = {
+    constructor: QuestionData,
+
+    // 点击事件
+    onClick: function (e) {
+      // 点击菜单将触发这里
+      this.editor.cmd.do('borderData');
+    }
+}
+
+export default QuestionData

+ 31 - 0
src/js/menus/questionHead/index.js

@@ -0,0 +1,31 @@
+/*
+    redo-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function QuestionHead(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            答题卡头
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+QuestionHead.prototype = {
+    constructor: QuestionHead,
+
+    // 点击事件
+    onClick: function (e) {
+      // 点击菜单将触发这里
+      this.editor.cmd.do('answerHeader');
+    }
+}
+
+export default QuestionHead

+ 123 - 0
src/js/menus/questiontype/index.js

@@ -0,0 +1,123 @@
+/*
+    menu - header
+*/
+import $ from '../../util/dom-core.js'
+import DropList from '../droplist.js'
+
+// 构造函数
+function Questiontype(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu">题型</div>')
+    this.type = 'droplist'
+
+    // 当前是否 active 状态
+    this._active = false
+
+    // 初始化 droplist
+    this.droplist = new DropList(this, {
+        width: 100,
+        $title: $('<p>设置题型</p>'),
+        type: 'list', // droplist 以列表形式展示
+        list: [
+            { $elem: $('<p>选择题</p>'), value: 'type_1' },
+            { $elem: $('<p>判断题</p>'), value: 'type_2' },
+            { $elem: $('<p>主观题</p>'), value: 'type_3' },
+            { $elem: $('<p>作文题</p>'), value: 'type_4' },
+        ],
+        onClick: (value) => {
+            // 注意 this 是指向当前的 Head 对象
+            this._command(value)
+        }
+    })
+}
+
+// 原型
+Questiontype.prototype = {
+    constructor: Questiontype,
+
+    // 执行命令
+    _command: function (value) {
+        let funName = 'lsiten_' + value;
+        if (this[funName]) {
+          this[funName]();
+        }
+    },
+    lsiten_type_1: function () {
+      this.editor.cmd.do('questiontype', {
+        type: 1,
+        data: {
+          content: '',
+          nums: '3',
+          pnum: '1',
+          proId: '1022',
+          quId: '1656',
+          proData: {
+            content:"",
+            pnum:"",
+            proId:"1223",
+            pureObjective:"1",
+            score:20,
+            sort:"1"
+          },
+          quType: '单选题',
+          score: 5,
+          visible: true
+        }
+      });
+    },
+    lsiten_type_2: function () {
+      this.editor.cmd.do('questiontype', {
+        type: 2,
+        data: {
+          content: '',
+          nums: '2',
+          pnum: '1',
+          proId: '1022',
+          quId: '1656',
+          quType: '判断题',
+          score: 5,
+          visible: true,
+          proData: {
+            content:"",
+            pnum:"",
+            proId:"1223",
+            pureObjective:"1",
+            score:20,
+            sort:"1"
+          }
+        }
+      });
+    },
+    lsiten_type_3: function () {
+      this.editor.cmd.do('questiontype', {
+        type: 3,
+        data: {
+          content: '',
+          nums: '',
+          pnum: '1',
+          proId: '1022',
+          quId: '1656',
+          quType: '解答题',
+          score: 5,
+          visible: true
+        }
+      });
+    },
+    lsiten_type_4: function () {
+      this.editor.cmd.do('questiontype', {
+        type: 4,
+        data: {
+          content: '',
+          nums: '',
+          pnum: '1',
+          proId: '1022',
+          quId: '1656',
+          quType: '作文题',
+          score: 5,
+          visible: true
+        }
+      });
+    }
+}
+
+export default Questiontype

+ 75 - 0
src/js/menus/quote/index.js

@@ -0,0 +1,75 @@
+/*
+    menu - quote
+*/
+import $ from '../../util/dom-core.js'
+import { UA } from '../../util/util.js'
+
+// 构造函数
+function Quote(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-quotes-left"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Quote.prototype = {
+    constructor: Quote,
+
+    onClick: function (e) {
+        const editor = this.editor
+        const $selectionElem = editor.selection.getSelectionContainerElem()
+        const nodeName = $selectionElem.getNodeName()
+
+        if (!UA.isIE()) {
+            if (nodeName === 'BLOCKQUOTE') {
+                // 撤销 quote
+                editor.cmd.do('formatBlock', '<P>')
+            } else {
+                // 转换为 quote
+                editor.cmd.do('formatBlock', '<BLOCKQUOTE>')
+            }
+            return
+        }
+        
+        // IE 中不支持 formatBlock <BLOCKQUOTE> ,要用其他方式兼容
+        let content, $targetELem
+        if (nodeName === 'P') {
+            // 将 P 转换为 quote
+            content = $selectionElem.text()
+            $targetELem = $(`<blockquote>${content}</blockquote>`)
+            $targetELem.insertAfter($selectionElem)
+            $selectionElem.remove()
+            return
+        }
+        if (nodeName === 'BLOCKQUOTE') {
+            // 撤销 quote
+            content = $selectionElem.text()
+            $targetELem = $(`<p>${content}</p>`)
+            $targetELem.insertAfter($selectionElem)
+            $selectionElem.remove()
+        }
+    },
+
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        const reg = /^BLOCKQUOTE$/i
+        const cmdValue = editor.cmd.queryCommandValue('formatBlock')
+        if (reg.test(cmdValue)) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Quote

+ 37 - 0
src/js/menus/redo/index.js

@@ -0,0 +1,37 @@
+/*
+    redo-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function Redo(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-redo"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Redo.prototype = {
+    constructor: Redo,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+
+        const undoManager = this.editor.page.undoManager;
+
+        // 执行 redo 命令
+        undoManager.redo();
+        this.editor.selection.saveRange();
+        
+    }
+}
+
+export default Redo

+ 60 - 0
src/js/menus/strikethrough/index.js

@@ -0,0 +1,60 @@
+/*
+    strikeThrough-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function StrikeThrough(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-strikethrough"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+StrikeThrough.prototype = {
+    constructor: StrikeThrough,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+        
+        const editor = this.editor
+        const isSeleEmpty = editor.selection.isSelectionEmpty()
+
+        if (isSeleEmpty) {
+            // 选区是空的,插入并选中一个“空白”
+            editor.selection.createEmptyRange()
+        }
+
+        // 执行 strikeThrough 命令
+        editor.cmd.do('strikeThrough')
+
+        if (isSeleEmpty) {
+            // 需要将选取折叠起来
+            editor.selection.collapseRange()
+            editor.selection.restoreSelection()
+        }
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor.cmd.queryCommandState('strikeThrough')) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default StrikeThrough

+ 376 - 0
src/js/menus/table/index.js

@@ -0,0 +1,376 @@
+/*
+    menu - table
+*/
+import $ from '../../util/dom-core.js'
+import { getRandom } from '../../util/util.js'
+import Panel from '../panel.js'
+
+// 构造函数
+function Table(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-table2"></i></div>')
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Table.prototype = {
+    constructor: Table,
+
+    onClick: function () {
+        if (this._active) {
+            // 编辑现有表格
+            this._createEditPanel()
+        } else {
+            // 插入新表格
+            this._createInsertPanel()
+        }
+    },
+
+    // 创建插入新表格的 panel
+    _createInsertPanel: function () {
+        // 用到的 id
+        const btnInsertId = getRandom('btn')
+        const textRowNum = getRandom('row')
+        const textColNum = getRandom('col')
+
+        const panel = new Panel(this, {
+            width: 250,
+            // panel 包含多个 tab
+            tabs: [
+                {
+                    // 标题
+                    title: '插入表格',
+                    // 模板
+                    tpl: `<div>
+                        <p style="text-align:left; padding:5px 0;">
+                            创建
+                            <input id="${textRowNum}" type="text" value="5" style="width:40px;text-align:center;"/>
+                            行
+                            <input id="${textColNum}" type="text" value="5" style="width:40px;text-align:center;"/>
+                            列的表格
+                        </p>
+                        <div class="lsiten-e-button-container">
+                            <button id="${btnInsertId}" class="right">插入</button>
+                        </div>
+                    </div>`,
+                    // 事件绑定
+                    events: [
+                        {
+                            // 点击按钮,插入表格
+                            selector: '#' + btnInsertId,
+                            type: 'click',
+                            fn: () => {
+                                const rowNum = parseInt($('#' + textRowNum).val())
+                                const colNum = parseInt($('#' + textColNum).val())
+
+                                if (rowNum && colNum && rowNum > 0 && colNum > 0) {
+                                    // form 数据有效
+                                    this._insert(rowNum, colNum)
+                                }
+
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        }
+                    ]
+                } // first tab end
+            ]  // tabs end
+        }) // panel end
+
+        // 展示 panel
+        panel.show()
+
+        // 记录属性
+        this.panel = panel
+    },
+
+    // 插入表格
+    _insert: function (rowNum, colNum) {
+        // 拼接 table 模板
+        let r, c
+        let html = '<table border="0" width="100%" cellpadding="0" cellspacing="0">'
+        for (r = 0; r < rowNum; r++) {
+            html += '<tr>'
+            if (r === 0) {
+                for (c = 0; c < colNum; c++) {
+                    html += '<th>&nbsp;</th>'
+                }
+            } else {
+                for (c = 0; c < colNum; c++) {
+                    html += '<td>&nbsp;</td>'
+                }
+            }
+            html += '</tr>'
+        }
+        html += '</table><p><br></p>'
+
+        // 执行命令
+        const editor = this.editor
+        editor.cmd.do('insertHTML', html)
+
+        // 防止 firefox 下出现 resize 的控制点
+        editor.cmd.do('enableObjectResizing', false)
+        editor.cmd.do('enableInlineTableEditing', false)
+    },
+
+    // 创建编辑表格的 panel
+    _createEditPanel: function () {
+        // 可用的 id
+        const addRowBtnId = getRandom('add-row')
+        const addColBtnId = getRandom('add-col')
+        const delRowBtnId = getRandom('del-row')
+        const delColBtnId = getRandom('del-col')
+        const delTableBtnId = getRandom('del-table')
+
+        // 创建 panel 对象
+        const panel = new Panel(this, {
+            width: 320,
+            // panel 包含多个 tab
+            tabs: [
+                {
+                    // 标题
+                    title: '编辑表格',
+                    // 模板
+                    tpl: `<div>
+                        <div class="lsiten-e-button-container" style="border-bottom:1px solid #f1f1f1;padding-bottom:5px;margin-bottom:5px;">
+                            <button id="${addRowBtnId}" class="left">增加行</button>
+                            <button id="${delRowBtnId}" class="red left">删除行</button>
+                            <button id="${addColBtnId}" class="left">增加列</button>
+                            <button id="${delColBtnId}" class="red left">删除列</button>
+                        </div>
+                        <div class="lsiten-e-button-container">
+                            <button id="${delTableBtnId}" class="gray left">删除表格</button>
+                        </dv>
+                    </div>`,
+                    // 事件绑定
+                    events: [
+                        {
+                            // 增加行
+                            selector: '#' + addRowBtnId,
+                            type: 'click',
+                            fn: () => {
+                                this._addRow()
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        },
+                        {
+                            // 增加列
+                            selector: '#' + addColBtnId,
+                            type: 'click',
+                            fn: () => {
+                                this._addCol()
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        },
+                        {
+                            // 删除行
+                            selector: '#' + delRowBtnId,
+                            type: 'click',
+                            fn: () => {
+                                this._delRow()
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        },
+                        {
+                            // 删除列
+                            selector: '#' + delColBtnId,
+                            type: 'click',
+                            fn: () => {
+                                this._delCol()
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        },
+                        {
+                            // 删除表格
+                            selector: '#' + delTableBtnId,
+                            type: 'click',
+                            fn: () => {
+                                this._delTable()
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        }
+                    ]
+                }
+            ]
+        })
+        // 显示 panel
+        panel.show()
+    },
+
+    // 获取选中的单元格的位置信息
+    _getLocationData: function () {
+        const result = {}
+        const editor = this.editor
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        const nodeName = $selectionELem.getNodeName()
+        if (nodeName !== 'TD' && nodeName !== 'TH') {
+            return
+        }
+
+        // 获取 td index
+        const $tr = $selectionELem.parent()
+        const $tds = $tr.children()
+        const tdLength = $tds.length
+        $tds.forEach((td, index) => {
+            if (td === $selectionELem[0]) {
+                // 记录并跳出循环
+                result.td = {
+                    index: index,
+                    elem: td,
+                    length: tdLength
+                }
+                return false
+            }
+        })
+
+        // 获取 tr index
+        const $tbody = $tr.parent()
+        const $trs = $tbody.children()
+        const trLength = $trs.length
+        $trs.forEach((tr, index) => {
+            if (tr === $tr[0]) {
+                // 记录并跳出循环
+                result.tr = {
+                    index: index,
+                    elem: tr,
+                    length: trLength
+                }
+                return false
+            }
+        })
+
+        // 返回结果
+        return result
+    },
+
+    // 增加行
+    _addRow: function () {
+        // 获取当前单元格的位置信息
+        const locationData = this._getLocationData()
+        if (!locationData) {
+            return
+        }
+        const trData = locationData.tr
+        const $currentTr = $(trData.elem)
+        const tdData = locationData.td
+        const tdLength = tdData.length
+
+        // 拼接即将插入的字符串
+        const newTr = document.createElement('tr')
+        let tpl = '', i
+        for (i = 0; i < tdLength; i++) {
+            tpl += '<td>&nbsp;</td>'
+        }
+        newTr.innerHTML = tpl
+        // 插入
+        $(newTr).insertAfter($currentTr)
+    },
+
+    // 增加列
+    _addCol: function () {
+        // 获取当前单元格的位置信息
+        const locationData = this._getLocationData()
+        if (!locationData) {
+            return
+        }
+        const trData = locationData.tr
+        const tdData = locationData.td
+        const tdIndex = tdData.index
+        const $currentTr = $(trData.elem)
+        const $trParent = $currentTr.parent()
+        const $trs = $trParent.children()
+
+        // 遍历所有行
+        $trs.forEach(tr => {
+            const $tr = $(tr)
+            const $tds = $tr.children()
+            const $currentTd = $tds.get(tdIndex)
+            const name = $currentTd.getNodeName().toLowerCase()
+
+            // new 一个 td,并插入
+            const newTd = document.createElement(name)
+            $(newTd).insertAfter($currentTd)
+        })
+    },
+
+    // 删除行
+    _delRow: function () {
+        // 获取当前单元格的位置信息
+        const locationData = this._getLocationData()
+        if (!locationData) {
+            return
+        }
+        const trData = locationData.tr
+        const $currentTr = $(trData.elem)
+        $currentTr.remove()
+    },
+
+    // 删除列
+    _delCol: function () {
+        // 获取当前单元格的位置信息
+        const locationData = this._getLocationData()
+        if (!locationData) {
+            return
+        }
+        const trData = locationData.tr
+        const tdData = locationData.td
+        const tdIndex = tdData.index
+        const $currentTr = $(trData.elem)
+        const $trParent = $currentTr.parent()
+        const $trs = $trParent.children()
+
+        // 遍历所有行
+        $trs.forEach(tr => {
+            const $tr = $(tr)
+            const $tds = $tr.children()
+            const $currentTd = $tds.get(tdIndex)
+            // 删除
+            $currentTd.remove()
+        })
+    },
+
+    // 删除表格
+    _delTable: function () {
+        const editor = this.editor
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        const $table = $selectionELem.parentUntil('table')
+        if (!$table) {
+            return
+        }
+        $table.remove()
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        const $selectionELem = editor.selection.getSelectionContainerElem()
+        if (!$selectionELem) {
+            return
+        }
+        const nodeName = $selectionELem.getNodeName()
+        if (nodeName === 'TD' || nodeName === 'TH') {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Table

+ 79 - 0
src/js/menus/underline/index.js

@@ -0,0 +1,79 @@
+/*
+    underline-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function Underline(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-underline"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Underline.prototype = {
+    constructor: Underline,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+        
+        const editor = this.editor
+        const isSeleEmpty = editor.selection.isSelectionEmpty()
+
+        if (isSeleEmpty) {
+            // 选区是空的,插入并选中一个“空白”
+            editor.selection.createEmptyRange()
+        }
+        let text =editor.selection.getRange();
+        let startText = text.startContainer;
+        if (startText.nodeType === 3) {
+          let parentNode = startText.parentNode;
+          let nextNode =startText.nextSibling;
+          let span = document.createElement('span');
+          span.style.textDecoration = 'underline';
+          span.appendChild(startText);
+          if (nextNode) {
+            parentNode.insertBefore(span, nextNode);
+          } else {
+            parentNode.appendChild(span);
+          }
+          // 最后,恢复选取保证光标在原来的位置闪烁
+          editor.selection.getRange().setStart(span, 1);
+        } else {
+          let html = '<span style="text-decoration: underline;">&#8203;</span>';
+          editor.cmd.do('insertHTML', html);
+        }
+        editor.selection.saveRange();
+        editor.selection.restoreSelection();
+        // 执行 underline 命令
+        // let result = editor.cmd.do('underline');
+        if (isSeleEmpty) {
+            // 需要将选取折叠起来
+            editor.selection.collapseRange()
+            editor.selection.restoreSelection()
+        }
+    },
+
+    // 试图改变 active 状态
+    tryChangeActive: function (e) {
+        const editor = this.editor
+        const $elem = this.$elem
+        if (editor.cmd.queryCommandState('underline')) {
+            this._active = true
+            $elem.addClass('lsiten-e-active')
+        } else {
+            this._active = false
+            $elem.removeClass('lsiten-e-active')
+        }
+    }
+}
+
+export default Underline

+ 36 - 0
src/js/menus/undo/index.js

@@ -0,0 +1,36 @@
+/*
+    undo-menu
+*/
+import $ from '../../util/dom-core.js'
+
+// 构造函数
+function Undo(editor) {
+    this.editor = editor
+    this.$elem = $(
+        `<div class="lsiten-e-menu">
+            <i class="lsiten-e-icon-undo"></i>
+        </div>`
+    )
+    this.type = 'click'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Undo.prototype = {
+    constructor: Undo,
+
+    // 点击事件
+    onClick: function (e) {
+        // 点击菜单将触发这里
+
+        const undoManager = this.editor.page.undoManager;
+
+        // 执行 redo 命令
+        undoManager.undo();
+        this.editor.selection.saveRange();
+    }
+}
+
+export default Undo

+ 86 - 0
src/js/menus/video/index.js

@@ -0,0 +1,86 @@
+/*
+    menu - video
+*/
+import $ from '../../util/dom-core.js'
+import { getRandom } from '../../util/util.js'
+import Panel from '../panel.js'
+
+// 构造函数
+function Video(editor) {
+    this.editor = editor
+    this.$elem = $('<div class="lsiten-e-menu"><i class="lsiten-e-icon-play"></i></div>')
+    this.type = 'panel'
+
+    // 当前是否 active 状态
+    this._active = false
+}
+
+// 原型
+Video.prototype = {
+    constructor: Video,
+
+    onClick: function () {
+        this._createPanel()
+    },
+
+    _createPanel: function () {
+        // 创建 id
+        const textValId = getRandom('text-val')
+        const btnId = getRandom('btn')
+
+        // 创建 panel
+        const panel = new Panel(this, {
+            width: 350,
+            // 一个 panel 多个 tab
+            tabs: [
+                {
+                    // 标题
+                    title: '插入视频',
+                    // 模板
+                    tpl: `<div>
+                        <input id="${textValId}" type="text" class="block" placeholder="格式如:<iframe src=... ><\/iframe>"/>
+                        <div class="lsiten-e-button-container">
+                            <button id="${btnId}" class="right">插入</button>
+                        </div>
+                    </div>`,
+                    // 事件绑定
+                    events: [
+                        {
+                            selector: '#' + btnId,
+                            type: 'click',
+                            fn: () => {
+                                const $text = $('#' + textValId)
+                                const val = $text.val().trim()
+
+                                // 测试用视频地址
+                                // <iframe height=498 width=510 src='http://player.youku.com/embed/XMjcwMzc3MzM3Mg==' frameborder=0 'allowfullscreen'></iframe>
+
+                                if (val) {
+                                    // 插入视频
+                                    this._insert(val)
+                                }
+
+                                // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
+                                return true
+                            }
+                        }
+                    ]
+                } // first tab end
+            ] // tabs end
+        }) // panel end
+
+        // 显示 panel
+        panel.show()
+
+        // 记录属性
+        this.panel = panel
+    },
+
+    // 插入视频
+    _insert: function (val) {
+        const editor = this.editor
+        editor.cmd.do('insertHTML', val + '<p><br></p>')
+    }
+}
+
+export default Video

+ 22 - 0
src/js/plugin/commands/answerHeader.js

@@ -0,0 +1,22 @@
+import questionGenerate from '../../question/index.js';
+let AnswerHeader = function ($page) {
+  let editor = this.editor;
+  let type = editor.layoutMode;
+  if (type === 2 && $page.pageIndex % 2 > 0) {
+    return false;
+  }
+  let pageSize = editor.pageSize;
+  let paperModel = editor.noMode; // editor.paperModel;
+  let columNumber = parseInt(editor.columnNumber);
+  let key = pageSize + '_' + paperModel + '_' + columNumber + '_';
+  if (parseInt(editor.noCount) > 9) {
+    key += 2;
+  } else {
+    key += 1;
+  }
+  return questionGenerate.generateQuestionHead(editor, key, $page);
+}
+
+export default {
+  _answerHeader: AnswerHeader
+}

+ 157 - 0
src/js/plugin/commands/border.js

@@ -0,0 +1,157 @@
+import $ from '../../util/dom-core.js';
+function _addParagraph (editor) {
+  let $para = editor.page._addParagraph();
+  if ($para) {
+    editor.selection.getRange().setStart($para.children(0)[0], 0);
+    if (editor.selection.getRange()) {
+      editor.selection.restoreSelection();
+    }
+    editor.selection.saveRange();
+  } else {
+    _addParagraph(editor);
+  }
+}
+// 客观题框
+function objectBorder (editor, data) {
+  let sortIndex = data.sort;
+  let border = $('<div class="js-lsiten-border lsiten-question-object"></div>');
+  border.attr('data-type', 1);
+  border.attr('data-sort', sortIndex);
+  border.attr('tabindex', 1);
+  border.attr('data-current-row', 0);
+  border.attr('contenteditable', false);
+  // style start
+  border.css('border', '1px solid #000')
+        .css('padding', '0')
+        .css('margin', '10px 0')
+        .css('overflow', 'hidden')
+        .css('height', 'auto')
+        .css('width', '100%')
+        .css('box-sizing', 'border-box')
+        .css('outline', 'none')
+        .css('border-color', '#000');
+  // style end
+
+  // 生成行列
+  let row = parseInt(data.row);
+  let column = parseInt(data.column);
+  border.attr('data-row', row);
+  border.attr('data-column', column);
+  let cWidth = 100 / column + '%';
+  let lastColumn = column - 1;
+  let columnIndex = 1;
+  for (let i = 0; i < row; i++) {
+    let $row = $('<div class = "js-options-row"></div>');
+    $row.css('float', 'left')
+        // .css('min-height', '100px')
+        .css('height', 'auto')
+        .css('width', '100%');
+    $row.attr('data-current-column', 0);
+    i > 0 && ($row.css('border-top', '1px solid #000').css('box-sizing', 'border-box'));
+    for (let j = 0; j < column; j++) {
+      let $column = $('<div class = "js-option-column"><div style="padding: 12px 0"></div></div>');
+      $column.attr('data-question-count', 0);
+      $column.attr('data-Index', columnIndex++);
+      $column.attr('data-sort', sortIndex);
+      $column.css('width', cWidth)
+             .css('float', 'left')
+            //  .css('min-height', '100px')
+             .css('height', 'auto');
+      j < lastColumn && ($column.css('border-right', '1px solid #000').css('box-sizing', 'border-box'));
+      $row.append($column);
+    }
+    border.append($row);
+  }
+  // 加一段,再插入框
+  _addParagraph(editor);
+  // 插入框内容
+  let html = $('<div></div>').append(border).html();
+  editor.cmd.do('insertHTML', html)
+  editor.selection.saveRange();
+}
+
+// 主观题框
+function subjectBorder (editor, data) {
+  let group = parseInt(data.group);
+  let sortIndex = data.sort;
+  let border = $('<div class="js-lsiten-border lsiten-question-subject"><div class="border-content"><p><br/></p></div></div>');
+  border.attr('data-type', 2);
+  border.attr('data-sort', sortIndex);
+  border.attr('tabindex', 1);
+  // style start
+  border.css('border', '1px solid #000')
+  .css('padding', '10px 15px')
+  .css('height', 'auto')
+  .css('margin', '10px 0')
+  .css('width', '100%')
+  .css('box-sizing', 'border-box')
+  .css('outline', 'none')
+  .css('border-color', '#000');
+  // style end
+  if (group > 0) {
+    let problems = data.pros;
+    let problemsLength = problems.length;
+    let $optional = $('<div class="js-lsiten-options-box" contenteditable="false"></div>');
+    $optional.css('margin-bottom', '10px');
+    let $optionalTips = $('<div>选做题</div>')
+    $optional.append($optionalTips);
+    let $optionalBody = $('<div></div>');
+    let $optionTitle = $('<div>题号:</div>');
+    $optionTitle.css('display', 'inline-block').css('width', 'auto').css('height', 'auto');
+    $optionalBody.append($optionTitle);
+    for (let i = 0; i < problemsLength; i++) {
+      let $problemBox = $('<div></div>');
+      $problemBox.css('display', 'inline-block').css('width', 'auto').css('height', 'auto').css('margin', '0 10px');
+      let pnum = problems[i].pnum;
+      // 如果小问没有pnum,则显示小问pnum
+      if (!pnum) {
+        let qutions = problems[i].qus;
+        let pnumArr = [];
+        for (let k = 0; k < qutions.length; k++) {
+          pnumArr.push(qutions[k].pnum);
+        }
+        pnum = '[' + pnumArr.join(',') + ']';
+      }
+      let $optionPnum = $('<div>' + pnum + '</div>');
+      $optionPnum.css('display', 'inline-block').css('width', 'auto').css('height', 'auto').css('margin', '0 3px');
+      let $optionBorder = $('<div>&#8203;</div>');
+      $optionBorder.css('border', '1px solid #000')
+           .css('text-align', 'center')
+           .css('width', '19px')
+           .css('height', 'auto')
+           .css('box-sizing', 'border-box')
+           .css('font-size', '12px')
+           .css('display', 'inline-block')
+           .css('line-height', '11px')
+           .css('margin', '0 0 0 5px');
+      $problemBox.append($optionPnum);
+      $problemBox.append($optionBorder);
+      $optionalBody.append($problemBox);
+    }
+    $optional.append($optionalBody);
+    border[0].firstChild.insertBefore($optional[0], border[0].firstChild.firstChild);
+  }
+
+  // 加一段,再插入框
+  _addParagraph(editor);
+  // 插入框内容
+  let html = $('<div></div>').append(border).html();
+  editor.cmd.do('insertHTML', html)
+  editor.selection.saveRange();
+}
+let Border = function (params) {
+  let editor = this.editor;
+  let type = params.type;
+  let data = params.data;
+  switch (parseInt(type)) {
+    case 1:
+    objectBorder(editor, data);
+    break;
+    case 2: 
+    subjectBorder(editor, data);
+    break;
+  }
+ }
+ export default {
+   _border: Border
+ }

+ 121 - 0
src/js/plugin/commands/getBorderData.js

@@ -0,0 +1,121 @@
+/**
+ * function 数组去重复
+ * @param {Array} arr 去重的数组
+ */
+function uniqueArray(arr){
+  let x = new Set(arr);
+ return [...x];
+}
+
+/**
+ * function 根据小问id获取小问数据
+ * @param {int} id 排序序号 
+ * @param {Array} data  小题数据
+ */
+function getQuestionsDataById (id, data) {
+  return data.filter(item => parseInt(item.quId) === id);
+}
+/**
+ * function 根据小题id获取小题框数据
+ * @param {int} id 排序序号 
+ * @param {Array} data  小题数据
+ */
+function getProblemDataById (id, data) {
+  return data.filter(item => parseInt(item.proId) === id);
+}
+
+/**
+ * function 根据框的sort获取框数据
+ * @param {int} sort 排序序号 
+ * @param {Array} data  框数据
+ */
+function getBorderDataBySort (sort, data) {
+  return data.filter(item => parseInt(item.sort) === sort);
+}
+
+/**
+ * 
+ * @param {int} pageIndex  页面index
+ * @param {Node} border 框node
+ * @param {int} sort 排序序号 
+ * @param {Array} data 框数据
+ * @param {Object} borderStart 框编号开始数据
+ */
+function updateQuestionPosition (pageIndex, border, sort, data, borderStart) {
+  let borderData = getBorderDataBySort(sort, data);
+  if (borderData[0]) {
+    let columns = border.getElementsByClassName('js-option-column');
+    if (columns) {
+      let columnLength = columns.length;
+      let lastColumnIndex = columnLength - 1;
+      for (let i = 0; i < columnLength; i++) {
+        let column = columns[i]
+        let columnIndex = parseInt(column.getAttribute('data-index'));
+        let sort = parseInt(column.getAttribute('data-sort'));
+        if (borderStart[sort]) {
+          columnIndex = borderStart[sort] + i + 1;
+        }
+        if (i === lastColumnIndex) {
+          borderStart[sort] = columnIndex;
+        }
+
+
+        let questions = column.getElementsByClassName('js-lsiten-question');
+        if (questions) {
+          let questionLength = questions.length;
+          let lastquestionIndex = columnLength - 1;
+          for (let j = 0; j < questionLength; j++) {
+            let questionDom = questions[j];
+            let pid = parseInt(questionDom.getAttribute('data-pid'));
+            let qid = parseInt(questionDom.getAttribute('data-qid'));
+            let problemData = getProblemDataById(pid, borderData[0].pros);
+            if (problemData[0]) {
+              let questionsData = problemData[0].qus;
+              let questionData = getQuestionsDataById(qid, questionsData);
+              questionData[0].rIndex = (pageIndex + 1) + '-' + columnIndex;
+            } else {
+              console.log("%c小问丢失", "color:red")
+            }
+          }
+        } else {
+          console.log("%c题丢失", "color:red");
+        }
+      }
+    }
+  } else {
+    console.log("%c框丢失", "color:red");
+  }
+}
+
+let borderData = function () {
+  let editor = this.editor;
+  let pages = editor.page.pages;
+  let data = editor.data;
+  let borderData = data.pageQus;
+  let BorderPages = {};
+  let borderStart = {};
+  pages.forEach(page => {
+    let borders = page.$textElem.find('.js-lsiten-border');
+    borders.forEach(border => {
+      let type = parseInt(border.getAttribute('data-type'));
+      let sort = parseInt(border.getAttribute('data-sort'));
+      if (!BorderPages[sort]) {
+        BorderPages[sort] = [];
+      }
+      if (type === 1) {
+        updateQuestionPosition(page.pageIndex, border, sort, borderData, borderStart);
+      }
+      BorderPages[sort].push((page.pageIndex + 1));
+    })
+  });
+
+  borderData.forEach(border => {
+    border.pIndex = uniqueArray(BorderPages[parseInt(border.sort)]).join('-');
+  });
+
+  data.examPageCnt = pages.length;
+  return data;
+}
+export default {
+  _borderData: borderData
+}

+ 36 - 0
src/js/plugin/commands/questiontype.js

@@ -0,0 +1,36 @@
+import { getParentByClassname } from '../../util/util.js';
+import questionGenerate from '../../question/index.js';
+let Questiontype = function (params) {
+  let editor = this.editor;
+  let type = params.type;
+  let data = params.data;
+  let visible = data.visible;
+  if (!visible) {
+    return false;
+  }
+  const selection = editor.selection;
+  let $selectionElem = selection.getSelectionContainerElem();
+  let border = getParentByClassname($selectionElem, 'js-lsiten-border');
+  if (!border) {
+    editor.message.showMessage('请在题框中添加题!');
+    return '';
+  }
+  switch (parseInt(type)) {
+    case 1:
+    questionGenerate.generateChoice(border, data, editor);
+    break;
+    case 2: 
+    questionGenerate.generateJudge(border, data, editor);
+    break;
+    case 3:
+    questionGenerate.generateSubjectBox(border, data, editor);
+    break;
+    case 4: 
+    questionGenerate.generateWriting(border, data, editor);
+    break;
+  }
+
+ }
+ export default {
+   _questiontype: Questiontype
+ }

+ 7 - 0
src/js/plugin/index.js

@@ -0,0 +1,7 @@
+const files = require.context('./commands', false, /\.js$/)
+let commands = {}
+
+files.keys().forEach(key => {
+  commands = Object.assign({}, commands, files(key).default)
+})
+export default commands;

+ 149 - 0
src/js/question/choice.js

@@ -0,0 +1,149 @@
+import $ from '../util/dom-core.js';
+import { getParentByClassname, isEmptyElement } from '../util/util.js';
+const _mapOptionNum = [
+  'A', 'B', 'C', 'D', 'E', 'F',
+  'G', 'H', 'I', 'J', 'K', 'L',
+  'M', 'N', 'O', 'P', 'Q', 'R',
+  'S', 'T', 'U', 'V', 'W', 'X',
+  'Y', 'Z'
+];
+/**
+ * function 添加选择题
+ * @param $border {DomElement} 需要添加题的框
+ * @param data {JSON} 题目数据
+ * @param editor {Objec} 编辑器对象 
+ */
+
+let generateChoice = function ($border, data, editor) {
+  // 1、判断框是主观题框还是客观题框
+  let proId = data.proId;
+  let quId = data.quId;
+  let type = parseInt($border.attr('data-type'));
+  let nums = parseInt(data.nums);
+  let pnum = data.pnum;
+  let $questionItem = $('<div class="lsiten-question-choice js-lsiten-question"></div>');
+  $questionItem.attr('data-type', 1);
+  $questionItem.attr('data-pid', proId);
+  $questionItem.attr('data-qid', quId);
+  $questionItem.css('margin', '8px 0')
+               .css('outline', 'none')
+               .css('overflow', 'hidden')
+               .css('position', 'relative');
+
+  let mergePnum = data.proData.pnum;
+             
+  if (mergePnum && mergePnum.length > 0) {
+    pnum = mergePnum +''+ pnum;
+  }
+  let $span = $('<span class = "lsiten-pnum-box"> ' + pnum + ' </span>');
+  $span.css('min-width', '25px')
+       .css('height', 'auto')
+       .css('width', 'auto')
+       .css('line-height', '14px')
+       .css('display', 'inline-block')
+       .css('text-align', 'right')
+       .css('float', 'left')
+       .css('margin-right', '0');
+  let $optionBox = generateOptions(nums);
+
+  $questionItem.append($span);
+  $questionItem.append($optionBox);
+  if (type === 1) {
+    // 在客观题里添加
+    let currentRow = parseInt($border.attr('data-current-row'));
+    let row = parseInt($border.attr('data-row'));
+    let column = parseInt($border.attr('data-column'));
+    let $currentRow = null;
+    try {
+      $currentRow = $($border.find('.js-options-row')[currentRow]);
+    } catch (err) {
+      console.log("%c" + err, "color:red");
+      return '';
+    }
+    if(!$currentRow.length) {
+      return '';
+    }
+    let currentColumn = parseInt($currentRow.attr('data-current-column'));
+    let $currentColumn = null;
+    try {
+      $currentColumn = $($currentRow.find('.js-option-column')[currentColumn]);
+    } catch (err) {
+      console.log("%c" + err, "color:red");      
+      return '';
+    }
+    let questionCount = parseInt($currentColumn.attr('data-question-count'));
+    if (questionCount === 5) {
+      // 下一栏
+      if (currentColumn === (column - 1)) {
+        // 下一行
+        $border.attr('data-current-row', ++currentRow);
+        let nextRowNode = $currentRow[0].nextSibling;
+        if (nextRowNode) {
+          $currentColumn = $(nextRowNode.firstChild);
+        } else {
+          console.log("%c该框问题个数已满了,请再添加框", "color:red");
+          return '';
+        }
+      } else {
+        $currentRow.attr('data-current-column', ++currentColumn);
+        let nextColumnNode = $currentColumn[0].nextSibling;
+        if (nextColumnNode) {
+          $currentColumn = $(nextColumnNode);
+        }
+      }
+
+      questionCount = 0;
+    }
+
+    $($currentColumn[0].firstChild).append($questionItem);
+    $currentColumn.attr('data-question-count', ++questionCount);
+
+    let size = $currentColumn.getSizeData();
+    $currentRow.find('.js-option-column').css('min-height', size.height + 'px');
+  } else {
+    // 在主观题内添加
+    let $selectionElem = editor.selection.getSelectionContainerElem();
+    let $borderContent = getParentByClassname($selectionElem, 'border-content');
+    if (!$borderContent) {
+      console.log("%c请在框内添加该题!", "color:red");
+      return '';
+    }
+    $borderContent.append($questionItem);
+    if ( isEmptyElement($borderContent[0].firstChild) ) {
+      $borderContent[0].removeChild($borderContent[0].firstChild);
+    }
+    editor.selection.getRange().setStartAfter($questionItem[0]);
+    if (editor.selection.getRange()) {
+      editor.selection.restoreSelection();
+    }
+    editor.selection.saveRange();
+    editor.selection.collapseRange();
+  }
+ }
+ /**
+  * function 根据选项个数生产选项
+  * @param num {int} 选项个数 
+  */
+ function generateOptions (num) {
+  let $box = $('<div class="lsiten-options-box"></div>');
+  $box.css('display', 'inline-block').css('float', 'left').css('width', 'auto').css('font-size', '0');
+  for (let i = 0; i < num; i++) {
+    let $option = $('<div class="lsiten-option">' + _mapOptionNum[i] + '</div>');
+    $option.css('border', '1px solid #000')
+           .css('text-align', 'center')
+           .css('width', '19px')
+           .css('height', 'auto')
+           .css('box-sizing', 'border-box')
+           .css('font-size', '12px')
+           .css('display', 'inline-block')
+           .css('line-height', '0.95')
+           .css('margin', '0 0 0 5px');
+    $box.append($option);
+  }
+
+  return $box;
+ }
+
+ export default {
+   generateChoice: generateChoice
+ }

+ 11 - 0
src/js/question/index.js

@@ -0,0 +1,11 @@
+const files = require.context('.', false, /\.js$/)
+let modules = {}
+
+files.keys().forEach(key => {
+  if (key === './index.js') {
+    return '';
+  }
+  modules = Object.assign({}, modules, files(key).default)
+})
+
+export default modules

+ 146 - 0
src/js/question/judge.js

@@ -0,0 +1,146 @@
+import $ from '../util/dom-core.js';
+import { getParentByClassname, isEmptyElement } from '../util/util.js';
+ /**
+  * function 生成判断题
+  * @param {DomElement} $border 需要添加题的框
+  * @param {JSON} data  题目数据
+  * @param {Objec} editor 编辑器对象
+  */
+ let generateJudge = function ($border, data, editor) {
+  let type = parseInt($border.attr('data-type'));
+  let proId = data.proId;
+  let quId = data.quId;
+  let pnum = data.pnum;
+  let $questionItem = $('<div class="lsiten-question-judge js-lsiten-question"></div>');
+  $questionItem.attr('data-type', 2);
+  $questionItem.attr('data-pid', proId);
+  $questionItem.attr('data-qid', quId);
+  $questionItem.css('margin', '8px 0')
+               .css('outline', 'none')
+               .css('overflow', 'hidden')
+               .css('position', 'relative');
+  let mergePnum = data.proData.pnum;
+             
+  if (mergePnum && mergePnum.length > 0) {
+    pnum = mergePnum +''+ pnum;
+  }
+  let $span = $('<span class = "lsiten-pnum-box"> ' + pnum + ' </span>');
+  $span.css('min-width', '25px')
+       .css('height', 'auto')
+       .css('width', 'auto')
+       .css('line-height', '14px')
+       .css('display', 'inline-block')
+       .css('text-align', 'right')
+       .css('float', 'left')
+       .css('margin-right', '0');
+  let $optionBox = generateJudgeOptions();
+
+  $questionItem.append($span);
+  $questionItem.append($optionBox);
+
+
+  if (type === 1) {
+    // 在客观题里添加
+    let currentRow = parseInt($border.attr('data-current-row'));
+    let row = parseInt($border.attr('data-row'));
+    let column = parseInt($border.attr('data-column'));
+    let $currentRow = null;
+    try {
+      $currentRow = $($border.find('.js-options-row')[currentRow]);
+    } catch (err) {
+      console.log("%c" + err, "color:red");   
+      return '';
+    }
+    let currentColumn = parseInt($currentRow.attr('data-current-column'));
+    let $currentColumn = null;
+    try {
+      $currentColumn = $($currentRow.find('.js-option-column')[currentColumn]);
+    } catch (err) {
+      console.log("%c" + err, "color:red");
+      return '';
+    }
+    let questionCount = parseInt($currentColumn.attr('data-question-count'));
+    if (questionCount === 5) {
+      // 下一栏
+      if (currentColumn === (column - 1)) {
+        // 下一行
+        $border.attr('data-current-row', ++currentRow);
+        let nextRowNode = $currentRow[0].nextSibling;
+        if (nextRowNode) {
+          $currentColumn = $(nextRowNode.firstChild);
+        } else {
+         console.log("%c该框问题个数已满了,请再添加框", "color:red");
+          return '';
+        }
+      } else {
+        $currentRow.attr('data-current-column', ++currentColumn);
+        let nextColumnNode = $currentColumn[0].nextSibling;
+        if (nextColumnNode) {
+          $currentColumn = $(nextColumnNode);
+        }
+      }
+
+      questionCount = 0;
+    }
+
+    $($currentColumn[0].firstChild).append($questionItem);
+    $currentColumn.attr('data-question-count', ++questionCount);
+
+    let size = $currentColumn.getSizeData();
+    $currentRow.find('.js-option-column').css('min-height', size.height + 'px');
+  } else {
+    // 在主观题内添加
+    let $selectionElem = editor.selection.getSelectionContainerElem();
+    let $borderContent = getParentByClassname($selectionElem, 'border-content');
+    if (!$borderContent) {
+      console.log("%c请在框内添加该题!", "color:red");      
+      return '';
+    }
+
+    $borderContent.append($questionItem);
+
+    if ( isEmptyElement($borderContent[0].firstChild) ) {
+      $borderContent[0].removeChild($borderContent[0].firstChild);
+    }
+    editor.selection.getRange().setStartAfter($questionItem[0]);
+    if (editor.selection.getRange()) {
+      editor.selection.restoreSelection();
+    }
+    editor.selection.saveRange();
+    editor.selection.collapseRange();
+  }
+ }
+ /**
+  * function 生成判断题选项
+  */
+ function generateJudgeOptions () {
+    let $box = $('<div class="lsiten-options-box"></div>');
+    $box.css('display', 'inline-block').css('float', 'left').css('width', 'auto').css('font-size', '0');
+    let $right = $('<div class="lsiten-option">√</div>');
+    $right.css('border', '1px solid #000')
+           .css('text-align', 'center')
+           .css('width', '19px')
+           .css('height', 'auto')
+           .css('box-sizing', 'border-box')
+           .css('font-size', '12px')
+           .css('display', 'inline-block')
+           .css('line-height', '0.95')
+           .css('margin', '0 0 0 5px');
+    let $err = $('<div class="lsiten-option">×</div>');
+    $err.css('border', '1px solid #000')
+           .css('text-align', 'center')
+           .css('width', '19px')
+           .css('height', 'auto')
+           .css('box-sizing', 'border-box')
+           .css('font-size', '12px')
+           .css('display', 'inline-block')
+           .css('line-height', '0.95')
+           .css('margin', '0 0 0 5px');
+    $box.append($right);
+    $box.append($err);
+    return $box;
+ }
+
+ export default {
+  generateJudge: generateJudge
+ }

+ 447 - 0
src/js/question/questionhead.js

@@ -0,0 +1,447 @@
+import $ from '../util/dom-core.js';
+import { getParentByClassname } from '../util/util.js';
+import QrCode from 'jr-qrcode';
+
+let tipsTemplate = `
+<p style='font-size: 12px; margin: 0; padding: 0 10px; line-height: 20px;'>1、考号、姓名、班级三项信息必须用黑色签字笔填清楚。</p>
+<p style='font-size: 12px; margin: 0; padding: 0 10px; line-height: 20px;'>2、选择题作答必须用2B铅笔填涂,非选择题作答必须用黑色中性笔或黑色墨迹钢笔填写。</p>
+<p style='font-size: 12px; margin: 0; padding: 0 10px; line-height: 20px;' >3、必须在指定区域答题,且不得超出黑色答题框。</p>
+<p style='font-size: 12px; margin: 0; padding: 0 10px; line-height: 20px;'>4、请保持答题卡卡面清洁,不要折叠或弄破答题卡。</p>
+`;
+
+/**
+ * 初始化答题卡头外层框样式
+ * @param {DomElement}  $head 头框
+ */
+function initStyleHeader ($head) {
+  $head.css('min-height', '100px')
+       .css('overflow', 'hidden')
+       .css('outline', 'none')
+       .css('font-size', '0px');
+}
+/**
+ * 初始化答题卡头标题框样式
+ * @param {DomElement}  $title 标题框
+ */
+function initStyleTitle ($title) {
+  $title.css('padding', '5px 0')
+       .css('overflow', 'hidden')
+       .css('outline', 'none')
+       .css('font-weight', '600')
+       .css('line-height', '25px')
+       .css('min-height', '25px')
+       .css('height', 'auto')
+       .css('font-size', '18px')
+       .css('max-height', '75px')
+       .css('text-align', 'center');
+}
+
+/**
+ * 初始化答题卡头标题框样式
+ * @param {DomElement}  $body 标题框
+ */
+function initStyleBody ($body) {
+  $body.css('border', '1px solid rgb(0, 0, 0)')
+       .css('overflow', 'hidden')
+       .css('min-height', '100px')
+       .css('height', 'auto')
+       .css('font-size', '0');
+}
+
+/**
+ * 生成卡头基本信息
+ * @param {string} 卡头基本信息宽度
+ */
+function generateBasicInfo (width, data, pindex) {
+  let $box = $('<div class="js-basic-head-info-box"></div>')
+  $box.css('width', width).css('float', 'left').css('border-right', '1px solid #000').css('padding', '10px 0').css('position', 'relative').css('min-height', '120px');
+  let $name = $('<div></div>');
+  $name.css('width', '100%');
+  let $label = $('<div>姓名:</div>');
+  $label.css('display', 'inline-block')
+        .css('line-height', '25px')
+        .css('font-size', '14px')
+        .css('margin-right', '15px')
+        .css('width', '40px')
+        .css('text-align', 'right');
+  let $line = $('<div>&#8203;</div>');
+  $line.css('display', 'inline-block')
+        .css('width', '130px')
+        .css('line-height', '25px')
+        .css('font-size', '14px')
+        .css('border-bottom', '1px solid #000');
+  $name.append($label);
+  $name.append($line);
+
+  let $class = $('<div></div>');
+  $class.css('width', '100%');
+  let $labelclass = $('<div>班级:</div>');
+  $labelclass.css('display', 'inline-block')
+        .css('line-height', '25px')
+        .css('font-size', '14px')
+        .css('margin-top', '10px')
+        .css('margin-right', '15px')
+        .css('width', '40px')
+        .css('text-align', 'right');
+  let $lineclass = $('<div>&#8203;</div>');
+  $lineclass.css('display', 'inline-block')
+        .css('width', '130px')
+        .css('line-height', '25px')
+        .css('font-size', '14px')
+        .css('border-bottom', '1px solid #000');
+  $class.append($labelclass);
+  $class.append($lineclass);
+
+  let mode = parseInt(data.noMode);
+
+  let src = QrCode.getQrBase64('+' + data.qrCode + '' + (pindex + 1), {
+      width: 100,  // 二维码图片宽度(默认为256px)
+      height: 100,  // 二维码图片高度(默认为256px)
+      padding: 5
+    });
+  let $qrcode = $('<div class="js-lsiten-qrcode"><img src="' + src + '"/></div>');
+  $qrcode.css('position', 'absolute').css('right', '5px').css('top', '5px').css('width', '100px').css('height', '100px');
+
+  if (mode === 1) {
+    let $xh = $('<div></div>');
+    $xh.css('width', '100%').css('margin-bottom', '10px');
+    let $labelxh = $('<div>学号:</div>');
+    $labelxh.css('display', 'inline-block')
+          .css('line-height', '25px')
+          .css('font-size', '14px')
+          .css('margin-top', '10px')
+          .css('margin-right', '15px')
+          .css('width', '40px')
+          .css('text-align', 'right');
+    let $linexh = $('<div>&#8203;</div>');
+    $linexh.css('display', 'inline-block')
+          .css('width', '130px')
+          .css('line-height', '25px')
+          .css('font-size', '14px')
+          .css('border-bottom', '1px solid #000');
+    $xh.append($labelxh);
+    $xh.append($linexh);
+    $box.append($xh);
+
+    $qrcode.css('top', '15px');
+    $box.css('padding', 0)
+  }
+  $box.append($name);
+  $box.append($class);
+  $box.append($qrcode);
+  return $box;
+}
+
+/**
+ * 生成条码区
+ */
+function generateBarCode () {
+  let $box = $('<div></div>')
+  $box.css('width', '50%').css('height', '100%').css('font-size', '18px').css('float', 'left').css('padding', '10px 0');
+  let $message = $('<div><p>条形码粘贴处</p><p style="font-size: 12px">(正面朝上,切勿贴出框外)</p></div>');
+  $message.css('width', '200px')
+      .css('height', '120px')
+      .css('text-align', 'center')
+      .css('line-height', '50px')
+      .css('margin', '0 auto');
+  $box.append($message);
+  return $box;
+}
+
+
+/**
+ * 生成模板1
+ * @param {JSON} data 头部信息
+ */
+function headType1 (data, pindex) {
+  let title = data.alias || '';
+  let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+    initStyleHeader($head);
+    let $title = $('<div class="js-answer-header-title" contenteditable = "true">' + title + '</div>');
+    initStyleTitle($title);
+    let $body = $('<div class="js-answer-header-body"></div>');
+    initStyleBody($body);
+
+    let $tips = $('<div class="js-answer-head-tips"></div>');
+    $tips.css('padding', '5px 0');
+    $tips.html(tipsTemplate);
+
+    let $bottom = $('<div class="js-head-bottom"></div>');
+    $bottom.css('width', '100%')
+           .css('border-top', '1px solid #000')
+           .css('overflow', 'hidden')
+           .css('height', 'auto')
+           .css('font-size', '0');
+    
+    let $basicInfo = $('<div class="js-answer-head-basic"></div>');
+    $basicInfo.css('width', '50%')
+              .css('float', 'left')
+              .css('padding-bottom', '10px')
+              .css('font-size', '0');
+    let $basic = generateBasicInfo('50%', data, pindex);
+    $basic.css('border-right', 'none');
+    $bottom.append($basic);
+
+    let $barCode = generateBarCode();
+    $barCode.css('border-left', '1px solid #000');
+    $bottom.append($barCode);
+    
+    $body.append($tips);
+    $body.append($bottom);
+
+    $head.append($title);
+    $head.append($body);
+    return $head;
+}
+/**
+ * 生成模板2
+ * @param {int} noCount 准考证号位数
+ * @param {JSON} data 头部信息
+ */
+function headType2 (noCount, data, pindex) {
+  let title = data.alias || '';
+  let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+  initStyleHeader($head);
+  let $title = $('<div class="js-answer-header-title" contenteditable = "true">' + title + '</div>');
+  initStyleTitle($title);
+  let $body = $('<div class="js-answer-header-body"></div>');
+  initStyleBody($body);
+
+  let $leftBox = $('<div></div>');
+  $leftBox.css('width', '50%')
+            .css('float', 'left')
+            .css('font-size', '0');
+  let $basicInfo = $('<div class="js-answer-head-basic"></div>');
+  $basicInfo.css('width', '100%')
+            .css('float', 'left')
+            .css('border-bottom', '1px solid #000')
+            .css('font-size', '0');
+  let $basic = generateBasicInfo('100%', data, pindex);
+  $basic.css('border-right', 'none')
+  $basicInfo.append($basic);
+  let $tips = $('<div class="js-answer-head-tips"></div>');
+  $tips.css('padding', '15px 0').css('float', 'left');
+  $tips.html(tipsTemplate);
+  
+  $leftBox.append($basicInfo);
+  $leftBox.append($tips);
+  $body.append($leftBox);
+
+
+  let $rightBox = $('<div></div>');
+  $rightBox.css('width', '50%')
+            .css('float', 'left')
+            .css('position', 'relative')
+            .css('padding', '5px')
+            .css('min-height', '293px')
+            .css('font-size', '0');
+
+
+  data.paperSize === 'A4' ? $leftBox.css('border-right', '1px solid #000'): $rightBox.css('border-left', '1px solid #000');
+  let $examNumberBox = generateExamNumber(noCount);
+  $rightBox.append($examNumberBox);
+  $body.append($rightBox);
+
+  $head.append($title);
+  $head.append($body);
+  return $head;
+}
+
+/**
+ * 生成模板3
+ * @param {int} noCount 准考证号位数
+ * @param {JSON} data 头部信息
+ */
+function headType3 (noCount, data, pindex) {
+  let title = data.alias || '';
+  let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+  initStyleHeader($head);
+  let $title = $('<div class="js-answer-header-title" contenteditable = "true">' + title + '</div>');
+  initStyleTitle($title);
+  let $body = $('<div class="js-answer-header-body"></div>');
+  initStyleBody($body);
+
+  let $top = $('<div></div>');
+  $top.css('overflow', 'hidden');
+  let $left = $('<div></div>');
+  $left.css('float', 'left').css('width', '50%');
+  let $basic = generateBasicInfo('100%', data, pindex);
+  $basic.css('border', 'none').css('margin-top', '24px');
+  $left.append($basic);
+
+  let $right = $('<div></div>');
+  $right.css('float', 'left').css('width', '50%').css('border-left', '1px solid #000');
+  let $tips = $('<div class="js-answer-head-tips"></div>');
+  $tips.css('padding', '15px 0').css('float', 'left');
+  $tips.html(tipsTemplate);
+  $right.append($tips);
+  $top.append($left);
+  $top.append($right);
+  
+  let $bottom = $('<div></div>');
+  $bottom.css('overflow', 'hidden').css('border-top', '1px solid #000').css('position', 'relative').css('padding', '5px 15px').css('font-size', '0').css('min-height','293px');
+
+  let $examNumberBox = generateExamNumber(noCount);
+  $bottom.append($examNumberBox);
+
+  $body.append($top);
+  $body.append($bottom);
+
+  $head.append($title);
+  $head.append($body);
+  return $head;
+}
+
+/** 
+ * 生成准考证号框
+ * @param {int} noCount 准考证号位数
+*/
+function generateExamNumber (noCount) {
+  let allWidth = noCount * 32 + 2;
+  if (noCount <= 1) {
+    allWidth = 2 * 32 + 2;
+  }
+  let $box = $('<div></div>');
+  $box.css('margin', '5px auto')
+      .css('display', 'inline-block')
+      .css('width', allWidth + 'px')
+      .css('overflow', 'hidden')
+      .css('position', 'absolute')
+      .css('left', '50%')
+      .css('margin-left', '-' + allWidth / 2 + 'px')
+      .css('top', '0')
+      .css('border', '1px solid #000');
+
+  let $title = $('<div>准考证号</div>');
+  $title.css('text-align', 'center')
+        .css('float', 'left')
+        .css('font-size', '14px')
+        .css('height', '24px')
+        .css('line-height', '23px')
+        .css('border-bottom', '1px solid #000')
+        .css('text-align', 'center');
+
+  $box.append($title);
+
+  let $inputGrid = $('<div></div>');
+  $inputGrid.css('border-bottom', '1px solid #000').css('float', 'left').css('height', '22px').css('line-height', '21px');
+  let lastIndex = noCount - 1;
+  let width = '32px';
+  for (let i =0; i < noCount; i++) {
+    let $colums = $('<div>&#8203;</div>');
+    $colums.css('width', width).css('font-size', '12px').css('display', 'inline-block').css('float', 'left');
+    i < lastIndex && ($colums.css('border-right', '1px solid #000'));
+    $inputGrid.append($colums);
+  }
+
+  $box.append($inputGrid);
+  for (let j = 0; j <= 9; j++) {
+    let $row = $('<div></div>');
+    $row.css('float', 'left').css('height', '21px').css('line-height', '21px');
+    for (let i =0; i < noCount; i++) {
+      let $colums = $('<div></div>');
+      $colums.css('width', width).css('font-size', '12px').css('display', 'inline-block').css('text-align', 'center').css('float', 'left');
+      i < lastIndex && ($colums.css('border-right', '1px solid #000'));
+      let $columnBox = $('<div>'+j+'</div>');
+      $columnBox.css('display', 'inline-block').css('width', '19px').css('border', '1px solid #000').css('line-height', '0.95');
+      $colums.append($columnBox);
+      $row.append($colums);
+
+      j === 9 && ($colums.css('padding-bottom', '12px'), $row.css('height', '33px'));
+      j === 0 && ($colums.css('padding-top', '12px'), $row.css('height', '33px'));
+    }
+    $box.append($row);
+  }
+  return $box;
+}
+
+let heads = {
+  'A4_1_1_1': function (noCount, data, pindex) {
+    return headType1(data, pindex);
+  },
+  'A4_1_1_2': function (noCount, data, pindex) {
+    return headType1(data, pindex);
+  },
+  'A4_2_1_1': function (noCount, data, pindex) {
+    return headType2(noCount, data, pindex);
+  },
+  'A4_2_1_2': function (noCount, data, pindex) {
+    return headType3(noCount, data, pindex);
+  },
+  'A3_1_2_1': function (noCount, data, pindex) {
+    return headType1(data, pindex);
+  },
+  'A3_1_2_2': function (noCount, data, pindex) {
+    return headType1(data, pindex);
+  },
+  'A3_2_2_1': function (noCount, data, pindex) {
+    return headType2(noCount, data, pindex);
+  },
+  'A3_2_2_2': function (noCount, data, pindex) {
+    return headType3(noCount, data, pindex);
+  },
+  'A3_1_3_1': function (noCount, data, pindex) {
+    let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+    return $head;
+  },
+  'A3_1_3_2': function (noCount, data, pindex) {
+    let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+    return $head;
+  },
+  'A3_2_3_1': function (noCount, data, pindex) {
+    let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+    return $head;
+  },
+  'A3_2_3_2': function (noCount, data, pindex) {
+    let $head = $('<div class="js-answer-header" contenteditable="false"></div>');
+    return $head;
+  }
+}
+
+/**
+ * 
+ * @param {Object} editor 编辑器对象
+ * @param {String} key 生成头的类型key值
+ * @param {DomElement} $page 生成头的页Dom
+ */
+let generateQuestionHead = function (editor, key, $page) {
+  let noCount = editor.noCount;
+  let currentPage = $page || editor.page.currentPage;
+  let $fisrstColumn = currentPage.$colums[0];
+  let exitHead = $fisrstColumn.find('.js-answer-header');
+  if (exitHead.length > 0) {
+    editor.message.showMessage('该页已经有答题卡头!');
+    return true;
+  }
+  let $header = heads[key](noCount, editor.data, currentPage.pageIndex);
+  
+  let firstParagraph = $fisrstColumn[0].firstChild;
+  if (!firstParagraph) {
+    return false;
+  }
+  let childs = firstParagraph.childNodes;
+  let html = firstParagraph.innerHTML;
+
+  if (childs.length > 0 && html !== '<br/>' && html !== '<br>' && html !== '<p></p>' && html !== '<p><br/></p>' && html !== '<p><br></p>') {
+    let $template =  $(firstParagraph.cloneNode());
+    let pageindex = editor.page.pageIndex + 1;
+    $template.attr('data-index', pageindex);
+    $template.append($header);
+    $template.insertBefore(firstParagraph);
+  } else {
+    firstParagraph.innerHTML = '';
+    $(firstParagraph).append($header);
+  }
+
+  editor.selection.getRange().setStart($header[0], 0);
+  if (editor.selection.getRange()) {
+    editor.selection.restoreSelection();
+  }
+  editor.selection.saveRange();
+  editor.selection.collapseRange();
+  return true;
+}
+
+export default {
+  generateQuestionHead: generateQuestionHead
+}

+ 49 - 0
src/js/question/subject.js

@@ -0,0 +1,49 @@
+import $ from '../util/dom-core.js';
+import { getParentByClassname, isEmptyElement } from '../util/util.js';
+
+/**
+ * function 生成主观题区
+* @param {DomElement} $border 需要添加题的框
+* @param {JSON} data  题目数据
+* @param {Objec} editor 编辑器对象
+ */
+let generateSubjectBox = function ($border, data, editor) {
+  let type = parseInt($border.attr('data-type'));
+  if (type === 1) {
+    editor.message.showMessage('请在主观题框中添加主观题!');
+    return '';
+  }
+
+  let mergePnum = data.proData.pnum;
+  let pnum = data.pnum;
+
+  if (mergePnum && mergePnum.length > 0) {
+    pnum = mergePnum +''+ pnum;
+  }
+
+  let $box = $('<div class = "lsiten-subject-box js-lsiten-question"><p>'+pnum+'、</p><p><br/></p><p><br/></p><p><br/></p></div>');
+  $box.attr('data-type', 3);
+  let $selectionElem = editor.selection.getSelectionContainerElem();
+  let $borderContent = getParentByClassname($selectionElem, 'border-content');
+  if (!$borderContent) {
+    console.log("%c请在框内添加该题!", "color:red");
+    return '';
+  }
+
+  $borderContent.append($box);
+
+  if ( isEmptyElement($borderContent[0].firstChild) ) {
+    $borderContent[0].removeChild($borderContent[0].firstChild);
+  }
+
+  editor.selection.getRange().setStart($box[0], 0);
+  if (editor.selection.getRange()) {
+    editor.selection.restoreSelection();
+  }
+  editor.selection.saveRange();
+  editor.selection.collapseRange();
+}
+
+export default {
+  generateSubjectBox: generateSubjectBox
+}

+ 131 - 0
src/js/question/writing.js

@@ -0,0 +1,131 @@
+import $ from '../util/dom-core.js';
+import { getParentByClassname, isEmptyElement } from '../util/util.js';
+
+/**
+ * 
+ * @param {DomElement} $border 需要添加题的框
+ * @param {JSON} data  题目数据
+ * @param {Objec} editor 编辑器对象
+ */
+let generateWriting = function ($border, data, editor) {
+  let type = parseInt($border.attr('data-type'));
+  if (type === 1) {
+    editor.message.showMessage('请在主观题框中添加作文题!');
+    return '';
+  }
+  $border.css('padding', '10px 0');
+  let pnum = data.pnum;
+  let total = parseInt(data.nums) || 1200;
+  let $box = $('<div class="lsiten-question-writing js-lsiten-question"></div>');
+  $box.attr('data-type', 4);
+  $box.attr('data-pnum', pnum);
+  $box.attr('data-total', total);
+  // 1、生成题号区
+  let $title = $('<div class="lsiten-title"><p>' + pnum + '</p></div>');
+  $title.css('padding', '0 15px');
+  $box.append($title);
+  // 2、生成格子区域
+  let column = getColumnCount(editor);
+  let $grid = generateGrid(column, total);
+  $box.append($grid);
+  let $selectionElem = editor.selection.getSelectionContainerElem();
+  let $borderContent = getParentByClassname($selectionElem, 'border-content');
+  if (!$borderContent) {
+    console.log("%c请在框内添加该题!", "color:red");
+    return '';
+  }
+  $borderContent.append($box);
+
+  if ( isEmptyElement($borderContent[0].firstChild) ) {
+    $borderContent[0].removeChild($borderContent[0].firstChild);
+  }
+
+  editor.selection.getRange().setStart($box[0], 0);
+  if (editor.selection.getRange()) {
+    editor.selection.restoreSelection();
+  }
+  editor.selection.saveRange();
+  editor.selection.collapseRange();
+}
+
+/**
+ * 
+ * @param {Object} editor 编辑器
+ */
+function getColumnCount (editor) {
+  let pageSize = editor.pageSize;
+  let column = parseInt(editor.columnNumber);
+  let maxColumn = 19;
+  if (pageSize === 'A4' && column === 1) {
+    maxColumn = 17;
+  }
+  if (pageSize === 'A3' && column === 1) {
+    maxColumn = 38;
+  }
+  if (pageSize === 'A3' && column === 2) {
+    maxColumn = 19;
+  }
+  if (pageSize === 'A3' && column === 3) {
+    maxColumn =9;
+  }
+
+  return maxColumn;
+}
+
+
+/**
+ * function 生成作文格子
+ * @param {int} column 行格子数
+ * @param {int} total 格子总数
+ */
+function generateGrid (column, total) {
+  let $borderBox = $('<div class="lsiten-border-box" contenteditable="false"></div>');
+  let rows = Math.ceil(total / column);
+  let columnIndex = 0;
+  let width = column * 34 + 'px';
+  let lastColumn = column - 1;
+  for (let i = 0; i < rows; i++) {
+    let $row = $('<div class="js-lsiten-writting-row"></div>');
+    $row.attr('data-row-index', i);
+    $row.css('height', '32px')
+        .css('width', width)
+        .css('line-height', '32px')
+        .css('white-space', 'nowrap')
+        .css('text-align', 'left')
+        .css('margin-top', '8px')
+        .css('margin-left', 'auto')
+        .css('margin-right', 'auto');
+    for (let j = 0; j < column; j++) {
+      let $column = $('<div class="js-lsiten-writting-column"></div>');
+      $column.attr('data-column-index', ++columnIndex);   
+      
+      $column.css('border-left', '1px solid #000')
+              .css('border-top', '1px solid #000')
+              .css('border-bottom', '1px solid #000')
+              .css('width', '34px')
+              .css('height', '34px')
+              .css('position', 'relative')
+              .css('box-sizing', 'border-box')
+              .css('display', 'inline-block')
+              .css('margin', '0');
+      j === lastColumn && $column.css('border-right', '1px solid #000');
+      if (columnIndex % 400 === 0) {
+        let $tips = $('<div class="lsiten-writting-border-tips">' + columnIndex +'字</div>');
+        $tips.css('font-size', '12px')
+             .css('position', 'absolute')
+             .css('transform', 'scale(0.5)')
+             .css('font-size', '8px')
+             .css('left', '0px')
+             .css('bottom', '-20px');
+        $column.append($tips);
+      }
+      $row.append($column);
+    }
+    $borderBox.append($row);
+  }
+
+  return $borderBox;
+}
+export default {
+  generateWriting: generateWriting
+}

+ 281 - 0
src/js/selection/index.js

@@ -0,0 +1,281 @@
+/*
+    selection range API
+    // 1.获取光标结束位置
+    var end = textarea.selectionEnd
+    // 2.通过匹配光标之前文本中的换行符计算所在行
+    var row = textarea.value.substring(0, end).match(/\r\n|\r|\n/).length
+    // 3.计算 top,行高 * 行数 + 上填充 + 边框宽度
+    var top = lineHeight * (row + 1) + paddingTop + borderWidth
+    // 4.获取光标左侧的文本
+    var leftText = textarea.value.split(/\r\n|\r|\n/)[row]
+    // 5.影响一段文字所占宽度的因素太多,除字体大小、中英文、符号、字符间距等,还有字体、浏览器、系统等客观因素
+    // var left = ...
+    这个方案的思路是没问题的,但是考虑所有问题的成本太高。
+    虽然可以创建测试元素去计算文本宽度,但这个方案本身是从严谨的角度出发的。与其混在一块,直接用取巧的办法更简单。
+*/
+
+import $ from '../util/dom-core.js'
+import { UA, getParentByClassname, getParentNodeByClass } from '../util/util.js'
+import Jquery from 'jquery'
+import 'jquery.caret'
+
+// 构造函数
+function API(editor) {
+    this.editor = editor;
+    this._currentRange = null;
+    this.rangeTemp = null;
+}
+
+// 修改原型
+API.prototype = {
+    constructor: API,
+    fillChar: '\u200B',
+    // 获取 range 对象
+    getRange: function () {
+        return this._currentRange
+    },
+    // 获取光标位置
+    getCursorPosition: function () {
+      if (!this._currentRange) {
+        this.saveRange();
+      }
+      return {
+        start: this._currentRange.startOffset,
+        end: this._currentRange.endOffset
+      }
+    },
+    // 保存选区
+    saveRange: function (_range) {
+        const selection = window.getSelection()
+        
+        if (_range) {
+            // 保存已有选区
+            this._currentRange = _range;
+            return this._currentRange;
+        }
+
+        // 获取当前的选区
+        if (selection.rangeCount === 0) {
+            return false;
+        }
+        const range = selection.getRangeAt(0)
+
+        // 判断选区内容是否在编辑内容之内
+        const $containerElem = this.getSelectionContainerElem(range)
+        if (!$containerElem) {
+            return false;
+        }
+
+        // 判断选区内容是否在不可编辑区域之内
+        const editor = this.editor
+        const $textElem = editor.page.$el;
+        if ($textElem.isContain($containerElem)) {
+          // 是编辑内容之内的
+          this._currentRange = range;        
+        }
+    },
+    isFillChar: function (node,isInStart) {
+      if(node.nodeType != 3) {
+        return false;
+      }
+      let text = node.nodeValue;
+      if(isInStart){
+          return new RegExp('^' + this.fillChar).test(text)
+      }
+      return !text.replace(new RegExp(this.fillChar,'g'), '').length
+    },
+    // 折叠选区
+    collapseRange: function (toStart) {
+        if (toStart == null) {
+            // 默认为 false
+            toStart = false
+        }
+        const range = this._currentRange
+        if (range) {
+            range.collapse(toStart)
+        }
+    },
+    storeRange: function (range) {
+      range = range || this._currentRange;
+      this.rangeTemp = {
+        start: {
+          dom: range.startContainer,
+          offset: range.startOffset,
+        },
+        end: {
+          dom: range.endContainer,
+          offset: range.endOffset,
+        },
+        collapsed: range.collapsed
+      };
+    },
+    recoverRange: function (rangeTempArg) {
+      let rangeTemp = rangeTempArg || this.rangeTemp;
+      if (rangeTemp && rangeTemp.start) {
+        const selection = window.getSelection()
+        let startDom = rangeTemp.start.dom;
+        let column = getParentNodeByClass(startDom, 'js-column');
+        if (column) {     
+          let $startDom = $(
+              rangeTemp.start.dom.nodeType === 3 ? rangeTemp.start.dom.parentNode : rangeTemp.start.dom
+          ); 
+          this.createRangeByElem($startDom, true);
+          this.saveRange();          
+          let range = this._currentRange;
+          try {
+            range.setStart(rangeTemp.start.dom, rangeTemp.start.offset);
+            range.setEnd(rangeTemp.end.dom, rangeTemp.end.offset);
+            this.saveRange(range);
+            rangeTemp.collapsed && this.collapseRange();
+          } catch (error) {
+          }
+        }
+      }
+    },
+    // 选中区域的文字
+    getSelectionText: function () {
+        const range = this._currentRange
+        if (range) {
+            return this._currentRange.toString()
+        } else {
+            return ''
+        }
+    },
+    // 选区的 $Elem
+    getSelectionContainerElem: function (range) {
+        range = range || this._currentRange
+        let elem
+        if (range) {
+            elem = range.commonAncestorContainer
+            return $(
+                elem.nodeType === 1 ? elem : elem.parentNode
+            )
+        }
+    },
+    getSelectionStartElem: function (range) {
+        range = range || this._currentRange
+        let elem
+        if (range) {
+            elem = range.startContainer
+            return $(
+                elem.nodeType === 1 ? elem : elem.parentNode
+            )
+        }
+    },
+    getSelectionEndElem: function (range) {
+        range = range || this._currentRange
+        let elem
+        if (range) {
+            elem = range.endContainer
+            return $(
+                elem.nodeType === 1 ? elem : elem.parentNode
+            )
+        }
+    },
+
+    // 选区是否为空
+    isSelectionEmpty: function () {
+        const range = this._currentRange
+        if (range && range.startContainer) {
+            if (range.startContainer === range.endContainer) {
+                if (range.startOffset === range.endOffset) {
+                    return true
+                }
+            }
+        }
+        return false
+    },
+
+    // 恢复选区
+    restoreSelection: function () {
+        const selection = window.getSelection()
+        selection.removeAllRanges()
+        selection.addRange(this._currentRange)
+    },
+
+    // 创建一个空白(即 &#8203 字符)选区
+    createEmptyRange: function () {
+        const editor = this.editor
+        const range = this.getRange()
+        let $elem
+
+        if (!range) {
+            // 当前无 range
+            return
+        }
+        if (!this.isSelectionEmpty()) {
+            // 当前选区必须没有内容才可以
+            return
+        }
+
+        try {
+            // 目前只支持 webkit 内核
+            if (UA.isWebkit()) {
+                // 插入 &#8203
+                editor.cmd.do('insertHTML', '&#8203;')
+                // 修改 offset 位置
+                range.setEnd(range.endContainer, range.endOffset + 1)
+                // 存储
+                this.saveRange(range)
+            } else {
+                $elem = $('<strong>&#8203;</strong>')
+                editor.cmd.do('insertElem', $elem)
+                this.createRangeByElem($elem, true)
+            }
+        } catch (ex) {
+            // 部分情况下会报错,兼容一下
+        }
+    },
+
+    // 根据 $Elem 设置选区
+    createRangeByElem: function ($elem, toStart, isContent) {
+        // $elem - 经过封装的 elem
+        // toStart - true 开始位置,false 结束位置
+        // isContent - 是否选中Elem的内容
+        if (!$elem.length) {
+            return
+        }
+
+        const elem = $elem[0]
+        const range = document.createRange()
+
+        if (isContent) {
+            range.selectNodeContents(elem)
+        } else {
+            range.selectNode(elem)
+        }
+
+        if (typeof toStart === 'boolean') {
+            range.collapse(toStart)
+        }
+        // 存储 range
+        this.saveRange(range)
+    },
+
+    getCaretPosition: function (editableDiv) {
+        let caretPos = 0, sel, range;
+        if (window.getSelection) {
+          sel = window.getSelection();
+          if (sel.rangeCount) {
+            range = sel.getRangeAt(0);
+            if (range.commonAncestorContainer.parentNode == editableDiv) {
+              caretPos = range.endOffset;
+            }
+          }
+        } else if (document.selection && document.selection.createRange) {
+          range = document.selection.createRange();
+          if (range.parentElement() == editableDiv) {
+            let tempEl = document.createElement("span");
+            editableDiv.insertBefore(tempEl, editableDiv.firstChild);
+            let tempRange = range.duplicate();
+            tempRange.moveToElementText(tempEl);
+            tempRange.setEndPoint("EndToEnd", range);
+            caretPos = tempRange.text.length;
+          }
+        }
+        return caretPos;
+    }
+
+}
+
+export default API

+ 46 - 0
src/js/util/ajax.js

@@ -0,0 +1,46 @@
+let Ajax = function () {
+}
+
+Ajax.prototype = {
+  constructor: Ajax,
+  get: function (url) {
+    return new Promise((resolve, reject) => {
+      let xhr = new XMLHttpRequest();
+      xhr.open('GET', url, true);
+      xhr.onreadystatechange = function() {
+        // readyState == 4说明请求已完成
+        if (parseInt(xhr.readyState) === 4) {
+          if (parseInt(xhr.status) === 200 || parseInt(xhr.status) === 304) {
+            resolve(xhr.responseText);
+          } else {
+            reject(xhr);
+          }
+        }
+      };
+      xhr.send();
+    })
+  },
+  post: function (url, data, contentType) {
+    return new Promise((resolve, reject) => {
+      let xhr = new XMLHttpRequest();
+      xhr.open("POST", url, true);
+      // 添加http头,发送信息至服务器时内容编码类型
+      if (!contentType || contentType !== 'formdata' ) {  
+        !contentType && (contentType = 'application/x-www-form-urlencoded');
+        xhr.setRequestHeader('Content-Type', contentType);
+      }
+
+      xhr.onreadystatechange = function() {
+        if (parseInt(xhr.readyState) === 4) {
+          if (parseInt(xhr.status) === 200 || parseInt(xhr.status) === 304) {
+            resolve(JSON.parse(xhr.responseText));
+          } else {
+            reject(JSON.parse(xhr.responseText));
+          }
+        }
+      };
+      xhr.send(data);
+    })
+  }
+}
+export default Ajax;

+ 538 - 0
src/js/util/dom-core.js

@@ -0,0 +1,538 @@
+/*
+    DOM 操作 API
+*/
+
+// 根据 html 代码片段创建 dom 对象
+function createElemByHTML(html) {
+    let div
+    div = document.createElement('div')
+    div.innerHTML = html
+    return div.children
+}
+
+// 是否是 DOM List
+function isDOMList(selector) {
+    if (!selector) {
+        return false
+    }
+    if (selector instanceof HTMLCollection || selector instanceof NodeList) {
+        return true
+    }
+    return false
+}
+
+// 封装 document.querySelectorAll
+function querySelectorAll(selector) {
+    const result = document.querySelectorAll(selector)
+    if (isDOMList(result)) {
+        return result
+    } else {
+        return [result]
+    }
+}
+
+// 记录所有的事件绑定
+const eventList = []
+
+// 创建构造函数
+function DomElement(selector) {
+    if (!selector) {
+        return
+    }
+
+    // selector 本来就是 DomElement 对象,直接返回
+    if (selector instanceof DomElement) {
+        return selector
+    }
+
+    this.selector = selector
+    const nodeType = selector.nodeType
+
+    // 根据 selector 得出的结果(如 DOM,DOM List)
+    let selectorResult = []
+    if (nodeType === 9) {
+        // document 节点
+        selectorResult = [selector]
+    } else if (nodeType === 1) {
+        // 单个 DOM 节点
+        selectorResult = [selector]
+    } else if (isDOMList(selector) || selector instanceof Array) {
+        // DOM List 或者数组
+        selectorResult = selector
+    } else if (typeof selector === 'string') {
+        // 字符串
+        selector = selector.replace('/\n/mg', '').trim()
+        if (selector.indexOf('<') === 0) {
+            // 如 <div>
+            selectorResult = createElemByHTML(selector)
+        } else {
+            // 如 #id .class
+            selectorResult = querySelectorAll(selector)
+        }
+    }
+
+    const length = selectorResult.length
+    if (!length) {
+        // 空数组
+        return this
+    }
+
+    // 加入 DOM 节点
+    let i
+    for (i = 0; i < length; i++) {
+        this[i] = selectorResult[i]
+    }
+    this.length = length
+}
+
+// 修改原型
+DomElement.prototype = {
+    constructor: DomElement,
+
+    // 类数组,forEach
+    forEach: function (fn) {
+        let i
+        for (i = 0; i < this.length; i++) {
+            const elem = this[i]
+            const result = fn.call(elem, elem, i)
+            if (result === false) {
+                break
+            }
+        }
+        return this
+    },
+
+    // clone
+    clone: function (deep) {
+        const cloneList = []
+        this.forEach(elem => {
+            cloneList.push(elem.cloneNode(!!deep))
+        })
+        return $(cloneList)
+    },
+
+    // 获取第几个元素
+    get: function (index) {
+        const length = this.length
+        if (index >= length) {
+            index = index % length
+        }
+        return $(this[index])
+    },
+
+    // 第一个
+    first: function () {
+        return this.get(0)
+    },
+
+    // 最后一个
+    last: function () {
+        const length = this.length
+        return this.get(length - 1)
+    },
+
+    // 绑定事件
+    on: function (type, selector, fn) {
+        // selector 不为空,证明绑定事件要加代理
+        if (!fn) {
+            fn = selector
+            selector = null
+        }
+
+        // type 是否有多个
+        let types = []
+        types = type.split(/\s+/)
+
+        return this.forEach(elem => {
+            types.forEach(type => {
+                if (!type) {
+                    return
+                }
+
+                // 记录下,方便后面解绑
+                eventList.push({
+                    elem: elem,
+                    type: type,
+                    fn: fn
+                })
+
+                if (!selector) {
+                    // 无代理
+                    elem.addEventListener(type, fn)
+                    return
+                }
+
+                // 有代理
+                elem.addEventListener(type, e => {
+                    const target = e.target
+                    if (target.matches(selector)) {
+                        fn.call(target, e)
+                    }
+                })
+            })
+        })
+    },
+
+    // 取消事件绑定
+    off: function (type, fn) {
+        return this.forEach(elem => {
+            elem.removeEventListener(type, fn)
+        })
+    },
+
+    // 获取/设置 属性
+    attr: function (key, val) {
+        if (val == null) {
+            // 获取值
+            return this[0].getAttribute(key)
+        } else {
+            // 设置值
+            return this.forEach(elem => {
+                elem.setAttribute(key, val)
+            })
+        }
+    },
+
+    // 添加 class
+    addClass: function(className) {
+        if (!className) {
+            return this
+        }
+        return this.forEach(elem => {
+            let arr
+            if (elem.className) {
+                // 解析当前 className 转换为数组
+                arr = elem.className.split(/\s/)
+                arr = arr.filter(item => {
+                    return !!item.trim()
+                })
+                // 添加 class
+                if (arr.indexOf(className) < 0) {
+                    arr.push(className)
+                }
+                // 修改 elem.class
+                elem.className = arr.join(' ')
+            } else {
+                elem.className = className
+            }
+        })
+    },
+
+    // 删除 class
+    removeClass: function (className) {
+        if (!className) {
+            return this
+        }
+        return this.forEach(elem => {
+            let arr
+            if (elem.className) {
+                // 解析当前 className 转换为数组
+                arr = elem.className.split(/\s/)
+                arr = arr.filter(item => {
+                    item = item.trim()
+                    // 删除 class
+                    if (!item || item === className) {
+                        return false
+                    }
+                    return true
+                })
+                // 修改 elem.class
+                elem.className = arr.join(' ')
+            }
+        })
+    },
+
+    // 修改 css
+    css: function (key, val) {
+        const currentStyle = `${key}:${val};`
+        return this.forEach(elem => {
+            const style = (elem.getAttribute('style') || '').trim()
+            let styleArr, resultArr = []
+            if (style) {
+                // 将 style 按照 ; 拆分为数组
+                styleArr = style.split(';')
+                styleArr.forEach(item => {
+                    // 对每项样式,按照 : 拆分为 key 和 value
+                    let arr = item.split(':').map(i => {
+                        return i.trim()
+                    })
+                    if (arr.length === 2) {
+                        resultArr.push(arr[0] + ':' + arr[1])
+                    }
+                })
+                // 替换或者新增
+                resultArr = resultArr.map(item => {
+                    if (item.indexOf(key) === 0) {
+                        return currentStyle
+                    } else {
+                        return item
+                    }
+                })
+                if (resultArr.indexOf(currentStyle) < 0) {
+                    resultArr.push(currentStyle)
+                }
+                // 结果
+                elem.setAttribute('style', resultArr.join('; '))
+            } else {
+                // style 无值
+                elem.setAttribute('style', currentStyle)
+            }
+        })
+    },
+
+    // 显示
+    show: function () {
+        return this.css('display', 'block')
+    },
+
+    // 隐藏
+    hide: function () {
+        return this.css('display', 'none')
+    },
+
+    // 获取子节点
+    children: function () {
+        const elem = this[0]
+        if (!elem) {
+            return null
+        }
+
+        return $(elem.children)
+    },
+
+    // 获取子节点(包括文本节点)
+    childNodes: function () {
+        const elem = this[0]
+        if (!elem) {
+            return null
+        }
+
+        return $(elem.childNodes)
+    },
+
+    // 增加子节点
+    append: function($children) {
+        return this.forEach(elem => {
+            $children.forEach(child => {
+                elem.appendChild(child)
+            })
+        })
+    },
+
+    // 移除当前节点
+    remove: function () {
+        return this.forEach(elem => {
+            if (elem.remove) {
+                elem.remove()
+            } else {
+                const parent = elem.parentElement
+                parent && parent.removeChild(elem)
+            }
+        })
+    },
+    scrollToTop: function (val) {
+      if (val) {
+        this[0].scrollTop = parseInt(val)
+      } else {
+          return this[0].scrollTop
+      }
+    },
+    // 是否包含某个子节点
+    isContain: function ($child) {
+        const elem = this[0]
+        const child = $child[0]
+        return elem.contains(child)
+    },
+
+    // 尺寸数据
+    getSizeData: function () {
+        const elem = this[0]
+        return elem.getBoundingClientRect()  // 可得到 bottom height left right top width 的数据
+    },
+
+    // 封装 nodeName
+    getNodeName: function () {
+        const elem = this[0]
+        return elem.nodeName
+    },
+    // 封装 nodeType
+    getNodeType: function () {
+        const elem = this[0]
+        return elem.nodeType
+    },
+    getNodeIndex: function () {
+      let preNode = this[0],
+        i = 0
+      while (preNode = preNode.previousSibling) {
+        if (preNode.nodeType == 3) {
+          if(preNode.nodeType != preNode.nextSibling.nodeType ){
+            i++;
+          }
+          continue;
+        }
+        i++
+      }
+      return i
+    },
+    // 从当前元素查找
+    find: function (selector) {
+        const elem = this[0]
+        return $(elem.querySelectorAll(selector))
+    },
+    // 获取当前元素的 text
+    text: function (val) {
+        if (!val) {
+            // 获取 text
+            const elem = this[0]
+            return elem.innerHTML.replace(/<.*?>/g, () => '')
+        } else {
+            // 设置 text
+            return this.forEach(elem => {
+                elem.innerHTML = val
+            })
+        }
+    },
+
+    // 获取 html
+    html: function (value) {
+        const elem = this[0]
+        if (value == null) {
+            return elem.innerHTML
+        } else {
+            elem.innerHTML = value
+            return this
+        }
+    },
+
+    // 获取 value
+    val: function (value) {
+        const elem = this[0]
+        if (value == null) {
+            return elem.value.trim()
+        } else {
+            elem.value = value
+            return this
+        }
+    },
+
+    // focus
+    focus: function () {
+        return this.forEach(elem => {
+            elem.focus()
+        })
+    },
+
+    // parent
+    parent: function () {
+        const elem = this[0]
+        return $(elem.parentElement)
+    },
+
+    // parentUntil 找到符合 selector 的父节点
+    parentUntil: function (selector, _currentElem) {
+        const results = document.querySelectorAll(selector)
+        const length = results.length
+        if (!length) {
+            // 传入的 selector 无效
+            return null
+        }
+
+        const elem = _currentElem || this[0]
+        if (elem.nodeName === 'BODY') {
+            return null
+        }
+
+        const parent = elem.parentElement
+        let i
+        for (i = 0; i < length; i++) {
+            if (parent === results[i]) {
+                // 找到,并返回
+                return $(parent)
+            }
+        }
+
+        // 继续查找
+        return this.parentUntil(selector, parent)
+    },
+
+    findParents: function (node, self, callback) {
+      if (!node) {
+        return null;
+      }
+      if (node.nodeName == 'BODY') {
+        return null;
+      }
+      if (node.className && node.className == 'lsiten_editor_text') {
+        return null;
+      }
+      let parents = [];
+      if (self) {
+        callback(node) && parents.push(node);
+      } else {
+        callback(node.parentNode) && parents.push(node.parentNode);
+      }
+      let pParents = this.findParents(node.parentNode, false, callback);
+      if (pParents) {
+        parents = [...parents, ...pParents];
+      }
+      return parents;
+    },
+    // 判断两个 elem 是否相等
+    equal: function ($elem) {
+        if ($elem.nodeType === 1) {
+            return this[0] === $elem
+        } else {
+            return this[0] === $elem[0]
+        }
+    },
+
+    // 将该元素插入到某个元素前面
+    insertBefore: function (selector) {
+        const $referenceNode = $(selector)
+        const referenceNode = $referenceNode[0]
+        if (!referenceNode) {
+            return this
+        }
+        return this.forEach(elem => {
+            const parent = referenceNode.parentNode
+            parent.insertBefore(elem, referenceNode)
+        })
+    },
+
+    // 将该元素插入到某个元素后面
+    insertAfter: function (selector) {
+        const $referenceNode = $(selector)
+        const referenceNode = $referenceNode[0]
+        if (!referenceNode) {
+            return this
+        }
+        return this.forEach(elem => {
+            const parent = referenceNode.parentNode
+            if (parent.lastChild === referenceNode) {
+                // 最后一个元素
+                parent.appendChild(elem)
+            } else {
+                // 不是最后一个元素
+                parent.insertBefore(elem, referenceNode.nextSibling)
+            }
+        })
+    }
+}
+
+// new 一个对象
+function $(selector) {
+    return new DomElement(selector)
+}
+
+// 解绑所有事件,用于销毁编辑器
+$.offAll = function () {
+    eventList.forEach(item => {
+        const elem = item.elem
+        const type = item.type
+        const fn = item.fn
+        // 解绑
+        elem.removeEventListener(type, fn)
+    })
+}
+
+export default $

+ 55 - 0
src/js/util/imageUtil.js

@@ -0,0 +1,55 @@
+let ImageUtil = function (src) {
+  this.src = src || '';
+  this._init();
+}
+
+ImageUtil.prototype = {
+  constructor: ImageUtil,
+  _init: function () {
+    this.image = new Image();
+    this.image.src = this.src;
+  },
+  getNaturalWidth: function (oimge) {
+    let image = oimge || this.image;
+    let naturalWidth = image.width
+    this.owidth = naturalWidth;
+    return naturalWidth
+  },
+  getNaturalHeight: function (oimge) {
+    let image = oimge || this.image;
+    let naturaHeight = image.height;
+    this.oheight = naturaHeight;
+    return naturaHeight
+  },
+
+  resizeByWidth: function (width) {
+    let owidth = this.owidth;
+    let ratio = parseInt(width / owidth * 100) / 100;
+    return this.resizeByRatio(ratio);
+  },
+  resizeByHeight: function (height) {
+    let oheight = this.oheight;
+    let ratio = parseInt(height / oheight * 100) / 100;
+    return this.resizeByRatio(radio);
+  },
+  resizeByRatio: function (ratio) {
+    !ratio && (ratio = 1);
+    let height = this.oheight;
+    let width = this.owidth;
+    this.size = {
+      w: width * ratio,
+      h: height * ratio
+    }
+  },
+  getSize: function (callback) {
+    let _this = this;
+    this.image.onload = function () {
+      _this.size = {
+        w: _this.getNaturalWidth(),
+        h: _this.getNaturalHeight()
+      };
+      typeof callback === 'function' && callback();
+    }
+  }
+}
+export default ImageUtil

+ 247 - 0
src/js/util/message.js

@@ -0,0 +1,247 @@
+/**
+ * 待完善
+ */
+import '../../less/message.less'
+import $ from './dom-core.js';
+let options = {
+  mask: {
+    backgroundColor: 'rgb(0, 0, 0)',
+    opacity: 0.8,
+    zIndex: 9999
+  },
+  backgroundColor: '#fff',
+  width: 200,
+  height: 100,
+  timeout: 2000
+}
+let Message = function (editor, customOptions) {
+  this.editor = editor;
+  this.$el = $('<div class="lsiten-e-message"></div>');
+  this.$mask = $('<div class="lsiten-e-message-mask"></div>');
+  this.options = Object.assign({}, options, customOptions || {});
+  this._init();
+}
+
+Message.prototype = {
+  constructor: Message,
+  _init: function () {
+    let editor = this.editor;
+    let styleType = '_initStyle' + this.type;
+    this._initBaseStyle();
+    if (this[styleType]) {
+      this[styleType]();
+    }
+    this._initEvents();
+    editor.$container.append(this.$el);
+    editor.$container.append(this.$mask);
+  },
+  _initOptions() {
+    this.type = this.options.type || 'message';
+    this.message = this.options.message || '';
+  },
+  _initBaseStyle: function () {
+    this.$mask.css('background-color', this.options.mask.backgroundColor)
+              .css('z-index', this.options.mask.zIndex)
+              .css('opacity', this.options.mask.opacity)
+              .css('top', 0)
+              .css('left', 0)
+              .css('width', '100%')
+              .css('height', '100%')
+              .css('position', 'fixed')
+              .css('pointer-events', 'auto')
+              .css('display', 'none')
+    this.$el.css('background-color', this.options.backgroundColor)
+            .css('z-index', (this.options.mask.zIndex + 1))
+            .css('top', '50%')
+            .css('left', '50%')
+            .css('transform', 'translate(' + (-this.options.width/2) + 'px,' + (-this.options.height/2) + 'px)')
+            .css('width', this.options.width + 'px')
+            .css('min-height', this.options.height + 'px')
+            .css('height',  'auto')
+            .css('position', 'fixed')
+            .css('pointer-events', 'auto')
+            .css('display', 'none')
+  },
+  _initShowMessage: function () {
+    this.$el.css('transform', 'translate(' + (-this.options.width/2) + 'px,' + (-this.options.height/2) + 'px)')
+            .css('width', this.options.width + 'px')
+            .css('min-height', this.options.height + 'px')
+            .css('height',  'auto')
+  },
+  _initHeader: function () {
+    this.$header = $('<div class="lsiten-e-message-header"></div>');
+    let headerType = '_initHeader' + this.type;
+    if (this[headerType]) {
+      this[headerType]();
+    }
+  },
+  _initBody: function () {
+    this.$body = $('<div class="lsiten-e-message-body"></div>');
+    let bodyType = '_initBody' + this.type;
+    if (this[bodyType]) {
+      this[bodyType]();
+    }
+  },
+  _initEvents: function () {
+    let _this = this;
+    function hidenMessage() {
+      _this.$mask.hide();
+      _this.$el.hide();
+    }
+    this.$mask.on('click', hidenMessage)
+  },
+  // message start
+  _initBodymessage: function () {
+    let $message = $('<div class="lsiten-e-message-body-content"></div>');
+    $message.css('line-height', '30px')
+            .css('text-align', 'center')
+            .css('margin', '5px 0')
+            .css('padding', '0 10px')
+    $message.html(this.message);
+    this.$el.html('');
+    this.$el.append($message);
+  },
+  _initHeadermessage: function () {
+
+  },
+  _initStylemessage: function () {
+    this.$el.css('right', '10px')
+            .css('top', '10px')
+            .css('left', 'inherit')
+            .css('background-color', 'rgb(0,0,0)')
+            .css('box-shadow', '0 0 10px 0 rgba(0, 0, 0, 0.5)')
+            .css('border-radius', '3px')
+            .css('opacity', '0.8')
+            .css('width', 'auto')
+            .css('height', 'auto')
+            .css('color', '#fff')
+            .css('min-width', '100px')
+            .css('min-height', '40px')
+            .css('transform', 'none')
+  },
+
+
+
+    // message start
+  _initBodydialog: function () {
+    let _this = this;
+    let $message = $('<div class="lsiten-e-message-body-content"></div>');
+    let content = this.options.content || '';
+    if (typeof content === 'string') {
+      $message.html(content);
+    } else {
+      $message.append(content);
+    }
+    let $button = $('<div class="lsiten-e-message-button"></div>');
+    let $buttons = this.options.$buttons;
+    if ($buttons) {
+      for (let i =0; i< $buttons.length; i++) {
+        $button.append($buttons[i]);
+      }
+    } else {
+       let $buttonDom1 = $('<span class="lsiten-message-button lsiten-message-cancel-button">取消</span>');
+       let $buttonDom2 = $('<span class="lsiten-message-button lsiten-message-success-button">确定</span>');
+
+       $buttonDom1.on('click', function() {
+        _this.hideMessage();
+       })
+       $buttonDom2.on('click', function() {
+        _this.hideMessage();
+       })
+       $button.append($buttonDom1);
+       $button.append($buttonDom2);
+    }
+    this.$el.append($message);
+    this.$el.append($button);
+  },
+  _initHeaderdialog: function () {
+    let _this = this;
+    let $header = $('<div class="lsiten-dialog-header"></div>');
+    $header.css('padding', '0 80px 0 20px');
+    $header.css('height', '42px');
+    $header.css('line-height', '42px');
+    $header.css('border-bottom', '1px solid #eee');
+    $header.css('font-size', '14px');
+    $header.css('color', '#333');
+    $header.css('overflow', 'hidden');
+    $header.css('background-color', '#F8F8F8');
+    $header.css('border-radius', '2px 2px 0 0');
+    $header.html(this.options.title || '温馨提示');
+
+
+    let $headerToolbar = $('<div class="header-toolbar"></div>');
+    $headerToolbar.css('position', 'absolute');
+    $headerToolbar.css('right', '15px');
+    $headerToolbar.css('width', 'auto');
+    $headerToolbar.css('height', 'auto');
+    $headerToolbar.css('top', '15px');
+    $headerToolbar.css('font-size', '0');
+    $headerToolbar.css('line-height', 'initial');
+    let $iconClose = $('<span class="lsiten-icon-close"></span>');
+    $iconClose.css('background-position', '1px -40px');
+    $iconClose.css('cursor', 'pointer');
+    $iconClose.css('display', 'inline-block');
+    $iconClose.css('vertical-align', 'top');
+    $iconClose.css('position', 'relative');
+    $iconClose.css('width', '16px');
+    $iconClose.css('height', '16px');
+    $iconClose.css('margin-left', '10px');
+    $iconClose.css('font-size', '12px');
+
+
+    $iconClose.on('click', function () {
+      _this.hideMessage()
+    });
+    $headerToolbar.append($iconClose);
+    this.$el.append($header);
+    this.$el.append($headerToolbar);
+  },
+  _initStyledialog: function () {
+    this.$el.css('background-color', this.options.backgroundColor)
+            .css('top', '50%')
+            .css('opacity', '1')
+            .css('color', '#333')
+            .css('left', '50%')
+  },
+  // message end  
+  hideMessage: function () {
+    this.$mask.hide();
+    this.$el.hide();
+  },
+  showMessage: function (options) {
+    if (typeof options === 'string') {
+      this.message = options;
+      this.type = 'message';
+    } else if (typeof options === 'object') {
+      this.options = Object.assign({}, this.options, options.options || {});
+      this._initOptions();
+      this.message = options.message || '';
+    } else {
+      console.log('参数错误!');
+    }
+    this._initShowMessage();
+    this.$el.html('');
+    this._initHeader();
+    this._initBody();
+    let _this = this;
+    if (this.type === 'message') {
+      this._initStylemessage();
+      this.$el.show();
+      if (this.settimeoutId) {
+        clearTimeout(this.settimeoutId);
+      } 
+      this.settimeoutId = setTimeout(function() {
+        _this.hideMessage();
+      }, this.options.timeout);
+    } else if (this.type === 'dialog') {
+      if (this.settimeoutId) {
+        clearTimeout(this.settimeoutId);
+        this.hideMessage();
+      } 
+      this._initStyledialog();
+      this.$mask.show();
+      this.$el.show();
+    }
+  }
+}
+export default Message

+ 92 - 0
src/js/util/paste-handle.js

@@ -0,0 +1,92 @@
+/*
+    粘贴信息的处理
+*/
+
+import $ from './dom-core.js'
+import { replaceHtmlSymbol } from './util.js'
+import { objForEach } from './util.js'
+
+// 获取粘贴的纯文本
+export function getPasteText(e) {
+    const clipboardData = e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData)
+    let pasteText
+    if (clipboardData == null) {
+        pasteText = window.clipboardData && window.clipboardData.getData('text')
+    } else {
+        pasteText = clipboardData.getData('text/plain')
+    }
+
+    return replaceHtmlSymbol(pasteText)
+}
+// 获取粘贴的html
+export function getPasteHtml(e, filterStyle, ignoreImg) {
+    debugger;
+    const clipboardData = e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData)
+    let pasteText, pasteHtml
+    if (clipboardData == null) {
+        pasteText = window.clipboardData && window.clipboardData.getData('text')
+    } else {
+        pasteText = clipboardData.getData('text/plain')
+        pasteHtml = clipboardData.getData('text/html')
+    }
+    if (!pasteHtml && pasteText) {
+        pasteHtml = '<p>' + replaceHtmlSymbol(pasteText) + '</p>'
+    }
+    if (!pasteHtml) {
+        return
+    }
+
+    // 过滤word中状态过来的无用字符
+    const docSplitHtml = pasteHtml.split('</html>')
+    if (docSplitHtml.length === 2) {
+        pasteHtml = docSplitHtml[0]
+    }
+
+    // 过滤无用标签
+    pasteHtml = pasteHtml.replace(/<(meta|script|link).+?>/igm, '')
+    pasteHtml = pasteHtml.replace(/<\/*(body|html).*?>/igm, '')
+    // 去掉注释
+    pasteHtml = pasteHtml.replace(/<!--.*?-->/mg, '')
+    // 过滤 data-xxx 属性
+    pasteHtml = pasteHtml.replace(/\s?data-.+?=('|").+?('|")/igm, '')
+
+    if (ignoreImg) {
+        // 忽略图片
+        pasteHtml = pasteHtml.replace(/<img.+?>/igm, '')
+    }
+
+    if (filterStyle) {
+        // 过滤样式
+        pasteHtml = pasteHtml.replace(/\s?(class|style)=('|").*?('|")/igm, '')
+    } else {
+        // 保留样式
+        pasteHtml = pasteHtml.replace(/\s?class=('|").*?('|")/igm, '')
+    }
+
+    return pasteHtml
+}
+
+// 获取粘贴的图片文件
+export function getPasteImgs(e) {
+    const result = []
+    const txt = getPasteText(e)
+    if (txt) {
+        // 有文字,就忽略图片
+        return result
+    }
+
+    const clipboardData = e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData) || {}
+    const items = clipboardData.items
+    if (!items) {
+        return result
+    }
+
+    objForEach(items, (key, value) => {
+        const type = value.type
+        if (/image/i.test(type)) {
+            result.push(value.getAsFile())
+        }
+    })
+
+    return result
+}

+ 48 - 0
src/js/util/poly-fill.js

@@ -0,0 +1,48 @@
+/*
+    poly-fill
+*/
+
+export default function () {
+
+    // Object.assign
+    if (typeof Object.assign != 'function') {
+        Object.assign = function(target, varArgs) { // .length of function is 2
+            if (target == null) { // TypeError if undefined or null
+                throw new TypeError('Cannot convert undefined or null to object')
+            }
+
+            var to = Object(target)
+
+            for (var index = 1; index < arguments.length; index++) {
+                var nextSource = arguments[index]
+
+                if (nextSource != null) { // Skip over if undefined or null
+                    for (var nextKey in nextSource) {
+                        // Avoid bugs when hasOwnProperty is shadowed
+                        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+                            to[nextKey] = nextSource[nextKey]
+                        }
+                    }
+                }
+            }
+            return to
+        }
+    }
+
+    // IE 中兼容 Element.prototype.matches
+    if (!Element.prototype.matches) {
+        Element.prototype.matches = 
+            Element.prototype.matchesSelector || 
+            Element.prototype.mozMatchesSelector ||
+            Element.prototype.msMatchesSelector || 
+            Element.prototype.oMatchesSelector || 
+            Element.prototype.webkitMatchesSelector ||
+            function(s) {
+                var matches = (this.document || this.ownerDocument).querySelectorAll(s),
+                    i = matches.length;
+                while (--i >= 0 && matches.item(i) !== this) {}
+                return i > -1;            
+            };
+    }
+
+}

+ 499 - 0
src/js/util/question.js

@@ -0,0 +1,499 @@
+import $ from './dom-core';
+import {isEmptyElement} from './util.js'
+let questionUtils = function () {
+
+}
+
+questionUtils.prototype = {
+  constructor: questionUtils,
+  /**
+   * function 切分题目框
+   * @param {DomElement} $border 题框
+   * @param {Object} csize 栏size
+   */
+  _splitBorder: function ($border, csize) {
+    let type = parseInt($border.attr('data-type'));
+    switch (type) {
+      case 1:
+      return this._spiltObjectBorder($border, csize);
+      break;
+      case 2:
+      return this._spiltSubjectBorder($border, csize);      
+      break;
+    }
+  },
+  /**
+   * function 切分客观题框
+   * @param {DomElement} $border 题框
+   */
+  _spiltObjectBorder: function ($border, csize) {
+    let $nextBorder = $border.clone(false);
+    let childs = $border.childNodes();
+    let cLength = childs.length;
+    if (cLength > 0) {
+      let i = cLength - 1;
+      while (i >= 0) {
+        let bSizeBorder = $border.getSizeData();
+        let row = childs[i];
+        let $row = $(row);
+        let $nextRow = $row.clone(false);
+        if (bSizeBorder.bottom > csize.bottom) {
+          this._splitObjectRow($nextRow, $row, $border, csize);
+          if ($nextRow.childNodes().length > 0) {
+            if ($nextBorder.childNodes().length > 0) {
+              let firstChildNode = $nextBorder[0].firstChild;
+              $nextBorder[0].insertBefore($nextRow[0], firstChildNode);
+              $(firstChildNode).removeClass('js-split-row' + (i+1));
+              $nextRow.addClass('js-split-row' + i);
+              $row.addClass('js-split-row'  + i)
+            } else {
+              $nextBorder.append($nextRow);
+              $nextRow.addClass('js-split-row' + i);
+              $row.addClass('js-split-row'  + i)
+            }
+          }
+        }
+        i--;
+        if (!$row.childNodes().length) {
+          $nextRow.removeClass('js-split-row' + i);
+          $row.remove();
+        }
+      }
+    }
+    if ($nextBorder.childNodes().length > 0) {
+      $nextBorder[0].firstChild.style.borderTop = 'none';
+      return $nextBorder;
+    } else {
+      return false;
+    }
+  },
+  /**
+   * function 处理客观题的row
+   * @param {DomElement} $nextDiv 下一段的div
+   * @param {DomElement} $item 文本节点
+   * @param {DomElement} $border 所在框
+   * @param {Object} csize 栏size
+   */
+  _splitObjectRow: function ($nextDiv, $item, $border, csize) {
+    // debugger;
+    let childs = $item.childNodes();
+    let nextDivContentChilds =  [];
+    let nextDivContentChildsLength =  0;
+    nextDivContentChilds = $nextDiv.childNodes();
+    nextDivContentChilds.length > 0 && (nextDivContentChildsLength = nextDivContentChilds.length);
+    let cLength = childs.length;
+    let firstColumn = childs[0];
+    if (!firstColumn) {
+      return '';
+    }
+    let $firstColumn = $(firstColumn);
+    let lastQuestion = parseInt($firstColumn.attr('data-question-count')) - 1;
+    lastQuestion < 0 ? lastQuestion = 0 : '';
+    for (let i =0; i < cLength; i++) {
+      let child = childs[i].firstChild; //栏下有一个内容div
+      let $child = $(child);
+      if (nextDivContentChildsLength > 0) {
+        if (!nextDivContentChilds[i]) {
+          console.log("%c数据错误!", "color:red");          
+          return '';
+        }
+        let $nextContent = $(nextDivContentChilds[i].firstChild);
+        let childsQuestion = $child.childNodes();
+        let nextQuestion =  childsQuestion ? childsQuestion[lastQuestion] : null;
+        if (nextQuestion) {
+          $nextContent[0].insertBefore(nextQuestion, $nextContent[0].firstChild);
+          let $childColumn = $(childs[i]);
+          let columnQuestion = parseInt($childColumn.attr('data-question-count')) - 1;
+          columnQuestion < 0 ? columnQuestion = 0: '';
+          $childColumn.attr('data-question-count', columnQuestion);
+        }
+      } else {
+        let $nextColum = $(childs[i]).clone(false);
+        let $nextContent = $child.clone(false);
+        let childsQuestion = $child.childNodes();
+        let nextQuestion =  childsQuestion ? childsQuestion[lastQuestion] : null;
+        if (nextQuestion) {
+          $nextContent.append($(nextQuestion));
+          let $childColumn = $(childs[i]);
+          let columnQuestion = parseInt($childColumn.attr('data-question-count')) - 1;
+          columnQuestion < 0 ? columnQuestion = 0: '';
+          $childColumn.attr('data-question-count', columnQuestion);
+          $nextColum.append($nextContent);
+        } else {
+          i === 0 ? $nextColum.append($child) : $nextColum.append($nextContent);
+        }
+        $nextDiv.append($nextColum);
+      }
+
+      if (i === 0) {
+        let $columnPrev = $child;
+        if (!$columnPrev.childNodes().length) {
+          $item.html('');
+        }
+      }
+    }
+    if (firstColumn.firstChild) {
+      let contentSize = $(firstColumn.firstChild).getSizeData();
+      $item.find('.js-option-column').css('min-height', contentSize.height + 'px');
+    }
+    let bSizeBorder = $border.getSizeData();
+    if (bSizeBorder.bottom > csize.bottom) {
+      this._splitObjectRow($nextDiv, $item, $border, csize);
+    }
+
+  },
+
+
+    /**
+   * function 切分主观题框
+   * @param {DomElement} $border 题框
+   * @param {Object} csize 栏size
+   */
+  _spiltSubjectBorder: function ($border, csize) {
+    let $nextBorder = $border.clone(false);
+    let $content = $($border[0].firstChild);
+    if ($content.attr('class') === 'border-content') {
+      let childrens = $content.childNodes();
+      let $nextContent = $content.clone(false);
+      let html = $content.html().toLowerCase();     
+      if (childrens.length < 1 || html === '' || html === '<p></p>' || html === '<p><br></p>' || html === '<p><br/></p>' || html === '<br/>' || html === '<br>') {
+        $nextBorder.append($content);
+      } else {
+        for (let i =(childrens.length -1); i >= 0; i--) {
+          let item = childrens[i];
+          let bSize = $border.getSizeData();
+          if (bSize.bottom > csize.bottom) {
+            if (item.nodeType === 3) {
+              this._handerText($nextBorder, item, csize, $border);
+            } else {
+              let $item = $(item);
+              let className = $item.attr('class');
+              if (className && className.indexOf('js-lsiten-question') > -1) {
+                let type = parseInt($item.attr('data-type'));
+                let nextDivFirstDiv = $nextContent[0].firstChild;
+                switch (type) {
+                  case 1:
+                  let $nextChoice = this._handleChoice($item, csize, $border);
+                  nextDivFirstDiv = $nextContent[0].firstChild;
+                  if (nextDivFirstDiv) {
+                    $nextContent[0].insertBefore($nextChoice[0].firstChild, nextDivFirstDiv);
+                  } else {
+                    $nextContent[0].appendChild($nextChoice[0].firstChild);
+                  }
+
+                  break;
+                  case 2:
+                  let $nextJudge = this._handleJudge($item, csize, $border);
+                  nextDivFirstDiv = $nextContent[0].firstChild;
+                  if (nextDivFirstDiv) {
+                    $nextContent[0].insertBefore($nextJudge[0].firstChild, nextDivFirstDiv);
+                  } else {
+                    $nextContent[0].appendChild($nextJudge[0].firstChild);
+                  }
+                  break;
+                  case 3:
+                  let $nextsubject = this._handleSubject($item, csize, $border);
+                  if ($nextsubject) {
+                    nextDivFirstDiv = $nextContent[0].firstChild;
+                    if (nextDivFirstDiv) {
+                      $nextContent[0].insertBefore($nextsubject[0], nextDivFirstDiv);
+                    } else {
+                      $nextContent.append($nextsubject);
+                    }
+                    $nextsubject.addClass('js-split-problem');
+                    $item.addClass('js-split-problem');
+                    $nextsubject.attr('data-rm', 0);
+                    if (isEmptyElement($item[0])) {
+                      $item.remove();
+                      $nextsubject.attr('data-rm', 1);
+                    }
+                  }
+                  break;
+                  case 4:
+                  let $nextWritting = this._handleWritting($item, csize, $border);
+                  if ($nextWritting) {
+                    nextDivFirstDiv = $nextContent[0].firstChild;
+                    if (nextDivFirstDiv) {
+                      $nextContent[0].insertBefore($nextWritting[0], nextDivFirstDiv);
+                    } else {
+                      $nextContent.append($nextWritting);
+                    }
+                    $nextWritting.addClass('js-split-writting-problem');
+                    $item.addClass('js-split-writting-problem');
+                    $nextWritting.attr('data-rm', 0);
+                    if (isEmptyElement($item[0])) {
+                      $item.remove();
+                      $nextWritting.attr('data-rm', 1);
+                    }
+                  }
+                  break;
+                }
+              } else if (className && className.indexOf('js-lsiten-options-box') > -1) {
+                // 选做题处理
+                let $nextOption = this._handleOptions($item, csize, $border);
+                let nextDivFirstDiv = $nextContent[0].firstChild;
+                if (nextDivFirstDiv) {
+                  $nextContent[0].insertBefore($nextOption[0].firstChild, nextDivFirstDiv);
+                } else {
+                  $nextContent[0].appendChild($nextOption[0].firstChild);
+                }
+              } else {
+                let $nextDiv = this._handerDiv($item, csize, $border);
+                if ($nextDiv.childNodes().length > 0) {
+                  let nextDivFirstDiv = $nextContent[0].firstChild;
+                  if (nextDivFirstDiv) {
+                    $nextContent[0].insertBefore($nextDiv[0], nextDivFirstDiv);
+                  } else {
+                    $nextContent.append($nextDiv)
+                  }
+                }
+
+                if (isEmptyElement($item[0])) {
+                  $item.remove();
+                }
+              }
+            }
+          }
+        }
+
+        if ($nextContent.childNodes().length > 0) {
+          $nextBorder.append($nextContent);
+        }
+      }
+      return $nextBorder.childNodes().length > 0 ? $nextBorder: false;
+    } else {
+      $border.remove();
+      return false;
+    }
+  },
+  /**
+   * function 处理纯文字
+   * @param {DomElement} $nextDiv 下一段的div
+   * @param {DomElement} item 文本节点
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handerText: function ($nextDiv, item, csize, $border) {
+    let text = item.nodeValue;
+    let nextNode = item.nextSibling;
+    let parentNode = item.parentNode;
+    let span = document.createElement('span');
+    span.appendChild(item);
+    if (nextNode) {
+      parentNode.insertBefore(span, nextNode);
+    } else {
+      parentNode.appendChild(span);
+    }
+    let $span =  $(span);
+    let textSize = $span.getSizeData();
+    if (textSize.top > csize.bottom) {
+      $nextDiv[0].appendChild(item);
+      $span.remove();
+    } else {
+      let textLength = text.length;
+      let i = 0;
+      while(--textLength) {
+        span.innerHTML = text.substr(0, textLength);
+        // 要看框的底部是否在栏内
+        textSize = $border.getSizeData();
+        if (textSize.bottom < csize.bottom) {
+          break;
+        }
+      }
+      let prevText = text.substr(0, textLength);
+      let nextText = text.substring(textLength);
+      span.outerHTML = prevText;
+      $(parentNode).addClass('js-split-item');
+      $nextDiv[0].appendChild(document.createTextNode(nextText));
+      $nextDiv.addClass('js-split-item');
+    }
+  },
+  /**
+   * function 处理纯普通div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   *  @param {DomElement} $border 所在框
+   */
+  _handerDiv: function ($item, csize, $border) {
+    let $nextDiv = $item.clone(false);
+    this._splitNormalDiv($nextDiv,$item, csize, $border);
+    return $nextDiv;
+  },
+  /**
+   * function 切分纯普通div
+   * @param {DomElement} $nextDiv 纯普通div
+   * @param {Node} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _splitNormalDiv: function ($nextDiv, $item, csize, $border) {
+    let bSize = $border.getSizeData();
+    if (bSize.bottom < csize.bottom) {
+      return false;
+    }
+    let childs = $item.childNodes();
+    let cLength = childs.length;
+    if (cLength > 0) {
+      for (let i =(cLength -1); i >= 0; i--) {
+        let child = childs[i];
+        if (child.nodeType === 3) {
+          this._handerText($nextDiv, child, csize, $border);
+        } else {
+          let tName = child.tagName.toLowerCase();
+          if (tName === 'br') {
+            let nextFirstChild = $nextDiv[0].firstChild;
+            if (nextFirstChild) {
+              $nextDiv[0].insertBefore(child, nextFirstChild);
+            } else {
+              $nextDiv[0].appendChild(child);
+            }
+          } else if (tName === 'img') {
+            let nextFirstChild = $nextDiv[0].firstChild;
+            if (nextFirstChild) {
+              $nextDiv[0].insertBefore(child, nextFirstChild);
+            } else {
+              $nextDiv[0].appendChild(child);
+            }
+          } else {
+            let $child = $(child);
+            let $nextChild = $child.clone(false);
+            this._splitNormalDiv($nextChild, $child, csize, $border);
+            if ($nextChild.childNodes().length > 0) {
+              let nextDivFirstDiv = $nextDiv[0].firstChild;
+              if (nextDivFirstDiv) {
+                $nextDiv[0].insertBefore($nextChild[0], nextDivFirstDiv);
+              } else {
+                $nextDiv.append($nextChild)
+              }
+            }
+            if ($child[0].innerHTML === '') {
+              $child.remove();
+            }
+          }
+        }
+      }
+    }
+  },
+  /**
+   * function 处理选择题div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handleOptions: function ($item, csize, $border) {
+    let $nextNext = $('<div></div>');
+    $nextNext.append($item);
+    return $nextNext;
+  },
+  /**
+   * function 处理选择题div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handleChoice: function ($item, csize, $border) {
+    let $nextNext = $('<div></div>');
+    $nextNext.append($item);
+    return $nextNext;
+  },
+  /**
+   * function 处理判断题div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handleJudge: function ($item, csize, $border) {
+    let $nextNext = $('<div></div>');
+    $nextNext.append($item);
+    return $nextNext;
+  },
+  /**
+   * function 处理客观题Div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handleSubject: function ($item, csize, $border) {
+    let $nextSubject = $item.clone(false);
+    this._splitNormalDiv($nextSubject, $item, csize, $border);
+    !$item.childNodes().length && $item.remove();
+    return $nextSubject.childNodes().length > 0 ? $nextSubject : false;
+  },
+  /**
+   * function 处理作文题Div
+   * @param {DomElement} $item 纯普通div
+   * @param {Object} csize 栏size
+   * @param {DomElement} $border 所在框
+   */
+  _handleWritting: function ($item, csize, $border) {
+    let childs = $item.childNodes();
+    let cLength = childs.length;
+    let $nextWritting = $item.clone(false);
+    if (cLength > 0) {
+      let i = cLength - 1;
+      while (i >= 0) {
+        let bSize = $border.getSizeData();
+        let child = childs[i];
+        let $child = $(child);
+        if (bSize.bottom > csize.bottom) {
+          if (child.className.indexOf('lsiten-title') > -1) {
+            if (!isEmptyElement(child)) { 
+              // 作文标题
+              let $nextLsitenTitle = $child.clone(false);
+              this._splitNormalDiv($nextLsitenTitle, $child, csize, $border);
+              if ( $nextLsitenTitle.childNodes().length > 0 ) {
+                let nextDivFirstDiv = $nextWritting[0].firstChild;
+                if (nextDivFirstDiv) {
+                  $nextWritting[0].insertBefore($nextLsitenTitle[0], nextDivFirstDiv);
+                } else {
+                  $nextWritting.append($nextLsitenTitle);
+                }
+                $nextLsitenTitle.addClass('js-split-writting-title');
+              }
+            } else {
+              $child.remove();
+            }
+          } else if (child.className.indexOf('lsiten-border-box') > -1){
+            // 作文格子
+            let $nextLsitenBorder = $child.clone(false);
+            let rows = $child.childNodes();
+            let rowLength = rows.length;
+            if ( rowLength > 0 ) {
+              let k = rowLength - 1;
+              while ( k >= 0 ) {
+                let bSizeBorder = $border.getSizeData();
+                if (bSizeBorder.bottom > csize.bottom) {
+                  let $rowItem = $(rows[k]);
+                  let nextDivFirstDiv = $nextLsitenBorder[0].firstChild;
+                  if (nextDivFirstDiv) {
+                    $nextLsitenBorder[0].insertBefore($rowItem[0], nextDivFirstDiv);
+                  } else {
+                    $nextLsitenBorder.append($rowItem);
+                  }
+                }
+                k--;
+              }
+            } else {
+              $nextLsitenBorder.remove();
+            }
+
+            if ($nextLsitenBorder.childNodes().length > 0) {
+              let nextDivFirstDiv = $nextWritting[0].firstChild;
+              if (nextDivFirstDiv) {
+                $nextWritting[0].insertBefore($nextLsitenBorder[0], nextDivFirstDiv);
+              } else {
+                $nextWritting.append($nextLsitenBorder);
+              }
+              $nextLsitenBorder.addClass('js-split-writting-border');
+            } 
+          }
+        }
+        i--;
+        isEmptyElement($child[0]) && $child.remove();
+      }
+    }
+    return $nextWritting.childNodes().length > 0 ? $nextWritting : false;
+  }
+}
+
+export default questionUtils;

+ 932 - 0
src/js/util/questionPrev.js

@@ -0,0 +1,932 @@
+import $ from './dom-core';
+import {getParentNodeByClass, isEmptyElement} from './util.js'
+let questionPrevUtils = function () {
+  this.currentParaghSize = {};
+}
+
+questionPrevUtils.prototype = {
+  constructor: questionPrevUtils,
+  /**
+  * function 客观题获取最后一行【不在框内】
+  * @param {DomElement}  $lastparagraph
+  */
+  _getLastNodeByLastparagraphNormal: function ($lastparagraph) {
+    let lastchild = $lastparagraph[0].lastChild;
+    if (lastchild.nodeType === 3) {
+      return {
+        type: 1,
+        dom: lastchild
+      };
+    } else if (lastchild.tagName.toLowerCase() === 'br'){
+      $(lastchild).remove();
+      return this._getLastNodeByLastparagraphNormal($lastparagraph);
+    } else {
+      if (lastchild.className) {
+        if (lastchild.className.indexOf('js-lsiten-border') > -1) {
+          return {
+            type: 0,
+            dom: null
+          };
+        }
+      } else {
+        return {
+          type: 2,
+          dom: lastchild
+        };
+      }
+    }
+  },
+   /**
+   * funciton 处理回退文字【不在框内】
+   * @param {Node} item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {Boolean} isMerge 是否需要合并段落
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {ndoe} currentParagh 当前编辑的段落
+   */
+  handerText: function (item, sizes, isMerge, $lastparagraph, currentParagh) {
+    if (isMerge) {
+      let lastNode = this._getLastNodeByLastparagraphNormal($lastparagraph);
+      if (lastNode.type === 1 || lastNode.type === 2) {
+        let textValue = item.nodeValue;
+        let textLength = textValue.length;
+        let parent = lastNode.dom.parentNode;
+        let i = 0;
+        while (i < textLength) {
+          let tempHtml = parent.innerHTML;
+          parent.innerHTML += textValue[i];
+          let lastSize = $lastparagraph.getSizeData();
+          if (lastSize.bottom > sizes.csize.bottom) {
+            parent.innerHTML = tempHtml;
+            break;
+          }
+  
+          i++;
+        }
+        let currentText = textValue.substring(i);
+        item.nodeValue = currentText;
+      } else {
+        console.log('%cerror:系统错误,请联系管理员!', 'color:red')
+        return  false;
+      }
+    } else {
+      let tempParagraph = currentParagh.cloneNode(false);
+      let $tempParagraph = $(tempParagraph);
+      $tempParagraph.insertAfter($lastparagraph);
+      let textValue = item.nodeValue;
+      let textLength = textValue.length;
+      let i = 0;
+      while (i < textLength) {
+        let tempHtml = tempParagraph.innerHTML;
+        tempParagraph.innerHTML += textValue[i];
+        let lastSize = $tempParagraph.getSizeData();
+        if (lastSize.bottom > sizes.csize.bottom) {
+          tempParagraph.innerHTML = tempHtml;
+          break;
+        }
+        i++;
+      }
+      let currentText = textValue.substring(i);
+      item.nodeValue = currentText;
+      if (isEmptyElement(tempParagraph)) {
+        $tempParagraph.remove();
+      }
+    }
+  },
+  /**
+   * funciton 处理回退div【不在框内】
+   * @param {Node} item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  handerNormalDiv: function ($item, sizes, $lastparagraph, isMerge) {
+    let currentParagh = getParentNodeByClass($item[0], 'js-paragraph-view');
+    if (currentParagh) {
+      this.currentParaghSize = $(currentParagh).getSizeData();
+    } else {
+      console.log('error');
+      return '';
+    }
+    let $prevDivSub = $item.clone(false);
+    $item.childNodes().forEach(node => {
+      if (node.nodeType === 3) {
+        this.handerText(node, sizes, isMerge, $lastparagraph, currentParagh);
+      } else if (node.tagName.toLowerCase() === 'br' || node.tagName.toLowerCase() === 'img') {
+        let brSize = $(node).getSizeData();
+        if (isMerge) {
+          if (brSize.height < sizes.diff) {
+            sizes.diff -= brSize.height;
+            $lastparagraph.append($(node));
+          } else {
+            return false;
+          }
+        } else {
+          let diff = brSize.bottom - this.currentParaghSize.top;
+          if (diff < sizes.diff) {
+            sizes.diff -= diff;
+            $lastparagraph.append($(node));
+          } else {
+            return false;
+          }
+        }
+      } else {
+        this._handerNormalDiv($(node), sizes, $prevDivSub, $lastparagraph, isMerge);
+        if (!isEmptyElement($prevDivSub[0])) {
+          if (isMerge) {
+            $lastparagraph[0].innerHTML += $prevDivSub.html();
+          } else {
+            $prevDivSub.insertAfter($lastparagraph);
+          }
+        }
+      }
+    })
+  },
+  /**
+   * function 计算字体是否可以回退【不在框内】
+   * @param {node} text 计算文字节点
+   * @param {object} sizes 参考尺寸
+   */
+  _checkoutNodeOut: function (text, sizes) {
+    let nextNode = text.nextSibling;
+    let parentNode = text.parentNode;
+    let span = document.createElement('span');
+    span.appendChild(text);
+    if (nextNode) {
+      parentNode.insertBefore(span, nextNode);
+    } else {
+      parentNode.appendChild(span);
+    }
+    let $span = $(span);
+    let currentParaghSize = this.currentParaghSize;
+    let textValue = text.nodeValue;
+    let length = textValue.length;
+    let i = length;
+    let prev = '';
+    while (i >= 0) {
+      let textTemp = textValue.substr(0, i--);
+      text.nodeValue = textTemp;
+      let textSize = $span.getSizeData();
+      let diff = textSize.bottom - currentParaghSize.top;
+      if (diff < sizes.diff) {
+        prev = textTemp;
+        span.outerHTML = textValue.substring(i);
+        sizes.diff -= diff;
+        break;
+      }
+    }
+    return prev;
+  },
+  /**
+   * function 插入上一段文字是否可以回退【不在框内】
+   * @param {node} lastNode 上一段最后一个节点
+   * @param {node} subNode 子节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  _checkInsertText: function (lastNode, subNode, sizes, $lastparagraph) {
+    let textValue = subNode.nodeValue;
+    let length = textValue.length;
+    let i = 0;
+    while (i < length) {
+      let tempHtml = lastNode.innerHTML;
+      lastNode.innerHTML += textValue[i];
+      let size = $lastparagraph.getSizeData();
+      if (size.bottom > sizes.csize.bottom) {
+        lastNode.innerHTML = tempHtml;
+        break;
+      }
+      i++;
+    }
+    subNode.nodeValue = textValue.substring(i);
+  },
+  /**
+   * funciton 处理回退div【不在框内】
+   * @param {DomElement} $item 处理的DOM
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prevDiv 该栏前移的div
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  _handerNormalDiv: function ($item, sizes, $prevDiv, $lastparagraph, isMerge) {
+    let $prevDivSub = $item.clone(false);
+    let currentParaghSize = this.currentParaghSize;
+    $item.childNodes().forEach(subNode => {
+      if (subNode.nodeType === 3) {
+        if (isMerge) {
+          let lastNode = this._getLastNodeByLastparagraphNormal($lastparagraph);
+          if (lastNode.type === 2) {
+            if (lastNode.dom.tagName === $item[0].tagName) {
+             this._checkInsertText(lastNode.dom, subNode, sizes, $lastparagraph);             
+            }
+          }
+        } else {
+          let prevText = this._checkoutNodeOut(subNode, sizes);
+          if (prevText.length > 0) {
+            let textNode = document.createTextNode(prevText);
+            $prevDivSub[0].appendChild(textNode);
+          }
+        }
+      } else if (subNode.tagName.toLowerCase() === 'br') {
+        let $br = $(subNode);
+        let brSize = $br.getSizeData();
+        let diff = brSize.bottom - currentParaghSize.top + 11;
+        if (diff < sizes.diff) {
+          $prevDivSub.append($br);
+          sizes.diff -= diff;
+        }
+      } else {
+        let $subnode = $(subNode);
+        let $prevTemp = $subnode.clone(false);
+        this._handerNormalDiv($subnode, sizes, $prevTemp, $lastparagraph, isMerge);
+        let html = $prevTemp.html();
+        if (html.length > 0) {
+          $prevDivSub.append($prevTemp);
+        }
+      }
+    });
+
+    let html2 = $prevDivSub.html();
+    if (html2.length > 0) {
+      $prevDiv.append($prevDivSub);
+    }
+
+    if (!$item.html().length) {
+      $item.remove();
+    }
+  },
+  /**
+   * funciton 处理回退框
+   * @param {DomElement} $border 题框
+   * @param {Object} sizes 尺寸对象
+   * @param {Boolean} isMerge 是否需要合并段落
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  handerBorder: function ($border, sizes, isMerge, $lastparagraph) {
+    // border有10px的margin-bottom
+    sizes.diff -= 10;
+    let currentParagh = getParentNodeByClass($border[0], 'js-paragraph-view');
+    if (currentParagh) {
+      this.currentParaghSize = $(currentParagh).getSizeData();
+    } else {
+      console.log('error');
+      return '';
+    }
+    let borderType = parseInt($border.attr('data-type'));
+    this.borderType = borderType;
+    if (borderType === 1) {
+      // 客观题
+      this._handerObjectBorder($border, sizes, isMerge, $lastparagraph, currentParagh);
+    } else {
+      // 主观题
+      this._handerSubjectBorder($border, sizes, isMerge, $lastparagraph);
+    }
+  },
+  /**
+   * function 客观题获取最后一行
+   * @param {DomElement}  $lastparagraph
+   */
+  _getLastRowByLastparagraph ($lastparagraph) {
+    let border = $lastparagraph[0].lastChild;
+    if (border.className && border.className.indexOf('lsiten-question-object') > -1) {
+      let lastRow = border.lastChild;
+      if (lastRow.className && lastRow.className.indexOf('js-options-row') > -1) {
+        return lastRow;
+      } else {
+        return false;
+      }
+    } else {
+      return false;
+    }
+  },
+  /**
+   * funciton 处理主观题回退框
+   * @param {DomElement} $border 题框
+   * @param {Object} sizes 尺寸对象
+   * @param {Boolean} isMerge 是否需要合并段落
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Node} currentParagh 当前处理的段落
+   */
+  _handerObjectBorder: function ($border, sizes, isMerge, $lastparagraph, currentParagh) {
+    if (isMerge) {
+      let lastRow = this._getLastRowByLastparagraph($lastparagraph);
+      let $lastRow = $(lastRow);
+      let splitClass = '';
+      let splitColumn = [];
+      if (lastRow.className) {
+        let splits = lastRow.className.match(/js-split-row[0-9]+/);
+        if (splits) {
+          splitClass = splits[0];
+          splitColumn = $lastRow.childNodes();
+        }
+      }
+      if (lastRow) {
+        $border.childNodes().forEach(row => {
+          let $row = $(row);
+          let $rowTemp = $row.clone(false);
+          let columns = $row.childNodes();
+          let rowClass = $row.attr('class');
+          if (!columns.length) {
+            return false;
+          } else {
+            let $firstColumn = $(columns[0].firstChild);
+            $firstColumn.childNodes().forEach(question => {
+              let size = $(question).getSizeData();
+              if (splitClass.length > 0 && rowClass.indexOf(splitClass) > -1) {
+                if (size.height + 16 < sizes.diff) {
+                  let j = 0;
+                  splitColumn.forEach(column => {
+                    let questions = $(columns[j++].firstChild).childNodes();
+                    questions[0] && column.firstChild.appendChild(questions[0]);
+                  })
+
+                  sizes.diff -= size.height;
+                }
+              } else {
+                let diff = size.height + 24 + 16;
+                if (diff < sizes.diff) {
+                  let j = 0;
+                  let columnTemps = $rowTemp.find('.js-option-column');  
+                  columns.forEach(column => {
+                    let columnTemp = columnTemps[j];
+                    let columnContentTemp = null;
+                    if (!columnTemp) {
+                      columnTemp = column.cloneNode(false);
+                      columnContentTemp = column.firstChild.cloneNode(false);
+                      columnTemp.appendChild(columnContentTemp);
+                      $rowTemp[0].appendChild(columnTemp);
+                    } else {
+                      columnContentTemp = columnTemp.firstChild;
+                    }
+                    let questions = $(columns[j++].firstChild).childNodes();
+                    questions[0] && columnContentTemp.appendChild(questions[0]);
+                  })
+                  sizes.diff -= size.height;
+                }
+              }
+            })
+
+            if (isEmptyElement($firstColumn[0])) {
+              $row.remove();
+            }
+
+            if (!isEmptyElement($rowTemp[0])) {
+              $lastparagraph[0].firstChild.appendChild($rowTemp[0]);
+              $lastparagraph[0].firstChild.childNodes.length > 0 && $rowTemp.css('border-top', '1px solid #000');
+            }
+          }
+        })
+        if (isEmptyElement($border[0])) {
+          $border.remove();
+        }
+      } else {
+        console.log("%cerror", "color:red")
+      } 
+    } else {
+      let $borderTemp = $border.clone(false);
+      $border.childNodes().forEach(row => {
+        let $row = $(row);
+        let firstColumn = row.firstChild;
+        let $rowTemp = $row.clone(false);
+        let columns = $row.childNodes();
+        let $columnContent = $(firstColumn.firstChild);
+        $columnContent.childNodes().forEach(question => {
+          let  size = $(question).getSizeData();
+          let diff = size.height + 24 + 16;
+          if (diff < sizes.diff) {
+            let j = 0;
+            let columnTemps = $rowTemp.find('.js-option-column');  
+            columns.forEach(column => {
+              let columnTemp = columnTemps[j];
+              let columnContentTemp = null;
+              if (!columnTemp) {
+                columnTemp = column.cloneNode(false);
+                columnContentTemp = column.firstChild.cloneNode(false);
+                columnTemp.appendChild(columnContentTemp);
+                $rowTemp[0].appendChild(columnTemp);
+              } else {
+                columnContentTemp = columnTemp.firstChild;
+              }
+              let questions = $(columns[j++].firstChild).childNodes();
+              questions[0] && columnContentTemp.appendChild(questions[0]);
+            })
+            sizes.diff -= size.height;
+          }
+        })
+        if (!isEmptyElement($rowTemp[0])) {
+          $borderTemp.append($rowTemp);
+        }
+
+        if (isEmptyElement($columnContent[0])) {
+          $row.remove();
+        }
+      })
+
+      if (isEmptyElement($border[0])) {
+        $border.remove();
+      }
+
+      if (!isEmptyElement($borderTemp[0])) {
+        let tempParagraph = currentParagh.cloneNode(false);
+        tempParagraph.appendChild($borderTemp[0]);
+        $(tempParagraph).insertAfter($lastparagraph);
+        if (isEmptyElement(currentParagh)) {
+          $(tempParagraph).remove();
+        }
+      }
+    }
+  },
+  /**
+   * funciton 处理主观题回退框
+   * @param {DomElement} $border 题框
+   * @param {Object} sizes 尺寸对象
+   * @param {Boolean} isMerge 是否需要合并段落
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  _handerSubjectBorder: function ($border, sizes, isMerge, $lastparagraph) {
+    let borderContent = $border[0].firstChild;
+    if (!borderContent || borderContent.nodeType !== 1) {
+      console.log("%cerror border", "color:red");   
+      return false;
+    } else {
+      let $borderContent = $(borderContent);
+      let $prevDiv = $borderContent.clone(false);  
+      $borderContent.childNodes().forEach(item => {
+        if (item.nodeType === 3) {
+          this._moveTxtToPrev(item, sizes, $prevDiv, $lastparagraph, isMerge);
+        } else {
+          let $item = $(item);
+          let className = $item.attr('class');
+          if (className && className.indexOf('js-lsiten-question') > -1) {
+            let type = parseInt($item.attr('data-type'));
+            switch (type) {
+              case 1:
+              case 2:
+              this._handerObjectContent($item, sizes, $prevDiv, $lastparagraph, isMerge);
+              break;
+              case 3:
+              this._handersubjectContent($item, sizes, $prevDiv, $lastparagraph, isMerge);
+              break;
+              case 4:
+              this._handerwrittingContent($item, sizes, $prevDiv, $lastparagraph, isMerge);
+              break;
+            }
+          } else {
+            this._handerNormal($item, sizes, $prevDiv, $lastparagraph, isMerge);
+          }
+        }
+      });
+      if (!isEmptyElement($prevDiv[0])) {
+        if (isMerge) {
+          let lastBorder = $lastparagraph[0].lastChild;
+          if (lastBorder) {
+            lastBorder.firstChild.innerHTML += $prevDiv[0].innerHTML;
+          }
+        } else {
+          let tempBorder = $border[0].cloneNode(false);
+          tempBorder.append($prevDiv[0]);
+          let tempPrevParagraph = $border[0].parentNode.cloneNode(false);
+          tempPrevParagraph.appendChild(tempBorder);
+          $lastparagraph[0].parentNode.appendChild(tempPrevParagraph);
+        }
+      }
+      if (isEmptyElement($borderContent[0])) {
+        $border.remove();
+      }
+    }
+  },
+  /**
+   * funciton 处理回退框内客观题
+   * @param {DomElement} $item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prev 向前移动的容器
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  _handerObjectContent: function ($item, sizes, $prevDiv, $lastparagraph, isMerge) {
+    let itemSize = $item.getSizeData();
+    if (isMerge) {
+      let diff = itemSize.height;
+      if (sizes.diff > diff) {
+        sizes.diff -= diff;
+        let borderContent = this.getBorderContentByParagh($lastparagraph);
+        borderContent.appendChild($item[0]);
+      }
+    } else {
+      let diff = itemSize.bottom - this.currentParaghSize.top + 11;
+      if (sizes.diff > diff) {
+        sizes.diff -= diff;
+        $prevDiv.append($item);
+      }
+    }
+  },
+  /**
+   * funciton 处理回退框内主观题
+   * @param {DomElement} $item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prev 向前移动的容器
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  _handersubjectContent: function ($item, sizes, $prevDiv, $lastparagraph, isMerge) {
+    let $prevDivSub = $item.clone(false);
+    $item.childNodes().forEach(node => {
+      if (node.nodeType === 3) {
+        this._moveTxtToPrev(node, sizes, $prevDivSub, $lastparagraph, isMerge);
+      } else {
+        this._handerNormal($(node), sizes, $prevDivSub, $lastparagraph, isMerge);
+
+        if (node.innerHTML === '') {
+          $(node).remove();
+        }
+      }
+
+    })
+    // console.log($prevDivSub[0], isMerge, sizes);
+    if(!isEmptyElement($prevDivSub[0])) {
+      let lastRow = this.getLastQestionByParagh($lastparagraph);
+      if (lastRow) {
+        if (isMerge) {
+          let type = parseInt(lastRow.getAttribute('data-type'));
+          if (type === 3) {   
+            if (lastRow.className && lastRow.className.indexOf('js-split-problem') > -1) {
+              lastRow.innerHTML += $prevDivSub.html();
+            } else {
+              $prevDiv.append($prevDivSub);
+            }
+          } 
+        } else {
+          $prevDiv.append($prevDivSub);
+        }
+      } else {
+        $prevDiv.append($prevDivSub);
+      }
+
+      if (isEmptyElement($item[0])) {
+        $(lastRow).removeClass('js-split-problem');
+        $item.remove();
+      }
+    }
+  },
+  /**
+   * function 获取最后一段最后一个div
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  getLastQestionByParagh: function ($lastparagraph) {
+    let lastChild = $lastparagraph[0].lastChild;
+    if (lastChild.className && lastChild.className.indexOf('js-lsiten-border') > -1) {
+      let borderType = parseInt(lastChild.getAttribute('data-type'));
+      if (borderType === this.borderType) {
+        if (borderType === 2) {
+          // 主观题
+          let content = lastChild.lastChild;
+          if (content.className && content.className.indexOf('border-content') > -1) {
+            let lastcontent = content.lastChild;
+            if (lastcontent.className && lastcontent.className.indexOf('js-lsiten-question') > -1) {
+              return lastcontent;
+            }
+          }
+        } else {
+          // 客观题
+        }
+      }
+    }
+    return null;
+  },
+  /**
+   * funciton 处理回退框内作文题
+   * @param {DomElement} $item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prev 向前移动的容器
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  _handerwrittingContent: function ($item, sizes, $prevDiv, $lastparagraph, isMerge) {
+    let $questionDivSub = $item.clone(false);
+    $item.childNodes().forEach(nodeItem => {
+      if (nodeItem.nodeType === 3) {
+        nodeItem.parentNode.removeChild(nodeItem);
+      } else {
+        let $nodeItem = $(nodeItem);
+        let className = $nodeItem.attr('class');
+        if (className && className.indexOf('lsiten-title') > -1) {
+          // 作文题目区
+          let $prevDivSub = $nodeItem.clone(false);      
+          $nodeItem.childNodes().forEach(node => {
+            if (node.nodeType === 3) {
+              this._moveTxtToPrev(node, sizes, $prevDivSub, $lastparagraph, isMerge);
+            } else {
+              this._handerNormal($(node), sizes, $prevDivSub, $lastparagraph, isMerge);
+            }
+          })
+    
+          if (!isEmptyElement($prevDivSub[0])) {
+            $questionDivSub.append($prevDivSub);
+          }
+        } else if (className && className.indexOf('lsiten-border-box') > -1) {
+          let $prevDivSub = $nodeItem.clone(false);   
+          let lastRow = null;
+          if (isMerge) {
+            lastRow = this.getLastContentByParagh($lastparagraph);
+          }
+          if (lastRow) {
+            // 作文网格区
+            $nodeItem.childNodes().forEach(row => {
+              let rsize = $(row).getSizeData();
+              if (this._checkIsNeedPrev(rsize, sizes, isMerge)) {
+                if (isMerge) {
+                  if (lastRow.className && lastRow.className.indexOf('lsiten-title') > -1) {
+                    let $prevDivSubTemp = $nodeItem.clone(false);   
+                    $prevDivSubTemp.append($(row));
+                    lastRow.parentNode.appendChild($prevDivSubTemp[0]);
+                  } else {
+                    lastRow.parentNode.appendChild(row);
+                  }
+                } else {
+                  $prevDivSub.append($(row));
+                }
+              }
+            })
+          }
+    
+          if (!isEmptyElement($prevDivSub[0])) {
+            $questionDivSub.append($prevDivSub);
+          }
+
+        }
+        if (isEmptyElement($nodeItem[0])) {
+          $nodeItem.remove();
+        }
+      }
+    })
+
+    if (!isEmptyElement($questionDivSub[0])) {
+      $prevDiv.append($questionDivSub);
+    }
+
+    if (isEmptyElement($item[0])) {
+      $item.remove();
+    }
+  },
+  /**
+   * function 判断是否可以回退回去
+   * @param {Object} osize 需要判断的尺寸
+   * @param {Object} sizes 参考的尺寸
+   * @param {Boolean} isMerge 是否合并段落
+   */
+  _checkIsNeedPrev: function (osize, sizes, isMerge) {
+    if (isMerge) {
+      if (osize.height < sizes.diff) {
+        sizes.diff -= osize.height;
+        return true;
+      } else {
+        return false;
+      }
+    } else {
+      let diff = osize.bottom - sizes.csize.bottom;
+      if (diff < sizes.diff) {
+        sizes.diff -= diff;
+        return true;
+      } else {
+        return false;
+      }
+    }
+  },
+  /**
+   * @param {node} item 需要处理的节点
+   * @param {object} sizes 参考的尺寸
+   * @param {DomElement} $prevDivSub 上移的div容器
+   * @param {DomElement} $lastparagraph 上一栏最后一段
+   */
+  _checkNormalText: function (item, sizes, $prevDivSub, $lastparagraph) {
+    let nextNode = item.nextSibling;
+    let parentNode = item.parentNode;
+    let span = document.createElement('span');
+    span.appendChild(item);
+    if (nextNode) {
+      parentNode.insertBefore(span, nextNode);
+    } else {
+      parentNode.appendChild(span);
+    }
+    let $span =  $(span);
+    if (!this.currentParaghSize.top) {
+      $span[0].outerHTML = $span[0].innerHTML;
+      return false;
+    }
+    let textValue = item.nodeValue;
+    let length = textValue.length;
+    let i = length;
+    while (i > 0) {
+      let textTemp = textValue.substring(0, length);
+      item.nodeValue = textTemp;
+      let textSize = $span.getSizeData();
+      let diff = textSize.bottom - this.currentParaghSize.top + 11;
+      if (diff < sizes.diff) {
+        $prevDivSub[0].appendChild(document.createTextNode(textTemp));
+        break;
+      }
+      i--;
+    }
+    item.nodeValue = textValue.substring(i);
+    $span[0].outerHTML = $span[0].innerHTML;
+  },
+  /**
+   * funciton 处理回退框内普通文字
+   * @param {DomElement} $item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prev 向前移动的容器
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   */
+  _handerNormal: function ($item, sizes, $prevDiv, $lastparagraph, isMerge) {
+    let $prevDivSub = $item.clone(false);
+    $item.childNodes().forEach(node => {
+      if (node.nodeType === 3) {
+        if (isMerge) {
+          this._moveTxtToPrev(node, sizes, $prevDivSub, $lastparagraph, isMerge);
+        } else {
+          this._checkNormalText(node, sizes, $prevDivSub, $lastparagraph);
+        }
+      } else if (node.tagName.toLowerCase() === 'br' || node.tagName.toLowerCase() === 'img') {
+        let brSize = $(node).getSizeData();
+        if (isMerge) {
+          let diff = brSize.bottom - this.currentParaghSize.top - 11;
+          if (diff < sizes.diff) {
+            sizes.diff -= brSize.height;
+            $prevDivSub.append($(node));
+          } else {
+             return false;
+          }
+        } else {
+          let diff = brSize.bottom - this.currentParaghSize.top + 11;
+          if (diff < sizes.diff) {
+            sizes.diff -= diff;
+            $prevDivSub.append($(node));
+          } else {
+            return false;
+          }
+        }
+      } else {
+        this._handerNormal($(node), sizes, $prevDivSub, $lastparagraph, isMerge);
+      }
+    })
+    if($prevDivSub[0].innerHTML !== '') {
+      $prevDiv.append($prevDivSub);
+    }
+  },
+  /**
+   * function 获取最后一段最后一个问题
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  getBorderContentByParagh: function ($lastparagraph) {
+    let lastChild = $lastparagraph[0].lastChild;
+    if (lastChild.className && lastChild.className.indexOf('js-lsiten-border') > -1) {
+      let borderType = parseInt(lastChild.getAttribute('data-type'));
+      if (borderType === this.borderType) {
+        if (borderType === 2) {
+          // 主观题
+          let content = lastChild.lastChild;
+          if (content.className && content.className.indexOf('border-content') > -1) {
+            return content;
+          } else {
+            return null;
+          }
+        } else {
+          // 客观题
+        }
+      }
+    }
+    return null;
+  },
+  /**
+   * function 获取最后一段最后一个div
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   */
+  getLastContentByParagh: function ($lastparagraph) {
+    let lastChild = $lastparagraph[0].lastChild;
+    if (lastChild.className && lastChild.className.indexOf('js-lsiten-border') > -1) {
+      let borderType = parseInt(lastChild.getAttribute('data-type'));
+      if (borderType === this.borderType) {
+        if (borderType === 2) {
+          // 主观题
+          let content = lastChild.lastChild;
+          if (content.className && content.className.indexOf('border-content') > -1) {
+            let lastcontent = content.lastChild;
+            let type = parseInt(lastcontent.getAttribute('data-type'));
+            if (type === 3) {
+              return lastcontent.lastChild;
+            } else if (type === 4) {
+              lastcontent = lastcontent.lastChild;
+              if (lastcontent.className && lastcontent.className.indexOf('lsiten-title') > -1) {
+                return lastcontent;
+              } else if (lastcontent.className && lastcontent.className.indexOf('lsiten-border-box') > -1) {
+                return lastcontent.lastChild;
+              }
+            }
+          }
+        } else {
+          // 客观题
+        }
+      }
+    } else {
+      return lastChild;
+    }
+  },
+    /**
+   * funciton 处理回退框 仅仅适合客观题
+   * @param {Node} item 文字节点
+   * @param {Object} sizes 尺寸对象
+   * @param {DomElement} $prev 向前移动的容器
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Boolean} isMerge 是否需要合并段落
+   * */
+  _moveTxtToPrev (item, sizes, $prev, $lastparagraph, isMerge) {
+    let nextNode = item.nextSibling;
+    let parentNode = item.parentNode;
+    let span = document.createElement('span');
+    span.appendChild(item);
+    if (nextNode) {
+      parentNode.insertBefore(span, nextNode);
+    } else {
+      parentNode.appendChild(span);
+    }
+    let $span =  $(span);
+    let textSize = $span.getSizeData();
+    if (!this.currentParaghSize.top) {
+      $span[0].outerHTML = $span[0].innerHTML;
+      return false;
+    }
+    let diff = textSize.bottom - this.currentParaghSize.top + 11;
+    if (diff) {
+      if (sizes.diff - diff > 0) {
+        $prev[0].appendChild(item);
+        $span.remove();
+      } else {
+        if (isMerge) {
+          let lastContent = this.getLastContentByParagh($lastparagraph);
+          if (lastContent.className && lastContent.className.indexOf('js-split-item') > -1) {
+            let content = lastContent.lastChild;
+            if (content.nodeType === 3) {
+              let prevText = this._textInsertTest(content, item, $lastparagraph, sizes);
+              $prev[0].innerHTML += prevText;
+            }
+          } else if ( lastContent.className && lastContent.className.indexOf('js-split-writting-title') > -1) {
+            let content = lastContent.lastChild;
+            if (content.nodeType === 3) {
+              let prevText = this._textInsertTest(content, item, $lastparagraph, sizes);
+              $prev[0].innerHTML += prevText;
+            }
+          } else {
+           diff = textSize.bottom - this.currentParaghSize.top - 11;
+           if ( diff < sizes.diff ) {
+            sizes.diff -= textSize.height;
+            $prev[0].appendChild(item);
+           }
+          }
+        }
+        $span[0].outerHTML = $span[0].innerHTML;
+      }
+    } else {
+      $span[0].outerHTML = $span[0].innerHTML;
+      console.log('error');
+    }
+  },
+  /**
+   * 
+   * @param {node} content 需要插入测试的文本节点node
+   * @param {node} insert  被插入测试的文本节点node
+   * @param {DomElement} $lastparagraph 该栏最后一段
+   * @param {Object} sizes 尺寸对象
+   */
+  _textInsertTest (content, insert, $lastparagraph, sizes) {
+    let nextNode = content.nextSibling;
+    let parentNode = content.parentNode;
+    let span = document.createElement('span');
+    span.appendChild(content);
+    if (nextNode) {
+      parentNode.insertBefore(span, nextNode);
+    } else {
+      parentNode.appendChild(span);
+    }
+
+    let textValue = insert.nodeValue;
+    let length = textValue.length;
+    let columnSize = sizes.csize;
+    let prev = '';
+    let i = 0;
+    for ( ;i < length; i++) {
+      let tempHtml = span.innerHTML;
+      span.innerHTML += textValue[i];
+      let psize = $lastparagraph.getSizeData();
+      if (psize.bottom > columnSize.bottom) {
+        span.outerHTML = tempHtml;
+        break;
+      } else {
+        prev += textValue[i];
+      }
+    }
+    insert.nodeValue = textValue.substring(i);
+    return prev;
+  }
+}
+export default questionPrevUtils;

+ 21 - 0
src/js/util/replace-lang.js

@@ -0,0 +1,21 @@
+/*
+    替换多语言
+ */
+
+export default function (editor, str) {
+    const langArgs = editor.config.langArgs || []
+    let result = str
+
+    langArgs.forEach(item => {
+        const reg = item.reg
+        const val = item.val
+
+        if (reg.test(result)) {
+            result = result.replace(reg, function () {
+                return val
+            })
+        }
+    })
+
+    return result
+}

+ 257 - 0
src/js/util/util.js

@@ -0,0 +1,257 @@
+/*
+    工具
+*/
+import Ajax from './ajax'
+// 和 UA 相关的属性
+export const UA = {
+    _ua: navigator.userAgent,
+
+    // 是否 webkit
+    isWebkit: function () {
+        const reg = /webkit/i
+        return reg.test(this._ua)
+    },
+
+    // 是否 IE
+    isIE: function () {
+        return 'ActiveXObject' in window
+    }
+}
+
+// 遍历对象
+export function objForEach(obj, fn) {
+    let key, result
+    for (key in obj) {
+        if (obj.hasOwnProperty(key)) {
+            result = fn.call(obj, key, obj[key])
+            if (result === false) {
+                break
+            }
+        }
+    }
+}
+
+// 遍历类数组
+export function arrForEach(fakeArr, fn) {
+    let i, item, result
+    const length = fakeArr.length || 0
+    for (i = 0; i < length; i++) {
+        item = fakeArr[i]
+        result = fn.call(fakeArr, item, i)
+        if (result === false) {
+            break
+        }
+    }
+}
+
+// 获取随机数
+export function getRandom(prefix) {
+    return prefix + Math.random().toString().slice(2)
+}
+
+// 替换 html 特殊字符
+export function replaceHtmlSymbol(html) {
+    if (html == null) {
+        return ''
+    }
+    return html.replace(/</gm, '&lt;')
+                .replace(/>/gm, '&gt;')
+                .replace(/"/gm, '&quot;')
+                .replace(/(\r\n|\r|\n)/g, '<br/>')
+}
+
+// 返回百分比的格式
+export function percentFormat(number) {
+    number = (parseInt(number * 100))
+    return number + '%'
+}
+
+// 判断是不是 function
+export function isFunction(fn) {
+    return typeof fn === 'function'
+}
+
+export function getParentNodeByClass (enode, className) {
+  if (!enode) {
+    return null;
+  }
+  if (enode.nodeType === 1 && enode.tagName.toLowerCase() === 'body') {
+    return null;
+  }
+  let nodeClass = enode.className;
+  if (nodeClass && nodeClass.indexOf(className) > -1) {
+    return enode
+  } else {
+    return getParentNodeByClass(enode.parentNode, className)
+  }
+}
+export function getParentByClassname($el, classname) {
+    if (!$el) {
+      return null;
+    }
+    if ($el.getNodeName().toLowerCase() === 'body' ) {
+      return null
+    }
+    let eclassname = $el.attr('class')
+    if (eclassname && eclassname.indexOf(classname) > -1) {
+      return $el
+    } else {
+      return getParentByClassname($el.parent(), classname)
+    }
+  }
+
+  export function trim(str) {
+    str = str.replace(/[ | ]*\n/g,'\n'); //去除行尾空白
+    str=str.replace(/ /ig,'');//去掉 
+    str=str.replace(/(^\s*)|(\s*$)/g,'');//去掉 
+    return str;
+  }
+
+  export function debounce (func,wait,immediate = false) {
+    // immediate 默认为false
+    let timeout = null; let args = null; let context = null; let timestamp; let result = null;
+  
+    let later = () => {
+      let last = new Date().getTime() - timestamp;
+      if( last < wait && last > 0 ){
+        timeout = setTimeout(later, wait - last);
+      }else{
+        timeout = null;
+        if (!immediate) {
+          result = func.apply(context, args);
+          if (!timeout) {
+            context = args = null;
+          }
+        }
+      }
+    };
+  
+    return function() {
+      context = this;
+      args = arguments;
+      timestamp = new Date().getTime();
+      // 第一次调用该方法时,且immediate为true,则调用func函数
+      let callNow = immediate && !timeout;
+      // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数
+      if (!timeout) timeout = setTimeout(later, wait);
+      if (callNow) {
+        result = func.apply(context, args);
+        context = args = null;
+      }
+      return result;
+    };
+  
+  }
+  
+  export function throttle (func, wait, options) {
+    /* options的默认值
+       *  表示首次调用返回值方法时,会马上调用func;否则仅会记录当前时刻,当第二次调用的时间间隔超过wait时,才调用func。
+       *  options.leading = true;
+       * 表示当调用方法时,未到达wait指定的时间间隔,则启动计时器延迟调用func函数,若后续在既未达到wait指定的时间间隔和func函数又未被调用的情况下调用返回值方法,则被调用请求将被丢弃。
+       *  options.trailing = true;
+       * 注意:当options.trailing = false时,效果与上面的简单实现效果相同
+       */
+    let context, args, result;
+    let timeout = null;
+    let previous = 0;
+    if (!options) {
+      options = {};
+    }
+    let later = function() {
+      previous = options.leading === false ? 0 : new Date().getTime();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) {
+        context = args = null;
+      }
+    };
+    return function() {
+      let now = new Date().getTime();
+      if (!previous && options.leading === false) {
+        previous = now;
+      }
+      // 计算剩余时间
+      let remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      // 当到达wait指定的时间间隔,则调用func函数
+      // 精彩之处:按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数。
+      if (remaining <= 0 || remaining > wait) {
+        // 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作
+        if (timeout) {
+          clearTimeout(timeout);
+          timeout = null;
+        }
+        previous = now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        // options.trailing=true时,延时执行func函数
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+  }
+
+export function throttleSimple(fn, delay){
+ 	let timer = null;
+ 	return function(){
+ 		let context = this, args = arguments;
+ 		clearTimeout(timer);
+ 		timer = setTimeout(function(){
+ 			fn.apply(context, args);
+ 		}, delay);
+ 	};
+ };
+ 
+ export function uploadToAliyun (file, policyUrl) {
+  return new Promise((resolve, reject) => {
+    let Api = new Ajax();
+    // 1、获取Policy
+    Api.post(policyUrl, []).then(data => {
+      let responce = data;
+      if (responce.result === '00') {
+        let resData = responce.data;
+        let formdata = new  window.FormData();
+        formdata.append('key', resData.dir + resData.filename.substring(0,resData.filename.lastIndexOf("/")) + file.name);
+        formdata.append('policy', resData.policy);
+        formdata.append('OSSAccessKeyId', resData.accessid);
+        formdata.append('callback', resData.callback);
+        formdata.append('signature', resData.signature);
+        formdata.append('file', file);
+        Api.post(resData.host, formdata, 'formdata').then(data => {
+          resolve(data);
+        }).catch(err => {
+          reject(err);
+        })
+      } else {
+        reject('policy获取失败!')
+      }
+    }).catch(err => {
+      reject(err);
+    })
+  })
+}
+
+/**
+ * function 判断是否为空的元素
+ * @param {Node}  node
+ */
+export function isEmptyElement (enode) {
+  if (enode.nodeType === 3) {
+    return !enode.nodeValue.length;
+  } else {
+    if (!enode.tagName || enode.tagName.toLowerCase() === 'br') {
+      return true;
+    } else if (!enode.childNodes || enode.childNodes.length < 1) {
+      return true;
+    } else {
+      let html = enode.innerHTML.toLowerCase();
+      if (html.length < 1 || html === '<br>' || html === '<br/>' || html === '') {
+        return true;
+      } else {
+        return false;
+      }
+    }
+  }
+}

File diff suppressed because it is too large
+ 1035 - 0
src/js/yzPage.js


File diff suppressed because it is too large
+ 1644 - 0
src/js/yzPageManager.js


+ 130 - 0
src/js/yzWebeditor.js

@@ -0,0 +1,130 @@
+import '../less/scroller.less';
+import '../less/common.less';
+import '../less/icon.less';
+import '../less/main.less';
+
+import $ from './util/dom-core.js';
+import _config from '../config.js';
+import Command from './command/index.js';
+import Menus from './menus/index.js';
+import Message from './util/message.js';
+import selectionAPI from './selection/index.js';
+import { arrForEach, objForEach } from './util/util.js';
+import { getRandom } from './util/util.js';
+
+import yzPageManager from './yzPageManager';
+
+ 
+let yzWebeditor = function (cfg) {
+  this.$container = $(cfg.dom) || $(document.body);
+  this.$editorArea = $('<div class="lsiten-editor-view"></div>')
+  this.data = cfg.data || {}; // 数据源
+  this.page = null;
+  this.pageSize = this.data.paperSize || 'A3';
+  this.columnNumber = parseInt(this.data.layoutType) || 2;
+  this.noMode = parseInt(this.data.noMode) || 2;
+  this.noCount = parseInt(this.data.noCount) || 12;
+  this.layoutMode = parseInt(this.data.layoutMode) || 1;
+
+  this.cardHtml = this.data.cardHtml || null;
+  this.cardHtml && (this.cardHtml = JSON.parse(this.cardHtml));
+
+  this.editorHeight = 800;
+  this.customConfig = cfg.config || {};
+  this.id = ('lsiten_editor_'+Math.random()).replace('.','_');
+  this.isDoObject = false;
+  this._init();
+}
+
+yzWebeditor.prototype = {
+  constructor: yzWebeditor,
+  // 初始化页面
+  _init: function () {
+    this.$container.addClass('lsiten-container');
+    this._initConfig();
+    this._initEditStyle();
+    this._initCommand();
+    this._initSelectionAPI();
+    this._initToolbar();
+    this._addPage();
+    this.$container.append(this.$toolbar);
+    this.$container.append(this.$editorArea);
+    this.initSelection(true);
+    this._initMessage();
+    this._bindEvent();
+  },
+  _initToolbar: function() {
+    this.$toolbar = $('<div class="lsiten-editor-toolbar"></div>');
+    this.menus = new Menus(this);
+    this.menus.init();
+  },
+  _initMessage: function () {
+    this.message = new Message(this, {});
+  },
+  _initEditStyle: function() {
+    this.$editorArea.css('height', this.editorHeight + 'px');
+  },
+  // 初始化配置文
+  _initConfig: function () {
+     // _config 是默认配置,this.customConfig 是用户自定义配置,将它们 merge 之后再赋值
+     let target = {};
+     this.config = Object.assign(target, _config, this.customConfig);
+
+     // 将语言配置,生成正则表达式
+     const langConfig = this.config.lang || {};
+     const langArgs = [];
+     objForEach(langConfig, (key, val) => {
+         // key 即需要生成正则表达式的规则,如“插入链接”
+         // val 即需要被替换成的语言,如“insert link”
+         langArgs.push({
+             reg: new RegExp(key, 'img'),
+             val: val
+         })
+     });
+     this.config.langArgs = langArgs;
+  },
+  // 添加页面 
+  _addPage: function() {
+    this.page = new yzPageManager(this);
+    this.$textElem = this.page.$textElem;
+    this.$editorArea.append(this.page.$el);
+  },
+  // 封装 command API
+  _initCommand: function () {
+    this.cmd = new Command(this);
+  },
+  // 封装 selection range API
+  _initSelectionAPI: function () {
+    this.selection = new selectionAPI(this);
+  },
+  // 初始化选区,将光标定位到内容尾部
+  // @todo
+  initSelection: function (newLine) {
+    const currentPage = this.page.currentPage;
+    if (currentPage) {
+      const $currentColumn = currentPage.$colums[currentPage.currentColumn];
+      const $children = $currentColumn.children();
+      if (!$children.length && !$currentColumn.html()) {
+          // 如果编辑器区域无内容,添加一个空行,重新设置选区
+          currentPage.addParagraph();
+          this.initSelection();
+          return ;
+      }
+  
+      const $last = $children.last();
+      this.selection.createRangeByElem($last.children(0), false, true);
+      if (this.selection.getRange()) {
+        this.selection.restoreSelection();
+      }
+      this.selection.saveRange();
+    }
+  },
+  // 绑定事件
+  _bindEvent: function () {
+  },
+  // 解绑所有事件(暂时不对外开放)
+  _offAllEvent: function () {
+    $.offAll();
+  }
+}
+export default yzWebeditor;

+ 19 - 0
src/less/common.less

@@ -0,0 +1,19 @@
+.lsiten-editor-toolbar,
+.lsiten-container,
+.lsiten-e-menu-panel {
+    padding: 0;
+    margin: 0;
+    box-sizing: border-box;
+
+    * {
+        padding: 0;
+        margin: 0;
+        box-sizing: border-box;
+    }
+}
+
+.lsiten-e-clear-fix:after {
+    content: "";
+    display: table;
+    clear: both;
+}

+ 78 - 0
src/less/droplist.less

@@ -0,0 +1,78 @@
+.lsiten-editor-toolbar {
+    .lsiten-e-droplist {
+        position: absolute;
+        left: 0;
+        top: 0;
+        background-color: #fff;
+        border-radius: 2px;
+        box-shadow: 0 0 4px 0 rgba(0,0,0,.1);
+        border: 1px solid #e0e1e5;
+
+        .lsiten-e-dp-title {
+            text-align: center;
+            color: #999;
+            line-height: 2;
+            border-bottom: 1px solid #f1f1f1;
+            font-size: 13px;
+        }
+
+        ul.lsiten-e-list {
+            list-style: none;
+            line-height: 1;
+
+            li.lsiten-e-item {
+                color: #333;
+                background-color: #ffffff;
+                font-size: 12px;
+                line-height: 30px;
+                height: 30px;
+                white-space: nowrap;
+                padding: 8px 0;
+                border-bottom: 1px solid #e0e1e5;
+                box-sizing: content-box;
+                cursor: pointer;
+                &:hover {
+                    background-color: #f1f1f1;
+                }
+                p {
+                  font-size: 14px;
+                }
+                h1 {
+                  font-size: 32px;
+                  font-weight: normal;                  
+                }
+                h2 {
+                  font-weight: normal;
+                  font-size: 28px;
+                }
+                h3 {
+                  font-weight: normal;
+                  font-size: 20px;
+                }
+                h4 {
+                  font-weight: normal;
+                  font-size: 18px;
+                }
+                h5 {
+                  font-weight: normal;
+                  font-size: 14px; 
+                }
+            }
+        }
+        
+        ul.lsiten-e-block {
+            list-style: none;
+            text-align: left;
+            padding: 5px;
+
+            li.lsiten-e-item {
+                display: inline-block;
+                padding: 3px 5px;
+
+                &:hover {
+                    background-color: #f1f1f1;
+                }
+            }
+        }
+    }   
+}

+ 108 - 0
src/less/icon.less

@@ -0,0 +1,108 @@
+@font-face {
+  font-family: 'lsiten-e-icon';
+  src:  url('../fonts/lsiten-e-icon.woff?ddq1c7')  format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+
+[class^="lsiten-e-icon-"], [class*=" lsiten-e-icon-"] {
+  /* use !important to prevent issues with browser extensions that change fonts */
+  font-family: 'lsiten-e-icon' !important;
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.lsiten-e-icon-close:before {
+  content: "\f00d";
+}
+.lsiten-e-icon-upload2:before {
+  content: "\e9c6";
+}
+.lsiten-e-icon-trash-o:before {
+  content: "\f014";
+}
+.lsiten-e-icon-header:before {
+  content: "\f1dc";
+}
+.lsiten-e-icon-pencil2:before {
+  content: "\e906";
+}
+.lsiten-e-icon-paint-brush:before {
+  content: "\f1fc";
+}
+.lsiten-e-icon-image:before {
+  content: "\e90d";
+}
+.lsiten-e-icon-play:before {
+  content: "\e912";
+}
+.lsiten-e-icon-location:before {
+  content: "\e947";
+}
+.lsiten-e-icon-undo:before {
+  content: "\e965";
+}
+.lsiten-e-icon-redo:before {
+  content: "\e966";
+}
+.lsiten-e-icon-quotes-left:before {
+  content: "\e977";
+}
+.lsiten-e-icon-list-numbered:before {
+  content: "\e9b9";
+}
+.lsiten-e-icon-list2:before {
+  content: "\e9bb";
+}
+.lsiten-e-icon-link:before {
+  content: "\e9cb";
+}
+.lsiten-e-icon-happy:before {
+  content: "\e9df";
+}
+.lsiten-e-icon-bold:before {
+  content: "\ea62";
+}
+.lsiten-e-icon-underline:before {
+  content: "\ea63";
+}
+.lsiten-e-icon-italic:before {
+  content: "\ea64";
+}
+.lsiten-e-icon-strikethrough:before {
+  content: "\ea65";
+}
+.lsiten-e-icon-table2:before {
+  content: "\ea71";
+}
+.lsiten-e-icon-paragraph-left:before {
+  content: "\ea77";
+}
+.lsiten-e-icon-paragraph-center:before {
+  content: "\ea78";
+}
+.lsiten-e-icon-paragraph-right:before {
+  content: "\ea79";
+}
+.lsiten-e-icon-terminal:before {
+  content: "\f120";
+}
+.lsiten-e-icon-page-break:before {
+  content: "\ea68";
+}
+.lsiten-e-icon-cancel-circle:before {
+  content: "\ea0d";
+}
+.lsiten-e-icon-font:before {
+  content: "\ea5c";
+}
+.lsiten-e-icon-text-heigh:before {
+  content: "\ea5f";
+}

+ 145 - 0
src/less/main.less

@@ -0,0 +1,145 @@
+::-webkit-scrollbar {
+  height: 8px;
+  width: 8px;
+  background: transparent;
+  border-radius: 4px;
+}
+::-webkit-scrollbar-button {
+  display: none;
+}
+::-webkit-scrollbar-thumb {
+  width: 8px;
+  min-height: 15px;
+  background: #c1c1c1;
+  border-radius: 4px;
+}
+::-webkit-scrollbar-track {
+  background-color: transparent;
+}
+::-webkit-scrollbar-track-piece {
+  background: transparent;
+}
+::selection {
+  background: #accef7;
+}
+.lsiten-container {
+  .lsiten-editor-toolbar {
+    height: 40px;
+    background: #ffffff;
+    border-bottom: 1px #bfbfbfbf solid;  
+    box-shadow: 0 3px 7px rgba(0,0,0,.1);
+    * {
+      width: auto;
+      height: auto;
+    }
+  }
+  .lsiten-editor-view {
+    overflow-x: scroll;
+    overflow-y: scroll;
+    position: relative;
+    * {
+      // box-sizing: content-box;
+      height: auto;
+      width: 100%;
+    }
+    // .page-break {
+    //   position: absolute;
+    //   text-align: center;
+    //   font-size: 1px;
+    //   z-index: 10;
+    //   left: 0;
+    //   width: 100%;
+    //   .spanMark {
+    //     width: 100%;
+    //     display: inline-block;
+    //   }
+    //   .topSpan, .bottomSpan{
+    //     display: block;
+    //   }
+    //   .middleSpan {
+    //     display: block;
+    //     background-color: #f6f6f6;
+    //     box-shadow: inset 0 7px 7px -5px rgba(0,0,0,.1), inset 0 -5px 5px -7px rgba(0,0,0,.1), 7px 0 7px #f6f6f6, -7px 0 7px #f6f6f6;
+    //   }
+    // }
+    // * {
+    //   box-sizing: content-box;
+    //   height: auto;
+    // }
+    // .lsiten-page-view {
+    //   background: #ffffff;
+    //   position: relative;
+    //   min-width: 0;
+    //   margin: 0 auto;
+    //   box-shadow: 0 2px 7px rgba(0,0,0,.1);
+    //   overflow: hidden;
+    //   .lsiten-editor-page {
+    //     width: 100%;
+    //   }
+    //   .lsiten-page-column {
+    //     float: left;
+    //   }
+    //   .header-footer-view {
+    //     position: absolute;
+    //     left: 0;
+    //     .footer-input {
+    //       width: 100%;
+    //       line-height: 30px;
+    //       outline: none;
+    //       color: #aaa;
+    //       font-size: 13px;
+    //       border: 1px dashed transparent;
+    //       white-space: nowrap;
+    //       overflow: hidden;
+    //       box-sizing: border-box;
+    //       text-align: center;
+    //       &:hover {
+    //         border-color: #ccc;
+    //       }
+    //       &:focus {
+    //         color: #666;
+    //         border-color: #666;
+    //       }
+    //     }
+    //   }
+    //   .page-header {
+    //     left: 90px;
+    //     right: 90px;
+    //     width: auto;
+    //     top: 30px;
+    //   }
+    //   .page-footer {
+    //     bottom: 30px;
+    //     left: 90px;
+    //     right: 90px;
+    //     width: auto;
+    //   }
+    //   .lsiten_editor_text {
+    //     min-height: 100%;
+    //     height: auto;
+    //     padding: 70px!important;
+    //     box-sizing: border-box;
+    //     min-width: 100%;
+    //     display: inline-block;
+    //     outline: none;
+    //     word-wrap: break-word;
+    //     -webkit-line-break: after-white-space;
+    //     .block-view {
+    //       margin: 0;
+    //       padding: 0 20px;
+    //       outline: none;
+    //       box-sizing: border-box;
+    //       text-align: left;
+    //       white-space: pre-wrap;
+    //       line-height: 1.75;
+    //     }
+    //   }
+    // }
+    // .lsiten-page-A4 {
+    //   width: 802px;
+    // }
+    // .lsiten-page-A3 {
+    //   width: 1464px;
+    // }
+  }
+}

+ 36 - 0
src/less/menus.less

@@ -0,0 +1,36 @@
+.lsiten-editor-toolbar {
+    display: flex;
+    padding: 0 5px;
+    /* flex-wrap: wrap; */
+    .pull-right {
+     right: 70px;
+     position:absolute !important;
+    }
+    /* 单个菜单 */
+    .lsiten-e-menu {
+        position: relative;
+        text-align: center;
+        padding: 5px 10px;
+        cursor: pointer;
+        
+        i {
+            color: #999;
+        }
+
+        &:hover {
+            i {
+                color: #333;
+            }
+        }
+    }
+    .lsiten-e-active {
+        i {
+            color: #1e88e5;
+        }
+        &:hover {
+            i {
+                color: #1e88e5;
+            }
+        }
+    }
+}

+ 84 - 0
src/less/message.less

@@ -0,0 +1,84 @@
+.lsiten-e-message {
+  .lsiten-icon-close {
+    background: url('../image/icon.png') no-repeat;
+  }
+  .lsiten-e-message-body-content {
+    position: relative;
+    width: 100%;
+    height: auto;
+    padding: 20px;
+    line-height: 24px;
+    word-break: break-all;
+    overflow: hidden;
+    font-size: 14px;
+    overflow-x: hidden;
+    overflow-y: auto;
+  }
+  .lsiten-e-message-button {
+    text-align: right;
+    padding: 0 15px 12px;
+    pointer-events: auto;
+    user-select: none;
+    -webkit-user-select: none;
+    width: 100%;
+    height: auto;
+    .lsiten-message-button {
+      height: 28px;
+      width: auto;
+      line-height: 28px;
+      margin: 5px 5px 0;
+      padding: 0 15px;
+      border: 1px solid #dedede;
+      background-color: #fff;
+      color: #333;
+      border-radius: 2px;
+      font-weight: 400;
+      cursor: pointer;
+      text-decoration: none;
+      box-sizing: content-box;
+      display: inline-block;
+      vertical-align: top;
+      font-size: 14px;
+    }
+    .lsiten-message-cancel-button {
+      border-color: #F8F8F8;
+      background-color: #F8F8F8;
+      color: #333;
+    }
+    .lsiten-message-success-button {
+      border-color: #1E9FFF;
+      background-color: #1E9FFF;
+      color: #fff;
+    }
+  }
+  .form {
+    .form-item {
+      display: flex;
+      align-items: baseline;
+      .label{
+        min-width: 60px;
+        width: auto;
+        text-align: right;
+        padding-right: 5px;
+      }
+      input[type=text] {
+        flex: 1;
+        border: none;
+        border-bottom: 1px solid #ccc;
+        font-size: 14px;
+        height: 20px;
+        color: #333;
+        text-align: left;
+        outline: none;
+        &:focus {
+          border-bottom: 2px solid #1e88e5;
+        }
+      }
+      .block {
+        display: block;
+        width: 100%;
+        margin: 10px 0;
+      }
+    }
+  }
+}

+ 164 - 0
src/less/panel.less

@@ -0,0 +1,164 @@
+.lsiten-container {
+    .lsiten-e-panel-container {
+        box-sizing: border-box;
+        position: absolute;
+        top: 0;
+        left: 50%;
+        border: 1px solid #ccc;
+        border-top: 0;
+        box-shadow: 1px 1px 2px #ccc;
+        color: #333;
+        background-color: #fff;
+        * {
+          width: auto;
+          height: auto;
+        }
+        .lsiten-e-panel-close {
+            position: absolute;
+            right: 0;
+            top: 0;
+            padding: 5px;
+            margin: 2px 5px 0 0;
+            cursor: pointer;
+            color: #999;
+
+            &:hover {
+                color: #333;
+            }
+        }
+
+        .lsiten-e-panel-tab-title {
+            list-style: none;
+            display: flex;
+            font-size: 14px;
+            margin: 2px 10px 0 10px;
+            border-bottom: 1px solid #f1f1f1;
+
+            .lsiten-e-item {
+                padding: 3px 5px;
+                color: #999;
+                cursor: pointer;
+                margin: 0 3px;
+                position: relative;
+                background: #ffffff;
+                top: 1px;
+            }
+
+            .lsiten-e-active {
+                color: #333;
+                border-bottom: 1px solid #333;
+                cursor: default;
+                font-weight: 700;
+            }
+        }
+
+        .lsiten-e-panel-tab-content {
+            padding: 10px 15px 10px 15px;
+            font-size: 16px;
+
+            /* 输入框的样式 */
+            input,textarea,button {
+                &:focus {
+                    outline: none;
+                }   
+            }
+            textarea {
+                width: 100%;
+                border: 1px solid #ccc;
+                padding: 5px;
+
+                &:focus {
+                    border-color: #1e88e5;
+                }
+            }
+            input[type=text] {
+                border: none;
+                border-bottom: 1px solid #ccc;
+                font-size: 14px;
+                height: 20px;
+                color: #333;
+                text-align: left;
+            }
+            input[type=text].small {
+                width: 30px;
+                text-align: center;
+            }
+
+            input[type=text].block {
+                display: block;
+                width: 100%;
+                margin: 10px 0;
+            }
+
+            input[type=text]:focus {
+                border-bottom: 2px solid #1e88e5;
+            }
+
+            /* 按钮的样式 */
+            .lsiten-e-button-container {
+                button {
+                    font-size: 14px;
+                    color: #1e88e5;
+                    border: none;
+                    padding: 5px 10px;
+                    background-color: #fff;
+                    cursor: pointer;
+                    border-radius: 3px;
+                }
+                button.left {
+                    float: left;
+                    margin-right: 10px;
+                }
+
+                button.right {
+                    float: right;
+                    margin-left: 10px;
+                }
+
+                button.gray {
+                    color: #999;
+                }
+
+                button.red {
+                    color: rgb(194, 79, 74);
+                }
+
+                button:hover {
+                    background-color: #f1f1f1;
+                }
+            } 
+            .lsiten-e-button-container:after {
+                content: "";
+                display: table;
+                clear: both;
+            }
+        }
+
+        /* 为 emotion panel 定制的样式 */
+        .lsiten-e-emoticon-container {
+            .lsiten-e-item {
+                cursor: pointer;
+                font-size: 18px;
+                padding: 0 3px;
+                display: inline-block;
+            }
+        }
+
+        /* 上传图片的 panel 定制样式 */
+        .lsiten-e-up-img-container {
+            text-align: center;
+
+            .lsiten-e-up-btn {
+                display: inline-block;
+                color: #999;
+                cursor: pointer;
+                font-size: 60px;
+                line-height: 1;
+
+                &:hover {
+                    color: #333;
+                }
+            }
+        }
+    }
+}

+ 25 - 0
src/less/scroller.less

@@ -0,0 +1,25 @@
+::-webkit-scrollbar {
+  height: 8px;
+  width: 8px;
+  background: transparent;
+  border-radius: 4px;
+}
+::-webkit-scrollbar-button {
+  display: none;
+}
+::-webkit-scrollbar-thumb {
+  width: 8px;
+  min-height: 15px;
+  background: #c1c1c1;
+  border-radius: 4px;
+  display: block;
+}
+::-webkit-scrollbar-track {
+  background-color: transparent;
+}
+::-webkit-scrollbar-track-piece {
+  background: transparent;
+}
+::selection {
+  background: #accef7;
+}

+ 9 - 0
static/js/serverWorker.js

@@ -0,0 +1,9 @@
+window.addEventListener("load", function() {
+  console.log("Will the service worker register?");
+  navigator.serviceWorker.register('/static/js/sw-3.js')
+  .then(function(reg){
+      console.log("Yes, it did.");
+  }).catch(function(err) {
+      console.log("No it didn't. This happened: ", err)
+  }); 
+});

+ 144 - 0
static/js/sw-3.js

@@ -0,0 +1,144 @@
+let pageUpdateTime = {};
+const CACHE_NAME = "fed-cache";
+let util = {
+  getPageName: function(url) {
+    if (url.pathname === "/" || !url.pathname) {
+      return "home"
+    } else {
+      let matches = url.pathname.match(/\/([^/] + )\/$/ );
+      if (!matches) console.log("get page name come across url: " + url);
+      return matches ? matches[1] : ""
+    }
+  },
+  isHtmlPage: function(url) {
+    return url.pathname === "/" || /^\/page\/\d+/.test(url.pathname) || /^\/\d{4}\/\d{2}\/\d{2}/.test(url.pathname)
+  },
+  updateHtmlPage: async function(url, htmlRequest) {
+    let pageName = util.getPageName(url);
+    let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json");
+    fetch(jsonRequest).then(response => {
+      if (response.status !== 200) {
+        console.warn(`$ {
+            response.status
+        }: fetch $ {
+            pageName
+        }.sw.json failed`);
+        return;
+      }
+      response.json().then(content => {
+        if (pageUpdateTime[pageName] !== content.updateTime) {
+          console.log("update page html");
+          util.fetchPut(htmlRequest, false,
+          function() {
+            util.postMessage({
+                type: 1,
+                desc: "html found updated",
+                url: url.href
+            })
+          });
+          pageUpdateTime[pageName] = content.updateTime
+        }
+      })
+    })
+  },
+  updateHtmlTime: function(url) {
+    let pageName = util.getPageName(url);
+    let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json");
+    fetch(jsonRequest).then(response => {
+      response.json().then(content => {
+        pageUpdateTime[pageName] = content.updateTime
+      })
+    })
+  },
+  putCache: function(request, resource) {
+    if (request.method === "GET" && request.url.indexOf("wp-admin") < 0 && request.url.indexOf("preview_id") < 0 && request.url.indexOf("test=true") < 0 && request.url.indexOf("/category/") < 0 && request.url.indexOf("/author/") < 0 && request.url.indexOf("/html/") < 0) {
+      caches.open(CACHE_NAME).then(cache => {
+      cache.put(request.url, resource)
+      })
+    }
+  },
+  fetchPut: function(request, putCache = true, callback) {
+    return fetch(request).then(response => {
+      if (!response || response.status !== 200 || response.type !== "basic") {
+        return response
+      }
+      util.putCache(request, response.clone());
+      typeof callback === "function" && callback();
+      return response
+    })
+  },
+  delCache: function(url) {
+    caches.open(CACHE_NAME).then(cache => {
+      console.log("delete cache " + url);
+      cache.delete(url, {
+        ignoreVary: true
+      })
+    })
+  },
+  postMessage: async function(msg) {
+    const allClients = await clients.matchAll();
+    allClients.forEach(client => client.postMessage(msg))
+  }
+};
+const messageProcess = {
+  1 : function(url) {
+      util.delCache(url)
+  }
+};
+this.addEventListener("install", function(event) {
+  this.skipWaiting();
+  console.log("install service worker");
+  caches.open(CACHE_NAME);
+  let cacheResources = ["https://fed.renren.com/?launcher=true"];
+  event.waitUntil(caches.open(CACHE_NAME).then(cache => {
+    cache.addAll(cacheResources)
+  }))
+});
+this.addEventListener("active", function(event) {
+  clients.claims();
+  console.log("service worker is active")
+});
+this.addEventListener("fetch", function(event) {
+  event.respondWith(caches.match(event.request).then(response => {
+    if (response) {
+      if (response.headers.get("Content-Type").indexOf("text/html") >= 0) {
+        console.log("update html");
+        let url = new URL(event.request.url);
+        util.updateHtmlPage(url, event.request.clone(), event.clientId)
+      }
+      return response
+    }
+    let url = new URL(event.request.url);
+    if (util.isHtmlPage(url)) {
+      console.log("update html time");
+      util.updateHtmlTime(url)
+    }
+    let request = event.request.url.indexOf("http://fed.renren.com") >= 0 ? new Request(event.request.url.replace("http://", "https://")) : event.request.clone();
+    return util.fetchPut(request)
+  }))
+});
+this.addEventListener("message", function(event) {
+  let msg = event.data;
+  console.log(msg);
+  if (typeof messageProcess[msg.type] === "function") {
+    messageProcess[msg.type](msg.url)
+  }
+});
+this.addEventListener("push", function(event) {
+  console.log("[Service Worker] Push Received.");
+  console.log(` [Service Worker] Push had this data: "${event.data.text()}"`);
+  let notificationData = event.data.json();
+  notificationData.requireInteraction = true;
+  const title = notificationData.title;
+  event.waitUntil(self.registration.showNotification(title, notificationData))
+});
+this.addEventListener("notificationclick", function(event) {
+  console.log("[Service Worker] Notification click Received.");
+  let notification = event.notification;
+  notification.close();
+  event.waitUntil(clients.openWindow(notification.data.url))
+});
+this.addEventListener("notificationclose", function(event) {
+  console.log("notification close");
+  console.log(JSON.stringify(event.notification))
+});

+ 98 - 0
webpack.config.js

@@ -0,0 +1,98 @@
+const path = require('path');
+const htmlPlugin= require('html-webpack-plugin');
+const PurifyCSSPlugin = require("purifycss-webpack");
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
+const glob = require('glob');
+var website ={
+    publicPath:"http://localhost:1717/"
+}
+module.exports={
+    //入口文件的配置项
+    entry:{
+      entry:'./src/index.js',
+      editor: './src/js/yzWebeditor'
+    },
+    //出口文件的配置项
+    output:{
+      //打包的路径文职
+      path:path.resolve(__dirname,'dist'),
+      //打包的文件名称
+      filename:'[name]-bundle.js',
+      publicPath:website.publicPath
+    },
+    node: {
+      fs: "empty"
+    },
+    //模块:例如解读CSS,图片如何转换,压缩
+    module:{
+      rules: [
+        {
+          test: /\.css/,
+          exclude:/node_modules/,
+          use: ["style-loader","css-loader", "postcss-loader"],
+        },{
+          test: /\.less$/,
+          use: [
+            'style-loader',
+            { loader: 'css-loader', options: { importLoaders: 1 } },
+            'postcss-loader',
+            'less-loader'
+          ]
+        },{
+          test:/\.(png|jpg|gif)/ ,
+          use:[{
+              loader:'url-loader',
+              options:{
+                  limit:5000,
+                  outputPath:'images/',
+              }
+          }]
+       },{
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000
+        }
+      },{
+          test: /\.(htm|html)$/i,
+          use:[ 'html-withimg-loader'] 
+        },{
+          test:/\.(jsx|js)$/,
+          use:{
+              loader:'babel-loader'
+          },
+          exclude:/node_modules/
+        }
+      ]
+    },
+    //插件,用于生产模版和各项功能
+    plugins:[
+      new htmlPlugin({
+        minify:{
+            removeAttributeQuotes:true
+        },
+        hash:true,
+        template:'./src/index.html'
+      }),
+      new UglifyJsPlugin({
+          uglifyOptions: {
+            ie8: true
+          }
+      }),
+      new PurifyCSSPlugin({
+        // Give paths to parse for rules. These should be absolute!
+        paths: glob.sync(path.join(__dirname, 'src/*.html')),
+      })
+    ],
+    //配置webpack开发服务功能
+    devServer:{
+      //设置基本目录结构
+      contentBase:path.resolve(__dirname,'dist'),
+      //服务器的IP地址,可以使用IP也可以使用localhost
+      host:'localhost',
+      //服务端压缩是否开启
+      compress:true,
+      //配置服务端口号
+      port:1717
+    }
+}