@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.
- package/README.md +253 -0
- package/babel.config.js +6 -0
- package/dist/index.js +23034 -0
- package/dist/math-rich-input.css +923 -0
- package/package.json +44 -0
- package/rollup.config.js +31 -0
- package/src/AccentBar.css +169 -0
- package/src/AccentBar.jsx +771 -0
- package/src/EquationEditor.css +258 -0
- package/src/EquationEditor.jsx +583 -0
- package/src/EquationEditorModal.css +164 -0
- package/src/EquationEditorModal.jsx +81 -0
- package/src/MathRichArea.jsx +103 -0
- package/src/MathRichInput.css +63 -0
- package/src/MathRichInput.jsx +1651 -0
- package/src/SymbolButton.css +130 -0
- package/src/SymbolButton.jsx +96 -0
- package/src/Toolbar.css +142 -0
- package/src/Toolbar.jsx +219 -0
- package/src/equationEditorButtonsHelper.js +590 -0
- package/src/index.js +5 -0
- package/src/katexHelper.js +39 -0
- package/src/mathRichInputHelper.js +785 -0
- package/src/mimeTypeHelper.js +201 -0
|
@@ -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
|
+
}
|