First version of categories tooltip.
- Use react-portal to display hovering menu.
- Add custom Mark to store category data.
--- a/client/package.json Wed Jun 07 18:18:44 2017 +0200
+++ b/client/package.json Thu Jun 08 11:13:41 2017 +0200
@@ -12,6 +12,7 @@
"react": "^15.5.4",
"react-bootstrap": "^0.31.0",
"react-dom": "^15.5.4",
+ "react-portal": "^3.1.0",
"react-redux": "^5.0.5",
"react-router-redux": "next",
"redux": "^3.6.0",
--- a/client/src/App.scss Wed Jun 07 18:18:44 2017 +0200
+++ b/client/src/App.scss Thu Jun 08 11:13:41 2017 +0200
@@ -35,6 +35,38 @@
}
}
+.hovering-menu {
+ position: absolute;
+ z-index: 1;
+ top: -10000px;
+ left: -10000px;
+ margin-top: -64px;
+ opacity: 0;
+ transition: opacity .75s;
+}
+
+.categories-tooltip {
+ background-color: #efefef;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ padding: 5px;
+ .buttons {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ button {
+ background-color: yellow;
+ border: 1px solid #ccc;
+ }
+ button:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+ .form-group:last-child {
+ margin-bottom: 0;
+ }
+}
+
.editor-wrapper {
border: 1px solid #efefef;
padding: 20px;
--- a/client/src/HtmlSerializer.js Wed Jun 07 18:18:44 2017 +0200
+++ b/client/src/HtmlSerializer.js Thu Jun 08 11:13:41 2017 +0200
@@ -13,12 +13,6 @@
annotation: 'span'
}
-const annotationStyle = {
- textDecoration: 'underline',
- textDecorationStyle: 'dotted',
- backgroundColor: 'yellow'
-}
-
const rules = [
// Block rules
{
@@ -57,7 +51,7 @@
case 'bold': return <strong>{children}</strong>
case 'italic': return <em>{children}</em>
case 'underline': return <u>{children}</u>
- case 'annotation': return <span style={annotationStyle}>{children}</span>
+ case 'annotation': return <span style={{ backgroundColor: object.data.get('color') }}>{children}</span>
default: return;
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/CategoriesTooltip.js Thu Jun 08 11:13:41 2017 +0200
@@ -0,0 +1,29 @@
+import React, { Component } from 'react';
+import { FormGroup, FormControl, Button } from 'react-bootstrap';
+
+class CategoriesTooltip extends Component {
+
+ onButtonClick = (category) => {
+ if (typeof this.props.onCategoryClick === 'function') {
+ this.props.onCategoryClick(category)
+ }
+ }
+
+ render() {
+ return (
+ <div className="categories-tooltip">
+ <FormGroup className="buttons">
+ {this.props.categories.map((category) =>
+ <Button key={ category.name } bsStyle="primary" style={{ backgroundColor: category.color }}
+ onClick={this.onButtonClick.bind(this, category)}>{ category.name }</Button>
+ )}
+ </FormGroup>
+ <FormGroup>
+ <FormControl />
+ </FormGroup>
+ </div>
+ );
+ }
+}
+
+export default CategoriesTooltip
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Clock.js Thu Jun 08 11:13:41 2017 +0200
@@ -0,0 +1,25 @@
+import React, { Component } from 'react';
+import { Label } from 'react-bootstrap';
+import moment from 'moment';
+
+class Clock extends Component {
+
+ state = {
+ time: moment().format('H:mm:ss'),
+ }
+
+ componentDidMount() {
+ setInterval(() => {
+ const time = moment().format('H:mm:ss');
+ this.setState({ time });
+ }, 1000);
+ }
+
+ render() {
+ return (
+ <Label bsStyle="info">{ this.state.time }</Label>
+ );
+ }
+}
+
+export default Clock
--- a/client/src/components/NoteInput.js Wed Jun 07 18:18:44 2017 +0200
+++ b/client/src/components/NoteInput.js Thu Jun 08 11:13:41 2017 +0200
@@ -4,12 +4,12 @@
import PropTypes from 'prop-types';
import SlateEditor from './SlateEditor';
+import Clock from './Clock'
class NoteInput extends Component {
state = {
buttonDisabled: false,
- time: moment().format('H:mm:ss'),
startedAt: null,
finishedAt: null,
}
@@ -43,10 +43,6 @@
componentDidMount() {
const text = this.refs.editor.asPlain();
this.setState({ buttonDisabled: text.length === 0 });
- setInterval(() => {
- const time = moment().format('H:mm:ss');
- this.setState({ time });
- }, 1000);
}
renderTiming() {
@@ -71,7 +67,7 @@
{ this.renderTiming() }
</Col>
<Col md={6} className="text-right">
- <Label bsStyle="info">{ this.state.time }</Label>
+ <Clock />
</Col>
</Row>
<hr />
--- 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`
+ }
+
}
/**