@zzish/math-rich-input 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1651 @@
1
+ import React from 'react'
2
+
3
+ import katex from "katex";
4
+ import {setupKatex} from "./katexHelper"
5
+
6
+ import EquationEditorModal from './EquationEditorModal';
7
+ import Toolbar from './Toolbar'
8
+
9
+ import {
10
+ isKatexNode,
11
+ elementToMarkedRawText,
12
+ rawTextToHtml,
13
+ getLatex,
14
+ setRangeParams,
15
+ getRangeParams,
16
+ getNodeTextLength,
17
+ findNodeWithIndex,
18
+ findIndexOfNode,
19
+ findFirstTextNode,
20
+ countNodeTypes,
21
+ startsWithSmallSpace,
22
+ getCharacterBeforeMarks,
23
+ replaceCharacterBeforeMarks,
24
+ replaceStringBeforeMarks,
25
+ insertCharacterBeforeMarks,
26
+ removeMarks,
27
+ replaceMarksWithMath}
28
+ from './mathRichInputHelper';
29
+
30
+ import './MathRichInput.css';
31
+ import AccentBar from './AccentBar';
32
+
33
+ const MIME_TYPE_PLAIN_TEXT = "text/plain"
34
+ const MIME_TYPE_HTML = "text/html"
35
+ const MIME_TYPE_TEX = "application/x-tex"
36
+ const MIME_TYPE_ZZISH_HTML_MATH = "application/x-zzish-html-math"
37
+
38
+ const SMALL_SPACE_CHAR_CODE = 8203
39
+ const SMALL_SPACE = String.fromCharCode(SMALL_SPACE_CHAR_CODE)
40
+ const SMALL_SPACE_LENGTH = SMALL_SPACE.length
41
+ // const SMALL_SPACE_REG_EXP = new RegExp(SMALL_SPACE, "g")
42
+
43
+ const MARK = "_M_A_R_K_"
44
+ const MARK_REG_EXP = new RegExp(MARK, "g")
45
+
46
+ // const LATEX_MARKER_START = "<annotation encoding=\"application/x-tex\">"
47
+ // const LATEX_MARKER_END = "</annotation>"
48
+
49
+ const MAX_HISTORY_LENGTH = 100
50
+
51
+ const DEFAULT_OPTIONS = {
52
+ useExpertMode:false,
53
+ selectedTab: null
54
+ }
55
+
56
+ export default class MathRichInput extends React.Component {
57
+
58
+ constructor(props) {
59
+ super(props);
60
+ this.state = {
61
+ showEquationEditor: false,
62
+ equationEditorLatex: "",
63
+ showAccentBar:false,
64
+ activeButtons: {
65
+ bold: false,
66
+ italic: false,
67
+ underline: false,
68
+ subscript: false,
69
+ superscript: false
70
+ },
71
+ keyPressed:""
72
+ };
73
+
74
+ // this.handleInput = this.handleInput.bind(this);
75
+ // this.handleKeyDown = this.handleKeyDown.bind(this);
76
+ // this.handleKeyUp = this.handleKeyUp.bind(this);
77
+
78
+ // Remember the last key pressed
79
+ this.keyPressed = null;
80
+ this.keyPressStartTime = -1;
81
+
82
+ // This gets updated in coomponent did mount, used to position the toolbar correctly
83
+ this.translateToolbar = "";
84
+
85
+ // Whether the text contains HTML or Maths
86
+ this.isHtml = false
87
+ this.hasMath = false
88
+
89
+ // Elements that need to be set before the equation editor is shown
90
+ this.markedRawText = null
91
+ this.oldRangeParams = {startNodeIndex:0,startOffset:0,endNodeIndex:0,endOffset:0};
92
+ this.moveCursorOnInsert = false
93
+
94
+ // Elements defining the current state of range
95
+ this.rangeParamsToSetAfterComponentUpdate = null // Set this to update the selection after rendering
96
+ this.lastSetRangeParmas = null // This is set automatically each time the range params are set with setRangeParams
97
+
98
+ this.lastSetActiveButtons = null
99
+
100
+ this.onChangeCallback = this.props.onChange
101
+
102
+ // set the initialHistoryState the first time an action is done that allows us to identify the cursor position
103
+ // 1. When the first mouse click is released (see onMouseUp)
104
+ // 2. When the first key is pressed (see handleKeyDown)
105
+ this.initialHistoryState = null
106
+ this.undoHistory = []
107
+ this.redoHistory = []
108
+ }
109
+
110
+ enableFontStyling() {
111
+ if (this.props.outputTex !== null && this.props.outputTex !== undefined &&
112
+ this.props.outputTex === true)
113
+ return false
114
+ else if (this.props.enableFontStyling !== null && this.props.enableFontStyling !== undefined)
115
+ return this.props.enableFontStyling
116
+ else
117
+ return true
118
+ }
119
+
120
+ enableMultiline() {
121
+ if (this.props.outputTex !== null && this.props.outputTex !== undefined &&
122
+ this.props.outputTex === true)
123
+ return false
124
+ else if (this.props.enableMultiline !== null && this.props.enableMultiline !== undefined)
125
+ return this.props.enableMultiline
126
+ else
127
+ return true
128
+ }
129
+
130
+ enableHtml() {
131
+ let outputTex = this.props.outputTex ?? false
132
+ return !outputTex
133
+ }
134
+
135
+ enableMath() {
136
+ if (this.props.enableMath !== null && this.props.enableMath !== undefined)
137
+ return this.props.enableMath
138
+ return true
139
+ }
140
+
141
+ // static isHtml(text) {
142
+ // return /<\s*p[^>]*>(.*?)<\s*\/\s*p\s*>/i.test(text) ||
143
+ // /<\s*b[^>]*>(.*?)<\s*\/\s*b\s*>/i.test(text) ||
144
+ // /<\s*i[^>]*>(.*?)<\s*\/\s*i\s*>/i.test(text) ||
145
+ // /<\s*u[^>]*>(.*?)<\s*\/\s*u\s*>/i.test(text) ||
146
+ // /<\s*sup[^>]*>(.*?)<\s*\/\s*sup\s*>/i.test(text) ||
147
+ // /<\s*sub[^>]*>(.*?)<\s*\/\s*sub\s*>/i.test(text)
148
+ // }
149
+
150
+ getMimeType(isHtml, hasMath) {
151
+ if (isHtml) {
152
+ if (hasMath)
153
+ return MIME_TYPE_ZZISH_HTML_MATH
154
+ else
155
+ return MIME_TYPE_HTML
156
+ } else {
157
+ if (hasMath)
158
+ return MIME_TYPE_TEX
159
+ else
160
+ return MIME_TYPE_PLAIN_TEXT
161
+ }
162
+ }
163
+
164
+ _countNodeTypes = () => {
165
+ return countNodeTypes(this.editableDiv)
166
+ }
167
+
168
+ applyChangesToComponent = (value, mimeType, rangeParams,
169
+ isExpertMode, selectedTab) => {
170
+ console.log("applyChangesToComponent")
171
+
172
+ // if (this.props.outputTex !== null && this.props.outputTex !== undefined && this.props.outputTex) {
173
+ // if (mimeType === MIME_TYPE_HTML) {
174
+ // console.warn("Html found when outputTex=true")
175
+ // value = convertHtmlToPlainText(value)
176
+ // mimeType = MIME_TYPE_PLAIN_TEXT
177
+ // } else if (mimeType === MIME_TYPE_ZZISH_HTML_MATH) {
178
+ // value = convertHtmlMathToTex(value)
179
+ // mimeType = MIME_TYPE_TEX
180
+ // }
181
+ // }
182
+
183
+ // rangeParams, isExpertMode and selectedTab can be null, in which case we will set them
184
+ if (rangeParams === null)
185
+ rangeParams = this.lastSetRangeParams
186
+ else
187
+ this.rangeParamsToSetAfterComponentUpdate = rangeParams
188
+
189
+ let options = this.props.options || DEFAULT_OPTIONS
190
+ if (isExpertMode === null)
191
+ isExpertMode = options.isExpertMode
192
+ if (selectedTab === null)
193
+ selectedTab = options.selectedTab
194
+
195
+ // Add this to the history
196
+ this.undoHistory.push({value, mimeType, rangeParams})
197
+ if (this.undoHistory.length > MAX_HISTORY_LENGTH)
198
+ this.undoHistory.shift()
199
+
200
+ // Call the parent handler to apply the changes
201
+ this.onChangeCallback({value, mimeType},
202
+ {isExpertMode, selectedTab})
203
+ }
204
+
205
+ initialiseHistory = () => {
206
+ if (this.initialHistoryState == null)
207
+ this.initialHistoryState = {value:this.props.value,
208
+ mimeType:this.props.mimeType,
209
+ rangeParams:this._getRangeParams()}
210
+ }
211
+
212
+ resetHistoryRedo = () => {
213
+ this.redoHistory = []
214
+ }
215
+
216
+ deepEqual = (v1, v2) => {
217
+ if (v1 === v2) return true
218
+
219
+ if (v1 == null || v2 == null || typeof v1 != "object" || typeof v2 != "object") return false
220
+
221
+ let v1keys = Object.keys(v1)
222
+ let v2keys = Object.keys(v2)
223
+ if (v1keys.length !== v2keys.length) return false
224
+
225
+ for (let key of v1keys) {
226
+ if (!v2keys.includes(key) || !this.deepEqual(v1[key], v2[key])) return false
227
+ }
228
+ return true
229
+ }
230
+
231
+ undo = () => {
232
+ console.log("undo")
233
+ console.log(this.undoHistory)
234
+ const currentState = this.undoHistory.pop()
235
+
236
+ let previousState = this.undoHistory.pop()
237
+ if (previousState === null || previousState === undefined) {
238
+ previousState = this.initialHistoryState
239
+ }
240
+
241
+ if (this.redoHistory.length > 0) {
242
+ let redoLast = this.redoHistory[this.redoHistory.length-1]
243
+ if (!this.deepEqual(redoLast, currentState))
244
+ this.redoHistory.push(currentState)
245
+ } else
246
+ this.redoHistory.push(currentState)
247
+
248
+ console.log(previousState)
249
+
250
+ this.applyChangesToComponent(previousState.value, previousState.mimeType, previousState.rangeParams)
251
+ }
252
+
253
+ redo = () => {
254
+ const redoState = this.redoHistory.pop()
255
+ if (redoState !== null & redoState !== undefined) {
256
+ this.applyChangesToComponent(redoState.value, redoState.mimeType, redoState.rangeParams)
257
+ }
258
+ }
259
+
260
+ setOldRangeParams(params) {
261
+ // console.log("Setting old range params: "+params.startNodeIndex+"->"+params.startOffset)
262
+ this.oldRangeParams = params
263
+ }
264
+
265
+ getOldRangeParams() {
266
+ if (this.oldRangeParams === null || this.oldRangeParams === undefined)
267
+ return null
268
+ let params = {...this.oldRangeParams}
269
+ // console.log("Getting old range params: "+params.startNodeIndex+"->"+params.startOffset)
270
+ return params
271
+ }
272
+
273
+ _getRangeParams(editableDiv=null) {
274
+ // console.log("_getRangeParams")
275
+ if (editableDiv === null)
276
+ editableDiv = this.editableDiv
277
+ let rangeParams = getRangeParams(editableDiv)
278
+ // console.log(rangeParams)
279
+ return rangeParams
280
+ }
281
+
282
+ _setRangeParams = (params) => {
283
+ try {
284
+ console.log("_setRangeParams")
285
+ console.log(params)
286
+ setRangeParams(this.editableDiv, params)
287
+
288
+ this.setOldRangeParams(params)
289
+ this.lastSetRangeParmas = params
290
+ } catch (error) {
291
+ console.error(error)
292
+ console.log(this.editableDiv.childNodes)
293
+ }
294
+ }
295
+
296
+ _findNodeWithIndex(index) {
297
+ return findNodeWithIndex(this.editableDiv, index)
298
+ }
299
+
300
+ _findIndexOfNode(node) {
301
+ return findIndexOfNode(this.editableDiv, node)
302
+ }
303
+
304
+ _findFirstTextNode() {
305
+ return findFirstTextNode(this.editableDiv)
306
+ }
307
+
308
+ handleKeyDownBackspace = (e) => {
309
+ let rangeParams = this._getRangeParams()
310
+ let start_node_index = rangeParams.startNodeIndex
311
+ let end_node_index = rangeParams.endNodeIndex
312
+ let start_offset = rangeParams.startOffset
313
+ let end_offset = rangeParams.endOffset
314
+
315
+ if (start_node_index < 0) // Empty input field
316
+ return
317
+
318
+ let startNode = this._findNodeWithIndex(start_node_index)
319
+ let endNode = this._findNodeWithIndex(end_node_index)
320
+
321
+ // Prevent backspace if start or end of selected region is in a katex node
322
+ if (isKatexNode(startNode) || isKatexNode(endNode)) {
323
+ console.error("Backspace inside latex node. This shouln't happen.")
324
+ e.preventDefault()
325
+ }
326
+
327
+ // Handle special case of deleting last character when in bold etc text
328
+ // in order to resolve a bug
329
+ let deleteToEmpty = false
330
+ if (start_node_index === 0 && start_offset === 1 &&
331
+ end_node_index === 0 && end_offset === 1) {
332
+ if (!isKatexNode(startNode) && getNodeTextLength(startNode) === 1)
333
+ deleteToEmpty = true
334
+ }
335
+ if (start_node_index === 0 && start_offset === 0 && end_node_index !== 0) {
336
+ if (getNodeTextLength(endNode) === end_offset &&
337
+ this._findNodeWithIndex(end_node_index + 1) === null)
338
+ deleteToEmpty = true
339
+ }
340
+
341
+ if (deleteToEmpty) {
342
+ let new_rangeParams = {
343
+ startNodeIndex: 0,
344
+ startOffset: 0,
345
+ endNodeIndex: 0,
346
+ endOffset: 0
347
+ }
348
+ this.applyChangesToComponent("", MIME_TYPE_PLAIN_TEXT, new_rangeParams,
349
+ this.props.useExpertMode, this.props.selectedTab)
350
+ return
351
+ }
352
+
353
+ // Treat backspace normally in the following situations
354
+ if (start_offset > 1)
355
+ return
356
+ if (start_node_index <= 0)
357
+ return
358
+ if (start_node_index !== rangeParams.endNodeIndex ||
359
+ start_offset !== rangeParams.endOffset)
360
+ return
361
+
362
+ // At this point we must have start_offset === 0 or 1 and a previous node
363
+
364
+ let prevNode = this._findNodeWithIndex(start_node_index-1)
365
+
366
+ if (!isKatexNode(prevNode)) {
367
+ if (this.nodeStartsWithSmallSpace(startNode)) {
368
+ this.removeNewLine()
369
+ e.preventDefault()
370
+ }
371
+ return
372
+ }
373
+
374
+ // The previous node is a katex node
375
+
376
+ // OK! We need to delete the whole Katex node and set the cursor
377
+ // console.log("Deleting content before: "+start_node_index+"->"+start_offset)
378
+ // console.log(rangeParams)
379
+
380
+ let node = prevNode
381
+ let new_index = 0;
382
+ let new_offset = 0;
383
+
384
+ // Note that if startNodeIndex == 2, then that means that the latex node is at the start
385
+ // of the whole input at index 1. This is because we insert a SMALL_SPACE
386
+ // at the beginning in index 0, if the whole input starts with a latex node
387
+ if (start_node_index === 2) {
388
+ if (this.inputStartsWithSmallSpace()) {
389
+ new_index = 0
390
+ new_offset = 0
391
+ } else {
392
+ new_index = 0
393
+ new_offset = getNodeTextLength(this._findFirstTextNode())
394
+ }
395
+ } else {
396
+ new_index = start_node_index - 2
397
+ let new_node = this._findNodeWithIndex(new_index)
398
+ if (isKatexNode(new_node)) {
399
+ console.error("Two adjacent latex nodes. This should not happen!")
400
+ new_index = 0
401
+ new_offset = 0
402
+ }
403
+ new_node = this._findNodeWithIndex(new_index)
404
+ new_offset = getNodeTextLength(new_node)
405
+ if (new_offset < 0)
406
+ new_offset = 0
407
+ }
408
+
409
+ let new_rangeParams = {
410
+ startNodeIndex: new_index,
411
+ startOffset: new_offset,
412
+ endNodeIndex: new_index,
413
+ endOffset: new_offset
414
+ }
415
+
416
+ // OK, time to delete the katex node!
417
+ node.parentNode.removeChild(node)
418
+ let raw_text = elementToMarkedRawText(this.editableDiv, null, 0, this.enableHtml())
419
+ // console.log("After deleting katex: "+raw_text)
420
+
421
+ let nodeCounts = this._countNodeTypes()
422
+ let mimeType = this.getMimeType(nodeCounts.html > 0, nodeCounts.math > 0)
423
+
424
+ this.applyChangesToComponent(raw_text, mimeType, new_rangeParams,
425
+ this.props.useExpertMode, this.props.selectedTab)
426
+
427
+ // Consume the event
428
+ e.preventDefault()
429
+ }
430
+
431
+ nodeStartsWithSmallSpace(node) {
432
+ if(node.nodeValue !== null && node.nodeValue !== undefined && node.nodeValue.length > 0 &&
433
+ node.nodeValue.charAt(0) === SMALL_SPACE)
434
+ return true
435
+
436
+ return false
437
+ }
438
+
439
+ inputStartsWithSmallSpace = () => {
440
+ return startsWithSmallSpace(this.editableDiv)
441
+ }
442
+
443
+ handleKeyDownArrowLeft = (e) => {
444
+ let rangeParams = this._getRangeParams()
445
+ let index = rangeParams.startNodeIndex
446
+ let offset = rangeParams.startOffset
447
+
448
+ // console.log("Old: "+index+"->"+offset)
449
+
450
+ // The user shouldn't be in a katex node, but lets deal with it anyway
451
+ let node = this._findNodeWithIndex(index)
452
+ if (isKatexNode(node)) {
453
+ console.warn("Cursor is inside katex node")
454
+ let new_index = index-1 // Jump out of the latex node
455
+ let prev_node = this._findNodeWithIndex(index-1)
456
+ let new_offset = getNodeTextLength(prev_node)
457
+
458
+ let new_rangeParams = {
459
+ startNodeIndex: new_index,
460
+ startOffset: new_offset,
461
+ endNodeIndex: new_index,
462
+ endOffset: new_offset
463
+ }
464
+ this._setRangeParams(new_rangeParams)
465
+ e.preventDefault()
466
+ return
467
+ }
468
+
469
+ if (offset > 2)
470
+ return
471
+
472
+ let new_index = -1
473
+ let new_offset = -1
474
+
475
+ // Deal with case of inserted small space at start of <p> element or after katex node
476
+ if (offset === 2) {
477
+ new_index = index
478
+ if (this.nodeStartsWithSmallSpace(node)) {
479
+ if (index > 0 && isKatexNode(this._findNodeWithIndex(index-1)))
480
+ new_offset = 1
481
+ else
482
+ new_offset = 0
483
+ } else
484
+ new_offset = 1
485
+ }
486
+
487
+ if (offset === 1) {
488
+ if (this.nodeStartsWithSmallSpace(node)) {
489
+ if (index === 0) {
490
+ e.preventDefault()
491
+ return
492
+ }
493
+ let prev_node = this._findNodeWithIndex(index-1)
494
+ if (index >= 2 && isKatexNode(prev_node))
495
+ new_index = index - 2
496
+ else
497
+ new_index = index - 1
498
+
499
+ let new_node = this._findNodeWithIndex(new_index)
500
+ new_offset = getNodeTextLength(new_node)
501
+ } else {
502
+ new_index = index
503
+ new_offset = 0
504
+ }
505
+ }
506
+
507
+ if (offset === 0) {
508
+ let prev_node = this._findNodeWithIndex(index-1)
509
+ if (index >= 2 && isKatexNode(prev_node))
510
+ new_index = index - 2
511
+ else
512
+ new_index = index - 1
513
+ let new_node = this._findNodeWithIndex(new_index)
514
+ new_offset = getNodeTextLength(new_node)
515
+ if (new_offset === 1) {
516
+ if (this.nodeStartsWithSmallSpace(new_node)) {
517
+ if (index > 0 && isKatexNode(this._findNodeWithIndex(index-1)))
518
+ new_offset = 1
519
+ else
520
+ new_offset = 0
521
+ }
522
+ }
523
+ }
524
+
525
+
526
+ // Move cursor to start of field if in starting small space
527
+ if (new_index === 0 && new_offset === 1 && this.inputStartsWithSmallSpace()) {
528
+ new_offset = 0
529
+ }
530
+
531
+ if (new_index === -1 || new_offset === -1)
532
+ return
533
+
534
+ let new_rangeParams = {
535
+ startNodeIndex: new_index,
536
+ startOffset: new_offset,
537
+ endNodeIndex: new_index,
538
+ endOffset: new_offset
539
+ }
540
+ this._setRangeParams(new_rangeParams)
541
+
542
+ e.preventDefault()
543
+ }
544
+
545
+ handleKeyDownArrowRight = (e) => {
546
+ // console.log("handleKeyDownArrowRight")
547
+ let rangeParams = this._getRangeParams()
548
+ let index = rangeParams.startNodeIndex
549
+ let offset = rangeParams.startOffset
550
+
551
+ if (index < 0) // Empty input field
552
+ return
553
+
554
+ let node = this._findNodeWithIndex(index)
555
+ let length = getNodeTextLength(node)
556
+
557
+ // The user shouldn't be in a katex node, but lets deal with it anyway
558
+ if (isKatexNode(node)) {
559
+ console.warn("Cursor is inside katex node")
560
+ let new_index = index+1 // Jump out of the latex node
561
+ let new_offset = SMALL_SPACE_LENGTH // As there will be a SMALL_SPACE at the start of the text node
562
+
563
+ let new_rangeParams = {
564
+ startNodeIndex: new_index,
565
+ startOffset: new_offset,
566
+ endNodeIndex: new_index,
567
+ endOffset: new_offset
568
+ }
569
+ this._setRangeParams(new_rangeParams)
570
+ e.preventDefault()
571
+ return
572
+ }
573
+
574
+ // Deal with case of inserted small space at start of <p> element
575
+ if (offset === 0) {
576
+ if (this.nodeStartsWithSmallSpace(node)) {
577
+ console.log("Offset = 0 and node starts with small space")
578
+ let new_index = -1
579
+ let new_offset = -1
580
+
581
+ if (node.nodeValue.length > 1) {
582
+ new_index = index
583
+ new_offset = SMALL_SPACE_LENGTH + 1
584
+ } else {
585
+ new_index = index + 1
586
+ let next_node = this._findNodeWithIndex(new_index)
587
+ if (next_node !== null) {
588
+ if (isKatexNode(next_node)) {
589
+ new_index = index + 2
590
+ new_offset = SMALL_SPACE_LENGTH
591
+ } else {
592
+ new_index = index + 1
593
+ new_offset = 0
594
+ }
595
+ } else {
596
+ new_index = index
597
+ new_offset = offset
598
+ }
599
+ }
600
+ let new_rangeParams = {
601
+ startNodeIndex: new_index,
602
+ startOffset: new_offset,
603
+ endNodeIndex: new_index,
604
+ endOffset: new_offset
605
+ }
606
+ console.log(new_rangeParams)
607
+ this._setRangeParams(new_rangeParams)
608
+
609
+ e.preventDefault()
610
+ return
611
+ }
612
+ }
613
+
614
+
615
+ // If at the end of a text node
616
+ if (offset === length || (index === 0 && this.inputStartsWithSmallSpace())) {
617
+ let next_node = this._findNodeWithIndex(index+1)
618
+
619
+ // If there is no next node, then we can't move, so do the default action
620
+ // If the next node is a normal node, do the default action
621
+ if (next_node === null || !isKatexNode(next_node))
622
+ return
623
+
624
+ let new_index = index+2 // Jump over the latex node
625
+ let new_offset = SMALL_SPACE_LENGTH // As there will be a SMALL_SPACE at the start of the text node
626
+
627
+ let new_rangeParams = {
628
+ startNodeIndex: new_index,
629
+ startOffset: new_offset,
630
+ endNodeIndex: new_index,
631
+ endOffset: new_offset
632
+ }
633
+ this._setRangeParams(new_rangeParams)
634
+
635
+ e.preventDefault()
636
+ return
637
+ }
638
+ }
639
+
640
+ editableDivIsPlainText = () => {
641
+ return this.editableDiv.childNodes === null || this.editableDiv.childNodes.length === 0
642
+ }
643
+
644
+ handleKeyDownDollar = (e) => {
645
+
646
+ // This method automatically converts text between a pair of typed dollars into
647
+ // a latex math component. It only does this when the second dollar is preceded by certain
648
+ // characters and NOT when the second dollar is preceded by a space.
649
+ //
650
+ // This is because when the user actually wants actual dollar symbols for ammounts
651
+ // the dollar symbol will usually be preceded by a space, eg:
652
+ //
653
+ // "The pie was $3.50 and the cheese was $1.30"
654
+ //
655
+ // will not be converted as the $1.30 is preceded by a space.
656
+ //
657
+ try {
658
+ let params = this._getRangeParams()
659
+ let node = this._findNodeWithIndex(params.startNodeIndex)
660
+ if (node.nodeType !== 3) // text node
661
+ return
662
+ let text = node.nodeValue
663
+ // Look for starting dollar to match this new second dollar
664
+ let first_dollar_pos = text.indexOf("$")
665
+ if (first_dollar_pos < 0)
666
+ return
667
+ if (first_dollar_pos >= params.startOffset)
668
+ return
669
+ let second_dollar_pos = params.startOffset
670
+
671
+ let c = text.charAt(second_dollar_pos-1)
672
+ let convert_to_latex = false
673
+ if ((c >= 'a' && c <= 'z') ||
674
+ (c >= 'A' && c <= 'Z') ||
675
+ (c >= '0' && c <= '9') ||
676
+ c === ')' || c === '}') {
677
+ convert_to_latex = true
678
+ }
679
+ if (!convert_to_latex)
680
+ return
681
+
682
+ node.nodeValue =
683
+ node.nodeValue.substring(0,first_dollar_pos) + MARK +
684
+ node.nodeValue.substring(first_dollar_pos+1, second_dollar_pos) + MARK +
685
+ node.nodeValue.substring(second_dollar_pos)
686
+
687
+ let raw_text = elementToMarkedRawText(e.target, null, 0, this.enableHtml())
688
+ raw_text = raw_text.replace(MARK_REG_EXP,"$")
689
+
690
+ // We are turning one node into three and want the cursor at the start of the third node
691
+ let new_rangeParams = {
692
+ startNodeIndex: params.startNodeIndex + 2,
693
+ startOffset: SMALL_SPACE_LENGTH,
694
+ endNodeIndex: params.startNodeIndex + 2,
695
+ endOffset: SMALL_SPACE_LENGTH
696
+ }
697
+
698
+ let nodeCounts = this._countNodeTypes()
699
+ let mimeType = this.getMimeType(nodeCounts.html > 0, true)
700
+
701
+ this.applyChangesToComponent(raw_text,
702
+ mimeType,
703
+ new_rangeParams,
704
+ this.props.useExpertMode,
705
+ this.props.selectedTab)
706
+ } catch (error) {
707
+ console.error(error)
708
+ }
709
+
710
+ e.preventDefault()
711
+ }
712
+
713
+ showEquationEditorOnKeyDown = (e) => {
714
+ this.showEquationEditor(true)
715
+ e.preventDefault()
716
+ }
717
+
718
+ handleKeyDownEnter = (e) => {
719
+ try {
720
+ console.log("handleKeyDownEnter")
721
+ if (this.enableMultiline())
722
+ this.insertNewLine(e)
723
+ } catch (error) {
724
+ console.error(error)
725
+ }
726
+ e.preventDefault()
727
+ }
728
+
729
+ handleKeyDown = (e) => {
730
+ try {
731
+ // Lets set up or update the undo redo history
732
+ this.initialiseHistory()
733
+
734
+ if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey)) {
735
+ if (e.shiftKey)
736
+ this.redo()
737
+ else
738
+ this.undo()
739
+ e.preventDefault()
740
+ return
741
+ }
742
+
743
+ if (!(e.ctrlKey || e.metaKey))
744
+ this.resetHistoryRedo()
745
+
746
+ if ((e.metaKey || e.ctrlKey) &&
747
+ (e.key === "b" || e.key === "i" || e.key === "u" ||
748
+ e.key === "6" || e.key === "^" ||
749
+ e.key === "-" || e.key === "_")) {
750
+ if (!this.enableHtml()) {
751
+ e.preventDefault()
752
+ return
753
+ }
754
+ if (e.key === "b")
755
+ this.styleText(e, "Bold")
756
+ if (e.key === "i")
757
+ this.styleText(e, "Italic")
758
+ if (e.key === "u")
759
+ this.styleText(e, "Underline")
760
+ if (e.key === "6" || e.key === "^")
761
+ this.styleText(e, "Superscript")
762
+
763
+ // don't apply subscript if it is the browser command key for "smaller"
764
+ if (/(Win)/i.test(window.navigator.platform) && e.ctrlKey && e.key === "-")
765
+ return
766
+ if (/(Mac)/i.test(window.navigator.platform) && e.metaKey && e.key === "-")
767
+ return
768
+ // otherwise
769
+ if (e.key === "-" || e.key === "_")
770
+ this.styleText(e, "Subscript")
771
+
772
+ e.preventDefault()
773
+ return
774
+ }
775
+
776
+
777
+ // Ignore automatic key repeat presses on Windows so that we can show an accent bar on
778
+ // long key press
779
+ if (!/(Win)/i.test(window.navigator.platform)) {
780
+ if (e.key === this.keyPressed && this.keyPressStartTime > 0) {
781
+ if (e.key.length === 1 && (
782
+ ("a" <= e.key && e.key <= "z") ||
783
+ ("A" <= e.key && e.key <= "Z") ||
784
+ e.key === "!" ||
785
+ e.key === "?")) {
786
+ e.preventDefault()
787
+ return
788
+ }
789
+ }
790
+ }
791
+
792
+ if (e.altKey && e.key === "Alt") {
793
+ let key = this.fetchAccentLetter()
794
+ if (key === "a" || key === "e" || key === "i" || key === "o" || key === "u" ||
795
+ key === "c" || key === "l" || key === "n" || key === "s" || key === "?" || key === "!") {
796
+ this.setState({showAccentBar:!this.state.showAccentBar})
797
+ } else {
798
+ this.setState({showAccentBar:false})
799
+ }
800
+ return
801
+ }
802
+
803
+ this.keyPressed = e.key // store for use in methods
804
+ this.setState({keyPressed:e.key})
805
+
806
+ // Hide the accent bar if a key is typed except when it is an automatic key repeat press
807
+ // If it is automatic key repeat, then keyPressStartTime will be greater than 0
808
+ // (keyPressStartTime is reset to -1 on key up)
809
+ if (this.keyPressStartTime < 0) {
810
+ this.keyPressStartTime = Date.now()
811
+ if (this.state.showAccentBar)
812
+ this.setState({showAccentBar:false})
813
+ }
814
+
815
+ if (e.key === "Backspace") {
816
+ this.handleKeyDownBackspace(e)
817
+ return
818
+ }
819
+ if (e.key === "ArrowLeft") {
820
+ this.handleKeyDownArrowLeft(e)
821
+ return
822
+ }
823
+ if (e.key === "ArrowRight") {
824
+ this.handleKeyDownArrowRight(e)
825
+ return
826
+ }
827
+ if (e.key === "$") {
828
+ this.handleKeyDownDollar(e)
829
+ return
830
+ }
831
+
832
+ // Implement special command + keys
833
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
834
+ this.showEquationEditorOnKeyDown(e)
835
+ return
836
+ } else if (/(Mac)/i.test(window.navigator.platform)) {
837
+ if (e.ctrlKey && e.key === "=") { // CTRL+= inserts equation in Word on Mac
838
+ this.showEquationEditorOnKeyDown(e)
839
+ return
840
+ }
841
+ } else if (/(Win)/i.test(window.navigator.platform)) {
842
+ if (e.altKey && e.key === "=") { // ALT+= inserts equation in Word on PC
843
+ this.showEquationEditorOnKeyDown(e)
844
+ return
845
+ }
846
+ }
847
+
848
+ // Lets handle the enter key (new line) ourselves
849
+ if (e.key === "Enter") {
850
+ this.handleKeyDownEnter(e)
851
+ return
852
+ }
853
+
854
+ // Show the accent bar on long key presses
855
+ if (/(Win)/i.test(window.navigator.platform)) {
856
+ let key= e.key.toLowerCase()
857
+ if (key === "a" || key === "e" || key === "i" || key === "o" || key === "u" ||
858
+ key === "c" || key === "l" || key === "n" || key === "s" || key === "?" || key === "!") {
859
+ let _this = this
860
+ setTimeout(function(){
861
+ console.log(Date.now() - _this.keyPressStartTime)
862
+ if (_this.keyPressStartTime > 0)
863
+ if (Date.now() - _this.keyPressStartTime > 450)
864
+ _this.setState({showAccentBar:true})
865
+ }, 500);
866
+ }
867
+ }
868
+
869
+ // Stop editing if within rendered katex node, if somehow the cursor ends up there (it shouldn't)
870
+ let params = this._getRangeParams()
871
+ let startNode = this._findNodeWithIndex(params.startNodeIndex)
872
+ let endNode = this._findNodeWithIndex(params.endNodeIndex)
873
+ if (!this.editableDivIsPlainText()) {
874
+ // console.log(params)
875
+ if (params.startNodeIndex === -1) {
876
+ console.error("Couldn't get range params")
877
+ return
878
+ }
879
+ // console.log("Start node: "+startNode)
880
+ // console.log("End node: "+endNode)
881
+
882
+ if (isKatexNode(startNode) || isKatexNode(endNode)) {
883
+ console.error("User trying to edit inside latex node")
884
+ e.preventDefault()
885
+ return
886
+ }
887
+ }
888
+
889
+ // Check if inserting character in element that starts with small space
890
+ if (e.key.length === 1 && params.startOffset === 0 && this.nodeStartsWithSmallSpace(startNode)) {
891
+ this.replaceNextLetter(e.key)
892
+ e.preventDefault()
893
+ return
894
+ }
895
+
896
+ } catch (error) {
897
+ console.error(error)
898
+ console.log("Key pressed: " +
899
+ (e.metaKey ? "META+" : "") +
900
+ (e.ctrlKey ? "CTRL+" : "") +
901
+ (e.altKey ? "ALT+" : "") +
902
+ (e.shiftKey ? "SHIFT+" : "") +
903
+ e.key
904
+ )
905
+ }
906
+ }
907
+
908
+ handleKeyUp = (e) => {
909
+ try {
910
+ this.keyPressed = e.key
911
+ this.keyPressStartTime = -1
912
+
913
+ // If the cursor is in a katex node, then move it to the next node (unless there is no
914
+ // net node in which case move it to the previous node)
915
+ if (e.key === "ArrowRight") {
916
+ let rangeParams = this._getRangeParams()
917
+ let index = rangeParams.startNodeIndex
918
+
919
+ if (index >= 0) {
920
+ if (isKatexNode(this._findNodeWithIndex(index))) {
921
+ let new_index = 0
922
+ let new_offset = 0
923
+
924
+ let nextNode = this._findNodeWithIndex(index+1)
925
+ if (nextNode === null) {
926
+ new_index = index + 1
927
+ new_offset = SMALL_SPACE_LENGTH // Nominally should be 0, but 1 due to inserted SMALL_SPACE
928
+ } else {
929
+ new_index = index - 1
930
+ new_offset = getNodeTextLength(this._findNodeWithIndex(new_index))
931
+ if (new_offset < 0)
932
+ {
933
+ console.error("Failed to get node text length")
934
+ new_offset = 0
935
+ }
936
+ }
937
+
938
+ let new_rangeParams = {
939
+ startNodeIndex:new_index,
940
+ startOffset: new_offset,
941
+ endNodeIndex: new_index,
942
+ endOffset: new_offset
943
+ }
944
+ this._setRangeParams(new_rangeParams)
945
+ }
946
+ }
947
+ }
948
+
949
+ if (e.key === "ArrowLeft") {
950
+ let rangeParams = this._getRangeParams()
951
+ let index = rangeParams.startNodeIndex
952
+
953
+ // If the cursor is in a katex node, then move it to the previous node (unless there is no
954
+ // previous node in which case move it to the next node)
955
+ if (index >= 0) {
956
+ if (isKatexNode(this._findNodeWithIndex(index))) {
957
+ let new_index = 0
958
+ let new_offset = 0
959
+
960
+ if (index - 1 >= 0) {
961
+ new_index = index - 1
962
+ new_offset = getNodeTextLength(this._findNodeWithIndex(new_index))
963
+ if (new_offset < 0)
964
+ {
965
+ console.error("Failed to get node text length")
966
+ new_offset = 0
967
+ }
968
+ } else {
969
+ new_index = index + 1
970
+ new_offset = SMALL_SPACE_LENGTH
971
+ }
972
+
973
+ let new_rangeParams = {
974
+ startNodeIndex:new_index,
975
+ startOffset: new_offset,
976
+ endNodeIndex: new_index,
977
+ endOffset: new_offset
978
+ }
979
+ // console.log(new_rangeParams)
980
+ this._setRangeParams(new_rangeParams)
981
+ }
982
+ }
983
+ }
984
+
985
+ this.updateActiveButtons()
986
+ } catch (error) {
987
+ console.error(error)
988
+ }
989
+ }
990
+
991
+ handleInput = (event) => {
992
+ try {
993
+ // console.log("handleInput")
994
+ const raw_text = elementToMarkedRawText(this.editableDiv, null, 0, this.enableHtml())
995
+ const params = this._getRangeParams()
996
+ if (params === null || params === undefined)
997
+ console.error("Could not get range params from input field")
998
+ else if (params.startNodeIndex === null || params.startNodeIndex === undefined)
999
+ console.error("Could not get range params from input field, startNodeIndex==="+params.startNodeIndex)
1000
+ else {
1001
+ // console.log("Current range params:")
1002
+ // console.log(params)
1003
+ }
1004
+
1005
+ let nodeCounts = this._countNodeTypes()
1006
+ let mimeType = this.getMimeType(nodeCounts.html > 0, nodeCounts.math > 0)
1007
+ this.applyChangesToComponent(raw_text, mimeType, params,
1008
+ this.props.useExpertMode, this.props.selectedTab)
1009
+ } catch (error) {
1010
+ console.error(error)
1011
+ }
1012
+ }
1013
+
1014
+ updateActiveButtons = () => {
1015
+ try {
1016
+ let rangeParams = this._getRangeParams()
1017
+ if (rangeParams.startNodeIndex < 0)
1018
+ return
1019
+ let node = this._findNodeWithIndex(rangeParams.startNodeIndex)
1020
+ let bold = false
1021
+ let italic = false
1022
+ let underline = false
1023
+ let superscript = false
1024
+ let subscript = false
1025
+ do {
1026
+ if (node.nodeType === 1) {
1027
+ let nodeName = node.nodeName.toLowerCase()
1028
+ if (nodeName === "b" || nodeName === "strong")
1029
+ bold = true
1030
+ else if (nodeName === "i" || nodeName === "em")
1031
+ italic = true
1032
+ else if (nodeName === "u")
1033
+ underline = true
1034
+ else if (nodeName === "sub")
1035
+ subscript = true
1036
+ else if (nodeName === "sup")
1037
+ superscript = true
1038
+ }
1039
+ else if (node.classList !== null && node.classList !== undefined &&
1040
+ node.classList.contains("MathRichInput"))
1041
+ break
1042
+ node = node.parentNode
1043
+ } while (node !== null)
1044
+
1045
+ let activeButtons = {bold, italic, underline, subscript, superscript}
1046
+ // console.log(activeButtons)
1047
+
1048
+ let update = true
1049
+ if (this.lastSetActiveButtons !== null) {
1050
+ update = false
1051
+ for(var style in activeButtons) {
1052
+ if(activeButtons[style] !== this.lastSetActiveButtons[style]) {
1053
+ update = true
1054
+ break
1055
+ }
1056
+ }
1057
+ }
1058
+ if (update) {
1059
+ this.lastSetActiveButtons = activeButtons
1060
+ this.setState({activeButtons:activeButtons})
1061
+ }
1062
+ } catch (error) {
1063
+ console.error(error)
1064
+ }
1065
+ }
1066
+
1067
+ componentDidMount(){
1068
+ try {
1069
+ setupKatex()
1070
+
1071
+ this.editableDiv.addEventListener('keydown',this.handleKeyDown);
1072
+ this.editableDiv.addEventListener('keyup',this.handleKeyUp);
1073
+ } catch (error) {
1074
+ console.error(error)
1075
+ }
1076
+ }
1077
+
1078
+ componentWillUnmount(){
1079
+ try {
1080
+ this.editableDiv.removeEventListener('keydown',this.handleKeyDown);
1081
+ this.editableDiv.removeEventListener('keyup',this.handleKeyUp);
1082
+ } catch (error) {
1083
+ console.error(error)
1084
+ }
1085
+ }
1086
+
1087
+ componentDidUpdate() {
1088
+ try {
1089
+ // console.log("Component updated:")
1090
+ // console.log(this.editableDiv.childNodes)
1091
+ if (this.rangeParamsToSetAfterComponentUpdate !== null &&
1092
+ this.rangeParamsToSetAfterComponentUpdate !== undefined ) {
1093
+ this._setRangeParams(this.rangeParamsToSetAfterComponentUpdate)
1094
+ this.rangeParamsToSetAfterComponentUpdate = null
1095
+ this.updateActiveButtons()
1096
+ }
1097
+ } catch (error) {
1098
+ console.error(error)
1099
+ }
1100
+ }
1101
+
1102
+ showEquationEditor = (moveCursorOnInsert=false, node=null) => {
1103
+ try {
1104
+ let rangeParams = this._getRangeParams()
1105
+ if (rangeParams.startNodeIndex < 0) {
1106
+ console.error("Could not find position of cursor in node list")
1107
+ console.log(rangeParams)
1108
+ console.log(this.editableDiv.childNodes)
1109
+
1110
+ this.setOldRangeParams({startNodeIndex:2, startOffset:1,
1111
+ endNodeIndex:2, endOffset:1})
1112
+ this.markedRawText = MARK + " " + MARK
1113
+ this.moveCursorOnInsert = moveCursorOnInsert
1114
+ this.setState({
1115
+ showEquationEditor:true,
1116
+ equationEditorLatex:""
1117
+ });
1118
+ return
1119
+ }
1120
+
1121
+ let latex = ""
1122
+ let offset = 0
1123
+
1124
+ if (node === null)
1125
+ node = this._findNodeWithIndex(rangeParams.startNodeIndex)
1126
+
1127
+ // console.log(rangeParams)
1128
+ // console.log(this.editableDiv.childNodes)
1129
+ // console.log(node)
1130
+
1131
+ if (node === null || node === undefined) {
1132
+ console.error("Could not find node with index: "+rangeParams.startNodeIndex)
1133
+ console.log("Child nodes:")
1134
+ console.log(this.editableDiv.childNodes)
1135
+ node = this._findFirstTextNode()
1136
+ offset = 0
1137
+ }
1138
+
1139
+ if (isKatexNode(node)){
1140
+ latex = getLatex(node)
1141
+ offset = 0
1142
+ this.editingExistingEquation = true
1143
+ this.editingNode = node
1144
+ } else {
1145
+ latex = ""
1146
+ offset = rangeParams.startOffset
1147
+ this.editingExistingEquation = false
1148
+ this.editingNode = null
1149
+ }
1150
+
1151
+ this.markedRawText = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1152
+ // console.log("Marked text:"+this.markedRawText)
1153
+ this.moveCursorOnInsert = moveCursorOnInsert
1154
+ this.setState({
1155
+ showEquationEditor:true,
1156
+ equationEditorLatex:latex
1157
+ });
1158
+ } catch (error) {
1159
+ console.error(error)
1160
+ }
1161
+ }
1162
+
1163
+ handleMouseDown = (event) => {
1164
+ this.showEquationEditorOnMouseUp = false
1165
+ let node = null
1166
+ try {
1167
+ node = event.target
1168
+ let isLatex = false
1169
+ do {
1170
+ if (isKatexNode(node)) {
1171
+ isLatex = true
1172
+ break
1173
+ }
1174
+ node = node.parentNode
1175
+ } while (node !== this.editableDiv && node !== null)
1176
+
1177
+ if (!isLatex)
1178
+ return
1179
+
1180
+ this.showEquationEditorOnMouseUp = true
1181
+ // Save current range parameters so that we can restore them later if we wish
1182
+ this.setOldRangeParams(this._getRangeParams())
1183
+
1184
+ // this.editableDiv.focus()
1185
+ // this.showEquationEditor(false, node)
1186
+
1187
+ // event.preventDefault()
1188
+ } catch (error) {
1189
+ console.error(error)
1190
+ console.log(node)
1191
+ }
1192
+ }
1193
+
1194
+ handleMouseUp = (event) => {
1195
+ try {
1196
+ this.initialiseHistory()
1197
+
1198
+ let params = this._getRangeParams()
1199
+ let startNode = this._findNodeWithIndex(params.startNodeIndex)
1200
+ let endNode = this._findNodeWithIndex(params.endNodeIndex)
1201
+
1202
+ if (startNode === null || endNode === null) {
1203
+ console.warn("Start or end node was null")
1204
+ return
1205
+ }
1206
+
1207
+ if (params.startNodeIndex === params.endNodeIndex &&
1208
+ params.startOffset === params.endOffset &&
1209
+ this.showEquationEditorOnMouseUp) {
1210
+ this.editableDiv.focus()
1211
+ this.showEquationEditor(false, startNode)
1212
+ return
1213
+ }
1214
+
1215
+ // If our range starts or ends inside a katex node, move it out
1216
+ let new_rangeParams = null
1217
+ if (isKatexNode(startNode)) {
1218
+ if (new_rangeParams === null)
1219
+ new_rangeParams = {...params}
1220
+ if (params.startNodeIndex === params.endNodeIndex &&
1221
+ params.startOffset === params.endOffset) {
1222
+ new_rangeParams.startNodeIndex = params.startNodeIndex + 1
1223
+ new_rangeParams.startOffset = SMALL_SPACE_LENGTH
1224
+ } else {
1225
+ new_rangeParams.startNodeIndex = params.startNodeIndex - 1
1226
+ let prev_node = this._findNodeWithIndex(params.startNodeIndex - 1)
1227
+ new_rangeParams.startOffset = getNodeTextLength(prev_node)
1228
+ }
1229
+ }
1230
+ if (isKatexNode(endNode)) {
1231
+ if (new_rangeParams === null)
1232
+ new_rangeParams = {...params}
1233
+ new_rangeParams.endNodeIndex = params.endNodeIndex + 1
1234
+ new_rangeParams.endOffset = SMALL_SPACE_LENGTH
1235
+ }
1236
+
1237
+ if (new_rangeParams !== null) {
1238
+ this._setRangeParams(new_rangeParams)
1239
+ event.preventDefault()
1240
+ return
1241
+ }
1242
+
1243
+ this.updateActiveButtons()
1244
+ } catch (error) {
1245
+ console.error(error)
1246
+ }
1247
+
1248
+ }
1249
+
1250
+ handleEquationEditorClose = () => {
1251
+ try {
1252
+ this.setState({showEquationEditor:false}, () => {
1253
+ this.editableDiv.focus()
1254
+ // this._setRangeParams(getOldRangeParams)
1255
+ })
1256
+ } catch (error) {
1257
+ console.error(error)
1258
+ }
1259
+ }
1260
+
1261
+ handleEquationEditorInsert = (latex) => {
1262
+ try {
1263
+ // console.log("Inserting latex: "+latex)
1264
+
1265
+ let start_pos = this.markedRawText.indexOf(MARK)
1266
+ let end_pos = this.markedRawText.indexOf(MARK, start_pos + MARK.length)
1267
+ let oldRangeParams = this.getOldRangeParams()
1268
+
1269
+ // Handle case of empty string returned
1270
+ if (latex === "") {
1271
+ // console.log("Empty latex returned from equation editor")
1272
+ let new_raw_text =
1273
+ this.markedRawText.substring(0,start_pos) +
1274
+ this.markedRawText.substring(end_pos + MARK.length)
1275
+
1276
+ // Delete node if it was a latex node
1277
+ let new_index = -1
1278
+ let new_offset = -1
1279
+ if (this.editingExistingEquation) {
1280
+
1281
+ let indexOfEditedNode = this._findIndexOfNode(this.editingNode)
1282
+
1283
+ if (oldRangeParams.startNodeIndex === indexOfEditedNode + 1) {
1284
+ new_index = oldRangeParams.startNodeIndex-2
1285
+ new_offset = getNodeTextLength(this._findNodeWithIndex(new_index))
1286
+ } else if (oldRangeParams.startNodeIndex > indexOfEditedNode + 1) {
1287
+ new_index = oldRangeParams.startNodeIndex-2
1288
+ new_offset = oldRangeParams.startOffset
1289
+ } else {
1290
+ new_index = oldRangeParams.startNodeIndex
1291
+ new_offset = oldRangeParams.startOffset
1292
+ }
1293
+ }
1294
+
1295
+ let new_rangeParams = {
1296
+ startNodeIndex:new_index,
1297
+ startOffset:new_offset,
1298
+ endNodeIndex:new_index,
1299
+ endOffset:new_offset}
1300
+ this.setOldRangeParams(new_rangeParams)
1301
+
1302
+ let nodeCounts = this._countNodeTypes()
1303
+ let mimeType = null
1304
+ if (this.editingExistingEquation)
1305
+ mimeType = this.getMimeType(nodeCounts.html > 0, nodeCounts.katex > 1)
1306
+ else
1307
+ mimeType = this.getMimeType(nodeCounts.html > 0, nodeCounts.katex > 0)
1308
+
1309
+ this.applyChangesToComponent(new_raw_text, mimeType, new_rangeParams,
1310
+ this.props.useExpertMode, this.props.selectedTab)
1311
+ this.setState({showEquationEditor:false})
1312
+ return
1313
+ }
1314
+
1315
+ // Handle case of latex returned, in which case we update the katex node
1316
+ // console.log("Latex returned from equation editor: "+latex)
1317
+ let new_raw_text = replaceMarksWithMath(this.markedRawText, latex, this.enableHtml())
1318
+ // let new_raw_text =
1319
+ // this.markedRawText.substring(0,start_pos) +
1320
+ // RAW_TEXT_MATH_START_TAG + latex + RAW_TEXT_MATH_END_TAG +
1321
+ // this.markedRawText.substring(end_pos + MARK.length)
1322
+
1323
+ console.log("Old cursor pos: "+oldRangeParams.startNodeIndex+"->"+oldRangeParams.startOffset)
1324
+ console.log("Old raw text: "+this.props.value)
1325
+ console.log("Old raw text length: "+this.props.value.length)
1326
+ console.log("Old marked raw text:"+this.markedRawText)
1327
+ console.log("New raw text: "+new_raw_text)
1328
+
1329
+ let new_rangeParams = this.getOldRangeParams()
1330
+ if (this.moveCursorOnInsert) {
1331
+ if (this.props.value === "") // Special case of previously empty text input
1332
+ new_rangeParams = {
1333
+ startNodeIndex:2,
1334
+ startOffset:SMALL_SPACE_LENGTH,
1335
+ endNodeIndex:2,
1336
+ endOffset:SMALL_SPACE_LENGTH }
1337
+ else // normal case
1338
+ new_rangeParams = {
1339
+ startNodeIndex:oldRangeParams.startNodeIndex+2,
1340
+ startOffset:SMALL_SPACE_LENGTH,
1341
+ endNodeIndex:oldRangeParams.startNodeIndex+2,
1342
+ endOffset:SMALL_SPACE_LENGTH }
1343
+ }
1344
+ // console.log("New cursor pos: "+new_rangeParams.startNodeIndex+"->"+new_rangeParams.startOffset)
1345
+ this.setOldRangeParams(new_rangeParams)
1346
+
1347
+ let nodeCounts = this._countNodeTypes()
1348
+ let mimeType = this.getMimeType(nodeCounts.html > 0, true)
1349
+
1350
+ this.applyChangesToComponent(new_raw_text, mimeType, new_rangeParams,
1351
+ this.props.useExpertMode, this.props.selectedTab)
1352
+ this.setState({showEquationEditor:false})
1353
+ } catch (error) {
1354
+ console.error(error)
1355
+ }
1356
+ }
1357
+
1358
+ // findEditableDiv = (node) => {
1359
+ // if (node === null)
1360
+ // return null
1361
+ // if (node.classList !== null && node.classList !== undefined &&
1362
+ // node.classList.contains("MathRichInput"))
1363
+ // return node
1364
+ // return this.findEditableDiv(node.parentNode);
1365
+ // }
1366
+
1367
+ styleText = (event, style) => {
1368
+ try {
1369
+ style = style.toLowerCase()
1370
+ let success = document.execCommand(style,false, null)
1371
+ let buttonState = this.state.activeButtons[style]
1372
+ if (buttonState === null || buttonState === undefined)
1373
+ return success
1374
+
1375
+ buttonState = !buttonState
1376
+ let new_activeButtons = {...this.state.activeButtons}
1377
+ new_activeButtons[style] = buttonState
1378
+ this.setState({activeButtons:new_activeButtons})
1379
+ return success
1380
+ } catch (error) {
1381
+ console.error(error)
1382
+ }
1383
+ }
1384
+
1385
+ handleToolbarButtonClick = (event, buttonName) => {
1386
+ console.log("button name: ",buttonName)
1387
+ try {
1388
+ if (buttonName === "Equation") {
1389
+ this.showEquationEditor(true)
1390
+ }
1391
+ let style = buttonName
1392
+ let success = this.styleText(event, style)
1393
+ if (!success)
1394
+ console.warn("execCommand returned false: "+style)
1395
+ } catch (error) {
1396
+ console.error(error)
1397
+ }
1398
+ }
1399
+
1400
+ fetchAccentLetter = (event) => {
1401
+ try {
1402
+ let rangeParams = this._getRangeParams()
1403
+ if (rangeParams === null) {
1404
+ console.error("Cound not get range params")
1405
+ return null
1406
+ }
1407
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1408
+ let offset = rangeParams.endOffset
1409
+
1410
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1411
+
1412
+ return getCharacterBeforeMarks(raw_text)
1413
+ } catch (error) {
1414
+ console.error(error)
1415
+ }
1416
+ }
1417
+
1418
+ insertRawText = (event, new_text) => {
1419
+ try {
1420
+ let rangeParams = this._getRangeParams()
1421
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1422
+ let offset = rangeParams.endOffset
1423
+
1424
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1425
+ let new_raw_text = null
1426
+ let new_rangeParams = {...rangeParams}
1427
+ new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, new_text))
1428
+ if (rangeParams.startNodeIndex === rangeParams.endNodeIndex &&
1429
+ rangeParams.startOffset === rangeParams.endOffset)
1430
+ new_rangeParams.startOffset = new_rangeParams.startOffset + new_text.length
1431
+ new_rangeParams.endOffset = new_rangeParams.endOffset + new_text.length
1432
+
1433
+ this.applyChangesToComponent(new_raw_text, this.state.mimeType, new_rangeParams,
1434
+ this.props.useExpertMode, this.props.selectedTab)
1435
+ } catch (error) {
1436
+ console.error(error)
1437
+ }
1438
+ }
1439
+
1440
+ removeNewLine = () => {
1441
+ try {
1442
+ let rangeParams = this._getRangeParams()
1443
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1444
+ let prevNode = this._findNodeWithIndex(rangeParams.endNodeIndex-1)
1445
+ let offset = rangeParams.endOffset
1446
+
1447
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1448
+ let new_raw_text = null
1449
+ new_raw_text = removeMarks(replaceStringBeforeMarks(raw_text, "</p><p>",""))
1450
+
1451
+ let new_rangeParams = {...rangeParams}
1452
+ let new_offset = getNodeTextLength(prevNode)
1453
+ new_rangeParams.startNodeIndex = rangeParams.startNodeIndex - 1
1454
+ new_rangeParams.startOffset = new_offset
1455
+ new_rangeParams.endNodeIndex = rangeParams.endNodeIndex - 1
1456
+ new_rangeParams.endOffset = new_offset
1457
+
1458
+ console.log("About to apply new range params")
1459
+ console.log(new_rangeParams)
1460
+ console.log("Old nodes")
1461
+ console.log(this.editableDiv.childNodes)
1462
+
1463
+ this.applyChangesToComponent(new_raw_text, this.state.mimeType, new_rangeParams,
1464
+ this.props.useExpertMode, this.props.selectedTab)
1465
+ } catch (error) {
1466
+ console.error(error)
1467
+ }
1468
+ }
1469
+
1470
+ insertNewLine = () => {
1471
+ try {
1472
+ let rangeParams = this._getRangeParams()
1473
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1474
+ let offset = rangeParams.endOffset
1475
+
1476
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1477
+ let new_raw_text = null
1478
+ let new_rangeParams = {...rangeParams}
1479
+
1480
+ if (this.enableHtml()) {
1481
+ new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, "</p><p>"))
1482
+ if (new_raw_text.substring(0,3).toLowerCase() !== "<p>")
1483
+ new_raw_text = "<p>" + new_raw_text + "</p>"
1484
+
1485
+ new_rangeParams.startNodeIndex = rangeParams.startNodeIndex + 1
1486
+ new_rangeParams.startOffset = 0
1487
+ new_rangeParams.endNodeIndex = rangeParams.endNodeIndex + 1
1488
+ new_rangeParams.endOffset = 0
1489
+ } else {
1490
+ new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, "\n"))
1491
+ new_rangeParams.startOffset ++
1492
+ new_rangeParams.endOffset ++
1493
+ }
1494
+
1495
+
1496
+ console.log("About to apply new range params")
1497
+ console.log(new_rangeParams)
1498
+ console.log("Old nodes")
1499
+ console.log(this.editableDiv.childNodes)
1500
+
1501
+ this.applyChangesToComponent(new_raw_text, this.state.mimeType, new_rangeParams,
1502
+ this.props.useExpertMode, this.props.selectedTab)
1503
+ } catch (error) {
1504
+ console.error(error)
1505
+ }
1506
+ }
1507
+
1508
+ replaceNextLetter = (new_char) => {
1509
+ try {
1510
+ console.log("Replace next letter")
1511
+ let rangeParams = this._getRangeParams()
1512
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1513
+ let offset = rangeParams.endOffset+1 // next letter
1514
+
1515
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1516
+ let new_raw_text = null
1517
+ let new_rangeParams = {...rangeParams}
1518
+ new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, new_char))
1519
+ if (rangeParams.startNodeIndex === rangeParams.endNodeIndex &&
1520
+ rangeParams.startOffset === rangeParams.endOffset)
1521
+ new_rangeParams.startOffset = new_rangeParams.startOffset + new_char.length + 1
1522
+ new_rangeParams.endOffset = new_rangeParams.endOffset + new_char.length + 1
1523
+
1524
+ this.applyChangesToComponent(new_raw_text, this.state.mimeType, new_rangeParams,
1525
+ this.props.useExpertMode, this.props.selectedTab)
1526
+ } catch (error) {
1527
+ console.error(error)
1528
+ }
1529
+ }
1530
+
1531
+ insertAccentLetter = (event, old_char, new_char) => {
1532
+ try {
1533
+ let rangeParams = this._getRangeParams()
1534
+ let node = this._findNodeWithIndex(rangeParams.endNodeIndex)
1535
+ let offset = rangeParams.endOffset
1536
+
1537
+ let raw_text = elementToMarkedRawText(this.editableDiv, node, offset, this.enableHtml())
1538
+ let new_raw_text = null
1539
+ let new_rangeParams = {...rangeParams}
1540
+ if (old_char === null) {
1541
+ new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, new_char))
1542
+ if (rangeParams.startNodeIndex === rangeParams.endNodeIndex &&
1543
+ rangeParams.startOffset === rangeParams.endOffset)
1544
+ new_rangeParams.startOffset = new_rangeParams.startOffset + new_char.length
1545
+ new_rangeParams.endOffset = new_rangeParams.endOffset + new_char.length
1546
+ }
1547
+ else
1548
+ new_raw_text = removeMarks(replaceCharacterBeforeMarks(raw_text, new_char))
1549
+
1550
+ this.applyChangesToComponent(new_raw_text, this.state.mimeType, new_rangeParams,
1551
+ this.props.useExpertMode, this.props.selectedTab)
1552
+ } catch (error) {
1553
+ console.error(error)
1554
+ }
1555
+ }
1556
+
1557
+ handleAccentButtonClick = (event, oldCharacter, newCharacter) => {
1558
+ try {
1559
+ this.insertAccentLetter(event, oldCharacter, newCharacter)
1560
+ this.setState({showAccentBar:false})
1561
+ } catch (error) {
1562
+ console.error(error)
1563
+ }
1564
+ }
1565
+
1566
+
1567
+ handleFocus = (event) => {
1568
+ // console.log("handleFocus")
1569
+ try {
1570
+ this._setRangeParams(this.getOldRangeParams())
1571
+ this.setState({hasFocus:true})
1572
+ } catch (error) {
1573
+ console.error(error)
1574
+ }
1575
+ }
1576
+
1577
+ handleBlur = (event) => {
1578
+ try {
1579
+ this.setState({hasFocus:false, showAccentBar:false})
1580
+ } catch (error) {
1581
+ console.error(error)
1582
+ }
1583
+ }
1584
+
1585
+ render() {
1586
+ console.log("Rendering MathRichInput. Latex: "+this.props.value)
1587
+
1588
+ if (this.props.value === undefined ) {
1589
+ console.error("MathRichInput must have prop called \"text\"")
1590
+ console.log(this.props)
1591
+ }
1592
+
1593
+ const html = rawTextToHtml(katex, this.props.value || "", this.props.mimeType);
1594
+
1595
+ let useClassName = "MathRichInput-Container" // Don't change this name - used by various components to find node
1596
+ if (this.props.className)
1597
+ useClassName = this.props.className + " " + useClassName
1598
+
1599
+ let accentLetter = null
1600
+ if (this.state.showAccentBar)
1601
+ accentLetter = this.fetchAccentLetter(null)
1602
+
1603
+ return <>
1604
+ <div className={useClassName}>
1605
+ <div className="MathRichInput-scrollview">
1606
+ {this.state.hasFocus &&
1607
+ <Toolbar
1608
+ className="Toolbar"
1609
+ enableMath={this.enableMath()}
1610
+ enableFontStyling={this.enableFontStyling()}
1611
+ style={this.translateToolbar}
1612
+ keyPressed={this.state.keyPressed}
1613
+ activeButtons={this.state.activeButtons}
1614
+ onButtonClick={this.handleToolbarButtonClick}
1615
+ fetchAccentLetter={this.fetchAccentLetter}
1616
+ insertAccentLetter={this.insertAccentLetter}
1617
+ />
1618
+ }
1619
+ {this.state.showAccentBar &&
1620
+ <AccentBar
1621
+ compactMode={true}
1622
+ accentLetter={accentLetter}
1623
+ keyPressed={this.state.keyPressed}
1624
+ fetchAccentLetter={this.fetchAccentLetter}
1625
+ onButtonClick={(e, oldCharacter, newCharacter) =>
1626
+ this.handleAccentButtonClick(e, oldCharacter, newCharacter)}/>
1627
+ }
1628
+ <span
1629
+ className="MathRichInput" // Don't change this name - used to various components to find node
1630
+ contentEditable="true"
1631
+ ref={(r) => this.editableDiv = r}
1632
+ onInput={this.handleInput}
1633
+ onMouseDown={this.handleMouseDown}
1634
+ onMouseUp={this.handleMouseUp}
1635
+ onFocus={this.handleFocus}
1636
+ onBlur={this.handleBlur}
1637
+ dangerouslySetInnerHTML={{__html: html}}
1638
+ >
1639
+ </span>
1640
+ </div>
1641
+ </div>
1642
+ { this.state.showEquationEditor &&
1643
+ <EquationEditorModal
1644
+ ref={(r) => this.equationEditorModal = r}
1645
+ handleClose={this.handleEquationEditorClose}
1646
+ handleInsert={this.handleEquationEditorInsert}
1647
+ latex={this.state.equationEditorLatex}/>
1648
+ }
1649
+ </>
1650
+ }
1651
+ }