Correct the Note editor.
Split the source file in sub components.
Correct a timing problem on the editor checkbox.
--- 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 (
<div className="note-content w-100 pl-2 pt-2">
<SlateEditor editorRef={this.editorInst}
- onButtonClick={ this.onClickButton }
+ submitNote={ this.submitNote }
note={ this.props.note }
annotationCategories={ this.props.annotationCategories } />
</div>
--- 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 @@
<div className="editor-left sticky-bottom px-2">
<SlateEditor editorRef={this.editorInst}
onChange={this.onEditorChange}
- onEnterKeyDown={this.onAddNoteClick}
- onButtonClick={this.onAddNoteClick}
- onCheckboxChange={this.onCheckboxChange}
- isChecked={this.props.autoSubmit}
+ submitNote={this.submitNote}
isButtonDisabled={this.state.buttonDisabled}
annotationCategories={ this.props.annotationCategories } />
--- 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 @@
<div className="col-lg-10 offset-md-2">
<NoteInput
session={this.props.currentSession}
- autoSubmit={this.props.autoSubmit}
addNote={this.props.notesActions.addNote}
- setAutoSubmit={this.props.userActions.setAutoSubmit}
annotationCategories={this.props.annotationCategories}/>
</div>
</div>
@@ -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)
}
}
--- 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;
--- /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}) => (
+ <span className={"button sticky-top" + ((!isActive)?" text-primary":" text-dark")} onMouseDown={onMouseDown} data-active={isActive} title={t("common."+icon)} >
+ <span className="material-icons">{icon}</span>
+ </span>
+));
--- /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 (
+ <PortalWithState
+ // closeOnOutsideClick
+ closeOnEsc
+ onOpen={this.updateMenu}
+ >
+ {({ openPortal, closePortal, isOpen, portal }) => {
+ const onMouseDown = R.partial(onClickCategoryButton, [openPortal, closePortal, isOpen]);
+ const onCategoryClickHandler = R.partial(onCategoryClick, [closePortal,]);
+ return (
+ <React.Fragment>
+ <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
+ <span className="material-icons">label</span>
+ </span>
+ {portal(
+ <div className="hovering-menu" ref={this.hoveringMenuRef}>
+ <CategoriesTooltip categories={annotationCategories || defaultAnnotationsCategories} onCategoryClick={onCategoryClickHandler} />
+ </div>
+ )}
+ </React.Fragment>
+ )}
+ }
+ </PortalWithState>
+ )
+ }
+}
--- 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 <ins>{children}</ins>
case 'category':
- return <span style={{ backgroundColor: obj.color }}>{children}</span>
+ return <span style={{ backgroundColor: obj.data.get('color') }}>{children}</span>
default: return;
}
}
--- /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}) => (
+ <span className={"button sticky-top" + ((!isActive)?" text-primary":" text-dark")} onMouseDown={onMouseDown} data-active={isActive} title={t("common." + icon)} >
+
+ <span className="material-icons">{icon}</span>
+ </span>
+));
+
--- /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 (
+ <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
+ <MarkButton icon='format_bold' isActive={this.hasMark('bold')} onMouseDown={(e) => this.onClickMark(e, 'bold')} />
+ <MarkButton icon='format_italic' isActive={this.hasMark('italic')} onMouseDown={(e) => this.onClickMark(e, 'italic')} />
+ <MarkButton icon='format_underlined' isActive={this.hasMark('underlined')} onMouseDown={(e) => this.onClickMark(e, 'underlined')} />
+
+ <CategoryButton
+ isActive={this.hasMark('category')}
+ onClickCategoryButton={this.onClickCategoryButton}
+ onCategoryClick={this.onCategoryClick}
+ annotationCategories={this.props.annotationCategories}
+ />
+
+ <BlockButton icon='format_list_numbered' isActive={this.isBlockActive('numbered-list')} onMouseDown={e => this.onClickBlock(e, 'numbered-list')} />
+ <BlockButton icon='format_list_bulleted' isActive={this.isBlockActive('bulleted-list')} onMouseDown={e => this.onClickBlock(e, 'bulleted-list')} />
+
+ <ToolbarButtons
+ hasNote={!!this.props.note}
+ isButtonDisabled={this.props.isButtonDisabled}
+ submitNote={this.props.submitNote}
+ />
+
+ </div>
+ )
+ }
+}
--- /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 }) => (
+ <div className="checkbox float-right">
+ <label className="mr-2">
+ <input type="checkbox" checked={autoSubmit} onChange={(e) => { setTimeout(userActions.setAutoSubmit, 0, e.target.checked) }} value="enterBox" /><small className="text-muted ml-1"><Trans i18nKey="slate_editor.press_enter_msg">Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</Trans></small>
+ </label>
+ </div>
+));
+
+export default ({ hasNote, isButtonDisabled, submitNote }) => (
+ <div>
+ <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right text-capitalize" disabled={isButtonDisabled} onClick={submitNote}>
+ { hasNote ? <Trans i18nKey="common.save">Save</Trans> : <Trans i18nKey="common.add">Add</Trans> }
+ </button>
+ { !hasNote && <ToolbarCheckbox /> }
+ </div>
+);
+
--- 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 => <ul {...props.attributes}>{props.children}</ul>,
- 'list-item': props => <li {...props.attributes}>{props.children}</li>,
- 'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
- },
- marks: {
- bold: {
- fontWeight: 'bold'
- },
- category: props => {
- const data = props.mark.data;
- return <span style={{ backgroundColor: data.color }} {...props.attributes}>{props.children}</span>
- },
- 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 (
- <div className="bg-secondary mb-5">
- <div className="sticky-top">
- {this.renderToolbar()}
- </div>
- {this.renderEditor()}
- </div>
- )
- }
-
- /**
- * Render the toolbar.
- *
- * @return {Element}
- */
-
- renderToolbar = () => {
- return (
- <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
- {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()}
- </div>
- )
- }
+ e.preventDefault();
- renderToolbarCheckbox = () => {
- return (
- <div className="checkbox float-right">
- <label className="mr-2">
- <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"><Trans i18nKey="slate_editor.press_enter_msg">Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</Trans></small>
- </label>
- </div>
- )
- }
-
- renderToolbarButtons = () => {
- const t = this.props.t;
- return (
- <div>
- <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right text-capitalize" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
- { this.props.note ? t('common.save') : t('common.add') }
- </button>
- { !this.props.note && this.renderToolbarCheckbox() }
- </div>
- );
- }
-
- /**
- * 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 (
- // <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
- <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
+ // 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;
+ }
+ }
- <span className="material-icons">{icon}</span>
- </span>
- )
- }
-
- /**
- * 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 (
- <PortalWithState
- // closeOnOutsideClick
- closeOnEsc
- onOpen={this.onPortalOpen}
- onClose={this.onPortalClose}
- >
- {({ 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 (
- <React.Fragment>
- <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
- <span className="material-icons">label</span>
- </span>
- {portal(
- <div className="hovering-menu" ref={this.hoveringMenuRef}>
- <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={onCategoryClick} />
- </div>
- )}
- </React.Fragment>
- )}
- }
- </PortalWithState>
- )
}
// 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 <strong {...attributes}>{children}</strong>
@@ -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 (
- <span className={blockActivation} onMouseDown={onMouseDown} data-active={isActive}>
- <span className="material-icons">{icon}</span>
- </span>
- )
- }
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 = () => (
+ <div className="bg-secondary mb-5">
+ <div className="sticky-top">
+ <Toolbar
+ value={this.state.value}
+ editor={this.editor}
+ note={this.props.note}
+ annotationCategories={this.props.annotationCategories}
+ isButtonDisabled={this.props.isButtonDisabled}
+ submitNote={this.submitNote}
+ />
+ </div>
<div className="editor-slatejs p-2">
- {/* {this.renderHoveringMenu()} */}
<Editor
ref={this.editorRef}
spellCheck
- placeholder={t('slate_editor.placeholder')}
- // schema={schema}
- plugins={plugins}
+ placeholder={this.props.t('slate_editor.placeholder')}
value={this.state.value}
onChange={this.onChange}
- // onKeyDown={this.onKeyDown}
+ onKeyDown={this.onKeyDown}
+ onKeyUp={this.onKeyUp}
renderMark={this.renderMark}
renderNode = {this.renderNode}
/>
</div>
- )
- }
-
- // renderHoveringMenu = () => {
- // return (
- // <Portal ref="portal"
- // isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen}
- // onOpen={this.onPortalOpen}
- // onClose={this.onPortalClose}
- // closeOnOutsideClick={false} closeOnEsc={true}>
- // <div className="hovering-menu">
- // <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={this.onCategoryClick} />
- // </div>
- // </Portal>
- // )
- // }
-
- updateMenu = () => {
+ </div>
+ );
- // 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));
--- 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"
}
}
--- 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"
+
}
}