diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/index.js
--- a/client/src/components/SlateEditor/index.js Tue Nov 13 16:46:15 2018 +0100
+++ b/client/src/components/SlateEditor/index.js Fri Nov 16 11:19:13 2018 +0100
@@ -2,76 +2,14 @@
import Plain from 'slate-plain-serializer';
import { Editor } from 'slate-react';
import React from 'react';
-import { PortalWithState } from 'react-portal';
-import { Trans, withNamespaces } from 'react-i18next';
+import { withNamespaces } from 'react-i18next';
+import { connect } from 'react-redux';
import * as R from 'ramda';
import HtmlSerializer from './HtmlSerializer';
-import AnnotationPlugin from './AnnotationPlugin';
-import CategoriesTooltip from './CategoriesTooltip';
import './SlateEditor.css';
import { now } from '../../utils';
-import { defaultAnnotationsCategories } from '../../constants';
-
-const plugins = [];
-
-/**
- * Define the default node type.
- */
-
-const DEFAULT_NODE = 'paragraph'
-
-/**
- * Define a schema.
- *
- * @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 =>
,
- 'list-item': props => {props.children},
- 'numbered-list': props => {props.children}
,
- },
- marks: {
- bold: {
- fontWeight: 'bold'
- },
- category: props => {
- const data = props.mark.data;
- return {props.children}
- },
- italic: {
- fontStyle: 'italic'
- },
- underlined: {
- textDecoration: 'underlined'
- }
- }
-
-}
-
-const initialValue = Value.fromJSON({
- document: {
- nodes: [
- {
- object: 'block',
- type: 'paragraph',
- nodes: [
- {
- object: 'text',
- leaves: [
- {
- text: '',
- },
- ],
- },
- ],
- },
- ],
- },
-})
+import Toolbar from './Toolbar';
+import { getAutoSubmit } from '../../selectors/authSelectors';
/**
@@ -89,35 +27,14 @@
constructor(props) {
super(props);
- const annotationPlugin = AnnotationPlugin({
- onChange: (text, start, end) => {
- this.setState({
- currentSelectionText: text,
- currentSelectionStart: start,
- currentSelectionEnd: end,
- });
- }
- });
-
- // plugins.push(annotationPlugin);
-
-
this.state = {
- value: props.note ? Value.fromJSON(initialValue) : Plain.deserialize(''),
+ value: props.note ? Value.fromJSON(JSON.parse(props.note.raw)) : Plain.deserialize(''),
startedAt: null,
finishedAt: null,
- currentSelectionText: '',
- currentSelectionStart: 0,
- currentSelectionEnd: 0,
- hoveringMenu: null,
- isPortalOpen: false,
- categories: [],
- isCheckboxChecked: false,
enterKeyValue: 0,
};
this.editorRef = React.createRef();
- this.hoveringMenuRef = React.createRef();
}
get editor() {
@@ -128,107 +45,42 @@
}
componentDidMount = () => {
- this.updateMenu();
this.focus();
}
- componentDidUpdate = () => {
- 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 = (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 = this.getDocumentLength(value.document) === 0;
-
- // Reset timers when the text is empty
- if (isEmpty) {
- Object.assign(newState, {
- startedAt: null,
- finishedAt: null
- });
- } else {
- Object.assign(newState, { finishedAt: now() });
- }
-
- // Store start time once when the first character is typed
- if (!isEmpty && this.state.startedAt === null) {
- Object.assign(newState, { startedAt: now() });
- }
+ onChange = ({value, operations}) => {
const oldState = R.clone(this.state);
- 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')
- })
+ const newState = {
+ value
+ };
+
+ (operations || []).some((op) => {
+ if(['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'set_mark', 'insert_node', 'merge_node', 'move_node', 'remove_node', 'set_node', 'split_node'].indexOf(op.type)>=0) {
+ const tsnow = now();
+ if(this.state.startedAt == null) {
+ newState.startedAt = tsnow;
+ }
+ newState.finishedAt = tsnow;
+ return true;
}
- return acc;
- },
- []);
-
- console.log("ON CHANGE categorie", categories);
-
- newState['categories'] = categories;
+ return false;
+ });
this.setState(newState, () => {
if (typeof this.props.onChange === 'function') {
- this.props.onChange(R.clone(this.state), oldState, newState);
+ this.props.onChange(R.clone(this.state), oldState, {value, operations});
}
})
-
}
- /**
- * Check if the current selection has a mark with `type` in it.
- *
- * @param {String} type
- * @return {Boolean}
- */
-
- hasMark = type => {
- const { value } = this.state;
- return value.activeMarks.some(mark => mark.type === type);
- }
-
- /**
- * Check if the any of the currently selected blocks are of `type`.
- *
- * @param {String} type
- * @return {Boolean}
- */
-
- hasBlock = type => {
- const { value } = this.state
- return value.blocks.some(node => node.type === type)
- }
asPlain = () => {
return Plain.serialize(this.state.value);
@@ -243,19 +95,15 @@
}
asCategories = () => {
- return this.state.categories
- }
-
- removeCategory = (categories, key, text) => {
- const categoryIndex = categories.findIndex(category => category.key === key && category.text === text)
- return categories.delete(categoryIndex)
+ return this.state.value.document.getMarksByType('category').map((mark) => mark.data.toJS()).toArray();
}
clear = () => {
const value = Plain.deserialize('');
- this.onChange({
+ this.setState({
value,
- });
+ enterKeyValue: 0
+ })
}
focus = () => {
@@ -264,225 +112,33 @@
}
}
- 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.
- *
- * @param {Event} e
- * @param {String} type
- */
-
- onClickMark = (e, type) => {
- this.editor.toggleMark(type)
+ submitNote = () => {
+ this.setState({ enterKeyValue: 0 }, () => {
+ if (typeof this.props.submitNote === 'function') {
+ this.props.submitNote();
+ }
+ });
}
/**
- * When a block button is clicked, toggle the block type.
+ * On key down, if it's a formatting command toggle a mark.
*
* @param {Event} e
- * @param {String} type
+ * @param {Change} change
+ * @return {Change}
*/
- onClickBlock = (e, type) => {
- e.preventDefault()
-
- const { editor } = this;
- const { value } = editor;
- const { document } = value;
-
- // Handle everything but list buttons.
- if (type !== 'bulleted-list' && type !== 'numbered-list') {
- const isActive = this.hasBlock(type)
- const isList = this.hasBlock('list-item')
-
- if (isList) {
- editor
- .setBlocks(isActive ? DEFAULT_NODE : type)
- .unwrapBlock('bulleted-list')
- .unwrapBlock('numbered-list')
- }
-
- else {
- editor
- .setBlocks(isActive ? DEFAULT_NODE : type)
- }
- }
-
- // Handle the extra wrapping required for list buttons.
- else {
- const isList = this.hasBlock('list-item')
- const isType = value.blocks.some((block) => {
- return !!document.getClosest(block.key, parent => parent.type === type)
- })
-
- if (isList && isType) {
- editor
- .setBlocks(DEFAULT_NODE)
- .unwrapBlock('bulleted-list')
- .unwrapBlock('numbered-list')
+ onKeyUp = (e, editor, next) => {
- } else if (isList) {
- editor
- .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
- .wrapBlock(type)
-
- } else {
- editor
- .setBlocks('list-item')
- .wrapBlock(type)
-
- }
- }
- // this.onChange(change)
- }
-
- onPortalOpen = () => {
- console.log("onPORTAL OPEN", this);
- this.updateMenu();
- // When the portal opens, cache the menu element.
- // this.setState({ hoveringMenu: this.portal.firstChild })
- }
-
- onPortalClose = (portal) => {
- console.log("onPORTAL CLOSE", this);
- // let { value } = this.state
-
- // this.setState({
- // value: value.change,
- // isPortalOpen: false
- // })
- }
-
- 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 } = this.state;
+ const noteText = value.document.text.trim();
- // 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;
+ if(e.key === "Enter" && noteText.length !== 0) {
+ this.setState({ enterKeyValue: this.state.enterKeyValue + 1 });
+ } else if ( e.getModifierState() || (e.key !== "Control" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Alt") ) {
+ this.setState({ enterKeyValue: 0 });
}
- console.log("ACTIVE MARKS", category, currentSelectionText, currentSelectionStart, currentSelectionEnd)
-
- const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
- categoryMarks.forEach(mark => this.editor.removeMark(mark));
-
- this.editor.addMark({
- type: 'category',
- data: {
- text: currentSelectionText,
- selection: {
- start: currentSelectionStart,
- end: currentSelectionEnd,
- },
- color: category.color,
- key: category.key,
- name: category.name,
- comment: category.comment
- }
- })
-
- Object.assign(category, {
- text: currentSelectionText,
- selection: {
- start: currentSelectionStart,
- end: currentSelectionEnd,
- },
- });
- categories.push(category);
-
- console.log("CATEGORIES", categories)
-
- this.setState({
- categories: categories,
- value: this.editor.value
- }, closePortal);
- }
-
- onButtonClick = () => {
- if (typeof this.props.onButtonClick === 'function') {
- this.props.onButtonClick();
- }
- }
-
- onCheckboxChange = (e) => {
- if (typeof this.props.onCheckboxChange === 'function') {
- this.props.onCheckboxChange(e);
- }
+ return next();
}
/**
@@ -493,205 +149,55 @@
* @return {Change}
*/
- onKeyDown = (e, change) => {
-
- const {value} = this.state;
+ onKeyDown = (e, editor, next) => {
- if (e.key === 'Enter' && value.document.text !== '') {
- this.setState({enterKeyValue: 1})
- }
+ const { value, enterKeyValue } = this.state;
+ const { autoSubmit } = this.props;
+ const noteText = value.document.text.trim();
- if (e.key !== 'Enter') {
- this.setState({
- enterKeyValue: 0,
- })
-
+ // we prevent empty first lines
+ if(e.key === "Enter" && noteText.length === 0) {
+ e.preventDefault();
+ return next();
}
- //TODO review the double enter case.
- if (e.key === 'Enter' && !this.props.isChecked && this.state.enterKeyValue === 1 && typeof this.props.onEnterKeyDown === 'function') {
+ // Enter submit the note
+ if(e.key === "Enter" && ( enterKeyValue === 2 || e.ctrlKey || autoSubmit ) && noteText.length !== 0) {
e.preventDefault();
- this.props.onEnterKeyDown();
- this.setState({
- enterKeyValue: 0,
- })
-
-
- return change
+ this.submitNote();
+ return next();
}
- else if (e.key === 'Enter' && value.document.text !== '' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
-
- e.preventDefault();
- this.props.onEnterKeyDown();
-
- return change
+ if (!e.ctrlKey) {
+ return next();
}
- if (!e.ctrlKey) return
- // Decide what to do based on the key code...
- switch (e.key) {
- default: {
- break;
- }
- // When "B" is pressed, add a "bold" mark to the text.
- case 'b': {
- e.preventDefault()
- change.toggleMark('bold')
-
- return true
- }
- case 'i': {
- // When "U" is pressed, add an "italic" mark to the text.
- e.preventDefault()
- change.toggleMark('italic')
-
- return true
- }
- case 'u': {
- // When "U" is pressed, add an "underline" mark to the text.
- e.preventDefault()
- change.toggleMark('underlined')
-
- return true
- }
- case 'Enter': {
- // When "ENTER" is pressed, autosubmit the note.
- if (value.document.text !== '' && typeof this.props.onEnterKeyDown === 'function') {
- e.preventDefault()
- this.props.onEnterKeyDown();
- this.setState({
- enterKeyValue: 0,
- })
-
- return true
- }
- }
- }
- }
-
- /**
- * Render.
- *
- * @return {Element}
- */
-
- render = () => {
- return (
-
-
- {this.renderToolbar()}
-
- {this.renderEditor()}
-
- )
- }
-
- /**
- * Render the toolbar.
- *
- * @return {Element}
- */
-
- renderToolbar = () => {
- return (
-
- {this.renderMarkButton('bold', 'format_bold')}
- {this.renderMarkButton('italic', 'format_italic')}
- {this.renderMarkButton('underlined', 'format_underlined')}
- {this.renderCategoryButton()}
-
- {this.renderBlockButton('numbered-list', 'format_list_numbered')}
- {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
-
- {this.renderToolbarButtons()}
-
- )
- }
+ e.preventDefault();
- renderToolbarCheckbox = () => {
- return (
-
-
-
- )
- }
-
- renderToolbarButtons = () => {
- const t = this.props.t;
- return (
-
-
- { !this.props.note && this.renderToolbarCheckbox() }
-
- );
- }
-
- /**
- * Render a mark-toggling toolbar button.
- *
- * @param {String} type
- * @param {String} icon
- * @return {Element}
- */
-
- renderMarkButton = (type, icon) => {
- const isActive = this.hasMark(type)
- const onMouseDown = e => this.onClickMark(e, type)
- const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
-
- return (
- //
-
+ // Decide what to do based on the key code...
+ switch (e.key) {
+ default: {
+ break;
+ }
+ // When "B" is pressed, add a "bold" mark to the text.
+ case 'b': {
+ editor.toggleMark('bold');
+ break;
+ }
+ case 'i': {
+ // When "U" is pressed, add an "italic" mark to the text.
+ editor.toggleMark('italic');
+ break;
+ }
+ case 'u': {
+ // When "U" is pressed, add an "underline" mark to the text.
+ editor.toggleMark('underlined');
+ break;
+ }
+ }
- {icon}
-
- )
- }
-
- /**
- * Render a mark-toggling toolbar button.
- *
- * @param {String} type
- * @param {String} icon
- * @return {Element}
- */
-
- renderCategoryButton = () => {
- const isActive = this.hasMark('category');
- //const onMouseDown = e => this.onClickMark(e, type)
- const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
+ return next();
- 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.
@@ -699,7 +205,6 @@
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}
@@ -718,34 +223,6 @@
return next();
}
}
- /**
- * Render a block-toggling toolbar button.
- *
- * @param {String} type
- * @param {String} icon
- * @return {Element}
- */
-
- renderBlockButton = (type, icon) => {
- let isActive = this.hasBlock(type)
-
- if (['numbered-list', 'bulleted-list'].includes(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");
-
- return (
-
- {icon}
-
- )
- }
renderNode = (props, editor, next) => {
const { attributes, children, node } = props
@@ -766,87 +243,66 @@
default:
return next();
}
-}
+ }
- /**
- * Render the Slate editor.
+ /**
+ * Render.
*
* @return {Element}
*/
- renderEditor = () => {
- const t = this.props.t;
- return (
+ render = () => (
+
+
+
+
- {/* {this.renderHoveringMenu()} */}
- )
- }
-
- // renderHoveringMenu = () => {
- // return (
- //
- //
- //
- //
- //
- // )
- // }
-
- updateMenu = () => {
+
+ );
- // const { hoveringMenu } = this.state
- const hoveringMenu = this.hoveringMenuRef.current;
-
- 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 + rect.height + window.scrollY + hoveringMenu.offsetHeight}px`
- hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px`
- }
}
/**
* Export.
*/
+function mapStateToProps(state, props) {
+
+ const autoSubmit = getAutoSubmit(state);
+
+ return {
+ autoSubmit,
+ };
+}
export default withNamespaces("", {
innerRef: (ref) => {
- const editorRef = (ref && ref.props) ? ref.props.editorRef : null;
+ if(!ref) {
+ return;
+ }
+ const wrappedRef = ref.getWrappedInstance();
+ const editorRef = (wrappedRef && wrappedRef.props) ? wrappedRef.props.editorRef : null;
if(editorRef && editorRef.hasOwnProperty('current')) {
- editorRef.current = ref;
+ editorRef.current = wrappedRef;
}
}
-})(SlateEditor);
-// export default SlateEditor;
+})(connect(mapStateToProps, null, null, { withRef: true })(SlateEditor));