# HG changeset patch # User Alexandre Segura # Date 1496913221 -7200 # Node ID 284e866f55c7ec25b444af5d5a548b94ebfab30d # Parent a8300ef1876e347715be121635e1b64662fcf8d1 First version of categories tooltip. - Use react-portal to display hovering menu. - Add custom Mark to store category data. diff -r a8300ef1876e -r 284e866f55c7 client/package.json --- 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", diff -r a8300ef1876e -r 284e866f55c7 client/src/App.scss --- 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; diff -r a8300ef1876e -r 284e866f55c7 client/src/HtmlSerializer.js --- 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 {children} case 'italic': return {children} case 'underline': return {children} - case 'annotation': return {children} + case 'annotation': return {children} default: return; } } diff -r a8300ef1876e -r 284e866f55c7 client/src/components/CategoriesTooltip.js --- /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 ( +
+ + {this.props.categories.map((category) => + + )} + + + + +
+ ); + } +} + +export default CategoriesTooltip diff -r a8300ef1876e -r 284e866f55c7 client/src/components/Clock.js --- /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 ( + + ); + } +} + +export default Clock diff -r a8300ef1876e -r 284e866f55c7 client/src/components/NoteInput.js --- 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() } - +
diff -r a8300ef1876e -r 284e866f55c7 client/src/components/SlateEditor.js --- 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 => , @@ -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 {props.children} }, 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 (
+ {this.renderHoveringMenu()} { + return ( + +
+ +
+
+ ) + } + + 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` + } + } /**