1 import { Editor, Plain, Raw } from 'slate' |
1 import { Editor, Plain, Raw } from 'slate' |
2 import React from 'react' |
2 import React from 'react' |
3 import moment from 'moment'; |
3 import Portal from 'react-portal' |
|
4 import moment from 'moment' |
4 import HtmlSerializer from '../HtmlSerializer' |
5 import HtmlSerializer from '../HtmlSerializer' |
5 import AnnotationPlugin from '../AnnotationPlugin'; |
6 import AnnotationPlugin from '../AnnotationPlugin' |
|
7 import CategoriesTooltip from './CategoriesTooltip' |
6 |
8 |
7 const plugins = []; |
9 const plugins = []; |
8 |
10 |
9 /** |
11 /** |
10 * Define the default node type. |
12 * Define the default node type. |
15 /** |
17 /** |
16 * Define a schema. |
18 * Define a schema. |
17 * |
19 * |
18 * @type {Object} |
20 * @type {Object} |
19 */ |
21 */ |
20 |
22 // TODO Check if we can move this to the plugin using the schema option |
|
23 // https://docs.slatejs.org/reference/plugins/plugin.html#schema |
21 const schema = { |
24 const schema = { |
22 nodes: { |
25 nodes: { |
23 'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>, |
26 'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>, |
24 'list-item': props => <li {...props.attributes}>{props.children}</li>, |
27 'list-item': props => <li {...props.attributes}>{props.children}</li>, |
25 'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>, |
28 'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>, |
26 }, |
29 }, |
27 marks: { |
30 marks: { |
28 bold: { |
31 bold: { |
29 fontWeight: 'bold' |
32 fontWeight: 'bold' |
30 }, |
33 }, |
31 // TODO Check if we can move this to the plugin using the schema option |
34 // This is a "temporary" mark added when the hovering menu is open |
32 // https://docs.slatejs.org/reference/plugins/plugin.html#schema |
35 highlight: { |
33 annotation: { |
|
34 textDecoration: 'underline', |
36 textDecoration: 'underline', |
35 textDecorationStyle: 'dotted', |
37 textDecorationStyle: 'dotted', |
36 backgroundColor: 'yellow', |
38 backgroundColor: '#ccc', |
|
39 }, |
|
40 // This is the mark actually used for annotations |
|
41 annotation: props => { |
|
42 const data = props.mark.data; |
|
43 return <span style={{ backgroundColor: data.get('color') }}>{props.children}</span> |
37 }, |
44 }, |
38 italic: { |
45 italic: { |
39 fontStyle: 'italic' |
46 fontStyle: 'italic' |
40 }, |
47 }, |
41 underlined: { |
48 underlined: { |
42 textDecoration: 'underline' |
49 textDecoration: 'underline' |
43 } |
50 } |
44 } |
51 } |
45 } |
52 } |
46 |
53 |
|
54 const annotationCategories = [ |
|
55 { key: 'important', name: 'Important', color: '#F1C40F' }, |
|
56 { key: 'keyword', name: 'Mot-clé', color: '#2ECC71' }, |
|
57 { key: 'comment', name: 'Commentaire', color: '#3498DB' } |
|
58 ]; |
|
59 |
47 /** |
60 /** |
48 * The rich text example. |
61 * The rich text example. |
49 * |
62 * |
50 * @type {Component} |
63 * @type {Component} |
51 */ |
64 */ |
205 * @param {String} type |
225 * @param {String} type |
206 */ |
226 */ |
207 |
227 |
208 onClickMark = (e, type) => { |
228 onClickMark = (e, type) => { |
209 e.preventDefault() |
229 e.preventDefault() |
210 let { state } = this.state |
230 let { state, hoveringMenu } = this.state |
211 |
231 |
212 let toggleMarkOptions; |
232 let toggleMarkOptions; |
213 if (type === 'annotation') { |
233 let isPortalOpen = false; |
|
234 |
|
235 if (type === 'highlight') { |
214 toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } } |
236 toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } } |
|
237 isPortalOpen = !this.state.isPortalOpen; |
215 } else { |
238 } else { |
216 toggleMarkOptions = type; |
239 toggleMarkOptions = type; |
217 } |
240 } |
218 |
241 |
219 state = state |
242 state = state |
220 .transform() |
243 .transform() |
221 .toggleMark(toggleMarkOptions) |
244 .toggleMark(toggleMarkOptions) |
222 .apply() |
245 .apply() |
223 |
246 |
224 this.setState({ state }) |
247 this.setState({ |
|
248 state: state, |
|
249 isPortalOpen: isPortalOpen |
|
250 }) |
225 } |
251 } |
226 |
252 |
227 /** |
253 /** |
228 * When a block button is clicked, toggle the block type. |
254 * When a block button is clicked, toggle the block type. |
229 * |
255 * |
280 |
306 |
281 state = transform.apply() |
307 state = transform.apply() |
282 this.setState({ state }) |
308 this.setState({ state }) |
283 } |
309 } |
284 |
310 |
|
311 onPortalOpen = (portal) => { |
|
312 // When the portal opens, cache the menu element. |
|
313 this.setState({ hoveringMenu: portal.firstChild }) |
|
314 } |
|
315 |
|
316 onPortalClose = (portal) => { |
|
317 |
|
318 let { state } = this.state |
|
319 const transform = state.transform(); |
|
320 |
|
321 state.marks.forEach(mark => { |
|
322 if (mark.type === 'highlight') { |
|
323 transform.removeMark(mark) |
|
324 } |
|
325 }); |
|
326 |
|
327 this.setState({ |
|
328 state: transform.apply(), |
|
329 isPortalOpen: false |
|
330 }) |
|
331 } |
|
332 |
|
333 onCategoryClick = (category) => { |
|
334 |
|
335 const { state } = this.state; |
|
336 const transform = state.transform(); |
|
337 |
|
338 state.marks.forEach(mark => transform.removeMark(mark)); |
|
339 |
|
340 transform.addMark({ |
|
341 type: 'annotation', |
|
342 data: { |
|
343 text: this.state.currentSelectionText, |
|
344 color: category.color, |
|
345 key: category.key |
|
346 } |
|
347 }) |
|
348 |
|
349 this.setState({ |
|
350 state: transform.apply(), |
|
351 isPortalOpen: false |
|
352 }); |
|
353 |
|
354 } |
|
355 |
285 /** |
356 /** |
286 * Render. |
357 * Render. |
287 * |
358 * |
288 * @return {Element} |
359 * @return {Element} |
289 */ |
360 */ |
307 return ( |
378 return ( |
308 <div className="menu toolbar-menu"> |
379 <div className="menu toolbar-menu"> |
309 {this.renderMarkButton('bold', 'format_bold')} |
380 {this.renderMarkButton('bold', 'format_bold')} |
310 {this.renderMarkButton('italic', 'format_italic')} |
381 {this.renderMarkButton('italic', 'format_italic')} |
311 {this.renderMarkButton('underlined', 'format_underlined')} |
382 {this.renderMarkButton('underlined', 'format_underlined')} |
312 {this.renderMarkButton('annotation', 'label')} |
383 {this.renderMarkButton('highlight', 'label')} |
313 |
384 |
314 {this.renderBlockButton('numbered-list', 'format_list_numbered')} |
385 {this.renderBlockButton('numbered-list', 'format_list_numbered')} |
315 {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} |
386 {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} |
316 </div> |
387 </div> |
317 ) |
388 ) |
376 /> |
448 /> |
377 </div> |
449 </div> |
378 ) |
450 ) |
379 } |
451 } |
380 |
452 |
|
453 renderHoveringMenu = () => { |
|
454 return ( |
|
455 <Portal ref="portal" |
|
456 isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen} |
|
457 onOpen={this.onPortalOpen} |
|
458 onClose={this.onPortalClose} |
|
459 closeOnOutsideClick={false} closeOnEsc={true}> |
|
460 <div className="hovering-menu"> |
|
461 <CategoriesTooltip categories={annotationCategories} onCategoryClick={this.onCategoryClick} /> |
|
462 </div> |
|
463 </Portal> |
|
464 ) |
|
465 } |
|
466 |
|
467 updateMenu = () => { |
|
468 |
|
469 const { hoveringMenu, state } = this.state |
|
470 |
|
471 if (!hoveringMenu) return |
|
472 |
|
473 // if (state.isBlurred || state.isCollapsed) { |
|
474 // hoveringMenu.removeAttribute('style') |
|
475 // return |
|
476 // } |
|
477 |
|
478 const selection = window.getSelection() |
|
479 |
|
480 if (selection.isCollapsed) { |
|
481 return |
|
482 } |
|
483 |
|
484 const range = selection.getRangeAt(0) |
|
485 const rect = range.getBoundingClientRect() |
|
486 |
|
487 hoveringMenu.style.opacity = 1 |
|
488 hoveringMenu.style.top = `${rect.top + window.scrollY + hoveringMenu.offsetHeight}px` |
|
489 hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px` |
|
490 } |
|
491 |
381 } |
492 } |
382 |
493 |
383 /** |
494 /** |
384 * Export. |
495 * Export. |
385 */ |
496 */ |