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 (
+ {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