--- a/client/src/components/SlateEditor.js Wed Sep 05 13:48:10 2018 +0200
+++ b/client/src/components/SlateEditor.js Tue Sep 25 02:02:13 2018 +0200
@@ -1,4 +1,6 @@
-import { Editor, Plain, Raw } from 'slate'
+import { Value } from 'slate'
+import Plain from 'slate-plain-serializer'
+import { Editor } from 'slate-react'
import React from 'react'
import Portal from 'react-portal'
import Immutable from 'immutable'
@@ -41,11 +43,32 @@
fontStyle: 'italic'
},
underlined: {
- textDecoration: 'underline'
+ textDecoration: 'underlined'
}
}
}
+const initialValue = Value.fromJSON({
+ document: {
+ nodes: [
+ {
+ object: 'block',
+ type: 'paragraph',
+ nodes: [
+ {
+ object: 'text',
+ leaves: [
+ {
+ text: '',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+})
+
/**
* The rich text example.
*
@@ -74,8 +97,9 @@
plugins.push(annotationPlugin);
+
this.state = {
- state: props.note ? Raw.deserialize(props.note.raw) : Plain.deserialize(''),
+ value: props.note ? initialValue : Plain.deserialize(''),
startedAt: null,
finishedAt: null,
currentSelectionText: '',
@@ -97,44 +121,20 @@
this.updateMenu();
}
- /**
- * Check if the current selection has a mark with `type` in it.
+ /**
+ * On change, save the new state.
*
- * @param {String} type
- * @return {Boolean}
+ * @param {Change} change
*/
- hasMark = (type) => {
- const { state } = this.state
- return state.marks.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 { state } = this.state
- return state.blocks.some(node => node.type === type)
- }
-
- /**
- * On change, save the new state.
- *
- * @param {State} state
- */
-
- onChange = (state) => {
+ onChange = ({value}) => {
let newState = {
- state: state,
+ value: value,
startedAt: this.state.startedAt
};
- const isEmpty = state.document.length === 0;
+ const isEmpty = value.document.length === 0;
// Reset timers when the text is empty
if (isEmpty) {
@@ -158,16 +158,40 @@
}
}
+ /**
+ * 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.state);
+ return Plain.serialize(this.state.value);
}
asRaw = () => {
- return Raw.serialize(this.state.state);
+ return JSON.stringify(this.state.value.toJSON());
}
asHtml = () => {
- return HtmlSerializer.serialize(this.state.state);
+ return HtmlSerializer.serialize(this.state.value);
}
asCategories = () => {
@@ -180,8 +204,8 @@
}
clear = () => {
- const state = Plain.deserialize('');
- this.onChange(state);
+ const value = Plain.deserialize('');
+ this.onChange({value});
}
focus = () => {
@@ -193,46 +217,48 @@
*
* @param {Event} e
* @param {Object} data
- * @param {State} state
- * @return {State}
+ * @param {Change} change
+ * @return {Change}
*/
- onKeyDown = (e, data, state) => {
+ onKeyDown = (e, change) => {
+ // if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
+
+ // e.preventDefault();
+ // this.props.onEnterKeyDown();
+
+ // return change;
+ // }
+
+ // if (!data.isMod) return
+ if (!e.ctrlKey) return
+ // Decide what to do based on the key code...
+ switch (e.key) {
+ // When "B" is pressed, add a "bold" mark to the text.
+ case 'b': {
+ e.preventDefault()
+ change.toggleMark('bold')
- if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
- e.preventDefault();
- this.props.onEnterKeyDown();
+ return true
+ }
+ case 'i': {
+ // When "U" is pressed, add an "italic" mark to the text.
+ e.preventDefault()
+ change.toggleMark('italic')
- return state;
+ return true
+ }
+ case 'u': {
+ // When "U" is pressed, add an "underline" mark to the text.
+ e.preventDefault()
+ change.toggleMark('underlined')
+
+ return true
+ }
+ }
}
- if (!data.isMod) return
- let mark
-
- switch (data.key) {
- case 'b':
- mark = 'bold'
- break
- case 'i':
- mark = 'italic'
- break
- case 'u':
- mark = 'underlined'
- break
- default:
- return
- }
-
- state = state
- .transform()
- .toggleMark(mark)
- .apply()
-
- e.preventDefault()
- return state
- }
-
- /**
+ /**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} e
@@ -240,10 +266,10 @@
*/
onClickMark = (e, type) => {
- e.preventDefault()
- const { state } = this.state
+
+ e.preventDefault()
+ const { value } = this.state
let { categories } = this.state
- const transform = state.transform()
let isPortalOpen = false;
@@ -251,24 +277,26 @@
// 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 = state.marks.filter(mark => mark.type === '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');
categories = this.removeCategory(categories, key, text)
- transform.removeMark(mark)
+ const change = value.change().removeMark(mark)
+ this.onChange(change)
})
} else {
isPortalOpen = !this.state.isPortalOpen;
}
} else {
- transform.toggleMark(type)
+ const change = value.change().toggleMark(type)
+ this.onChange(change)
}
this.setState({
- state: transform.apply(),
+ state: value.change,
isPortalOpen: isPortalOpen,
categories: categories
})
@@ -283,9 +311,9 @@
onClickBlock = (e, type) => {
e.preventDefault()
- let { state } = this.state
- const transform = state.transform()
- const { document } = state
+ const { value } = this.state
+ const change = value.change()
+ const { document } = value
// Handle everything but list buttons.
if (type !== 'bulleted-list' && type !== 'numbered-list') {
@@ -293,43 +321,46 @@
const isList = this.hasBlock('list-item')
if (isList) {
- transform
- .setBlock(isActive ? DEFAULT_NODE : type)
+ change
+ .setBlocks(isActive ? DEFAULT_NODE : type)
.unwrapBlock('bulleted-list')
.unwrapBlock('numbered-list')
}
else {
- transform
- .setBlock(isActive ? DEFAULT_NODE : type)
+ change
+ .setBlocks(isActive ? DEFAULT_NODE : type)
}
}
// Handle the extra wrapping required for list buttons.
else {
const isList = this.hasBlock('list-item')
- const isType = state.blocks.some((block) => {
+ const isType = value.blocks.some((block) => {
return !!document.getClosest(block.key, parent => parent.type === type)
})
if (isList && isType) {
- transform
- .setBlock(DEFAULT_NODE)
+ change
+ .setBlocks(DEFAULT_NODE)
.unwrapBlock('bulleted-list')
.unwrapBlock('numbered-list')
+
} else if (isList) {
- transform
+ change
.unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
.wrapBlock(type)
+
} else {
- transform
- .setBlock('list-item')
+ change
+ .setBlocks('list-item')
.wrapBlock(type)
+
}
}
- state = transform.apply()
- this.setState({ state })
+
+ this.onChange(change)
}
onPortalOpen = (portal) => {
@@ -338,25 +369,24 @@
}
onPortalClose = (portal) => {
- let { state } = this.state
- const transform = state.transform();
+ let { value } = this.state
this.setState({
- state: transform.apply(),
+ value: value.change,
isPortalOpen: false
})
}
onCategoryClick = (category) => {
- const { state, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state;
+ const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state;
+ const change = value.change()
let { categories } = this.state;
- const transform = state.transform();
- const categoryMarks = state.marks.filter(mark => mark.type === 'category')
- categoryMarks.forEach(mark => transform.removeMark(mark));
+ const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
+ categoryMarks.forEach(mark => change.removeMark(mark));
- transform.addMark({
+ change.addMark({
type: 'category',
data: {
text: currentSelectionText,
@@ -368,6 +398,7 @@
key: category.key
}
})
+ this.onChange(change)
Object.assign(category, {
text: currentSelectionText,
@@ -379,7 +410,7 @@
categories = categories.push(category);
this.setState({
- state: transform.apply(),
+ value: value,
isPortalOpen: false,
categories: categories
});
@@ -405,10 +436,12 @@
render = () => {
return (
- <div>
+ <div className="bg-secondary mb-5">
+ <div className="sticky-top">
{this.renderToolbar()}
+ </div>
{this.renderEditor()}
- </div>
+ </div>
)
}
@@ -420,25 +453,26 @@
renderToolbar = () => {
return (
- <div className="menu toolbar-menu">
- {this.renderMarkButton('bold', 'format_bold')}
- {this.renderMarkButton('italic', 'format_italic')}
- {this.renderMarkButton('underlined', 'format_underlined')}
- {this.renderMarkButton('category', 'label')}
+ <div className="menu toolbar-menu d-flex bg-secondary">
+ {this.renderMarkButton('bold', 'format_bold')}
+ {this.renderMarkButton('italic', 'format_italic')}
+ {this.renderMarkButton('underlined', 'format_underlined')}
+ {this.renderMarkButton('category', 'label')}
- {this.renderBlockButton('numbered-list', 'format_list_numbered')}
- {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
- {this.renderToolbarButtons()}
+ {this.renderBlockButton('numbered-list', 'format_list_numbered')}
+ {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
+
+ {this.renderToolbarButtons()}
</div>
)
}
renderToolbarCheckbox = () => {
return (
- <div className="checkbox">
- <label>
- <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /> <kbd>Entrée</kbd>= Ajouter une note
+ <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"> Appuyer sur <kbd className="bg-danger text-muted ml-1">Entrée</kbd> pour ajouter une note</small>
</label>
</div>
)
@@ -447,10 +481,10 @@
renderToolbarButtons = () => {
return (
<div>
- { !this.props.note && this.renderToolbarCheckbox() }
- <button type="button" className="btn btn-primary btn-lg" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
+ <button type="button" className="btn btn-primary btn-sm text-secondary float-right mr-5" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
{ this.props.note ? 'Save note' : 'Ajouter' }
</button>
+ { !this.props.note && this.renderToolbarCheckbox() }
</div>
);
}
@@ -467,13 +501,30 @@
const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type)
+
return (
- <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
+ <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
<span className="material-icons">{icon}</span>
</span>
)
}
+ // Add a `renderMark` method to render marks.
+
+ renderMark = props => {
+ const { children, mark, attributes } = props
+
+ switch (mark.type) {
+ case 'bold':
+ return <strong {...attributes}>{children}</strong>
+ case 'code':
+ return <code {...attributes}>{children}</code>
+ case 'italic':
+ return <em {...attributes}>{children}</em>
+ case 'underlined':
+ return <ins {...attributes}>{children}</ins>
+ }
+ }
/**
* Render a block-toggling toolbar button.
*
@@ -483,16 +534,41 @@
*/
renderBlockButton = (type, icon) => {
- const isActive = this.hasBlock(type)
+ let isActive = this.hasBlock(type)
+
+ if (['numbered-list', 'bulleted-list'].includes(type)) {
+ const { value } = this.state
+ const parent = value.document.getParent(value.blocks.first().key)
+ isActive = this.hasBlock('list-item') && parent && parent.type === type
+ }
const onMouseDown = e => this.onClickBlock(e, type)
return (
- <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
+ <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
<span className="material-icons">{icon}</span>
</span>
)
}
+ renderNode = props => {
+ const { attributes, children, node } = props
+
+ switch (node.type) {
+ case 'block-quote':
+ return <blockquote {...attributes}>{children}</blockquote>
+ case 'bulleted-list':
+ return <ul {...attributes}>{children}</ul>
+ case 'heading-one':
+ return <h1 {...attributes}>{children}</h1>
+ case 'heading-two':
+ return <h2 {...attributes}>{children}</h2>
+ case 'list-item':
+ return <li {...attributes}>{children}</li>
+ case 'numbered-list':
+ return <ol {...attributes}>{children}</ol>
+ }
+}
+
/**
* Render the Slate editor.
*
@@ -501,19 +577,24 @@
renderEditor = () => {
return (
+ <div className="editor-wrapper sticky-bottom p-2">
<div className="editor-slatejs">
{this.renderHoveringMenu()}
<Editor
ref="editor"
spellCheck
- placeholder={'Enter some rich text...'}
+ autoFocus
+ placeholder={'Votre espace de prise de note...'}
schema={schema}
plugins={plugins}
- state={this.state.state}
+ value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
+ renderMark={this.renderMark}
+ renderNode = {this.renderNode}
/>
</div>
+ </div>
)
}