diff -r 03334a31130a -r 4b780ebbedc6 client/src/components/SlateEditor/index.js --- a/client/src/components/SlateEditor/index.js Thu Nov 08 16:03:28 2018 +0100 +++ b/client/src/components/SlateEditor/index.js Tue Nov 13 16:46:15 2018 +0100 @@ -2,7 +2,7 @@ import Plain from 'slate-plain-serializer'; import { Editor } from 'slate-react'; import React from 'react'; -import { Portal } from 'react-portal'; +import { PortalWithState } from 'react-portal'; import { Trans, withNamespaces } from 'react-i18next'; import * as R from 'ramda'; import HtmlSerializer from './HtmlSerializer'; @@ -40,7 +40,7 @@ }, category: props => { const data = props.mark.data; - return {props.children} + return {props.children} }, italic: { fontStyle: 'italic' @@ -75,7 +75,6 @@ /** - * The rich text example. * * @type {Component} */ @@ -100,7 +99,7 @@ } }); - plugins.push(annotationPlugin); + // plugins.push(annotationPlugin); this.state = { @@ -117,7 +116,15 @@ enterKeyValue: 0, }; - this.editor = React.createRef(); + this.editorRef = React.createRef(); + this.hoveringMenuRef = React.createRef(); + } + + get editor() { + if(this.editorRef) { + return this.editorRef.current; + } + return null; } componentDidMount = () => { @@ -129,20 +136,28 @@ this.updateMenu(); } + getDocumentLength = (document) => { + return document.getBlocks().reduce((l, b) => l + b.text.length, 0) + } + /** * On change, save the new state. * * @param {Change} change */ - onChange = ({value}) => { + onChange = (change) => { + + const operationTypes = (change && change.operations) ? change.operations.map((o) => o.type).toArray() : []; + console.log("CHANGE", change, operationTypes); + const { value } = change; let newState = { value: value, startedAt: this.state.startedAt }; - const isEmpty = value.document.length === 0; + const isEmpty = this.getDocumentLength(value.document) === 0; // Reset timers when the text is empty if (isEmpty) { @@ -160,11 +175,35 @@ } const oldState = R.clone(this.state); - this.setState(newState) - if (typeof this.props.onChange === 'function') { - this.props.onChange(R.clone(this.state), oldState, newState); - } + const categories = value.marks.reduce((acc, mark) => { + if(mark.type === 'category') { + acc.push({ + key: mark.data.get('key'), + name: mark.data.get('name'), + color: mark.data.get('color'), + text: mark.data.get('text'), + selection: { + start: mark.data.get('selection').start, + end: mark.data.get('selection').end, + }, + comment: mark.data.get('comment') + }) + } + return acc; + }, + []); + + console.log("ON CHANGE categorie", categories); + + newState['categories'] = categories; + + this.setState(newState, () => { + if (typeof this.props.onChange === 'function') { + this.props.onChange(R.clone(this.state), oldState, newState); + } + }) + } /** @@ -175,8 +214,8 @@ */ hasMark = type => { - const { value } = this.state - return value.activeMarks.some(mark => mark.type === type) + const { value } = this.state; + return value.activeMarks.some(mark => mark.type === type); } /** @@ -214,15 +253,47 @@ clear = () => { const value = Plain.deserialize(''); - this.onChange({value}); + this.onChange({ + value, + }); } focus = () => { - if(this.editor.current) { - this.editor.current.focus(); + if(this.editor) { + this.editor.focus(); } } + onClickCategoryButton = (openPortal, closePortal, isOpen, e) => { + e.preventDefault(); + const { categories, value } = this.state + + let newCategories = categories.slice(0); + + // Can't use toggleMark here, because it expects the same object + // @see https://github.com/ianstormtaylor/slate/issues/873 + if (this.hasMark('category')) { + const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') + categoryMarks.forEach(mark => { + const key = mark.data.get('key'); + const text = mark.data.get('text'); + + newCategories = R.reject(category => category.key === key && category.text === text, newCategories); + this.editor.removeMark(mark) + }) + this.setState({ + value: this.editor.value, + categories: newCategories + }); + closePortal(); + } else { + openPortal(); + } + // } else { + // isOpen ? closePortal() : openPortal(); + // } + } + /** * When a mark button is clicked, toggle the current mark. * @@ -231,40 +302,7 @@ */ onClickMark = (e, type) => { - - e.preventDefault() - const { value } = this.state - let { categories } = this.state - - let isPortalOpen = false; - - if (type === 'category') { - // Can't use toggleMark here, because it expects the same object - // @see https://github.com/ianstormtaylor/slate/issues/873 - if (this.hasMark('category')) { - const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') - categoryMarks.forEach(mark => { - const key = mark.data.key; - const text = mark.data.text; - - categories = this.removeCategory(categories, key, text) - const change = value.change().removeMark(mark) - this.onChange(change) - }) - - } else { - isPortalOpen = !this.state.isPortalOpen; - } - } else { - const change = value.change().toggleMark(type) - this.onChange(change) - } - - this.setState({ - state: value.change, - isPortalOpen: isPortalOpen, - categories: categories - }) + this.editor.toggleMark(type) } /** @@ -276,9 +314,10 @@ onClickBlock = (e, type) => { e.preventDefault() - const { value } = this.state - const change = value.change() - const { document } = value + + const { editor } = this; + const { value } = editor; + const { document } = value; // Handle everything but list buttons. if (type !== 'bulleted-list' && type !== 'numbered-list') { @@ -286,14 +325,14 @@ const isList = this.hasBlock('list-item') if (isList) { - change + editor .setBlocks(isActive ? DEFAULT_NODE : type) .unwrapBlock('bulleted-list') .unwrapBlock('numbered-list') } else { - change + editor .setBlocks(isActive ? DEFAULT_NODE : type) } } @@ -306,52 +345,103 @@ }) if (isList && isType) { - change + editor .setBlocks(DEFAULT_NODE) .unwrapBlock('bulleted-list') .unwrapBlock('numbered-list') } else if (isList) { - change + editor .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') .wrapBlock(type) } else { - change + editor .setBlocks('list-item') .wrapBlock(type) } } - - - this.onChange(change) + // this.onChange(change) } - onPortalOpen = (portal) => { + onPortalOpen = () => { + console.log("onPORTAL OPEN", this); + this.updateMenu(); // When the portal opens, cache the menu element. - this.setState({ hoveringMenu: portal.firstChild }) + // this.setState({ hoveringMenu: this.portal.firstChild }) } onPortalClose = (portal) => { - let { value } = this.state + console.log("onPORTAL CLOSE", this); + // let { value } = this.state - this.setState({ - value: value.change, - isPortalOpen: false - }) + // this.setState({ + // value: value.change, + // isPortalOpen: false + // }) } - onCategoryClick = (category) => { + getSelectionParams = () => { + + const { value } = this.editor + const { selection } = value + const { start, end} = selection + + if (selection.isCollapsed) { + return {}; + } + + const nodes = []; + let hasStarted = false; + let hasEnded = false; - const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state; - const change = value.change() + // Keep only the relevant nodes, + // i.e. nodes which are contained within selection + value.document.nodes.forEach((node) => { + if (start.isInNode(node)) { + hasStarted = true; + } + if (hasStarted && !hasEnded) { + nodes.push(node); + } + if (end.isAtEndOfNode(node)) { + hasEnded = true; + } + }); + + // Concatenate the nodes text + const text = nodes.map((node) => { + let textStart = start.isInNode(node) ? start.offset : 0; + let textEnd = end.isInNode(node) ? end.offset : node.text.length; + return node.text.substring(textStart,textEnd); + }).join('\n'); + + return { + currentSelectionText: text, + currentSelectionStart: start.offset, + currentSelectionEnd: end.offset + }; + } + + onCategoryClick = (closePortal, category) => { + + console.log("ON CATEGORY CLICK"); + const { value } = this.state; let { categories } = this.state; + const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(); + + if(!currentSelectionText) { + closePortal(); + return; + } + console.log("ACTIVE MARKS", category, currentSelectionText, currentSelectionStart, currentSelectionEnd) + const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') - categoryMarks.forEach(mark => change.removeMark(mark)); + categoryMarks.forEach(mark => this.editor.removeMark(mark)); - change.addMark({ + this.editor.addMark({ type: 'category', data: { text: currentSelectionText, @@ -360,7 +450,9 @@ end: currentSelectionEnd, }, color: category.color, - key: category.key + key: category.key, + name: category.name, + comment: category.comment } }) @@ -371,15 +463,14 @@ end: currentSelectionEnd, }, }); - categories = categories.push(category); + categories.push(category); - this.onChange(change) + console.log("CATEGORIES", categories) this.setState({ - value: value, - isPortalOpen: false, - categories: categories - }); + categories: categories, + value: this.editor.value + }, closePortal); } onButtonClick = () => { @@ -508,8 +599,7 @@ {this.renderMarkButton('bold', 'format_bold')} {this.renderMarkButton('italic', 'format_italic')} {this.renderMarkButton('underlined', 'format_underlined')} - {this.renderMarkButton('category', 'label')} - + {this.renderCategoryButton()} {this.renderBlockButton('numbered-list', 'format_list_numbered')} {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} @@ -563,23 +653,70 @@ ) } - // Add a `renderMark` method to render marks. + /** + * Render a mark-toggling toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ - renderMark = props => { - const { children, mark, attributes } = props + renderCategoryButton = () => { + const isActive = this.hasMark('category'); + //const onMouseDown = e => this.onClickMark(e, type) + const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); - switch (mark.type) { - case 'bold': - return {children} - case 'code': - return {children} - case 'italic': - return {children} - case 'underlined': - return {children} - default: - return {children}; - } + return ( + + {({ openPortal, closePortal, isOpen, portal }) => { + console.log("PORTAL", isOpen); + const onMouseDown = R.partial(this.onClickCategoryButton, [openPortal, closePortal, isOpen]); + const onCategoryClick = R.partial(this.onCategoryClick, [closePortal,]); + return ( + + + label + + {portal( +
+ +
+ )} +
+ )} + } +
+ ) + } + + // Add a `renderMark` method to render marks. + + renderMark = (props, editor, next) => { + const { children, mark, attributes } = props + + console.log("renderMark", mark, mark.type, mark.data.color); + switch (mark.type) { + case 'bold': + return {children} + case 'code': + return {children} + case 'italic': + return {children} + case 'underlined': + return {children} + case 'category': + let spanStyle = { + backgroundColor: mark.data.get('color') + }; + return {children} + default: + return next(); + } } /** * Render a block-toggling toolbar button. @@ -593,9 +730,12 @@ let isActive = this.hasBlock(type) if (['numbered-list', 'bulleted-list'].includes(type)) { - const { value } = this.state - const parent = value.document.getParent(value.blocks.first().key) - isActive = this.hasBlock('list-item') && parent && parent.type === type + const { value } = this.state; + const firstBlock = value.blocks.first(); + if(firstBlock) { + const parent = value.document.getParent(firstBlock.key); + isActive = this.hasBlock('list-item') && parent && parent.type === type; + } } const onMouseDown = e => this.onClickBlock(e, type) const blockActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); @@ -607,7 +747,7 @@ ) } - renderNode = props => { + renderNode = (props, editor, next) => { const { attributes, children, node } = props switch (node.type) { @@ -624,7 +764,7 @@ case 'numbered-list': return
    {children}
default: - return null; + return next(); } } @@ -638,16 +778,16 @@ const t = this.props.t; return (
- {this.renderHoveringMenu()} + {/* {this.renderHoveringMenu()} */} @@ -655,23 +795,24 @@ ) } - renderHoveringMenu = () => { - return ( - -
- -
-
- ) - } + // renderHoveringMenu = () => { + // return ( + // + //
+ // + //
+ //
+ // ) + // } updateMenu = () => { - const { hoveringMenu } = this.state + // const { hoveringMenu } = this.state + const hoveringMenu = this.hoveringMenuRef.current; if (!hoveringMenu) return @@ -690,7 +831,7 @@ const rect = range.getBoundingClientRect() hoveringMenu.style.opacity = 1 - hoveringMenu.style.top = `${rect.top + window.scrollY + hoveringMenu.offsetHeight}px` + hoveringMenu.style.top = `${rect.top + rect.height + window.scrollY + hoveringMenu.offsetHeight}px` hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px` }