十分钟打造一款在线的数学公式编辑器

最近,一个朋友要求做一个数学编辑器,方便数学公式的录入,特别是微积分、矩阵等公式,普通录入非常麻烦,这里,花了一周时间,做了一个数学公式在线编辑功能。

下面记录一下打造的过程。但是,目前很遗憾,这个系统还不支持导入导出功能。

如何实现web录入的试题导出到word或者把word试题导入到系统,如果您有好的方法,欢迎推荐。(感觉要自己写解析Latex)

在线体验  http://demo.dotnetcms.org/math  免费下载  https://files.cnblogs.com/files/mqingqing123/math5.0.rar

1.MathJax

在数学公式里,最流行的是 http://www.mathjax.org ,Mathjax支持数理化等各种公式,其实如果你希望只针对数学录入,可以使用 https://katex.org/ KaTex更简单、速度更快。

Mathjax的文档里列出了MathJax目前支持的LaTex语法。对于未实现的语法,可以自定义宏来实现。

从声明里看到实现了 sin,cos,tan,ctan等都支持,但是一些反正切没实现。

所以,在MathJax的全局配置里,定义一个macros

复制代码
    <script>
        MathJax = {
            options: {
                enableMenu: false,
                a11y: {
                speech: false,                      // switch on speech output
                braille: false,                     // switch on Braille output
                subtitles: false
               }
        },

            tex: {
                inlineMath: [['@', '@'], ['\(', '\)']],
                displayMath: [['@@', '@@'], ['\[', '\]']],
                macros: {
                    arcsec: '\DeclareMathOperator{\arcsec}{arcsec}\arcsec',
                    arccsc: '\DeclareMathOperator{\arccsc}{arccsc}\arccsc',
                    arccot: '\DeclareMathOperator{\arccot}{arccot}\arccot'
                }
            }
        }
</script>
复制代码

然后引入Mathjax库

1
<script src="../js/math/tex-chtml-full.js"></script>

  

另外,对于数学公式的“开始”和“结束”,MathJax默认使用""""和" "作为分割的,

如果是块状的则使用"\["和"\]"区分,

参考下图,左边是录入的内容,右边是显示的结果。

但是Mathjax允许你自定义公式识别符,

上面代码,我增加了“@”作为行内公式,使用"@@"作为块公式。

其实,在选型时,作者测试了“$”或者“#”作为分隔符,但是最终确定使用@符号,最根本的原因是:

在录入时,只有@符号,在中英模式下是一样的。

现在老师可以像写文本一样,写题目了。

2.引入CodeMirror

在录入页面,引入Codemirror美化录入界面。

毕竟,textarea默认太丑了。

1
2
<link href="../js/codeMirror/lib/codemirror.css" rel="stylesheet" />
<script src="../js/codeMirror/lib/codemirror.js"></script>

  

初始化文本框,整个布局分左右布局,

左边是文本框textarea进入录入,右边是iframe进行预览,

在父div里,设置display为flex,进行左右布局,这样就不用 float 飞来飞去的了。

1
 

<div style="display:flex">
<div style="50%">
<textarea id="txt_question"></textarea>
</div>


<div style="50%; background-color:#f2f2f2">

<iframe id=preview frameborder="0"
width="100%"
scrolling="no" >
</iframe>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<br>    <br><script>
 
        var delay;
        var editor = CodeMirror.fromTextArea(document.getElementById('txt_question'), {
            lineNumbers: true,
            mode: 'text/html',
            lineWrapping:true
        });
 
 
        editor.on("change", function () {
            clearTimeout(delay);
            delay = setTimeout(updatePreview, 500);
        });
 
 
 
        function updatePreview() {
            var iframe = document.getElementById('preview');
            var doc2 = iframe.contentDocument || iframe.contentWindow.document;
            let body2 = doc2.getElementsByTagName('body')[0];
            var data = editor.getValue().replace(/ /g, "<br>");
            body2.innerHTML = "<div class=mathjax-qmx>" + data + "</div> ";
            if(doc2.defaultView.MathJax!=null)
            {
                doc2.defaultView.MathJax.typeset();
            }
        }
 
        setTimeout(updatePreview, 500);
 
    </script>

  

在预览时,需要通过JS引入Mathjax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
 
      $(document).ready(function () {
          let iframe = document.getElementById("preview");
          let iframeWindow = iframe.contentWindow || iframe.contentDocument.document || iframe.contentDocument;
          let doc3 = iframeWindow.document;
 
          let head3 = doc3.getElementsByTagName('head')[0];
          let body3 = doc3.getElementsByTagName('body')[0]; 
 
      
          let js1 = doc3.createElement('script');
          js1.src = "../js/math/math-config.js";
          js1.type = 'text/javascript'
          head3.appendChild(js1);
          
 
          let js2 = doc3.createElement('script');
          js2.src = "../js/math/tex-mml-chtml.js";
          js2.type = 'text/javascript';
          js2.async = true;
          js2.charset = 'utf-8';
          head3.appendChild(js2);
      });
 
 
  </script>

  

最后使用codemirror提供的getValue可以获取值。

另外,在预览时,会把回车“ ”替换为“<br>”

1
var question = editor.getValue().replace(/ /g, "<br>")+"";

  

这样就可以获取录入的值。

3.打造菜单

为了方便录入,打造了一个菜单,

菜单布局父class是math-menu,子菜单由sub-math-menu包裹。下面是HTML代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
       <div class="math-menu"  data-editorid="editor">
 
           
          <a href="###">菜单1</a>
           <div class="sub-math-menu">
               <span class="subnavbtn9">希腊字母  <span class="drop"></span> </span>
               <div class="subnav-content9">
                   <div>小写字母</div>
                   <a class="add" data-math="alpha">@alpha@</a>
<div style="clear:both"></div>
 
             </div>
 </div>
 </div>

  

下图是预览效果。

下面是CSS样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
.math-menu {
  overflow: hidden;
  background-color: #f2f2f2;
}
  
 
.math-menu a {
  float: left;
  font-size: 16px;
  color: #000;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
}
 
.math-menu .sub-math-menu a {
  
  font-size: 14px;
  padding: 12px 14px;
  
}
 
.sub-math-menu {
  float: left;
  overflow: hidden;
}
 
 
.sub-math-menu .subnavbtn9 {
  font-size: 16px; 
  border: none;
  outline: none;
  color: #000;
  padding: 14px 16px;
  background-color: inherit;
  font-family: inherit;
  margin: 0;
  display:flex;
}
 
 
.math-menu a:hover, .sub-math-menu:hover .subnavbtn9 {
  background-color: #ccc;
}
 
 
 
.subnav-content9 {
  display: none;
  position:absolute;
  background-color: #ccc;
  z-index: 1000;
  left:12.5%;
   75%;
}
 
 
 
.subnav-content9 a {
  float: left;
  color: #000;
  text-decoration: none;
   height:50px;
}
 
.subnav-content9 a:hover {
  background-color: #ffffff;
  color: black;
}
 
  
 
 .drop{
        margin-top:10px;
        margin-left:2px;
     0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 7px solid #333;
}
 
   .CodeMirror {
  border: 1px solid #eee;
  height: 400px;
    
  word-break:break-all;
   font-family:Verdana;
}
    .add{ cursor:pointer; }
          .layui-card{ margin-bottom:15px; }

  

增加鼠标经过,菜单显示效果。

注意:这里使用的是mouseover事件,而不是mouseenter事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
              <script>
 
                  $('.sub-math-menu').mouseover(function () {
      
                      $(this).find(".subnav-content9").show();
 
                  })
 
                  $('.sub-math-menu').mouseout(function () {
                      $(this).find(".subnav-content9").hide();
                  })
 
                  $(".add").click(
                      function ()
                      {
                          var ed=  $(this).parent().parent().parent().data("editorid");
                            
                          if(ed=="editor")
                          {
                              editor.replaceSelection("@"+$(this).data("math")+"@")
                          }
                          else
                          {
                              editor2.replaceSelection("@"+$(this).data("math")+"@")
                          }
 
                          $(this).parent().parent().find(".subnav-content9").hide();
 
                      }
 
                      );
</script>

  

到此,大功告成。

4.打造普通模式(小白模式)

 当然,有时候你可能希望更多的控制,例如插入表格)

这里使用Tinymce集成Mathjax实现,其中,这里使用一个插件:https://github.com/dimakorotkov/tinymce-mathjax

代码里,扩展了Tinymce菜单的定制。

默认这个插件提供的弹窗太小,可以放大,修改后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
tinymce.PluginManager.add('mathjax', function(editor, url) {
 
  // plugin configuration options
  let mathjaxClassName = editor.settings.mathjax.className || "math-tex";
  let mathjaxTempClassName = mathjaxClassName + '-original';
 
 
  mathjaxSymbols = editor.settings.mathjax.symbols || { start: '\(', end: '\) ' };
 
 
  let mathjaxUrl = editor.settings.mathjax.lib || null;
  let mathjaxConfigUrl = (editor.settings.mathjax.configUrl || url + '/config.js') + '?class=' + mathjaxTempClassName;
  let mathjaxScripts = [mathjaxConfigUrl];
  if (mathjaxUrl) {
    mathjaxScripts.push(mathjaxUrl);
  }
 
  // load mathjax and its config on editor init
  editor.on('init', function () {
    for (let i = 0; i < mathjaxScripts.length; i++) {
      let id = editor.dom.uniqueId();
      let script = editor.dom.create('script', {id: id, type: 'text/javascript', src: mathjaxScripts[i]});
      editor.getDoc().getElementsByTagName('head')[0].appendChild(script);
    }
  });
 
  // remove extra tags on get content
  editor.on('GetContent', function (e) {
    let div = editor.dom.create('div');
    div.innerHTML = e.content;
    let elements = div.querySelectorAll('.' + mathjaxClassName);
    for (let i = 0; i < elements.length; i++) {
      let children = elements[i].querySelectorAll('span');
      for (let j = 0; j < children.length; j++) {
        children[j].remove();
      }
      let latex = elements[i].getAttribute('data-latex');
      elements[i].removeAttribute('contenteditable');
      elements[i].removeAttribute('style');
      elements[i].removeAttribute('data-latex');
      elements[i].innerHTML = latex;
    }
    e.content = div.innerHTML;
  });
 
  let checkElement = function(element) {
    if (element.childNodes.length != 2) {
      element.setAttribute('contenteditable', false);
      element.style.cursor = 'pointer';
      let latex = element.getAttribute('data-latex') || element.innerHTML;
      element.setAttribute('data-latex', latex);
      element.innerHTML = '';
 
      let math = editor.dom.create('span');
      math.innerHTML = latex;
      math.classList.add(mathjaxTempClassName);
      element.appendChild(math);
 
      let dummy = editor.dom.create('span');
      dummy.classList.add('dummy');
      dummy.innerHTML = 'dummy';
      dummy.setAttribute('hidden', 'hidden');
      element.appendChild(dummy);
    }
  };
 
  // add dummy tag on set content
  editor.on('BeforeSetContent', function (e) {
    let div = editor.dom.create('div');
    div.innerHTML = e.content;
    let elements = div.querySelectorAll('.' + mathjaxClassName);
    for (let i = 0 ; i < elements.length; i++) {
      checkElement(elements[i]);
    }
    e.content = div.innerHTML;
        
  });
 
  // refresh mathjax on set content
  editor.on('SetContent', function(e) {
    if (editor.getDoc().defaultView.MathJax) {
      editor.getDoc().defaultView.MathJax.startup.getComponents();
      editor.getDoc().defaultView.MathJax.typeset();
    }
  });
 
  // add button to tinimce
  editor.ui.registry.addButton('插入公式', {
    text: '插入公式',
    tooltip: '插入公式',
    onAction: function () {
        openMathjaxEditor();
 
        
    }
  });
 
  // handle click on existing
  editor.on("click", function (e) {
    let closest = e.target.closest('.' + mathjaxClassName);
    if (closest) {
      openMathjaxEditor(closest);
    }
  });
 
 
 
 
 
 
  // open window with editor
  let openMathjaxEditor = function(target) {
      
    let mathjaxId = editor.dom.uniqueId();
     
    let latex = '';
    if (target) {
      latex_attribute = target.getAttribute('data-latex');
      if (latex_attribute.length >= (mathjaxSymbols.start + mathjaxSymbols.end).length) {
        latex = latex_attribute.substr(mathjaxSymbols.start.length, latex_attribute.length - (mathjaxSymbols.start + mathjaxSymbols.end).length);
      }
    }
   
 
    // show new window
    editor.windowManager.open({
        title: 'Mathjax',
        size: 'medium',
        body: {
         type: 'panel',
         items: [
             {
                 type: 'htmlpanel',
                 html: '<div > <input onclick=changesybol() type=checkbox id=cb_br name=cb_br>换行 <a href="https://www.cnblogs.com/mqingqing123/p/12063096.html" target="blank" >LaTex说明</a>   <a href="http://www.dotnetcms.org" target="blank" >启明星官网</a> <style>.tox-textarea{height:150px !important;  border-radius:0px;}</style> </div>'
             },
            {
            type: 'textarea',
            name: 'title'
            },
             {
                type: 'htmlpanel',
                html: '<iframe id="' + mathjaxId + '" style="98%; min-height: 50px;    "  ></iframe>'
            }
         ]
      },
 
      buttons: [{ type: 'submit', text: '确定' }],
 
      onSubmit: function onsubmit(api) {
        let value = api.getData().title.trim();
        if (target) {
          target.innerHTML = '';
          target.setAttribute('data-latex', getMathText(value));
          checkElement(target);
        } else {
          let newElement = editor.getDoc().createElement('span');
          newElement.innerHTML = getMathText(value);
          newElement.classList.add(mathjaxClassName);
          checkElement(newElement);
          editor.insertContent(newElement.outerHTML);
        }
        editor.getDoc().defaultView.MathJax.startup.getComponents();
        editor.getDoc().defaultView.MathJax.typeset();
        api.close();
      },
      onChange: function(api) {
        var value = api.getData().title.trim();
        if (value != latex) {
          refreshDialogMathjax(value, document.getElementById(mathjaxId));
          latex = value;
        }
      },
      initialData: {title: latex}
    });
  
    if (mathjaxSymbols.start == "\(") {
        document.getElementById("cb_br").checked = false;
    }
    else {
        document.getElementById("cb_br").checked = true;
    }
   
 
    
 
    // add scripts to iframe
    let iframe = document.getElementById(mathjaxId);
 
    let iframeWindow = iframe.contentWindow || iframe.contentDocument.document || iframe.contentDocument;
    let iframeDocument = iframeWindow.document;
    let iframeHead = iframeDocument.getElementsByTagName('head')[0];
    let iframeBody = iframeDocument.getElementsByTagName('body')[0];
   
    // get latex for mathjax from simple text
    let getMathText = function (value, symbols) {
      if (!symbols) {
        symbols = mathjaxSymbols;
      }
      
      return symbols.start + ' ' + value + ' ' + symbols.end ;
    };
 
    // refresh latex in mathjax iframe
    let refreshDialogMathjax = function(latex) {
      let MathJax = iframeWindow.MathJax;
      let div = iframeBody.querySelector('div');
      if (!div) {
        div = iframeDocument.createElement('div');
        div.classList.add(mathjaxTempClassName);
        iframeBody.appendChild(div);
      }
      div.innerHTML = getMathText(latex, {start: '$$', end: '$$'});
      if (MathJax && MathJax.startup) {
        MathJax.startup.getComponents();
        MathJax.typeset();
      }
    };
    refreshDialogMathjax(latex);
 
    // add scripts for dialog iframe
    for (let i = 0; i < mathjaxScripts.length; i++) {
      let node = iframeWindow.document.createElement('script');
      node.src = mathjaxScripts[i];
      node.type = 'text/javascript';
      node.async = false;
      node.charset = 'utf-8';
      iframeHead.appendChild(node);
    }
 
  };
});
 
 
 
function changesybol() {
    if (document.getElementById("cb_br").checked) {
        mathjaxSymbols = { start: '\[', end: '\] ' };
    }
    else {
        mathjaxSymbols = { start: '\(', end: '\) ' };
    }
 
 
}

  

这样,这个系统核心就完成了。

在线体验  http://demo.dotnetcms.org/math

出处:https://www.cnblogs.com/mqingqing123/p/14509366.html

=======================================================================================

备份下载:MathEditor5.0.rar

您的资助是我最大的动力!
金额随意,欢迎来赏!
款后有任何问题请给我留言。

如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的推荐按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的关注我。(●'◡'●)

如果你觉得本篇文章对你有所帮助,请给予我更多的鼓励,求打             付款后有任何问题请给我留言!!!

因为,我的写作热情也离不开您的肯定支持,感谢您的阅读,我是【Jack_孟】!

原文地址:https://www.cnblogs.com/mq0036/p/14509808.html