--- a/client/src/components/SlateEditor.js Wed Jun 07 18:18:44 2017 +0200
+++ b/client/src/components/SlateEditor.js Thu Jun 08 11:13:41 2017 +0200
@@ -1,8 +1,10 @@
import { Editor, Plain, Raw } from 'slate'
import React from 'react'
-import moment from 'moment';
+import Portal from 'react-portal'
+import moment from 'moment'
import HtmlSerializer from '../HtmlSerializer'
-import AnnotationPlugin from '../AnnotationPlugin';
+import AnnotationPlugin from '../AnnotationPlugin'
+import CategoriesTooltip from './CategoriesTooltip'
const plugins = [];
@@ -17,7 +19,8 @@
*
* @type {Object}
*/
-
+// TODO Check if we can move this to the plugin using the schema option
+// https://docs.slatejs.org/reference/plugins/plugin.html#schema
const schema = {
nodes: {
'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
@@ -28,12 +31,16 @@
bold: {
fontWeight: 'bold'
},
- // TODO Check if we can move this to the plugin using the schema option
- // https://docs.slatejs.org/reference/plugins/plugin.html#schema
- annotation: {
+ // This is a "temporary" mark added when the hovering menu is open
+ highlight: {
textDecoration: 'underline',
textDecorationStyle: 'dotted',
- backgroundColor: 'yellow',
+ backgroundColor: '#ccc',
+ },
+ // This is the mark actually used for annotations
+ annotation: props => {
+ const data = props.mark.data;
+ return <span style={{ backgroundColor: data.get('color') }}>{props.children}</span>
},
italic: {
fontStyle: 'italic'
@@ -44,6 +51,12 @@
}
}
+const annotationCategories = [
+ { key: 'important', name: 'Important', color: '#F1C40F' },
+ { key: 'keyword', name: 'Mot-clé', color: '#2ECC71' },
+ { key: 'comment', name: 'Commentaire', color: '#3498DB' }
+];
+
/**
* The rich text example.
*
@@ -72,14 +85,21 @@
state: Plain.deserialize(''),
startedAt: null,
finishedAt: null,
- currentSelectionText: ''
+ currentSelectionText: '',
+ hoveringMenu: null,
+ isPortalOpen: false
};
}
- componentDidMount() {
+ componentDidMount = () => {
+ this.updateMenu();
this.focus();
}
+ componentDidUpdate = () => {
+ this.updateMenu();
+ }
+
/**
* Check if the current selection has a mark with `type` in it.
*
@@ -207,11 +227,14 @@
onClickMark = (e, type) => {
e.preventDefault()
- let { state } = this.state
+ let { state, hoveringMenu } = this.state
let toggleMarkOptions;
- if (type === 'annotation') {
+ let isPortalOpen = false;
+
+ if (type === 'highlight') {
toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } }
+ isPortalOpen = !this.state.isPortalOpen;
} else {
toggleMarkOptions = type;
}
@@ -221,7 +244,10 @@
.toggleMark(toggleMarkOptions)
.apply()
- this.setState({ state })
+ this.setState({
+ state: state,
+ isPortalOpen: isPortalOpen
+ })
}
/**
@@ -282,6 +308,51 @@
this.setState({ state })
}
+ onPortalOpen = (portal) => {
+ // When the portal opens, cache the menu element.
+ this.setState({ hoveringMenu: portal.firstChild })
+ }
+
+ onPortalClose = (portal) => {
+
+ let { state } = this.state
+ const transform = state.transform();
+
+ state.marks.forEach(mark => {
+ if (mark.type === 'highlight') {
+ transform.removeMark(mark)
+ }
+ });
+
+ this.setState({
+ state: transform.apply(),
+ isPortalOpen: false
+ })
+ }
+
+ onCategoryClick = (category) => {
+
+ const { state } = this.state;
+ const transform = state.transform();
+
+ state.marks.forEach(mark => transform.removeMark(mark));
+
+ transform.addMark({
+ type: 'annotation',
+ data: {
+ text: this.state.currentSelectionText,
+ color: category.color,
+ key: category.key
+ }
+ })
+
+ this.setState({
+ state: transform.apply(),
+ isPortalOpen: false
+ });
+
+ }
+
/**
* Render.
*
@@ -309,7 +380,7 @@
{this.renderMarkButton('bold', 'format_bold')}
{this.renderMarkButton('italic', 'format_italic')}
{this.renderMarkButton('underlined', 'format_underlined')}
- {this.renderMarkButton('annotation', 'label')}
+ {this.renderMarkButton('highlight', 'label')}
{this.renderBlockButton('numbered-list', 'format_list_numbered')}
{this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
@@ -364,6 +435,7 @@
renderEditor = () => {
return (
<div className="editor">
+ {this.renderHoveringMenu()}
<Editor
ref="editor"
spellCheck
@@ -378,6 +450,45 @@
)
}
+ renderHoveringMenu = () => {
+ return (
+ <Portal ref="portal"
+ isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen}
+ onOpen={this.onPortalOpen}
+ onClose={this.onPortalClose}
+ closeOnOutsideClick={false} closeOnEsc={true}>
+ <div className="hovering-menu">
+ <CategoriesTooltip categories={annotationCategories} onCategoryClick={this.onCategoryClick} />
+ </div>
+ </Portal>
+ )
+ }
+
+ updateMenu = () => {
+
+ const { hoveringMenu, state } = this.state
+
+ if (!hoveringMenu) return
+
+ // if (state.isBlurred || state.isCollapsed) {
+ // hoveringMenu.removeAttribute('style')
+ // return
+ // }
+
+ const selection = window.getSelection()
+
+ if (selection.isCollapsed) {
+ return
+ }
+
+ const range = selection.getRangeAt(0)
+ const rect = range.getBoundingClientRect()
+
+ hoveringMenu.style.opacity = 1
+ hoveringMenu.style.top = `${rect.top + window.scrollY + hoveringMenu.offsetHeight}px`
+ hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px`
+ }
+
}
/**