87 * @type {Object} |
25 * @type {Object} |
88 */ |
26 */ |
89 constructor(props) { |
27 constructor(props) { |
90 super(props); |
28 super(props); |
91 |
29 |
92 const annotationPlugin = AnnotationPlugin({ |
|
93 onChange: (text, start, end) => { |
|
94 this.setState({ |
|
95 currentSelectionText: text, |
|
96 currentSelectionStart: start, |
|
97 currentSelectionEnd: end, |
|
98 }); |
|
99 } |
|
100 }); |
|
101 |
|
102 // plugins.push(annotationPlugin); |
|
103 |
|
104 |
|
105 this.state = { |
30 this.state = { |
106 value: props.note ? Value.fromJSON(initialValue) : Plain.deserialize(''), |
31 value: props.note ? Value.fromJSON(JSON.parse(props.note.raw)) : Plain.deserialize(''), |
107 startedAt: null, |
32 startedAt: null, |
108 finishedAt: null, |
33 finishedAt: null, |
109 currentSelectionText: '', |
|
110 currentSelectionStart: 0, |
|
111 currentSelectionEnd: 0, |
|
112 hoveringMenu: null, |
|
113 isPortalOpen: false, |
|
114 categories: [], |
|
115 isCheckboxChecked: false, |
|
116 enterKeyValue: 0, |
34 enterKeyValue: 0, |
117 }; |
35 }; |
118 |
36 |
119 this.editorRef = React.createRef(); |
37 this.editorRef = React.createRef(); |
120 this.hoveringMenuRef = React.createRef(); |
|
121 } |
38 } |
122 |
39 |
123 get editor() { |
40 get editor() { |
124 if(this.editorRef) { |
41 if(this.editorRef) { |
125 return this.editorRef.current; |
42 return this.editorRef.current; |
126 } |
43 } |
127 return null; |
44 return null; |
128 } |
45 } |
129 |
46 |
130 componentDidMount = () => { |
47 componentDidMount = () => { |
131 this.updateMenu(); |
|
132 this.focus(); |
48 this.focus(); |
133 } |
|
134 |
|
135 componentDidUpdate = () => { |
|
136 this.updateMenu(); |
|
137 } |
|
138 |
|
139 getDocumentLength = (document) => { |
|
140 return document.getBlocks().reduce((l, b) => l + b.text.length, 0) |
|
141 } |
49 } |
142 |
50 |
143 /** |
51 /** |
144 * On change, save the new state. |
52 * On change, save the new state. |
145 * |
53 * |
146 * @param {Change} change |
54 * @param {Change} change |
147 */ |
55 */ |
148 |
56 |
149 onChange = (change) => { |
57 onChange = ({value, operations}) => { |
150 |
58 |
151 const operationTypes = (change && change.operations) ? change.operations.map((o) => o.type).toArray() : []; |
59 const oldState = R.clone(this.state); |
152 console.log("CHANGE", change, operationTypes); |
60 |
153 const { value } = change; |
61 const newState = { |
154 |
62 value |
155 let newState = { |
|
156 value: value, |
|
157 startedAt: this.state.startedAt |
|
158 }; |
63 }; |
159 |
64 |
160 const isEmpty = this.getDocumentLength(value.document) === 0; |
65 (operations || []).some((op) => { |
161 |
66 if(['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'set_mark', 'insert_node', 'merge_node', 'move_node', 'remove_node', 'set_node', 'split_node'].indexOf(op.type)>=0) { |
162 // Reset timers when the text is empty |
67 const tsnow = now(); |
163 if (isEmpty) { |
68 if(this.state.startedAt == null) { |
164 Object.assign(newState, { |
69 newState.startedAt = tsnow; |
165 startedAt: null, |
70 } |
166 finishedAt: null |
71 newState.finishedAt = tsnow; |
167 }); |
72 return true; |
168 } else { |
73 } |
169 Object.assign(newState, { finishedAt: now() }); |
74 return false; |
170 } |
75 }); |
171 |
|
172 // Store start time once when the first character is typed |
|
173 if (!isEmpty && this.state.startedAt === null) { |
|
174 Object.assign(newState, { startedAt: now() }); |
|
175 } |
|
176 |
|
177 const oldState = R.clone(this.state); |
|
178 |
|
179 const categories = value.marks.reduce((acc, mark) => { |
|
180 if(mark.type === 'category') { |
|
181 acc.push({ |
|
182 key: mark.data.get('key'), |
|
183 name: mark.data.get('name'), |
|
184 color: mark.data.get('color'), |
|
185 text: mark.data.get('text'), |
|
186 selection: { |
|
187 start: mark.data.get('selection').start, |
|
188 end: mark.data.get('selection').end, |
|
189 }, |
|
190 comment: mark.data.get('comment') |
|
191 }) |
|
192 } |
|
193 return acc; |
|
194 }, |
|
195 []); |
|
196 |
|
197 console.log("ON CHANGE categorie", categories); |
|
198 |
|
199 newState['categories'] = categories; |
|
200 |
76 |
201 this.setState(newState, () => { |
77 this.setState(newState, () => { |
202 if (typeof this.props.onChange === 'function') { |
78 if (typeof this.props.onChange === 'function') { |
203 this.props.onChange(R.clone(this.state), oldState, newState); |
79 this.props.onChange(R.clone(this.state), oldState, {value, operations}); |
204 } |
80 } |
205 }) |
81 }) |
206 |
82 } |
207 } |
83 |
208 |
|
209 /** |
|
210 * Check if the current selection has a mark with `type` in it. |
|
211 * |
|
212 * @param {String} type |
|
213 * @return {Boolean} |
|
214 */ |
|
215 |
|
216 hasMark = type => { |
|
217 const { value } = this.state; |
|
218 return value.activeMarks.some(mark => mark.type === type); |
|
219 } |
|
220 |
|
221 /** |
|
222 * Check if the any of the currently selected blocks are of `type`. |
|
223 * |
|
224 * @param {String} type |
|
225 * @return {Boolean} |
|
226 */ |
|
227 |
|
228 hasBlock = type => { |
|
229 const { value } = this.state |
|
230 return value.blocks.some(node => node.type === type) |
|
231 } |
|
232 |
84 |
233 asPlain = () => { |
85 asPlain = () => { |
234 return Plain.serialize(this.state.value); |
86 return Plain.serialize(this.state.value); |
235 } |
87 } |
236 |
88 |
241 asHtml = () => { |
93 asHtml = () => { |
242 return HtmlSerializer.serialize(this.state.value); |
94 return HtmlSerializer.serialize(this.state.value); |
243 } |
95 } |
244 |
96 |
245 asCategories = () => { |
97 asCategories = () => { |
246 return this.state.categories |
98 return this.state.value.document.getMarksByType('category').map((mark) => mark.data.toJS()).toArray(); |
247 } |
|
248 |
|
249 removeCategory = (categories, key, text) => { |
|
250 const categoryIndex = categories.findIndex(category => category.key === key && category.text === text) |
|
251 return categories.delete(categoryIndex) |
|
252 } |
99 } |
253 |
100 |
254 clear = () => { |
101 clear = () => { |
255 const value = Plain.deserialize(''); |
102 const value = Plain.deserialize(''); |
256 this.onChange({ |
103 this.setState({ |
257 value, |
104 value, |
258 }); |
105 enterKeyValue: 0 |
|
106 }) |
259 } |
107 } |
260 |
108 |
261 focus = () => { |
109 focus = () => { |
262 if(this.editor) { |
110 if(this.editor) { |
263 this.editor.focus(); |
111 this.editor.focus(); |
264 } |
112 } |
265 } |
113 } |
266 |
114 |
267 onClickCategoryButton = (openPortal, closePortal, isOpen, e) => { |
115 submitNote = () => { |
268 e.preventDefault(); |
116 this.setState({ enterKeyValue: 0 }, () => { |
269 const { categories, value } = this.state |
117 if (typeof this.props.submitNote === 'function') { |
270 |
118 this.props.submitNote(); |
271 let newCategories = categories.slice(0); |
|
272 |
|
273 // Can't use toggleMark here, because it expects the same object |
|
274 // @see https://github.com/ianstormtaylor/slate/issues/873 |
|
275 if (this.hasMark('category')) { |
|
276 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
|
277 categoryMarks.forEach(mark => { |
|
278 const key = mark.data.get('key'); |
|
279 const text = mark.data.get('text'); |
|
280 |
|
281 newCategories = R.reject(category => category.key === key && category.text === text, newCategories); |
|
282 this.editor.removeMark(mark) |
|
283 }) |
|
284 this.setState({ |
|
285 value: this.editor.value, |
|
286 categories: newCategories |
|
287 }); |
|
288 closePortal(); |
|
289 } else { |
|
290 openPortal(); |
|
291 } |
|
292 // } else { |
|
293 // isOpen ? closePortal() : openPortal(); |
|
294 // } |
|
295 } |
|
296 |
|
297 /** |
|
298 * When a mark button is clicked, toggle the current mark. |
|
299 * |
|
300 * @param {Event} e |
|
301 * @param {String} type |
|
302 */ |
|
303 |
|
304 onClickMark = (e, type) => { |
|
305 this.editor.toggleMark(type) |
|
306 } |
|
307 |
|
308 /** |
|
309 * When a block button is clicked, toggle the block type. |
|
310 * |
|
311 * @param {Event} e |
|
312 * @param {String} type |
|
313 */ |
|
314 |
|
315 onClickBlock = (e, type) => { |
|
316 e.preventDefault() |
|
317 |
|
318 const { editor } = this; |
|
319 const { value } = editor; |
|
320 const { document } = value; |
|
321 |
|
322 // Handle everything but list buttons. |
|
323 if (type !== 'bulleted-list' && type !== 'numbered-list') { |
|
324 const isActive = this.hasBlock(type) |
|
325 const isList = this.hasBlock('list-item') |
|
326 |
|
327 if (isList) { |
|
328 editor |
|
329 .setBlocks(isActive ? DEFAULT_NODE : type) |
|
330 .unwrapBlock('bulleted-list') |
|
331 .unwrapBlock('numbered-list') |
|
332 } |
|
333 |
|
334 else { |
|
335 editor |
|
336 .setBlocks(isActive ? DEFAULT_NODE : type) |
|
337 } |
|
338 } |
|
339 |
|
340 // Handle the extra wrapping required for list buttons. |
|
341 else { |
|
342 const isList = this.hasBlock('list-item') |
|
343 const isType = value.blocks.some((block) => { |
|
344 return !!document.getClosest(block.key, parent => parent.type === type) |
|
345 }) |
|
346 |
|
347 if (isList && isType) { |
|
348 editor |
|
349 .setBlocks(DEFAULT_NODE) |
|
350 .unwrapBlock('bulleted-list') |
|
351 .unwrapBlock('numbered-list') |
|
352 |
|
353 } else if (isList) { |
|
354 editor |
|
355 .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') |
|
356 .wrapBlock(type) |
|
357 |
|
358 } else { |
|
359 editor |
|
360 .setBlocks('list-item') |
|
361 .wrapBlock(type) |
|
362 |
|
363 } |
|
364 } |
|
365 // this.onChange(change) |
|
366 } |
|
367 |
|
368 onPortalOpen = () => { |
|
369 console.log("onPORTAL OPEN", this); |
|
370 this.updateMenu(); |
|
371 // When the portal opens, cache the menu element. |
|
372 // this.setState({ hoveringMenu: this.portal.firstChild }) |
|
373 } |
|
374 |
|
375 onPortalClose = (portal) => { |
|
376 console.log("onPORTAL CLOSE", this); |
|
377 // let { value } = this.state |
|
378 |
|
379 // this.setState({ |
|
380 // value: value.change, |
|
381 // isPortalOpen: false |
|
382 // }) |
|
383 } |
|
384 |
|
385 getSelectionParams = () => { |
|
386 |
|
387 const { value } = this.editor |
|
388 const { selection } = value |
|
389 const { start, end} = selection |
|
390 |
|
391 if (selection.isCollapsed) { |
|
392 return {}; |
|
393 } |
|
394 |
|
395 const nodes = []; |
|
396 let hasStarted = false; |
|
397 let hasEnded = false; |
|
398 |
|
399 // Keep only the relevant nodes, |
|
400 // i.e. nodes which are contained within selection |
|
401 value.document.nodes.forEach((node) => { |
|
402 if (start.isInNode(node)) { |
|
403 hasStarted = true; |
|
404 } |
|
405 if (hasStarted && !hasEnded) { |
|
406 nodes.push(node); |
|
407 } |
|
408 if (end.isAtEndOfNode(node)) { |
|
409 hasEnded = true; |
|
410 } |
119 } |
411 }); |
120 }); |
412 |
|
413 // Concatenate the nodes text |
|
414 const text = nodes.map((node) => { |
|
415 let textStart = start.isInNode(node) ? start.offset : 0; |
|
416 let textEnd = end.isInNode(node) ? end.offset : node.text.length; |
|
417 return node.text.substring(textStart,textEnd); |
|
418 }).join('\n'); |
|
419 |
|
420 return { |
|
421 currentSelectionText: text, |
|
422 currentSelectionStart: start.offset, |
|
423 currentSelectionEnd: end.offset |
|
424 }; |
|
425 } |
|
426 |
|
427 onCategoryClick = (closePortal, category) => { |
|
428 |
|
429 console.log("ON CATEGORY CLICK"); |
|
430 const { value } = this.state; |
|
431 let { categories } = this.state; |
|
432 |
|
433 const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(); |
|
434 |
|
435 if(!currentSelectionText) { |
|
436 closePortal(); |
|
437 return; |
|
438 } |
|
439 console.log("ACTIVE MARKS", category, currentSelectionText, currentSelectionStart, currentSelectionEnd) |
|
440 |
|
441 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
|
442 categoryMarks.forEach(mark => this.editor.removeMark(mark)); |
|
443 |
|
444 this.editor.addMark({ |
|
445 type: 'category', |
|
446 data: { |
|
447 text: currentSelectionText, |
|
448 selection: { |
|
449 start: currentSelectionStart, |
|
450 end: currentSelectionEnd, |
|
451 }, |
|
452 color: category.color, |
|
453 key: category.key, |
|
454 name: category.name, |
|
455 comment: category.comment |
|
456 } |
|
457 }) |
|
458 |
|
459 Object.assign(category, { |
|
460 text: currentSelectionText, |
|
461 selection: { |
|
462 start: currentSelectionStart, |
|
463 end: currentSelectionEnd, |
|
464 }, |
|
465 }); |
|
466 categories.push(category); |
|
467 |
|
468 console.log("CATEGORIES", categories) |
|
469 |
|
470 this.setState({ |
|
471 categories: categories, |
|
472 value: this.editor.value |
|
473 }, closePortal); |
|
474 } |
|
475 |
|
476 onButtonClick = () => { |
|
477 if (typeof this.props.onButtonClick === 'function') { |
|
478 this.props.onButtonClick(); |
|
479 } |
|
480 } |
|
481 |
|
482 onCheckboxChange = (e) => { |
|
483 if (typeof this.props.onCheckboxChange === 'function') { |
|
484 this.props.onCheckboxChange(e); |
|
485 } |
|
486 } |
121 } |
487 |
122 |
488 /** |
123 /** |
489 * On key down, if it's a formatting command toggle a mark. |
124 * On key down, if it's a formatting command toggle a mark. |
490 * |
125 * |
491 * @param {Event} e |
126 * @param {Event} e |
492 * @param {Change} change |
127 * @param {Change} change |
493 * @return {Change} |
128 * @return {Change} |
494 */ |
129 */ |
495 |
130 |
496 onKeyDown = (e, change) => { |
131 onKeyUp = (e, editor, next) => { |
497 |
132 |
498 const {value} = this.state; |
133 const { value } = this.state; |
499 |
134 const noteText = value.document.text.trim(); |
500 if (e.key === 'Enter' && value.document.text !== '') { |
135 |
501 this.setState({enterKeyValue: 1}) |
136 if(e.key === "Enter" && noteText.length !== 0) { |
502 } |
137 this.setState({ enterKeyValue: this.state.enterKeyValue + 1 }); |
503 |
138 } else if ( e.getModifierState() || (e.key !== "Control" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Alt") ) { |
504 if (e.key !== 'Enter') { |
139 this.setState({ enterKeyValue: 0 }); |
505 this.setState({ |
140 } |
506 enterKeyValue: 0, |
141 return next(); |
507 }) |
142 } |
508 |
143 |
509 } |
144 /** |
510 |
145 * On key down, if it's a formatting command toggle a mark. |
511 //TODO review the double enter case. |
146 * |
512 if (e.key === 'Enter' && !this.props.isChecked && this.state.enterKeyValue === 1 && typeof this.props.onEnterKeyDown === 'function') { |
147 * @param {Event} e |
|
148 * @param {Change} change |
|
149 * @return {Change} |
|
150 */ |
|
151 |
|
152 onKeyDown = (e, editor, next) => { |
|
153 |
|
154 const { value, enterKeyValue } = this.state; |
|
155 const { autoSubmit } = this.props; |
|
156 const noteText = value.document.text.trim(); |
|
157 |
|
158 // we prevent empty first lines |
|
159 if(e.key === "Enter" && noteText.length === 0) { |
513 e.preventDefault(); |
160 e.preventDefault(); |
514 this.props.onEnterKeyDown(); |
161 return next(); |
515 this.setState({ |
162 } |
516 enterKeyValue: 0, |
163 |
517 }) |
164 // Enter submit the note |
518 |
165 if(e.key === "Enter" && ( enterKeyValue === 2 || e.ctrlKey || autoSubmit ) && noteText.length !== 0) { |
519 |
|
520 return change |
|
521 } |
|
522 |
|
523 else if (e.key === 'Enter' && value.document.text !== '' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') { |
|
524 |
|
525 e.preventDefault(); |
166 e.preventDefault(); |
526 this.props.onEnterKeyDown(); |
167 this.submitNote(); |
527 |
168 return next(); |
528 return change |
169 } |
529 } |
170 |
530 |
171 if (!e.ctrlKey) { |
531 if (!e.ctrlKey) return |
172 return next(); |
532 // Decide what to do based on the key code... |
173 } |
533 switch (e.key) { |
174 |
534 default: { |
175 e.preventDefault(); |
535 break; |
176 |
536 } |
177 // Decide what to do based on the key code... |
537 // When "B" is pressed, add a "bold" mark to the text. |
178 switch (e.key) { |
538 case 'b': { |
179 default: { |
539 e.preventDefault() |
180 break; |
540 change.toggleMark('bold') |
181 } |
541 |
182 // When "B" is pressed, add a "bold" mark to the text. |
542 return true |
183 case 'b': { |
543 } |
184 editor.toggleMark('bold'); |
544 case 'i': { |
185 break; |
545 // When "U" is pressed, add an "italic" mark to the text. |
186 } |
546 e.preventDefault() |
187 case 'i': { |
547 change.toggleMark('italic') |
188 // When "U" is pressed, add an "italic" mark to the text. |
548 |
189 editor.toggleMark('italic'); |
549 return true |
190 break; |
550 } |
191 } |
551 case 'u': { |
192 case 'u': { |
552 // When "U" is pressed, add an "underline" mark to the text. |
193 // When "U" is pressed, add an "underline" mark to the text. |
553 e.preventDefault() |
194 editor.toggleMark('underlined'); |
554 change.toggleMark('underlined') |
195 break; |
555 |
196 } |
556 return true |
197 } |
557 } |
198 |
558 case 'Enter': { |
199 return next(); |
559 // When "ENTER" is pressed, autosubmit the note. |
200 |
560 if (value.document.text !== '' && typeof this.props.onEnterKeyDown === 'function') { |
|
561 e.preventDefault() |
|
562 this.props.onEnterKeyDown(); |
|
563 this.setState({ |
|
564 enterKeyValue: 0, |
|
565 }) |
|
566 |
|
567 return true |
|
568 } |
|
569 } |
|
570 } |
|
571 } |
|
572 |
|
573 /** |
|
574 * Render. |
|
575 * |
|
576 * @return {Element} |
|
577 */ |
|
578 |
|
579 render = () => { |
|
580 return ( |
|
581 <div className="bg-secondary mb-5"> |
|
582 <div className="sticky-top"> |
|
583 {this.renderToolbar()} |
|
584 </div> |
|
585 {this.renderEditor()} |
|
586 </div> |
|
587 ) |
|
588 } |
|
589 |
|
590 /** |
|
591 * Render the toolbar. |
|
592 * |
|
593 * @return {Element} |
|
594 */ |
|
595 |
|
596 renderToolbar = () => { |
|
597 return ( |
|
598 <div className="menu toolbar-menu d-flex sticky-top bg-secondary"> |
|
599 {this.renderMarkButton('bold', 'format_bold')} |
|
600 {this.renderMarkButton('italic', 'format_italic')} |
|
601 {this.renderMarkButton('underlined', 'format_underlined')} |
|
602 {this.renderCategoryButton()} |
|
603 |
|
604 {this.renderBlockButton('numbered-list', 'format_list_numbered')} |
|
605 {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} |
|
606 |
|
607 {this.renderToolbarButtons()} |
|
608 </div> |
|
609 ) |
|
610 } |
|
611 |
|
612 renderToolbarCheckbox = () => { |
|
613 return ( |
|
614 <div className="checkbox float-right"> |
|
615 <label className="mr-2"> |
|
616 <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"><Trans i18nKey="slate_editor.press_enter_msg">Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</Trans></small> |
|
617 </label> |
|
618 </div> |
|
619 ) |
|
620 } |
|
621 |
|
622 renderToolbarButtons = () => { |
|
623 const t = this.props.t; |
|
624 return ( |
|
625 <div> |
|
626 <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right text-capitalize" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}> |
|
627 { this.props.note ? t('common.save') : t('common.add') } |
|
628 </button> |
|
629 { !this.props.note && this.renderToolbarCheckbox() } |
|
630 </div> |
|
631 ); |
|
632 } |
|
633 |
|
634 /** |
|
635 * Render a mark-toggling toolbar button. |
|
636 * |
|
637 * @param {String} type |
|
638 * @param {String} icon |
|
639 * @return {Element} |
|
640 */ |
|
641 |
|
642 renderMarkButton = (type, icon) => { |
|
643 const isActive = this.hasMark(type) |
|
644 const onMouseDown = e => this.onClickMark(e, type) |
|
645 const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); |
|
646 |
|
647 return ( |
|
648 // <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}> |
|
649 <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}> |
|
650 |
|
651 <span className="material-icons">{icon}</span> |
|
652 </span> |
|
653 ) |
|
654 } |
|
655 |
|
656 /** |
|
657 * Render a mark-toggling toolbar button. |
|
658 * |
|
659 * @param {String} type |
|
660 * @param {String} icon |
|
661 * @return {Element} |
|
662 */ |
|
663 |
|
664 renderCategoryButton = () => { |
|
665 const isActive = this.hasMark('category'); |
|
666 //const onMouseDown = e => this.onClickMark(e, type) |
|
667 const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); |
|
668 |
|
669 return ( |
|
670 <PortalWithState |
|
671 // closeOnOutsideClick |
|
672 closeOnEsc |
|
673 onOpen={this.onPortalOpen} |
|
674 onClose={this.onPortalClose} |
|
675 > |
|
676 {({ openPortal, closePortal, isOpen, portal }) => { |
|
677 console.log("PORTAL", isOpen); |
|
678 const onMouseDown = R.partial(this.onClickCategoryButton, [openPortal, closePortal, isOpen]); |
|
679 const onCategoryClick = R.partial(this.onCategoryClick, [closePortal,]); |
|
680 return ( |
|
681 <React.Fragment> |
|
682 <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}> |
|
683 <span className="material-icons">label</span> |
|
684 </span> |
|
685 {portal( |
|
686 <div className="hovering-menu" ref={this.hoveringMenuRef}> |
|
687 <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={onCategoryClick} /> |
|
688 </div> |
|
689 )} |
|
690 </React.Fragment> |
|
691 )} |
|
692 } |
|
693 </PortalWithState> |
|
694 ) |
|
695 } |
201 } |
696 |
202 |
697 // Add a `renderMark` method to render marks. |
203 // Add a `renderMark` method to render marks. |
698 |
204 |
699 renderMark = (props, editor, next) => { |
205 renderMark = (props, editor, next) => { |
700 const { children, mark, attributes } = props |
206 const { children, mark, attributes } = props |
701 |
207 |
702 console.log("renderMark", mark, mark.type, mark.data.color); |
|
703 switch (mark.type) { |
208 switch (mark.type) { |
704 case 'bold': |
209 case 'bold': |
705 return <strong {...attributes}>{children}</strong> |
210 return <strong {...attributes}>{children}</strong> |
706 case 'code': |
211 case 'code': |
707 return <code {...attributes}>{children}</code> |
212 return <code {...attributes}>{children}</code> |
764 case 'numbered-list': |
241 case 'numbered-list': |
765 return <ol {...attributes}>{children}</ol> |
242 return <ol {...attributes}>{children}</ol> |
766 default: |
243 default: |
767 return next(); |
244 return next(); |
768 } |
245 } |
769 } |
246 } |
770 |
247 |
771 /** |
248 /** |
772 * Render the Slate editor. |
249 * Render. |
773 * |
250 * |
774 * @return {Element} |
251 * @return {Element} |
775 */ |
252 */ |
776 |
253 |
777 renderEditor = () => { |
254 render = () => ( |
778 const t = this.props.t; |
255 <div className="bg-secondary mb-5"> |
779 return ( |
256 <div className="sticky-top"> |
|
257 <Toolbar |
|
258 value={this.state.value} |
|
259 editor={this.editor} |
|
260 note={this.props.note} |
|
261 annotationCategories={this.props.annotationCategories} |
|
262 isButtonDisabled={this.props.isButtonDisabled} |
|
263 submitNote={this.submitNote} |
|
264 /> |
|
265 </div> |
780 <div className="editor-slatejs p-2"> |
266 <div className="editor-slatejs p-2"> |
781 {/* {this.renderHoveringMenu()} */} |
|
782 <Editor |
267 <Editor |
783 ref={this.editorRef} |
268 ref={this.editorRef} |
784 spellCheck |
269 spellCheck |
785 placeholder={t('slate_editor.placeholder')} |
270 placeholder={this.props.t('slate_editor.placeholder')} |
786 // schema={schema} |
|
787 plugins={plugins} |
|
788 value={this.state.value} |
271 value={this.state.value} |
789 onChange={this.onChange} |
272 onChange={this.onChange} |
790 // onKeyDown={this.onKeyDown} |
273 onKeyDown={this.onKeyDown} |
|
274 onKeyUp={this.onKeyUp} |
791 renderMark={this.renderMark} |
275 renderMark={this.renderMark} |
792 renderNode = {this.renderNode} |
276 renderNode = {this.renderNode} |
793 /> |
277 /> |
794 </div> |
278 </div> |
795 ) |
279 </div> |
796 } |
280 ); |
797 |
281 |
798 // renderHoveringMenu = () => { |
|
799 // return ( |
|
800 // <Portal ref="portal" |
|
801 // isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen} |
|
802 // onOpen={this.onPortalOpen} |
|
803 // onClose={this.onPortalClose} |
|
804 // closeOnOutsideClick={false} closeOnEsc={true}> |
|
805 // <div className="hovering-menu"> |
|
806 // <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={this.onCategoryClick} /> |
|
807 // </div> |
|
808 // </Portal> |
|
809 // ) |
|
810 // } |
|
811 |
|
812 updateMenu = () => { |
|
813 |
|
814 // const { hoveringMenu } = this.state |
|
815 const hoveringMenu = this.hoveringMenuRef.current; |
|
816 |
|
817 if (!hoveringMenu) return |
|
818 |
|
819 // if (state.isBlurred || state.isCollapsed) { |
|
820 // hoveringMenu.removeAttribute('style') |
|
821 // return |
|
822 // } |
|
823 |
|
824 const selection = window.getSelection() |
|
825 |
|
826 if (selection.isCollapsed) { |
|
827 return |
|
828 } |
|
829 |
|
830 const range = selection.getRangeAt(0) |
|
831 const rect = range.getBoundingClientRect() |
|
832 |
|
833 hoveringMenu.style.opacity = 1 |
|
834 hoveringMenu.style.top = `${rect.top + rect.height + window.scrollY + hoveringMenu.offsetHeight}px` |
|
835 hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px` |
|
836 } |
|
837 |
282 |
838 } |
283 } |
839 |
284 |
840 /** |
285 /** |
841 * Export. |
286 * Export. |
842 */ |
287 */ |
|
288 function mapStateToProps(state, props) { |
|
289 |
|
290 const autoSubmit = getAutoSubmit(state); |
|
291 |
|
292 return { |
|
293 autoSubmit, |
|
294 }; |
|
295 } |
843 |
296 |
844 export default withNamespaces("", { |
297 export default withNamespaces("", { |
845 innerRef: (ref) => { |
298 innerRef: (ref) => { |
846 const editorRef = (ref && ref.props) ? ref.props.editorRef : null; |
299 if(!ref) { |
|
300 return; |
|
301 } |
|
302 const wrappedRef = ref.getWrappedInstance(); |
|
303 const editorRef = (wrappedRef && wrappedRef.props) ? wrappedRef.props.editorRef : null; |
847 if(editorRef && editorRef.hasOwnProperty('current')) { |
304 if(editorRef && editorRef.hasOwnProperty('current')) { |
848 editorRef.current = ref; |
305 editorRef.current = wrappedRef; |
849 } |
306 } |
850 } |
307 } |
851 })(SlateEditor); |
308 })(connect(mapStateToProps, null, null, { withRef: true })(SlateEditor)); |
852 // export default SlateEditor; |
|