# HG changeset patch # User ymh # Date 1542363553 -3600 # Node ID 0e6703cd0968d82adbaa5ae4197be9114a7d50c8 # Parent 4b780ebbedc611eb25f0066b96d7551b58eaf292 Correct the Note editor. Split the source file in sub components. Correct a timing problem on the editor checkbox. diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/Note.js --- a/client/src/components/Note.js Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/components/Note.js Fri Nov 16 11:19:13 2018 +0100 @@ -25,7 +25,7 @@ this.props.onDelete(); } - onClickButton = (e) => { + submitNote = () => { const plain = this.editor.asPlain(); const raw = this.editor.asRaw(); @@ -56,7 +56,7 @@ return (
diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/NoteInput.js --- a/client/src/components/NoteInput.js Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/components/NoteInput.js Fri Nov 16 11:19:13 2018 +0100 @@ -28,7 +28,7 @@ }); } - onAddNoteClick = () => { + submitNote = () => { const plain = this.editor.asPlain(); const raw = this.editor.asRaw(); @@ -52,10 +52,6 @@ setTimeout(() => this.editor.focus(), 250); } - onCheckboxChange = (e) => { - this.props.setAutoSubmit(e.target.checked); - } - componentDidMount() { if(this.editor) { const text = this.editor.asPlain(); @@ -70,10 +66,7 @@
diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/Session.js --- a/client/src/components/Session.js Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/components/Session.js Fri Nov 16 11:19:13 2018 +0100 @@ -12,9 +12,7 @@ import ProtocolSummary from './ProtocolSummary'; import * as sessionsActions from '../actions/sessionsActions'; import * as notesActions from '../actions/notesActions'; -import * as userActions from '../actions/userActions'; import { getSession, getSessionNotes } from '../selectors/coreSelectors'; -import { getAutoSubmit } from '../selectors/authSelectors'; import { extractAnnotationCategories, defaultAnnotationsCategories } from '../constants'; class Session extends Component { @@ -88,9 +86,7 @@
@@ -106,7 +102,6 @@ const sessionId = props.match.params.id; - const autoSubmit = getAutoSubmit(state); const currentSession = getSession(sessionId, state); const currentNotes = getSessionNotes(sessionId, state); const annotationCategories = currentSession?extractAnnotationCategories(currentSession.protocol):defaultAnnotationsCategories; @@ -114,7 +109,6 @@ return { currentSession, notes: currentNotes, - autoSubmit, annotationCategories }; } @@ -122,8 +116,7 @@ function mapDispatchToProps(dispatch) { return { sessionsActions: bindActionCreators(sessionsActions, dispatch), - notesActions: bindActionCreators(notesActions, dispatch), - userActions: bindActionCreators(userActions, dispatch) + notesActions: bindActionCreators(notesActions, dispatch) } } diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/AnnotationPlugin.js --- a/client/src/components/SlateEditor/AnnotationPlugin.js Tue Nov 13 16:46:15 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -function AnnotationPlugin(options) { - - const { onChange } = options - - return { - onSelect(event, editor, next) { - event.preventDefault() - - const { value } = editor - const { selection } = value - const { start, end} = selection - - if (selection.isCollapsed) { - return next(); - } - - const nodes = []; - let hasStarted = false; - let hasEnded = false; - - // 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.isAtEndOfNode(node) ? end.offset : node.text.length; - return node.text.substring(textStart,textEnd); - }).join('\n'); - - if (onChange) { - onChange(text, start.offset, end.offset); - } - - return next(); - - } - - }; -} - -export default AnnotationPlugin; diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/BlockButton.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/BlockButton.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,8 @@ +import React from 'react'; +import { withNamespaces } from 'react-i18next'; + +export default withNamespaces("")(({icon, isActive, onMouseDown, t}) => ( + + {icon} + +)); diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/CategoryButton.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/CategoryButton.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,82 @@ +import React from 'react'; +import * as R from 'ramda'; +import { PortalWithState } from 'react-portal'; +import CategoriesTooltip from './CategoriesTooltip'; +import { defaultAnnotationsCategories } from '../../constants'; + +/** + * Render a category toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ +export default class CategoryButton extends React.Component { + + constructor(props) { + super(props); + + this.hoveringMenuRef = React.createRef(); + } + + get hoveringMenu() { + if(this.hoveringMenuRef) { + return this.hoveringMenuRef.current; + } + return null; + } + + updateMenu = () => { + + const hoveringMenu = this.hoveringMenu; + + if (!hoveringMenu) 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` + } + + render = () => { + const isActive = this.props.isActive; + const onClickCategoryButton = this.props.onClickCategoryButton; + const onCategoryClick = this.props.onCategoryClick; + const annotationCategories = this.props.annotationCategories; + + const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); + + return ( + + {({ openPortal, closePortal, isOpen, portal }) => { + const onMouseDown = R.partial(onClickCategoryButton, [openPortal, closePortal, isOpen]); + const onCategoryClickHandler = R.partial(onCategoryClick, [closePortal,]); + return ( + + + label + + {portal( +
+ +
+ )} +
+ )} + } +
+ ) + } +} diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/HtmlSerializer.js --- a/client/src/components/SlateEditor/HtmlSerializer.js Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/components/SlateEditor/HtmlSerializer.js Fri Nov 16 11:19:13 2018 +0100 @@ -65,7 +65,7 @@ case 'underlined': return {children} case 'category': - return {children} + return {children} default: return; } } diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/MarkButton.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/MarkButton.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,17 @@ +import React from 'react'; +import { withNamespaces } from 'react-i18next'; + +/** + * Render a mark-toggling toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ +export default withNamespaces("")(({icon, isActive, onMouseDown, t}) => ( + + + {icon} + +)); + diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/Toolbar.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/Toolbar.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,254 @@ +import React from 'react'; +import ToolbarButtons from './ToolbarButtons'; +import MarkButton from './MarkButton'; +import CategoryButton from './CategoryButton'; +import BlockButton from './BlockButton'; + +/** + * Define the default node type. + */ + +const DEFAULT_NODE = 'paragraph' + + +/** + * Render the toolbar. + * + * @return {Element} + */ +export default class Toolbar extends React.Component { + + /** + * Deserialize the initial editor state. + * + * @type {Object} + */ + constructor(props) { + super(props); + this.editorRef = React.createRef(); + } + + /** + * Check if the current selection has a mark with `type` in it. + * + * @param {String} type + * @return {Boolean} + */ + + hasMark = type => { + const { value } = this.props; + 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.props; + return value.blocks.some(node => node.type === type) + } + + + /** + * When a mark button is clicked, toggle the current mark. + * + * @param {Event} e + * @param {String} type + */ + + onClickMark = (e, type) => { + this.props.editor.toggleMark(type) + } + + isBlockActive = (type) => { + let isActive = this.hasBlock(type) + + if (['numbered-list', 'bulleted-list'].includes(type)) { + const { value } = this.props; + const firstBlock = value.blocks.first(); + if(firstBlock) { + const parent = value.document.getParent(firstBlock.key); + isActive = this.hasBlock('list-item') && parent && parent.type === type; + } + } + + return isActive; + } + + onClickCategoryButton = (openPortal, closePortal, isOpen, e) => { + e.preventDefault(); + const { value, editor } = this.props; + + // Can't use toggleMark here, because it expects the same object + // @see https://github.com/ianstormtaylor/slate/issues/873 + if (this.hasMark('category')) { + value.activeMarks.filter(mark => mark.type === 'category') + .forEach(mark => editor.removeMark(mark)); + closePortal(); + } else { + openPortal(); + } + } + + getSelectionParams = (value) => { + + const { selection } = value + const { start, end} = selection + + if (selection.isCollapsed) { + return {}; + } + + const nodes = []; + let hasStarted = false; + let hasEnded = false; + + // 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) => { + + const { value, editor } = this.props; + + const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(value); + + if(!currentSelectionText) { + closePortal(); + return; + } + + const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') + categoryMarks.forEach(mark => this.editor.removeMark(mark)); + + editor.addMark({ + type: 'category', + data: { + text: currentSelectionText, + selection: { + start: currentSelectionStart, + end: currentSelectionEnd, + }, + color: category.color, + key: category.key, + name: category.name, + comment: category.comment + } + }) + + closePortal(); + } + + /** + * When a block button is clicked, toggle the block type. + * + * @param {Event} e + * @param {String} type + */ + + onClickBlock = (e, type) => { + e.preventDefault(); + + const { editor, value } = this.props; + + // 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') + + } else if (isList) { + editor + .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') + .wrapBlock(type) + + } else { + editor + .setBlocks('list-item') + .wrapBlock(type) + + } + } + } + + + render = () => { + return ( +
+ this.onClickMark(e, 'bold')} /> + this.onClickMark(e, 'italic')} /> + this.onClickMark(e, 'underlined')} /> + + + + this.onClickBlock(e, 'numbered-list')} /> + this.onClickBlock(e, 'bulleted-list')} /> + + + +
+ ) + } +} diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/ToolbarButtons.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/ToolbarButtons.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,41 @@ +import React from 'react'; +import { Trans } from 'react-i18next'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { getAutoSubmit } from '../../selectors/authSelectors'; +import * as userActions from '../../actions/userActions'; + +function mapStateToProps(state, props) { + + const autoSubmit = getAutoSubmit(state); + + return { + autoSubmit, + }; +} + +function mapDispatchToProps(dispatch) { + return { + userActions: bindActionCreators(userActions, dispatch) + } +} + +// see https://github.com/facebook/react/issues/3005 for explanation about the timeout. +const ToolbarCheckbox = connect(mapStateToProps, mapDispatchToProps)(({ autoSubmit, userActions }) => ( +
+ +
+)); + +export default ({ hasNote, isButtonDisabled, submitNote }) => ( +
+ + { !hasNote && } +
+); + 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)); diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/locales/en/translation.json --- a/client/src/locales/en/translation.json Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/locales/en/translation.json Fri Nov 16 11:19:13 2018 +0100 @@ -47,6 +47,12 @@ "create": "create", "cancel": "cancel", "add": "add", - "save": "save" + "save": "save", + "format_bold": "bold", + "format_italic": "italic", + "format_underlined": "underlined", + "format_category": "meta-categories", + "format_list_numbered": "numbered list", + "format_list_bulleted": "bulleted list" } } diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/locales/fr/translation.json --- a/client/src/locales/fr/translation.json Tue Nov 13 16:46:15 2018 +0100 +++ b/client/src/locales/fr/translation.json Fri Nov 16 11:19:13 2018 +0100 @@ -47,6 +47,13 @@ "create": "créer", "cancel": "annuler", "add": "ajouter", - "save": "sauvegarder" + "save": "sauvegarder", + "format_bold": "gras", + "format_italic": "italique", + "format_underlined": "souligné", + "format_category": "meta-catégories", + "format_list_numbered": "liste numérotée", + "format_list_bulleted": "liste à puce" + } }