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));