--- a/client/src/components/SlateEditor.js Tue Oct 09 19:07:47 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,706 +0,0 @@
-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'
-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.get('color') }}>{props.children}</span>
- },
- italic: {
- fontStyle: 'italic'
- },
- underlined: {
- textDecoration: 'underlined'
- }
- }
-
-}
-
-const initialValue = Value.fromJSON({
- document: {
- nodes: [
- {
- object: 'block',
- type: 'paragraph',
- nodes: [
- {
- object: 'text',
- leaves: [
- {
- text: '',
- },
- ],
- },
- ],
- },
- ],
- },
-})
-
-/**
- * The rich text example.
- *
- * @type {Component}
- */
-
-class SlateEditor extends React.Component {
-
- /**
- * Deserialize the initial editor state.
- *
- * @type {Object}
- */
- 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(''),
- startedAt: null,
- finishedAt: null,
- currentSelectionText: '',
- currentSelectionStart: 0,
- currentSelectionEnd: 0,
- hoveringMenu: null,
- isPortalOpen: false,
- categories: Immutable.List([]),
- isCheckboxChecked: false,
- enterKeyValue: 0,
- };
- }
-
- componentDidMount = () => {
- this.updateMenu();
- this.focus();
- }
-
- componentDidUpdate = () => {
- this.updateMenu();
- }
-
- /**
- * On change, save the new state.
- *
- * @param {Change} change
- */
-
- onChange = ({value}) => {
-
- let newState = {
- value: value,
- startedAt: this.state.startedAt
- };
-
- const isEmpty = value.document.length === 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() });
- }
-
- this.setState(newState)
-
- if (typeof this.props.onChange === 'function') {
- this.props.onChange(newState);
- }
- }
-
- /**
- * 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);
- }
-
- asRaw = () => {
- return JSON.stringify(this.state.value.toJSON());
- }
-
- asHtml = () => {
- return HtmlSerializer.serialize(this.state.value);
- }
-
- asCategories = () => {
- return this.state.categories
- }
-
- removeCategory = (categories, key, text) => {
- const categoryIndex = categories.findIndex(category => category.key === key && category.text === text)
- return categories.delete(categoryIndex)
- }
-
- clear = () => {
- const value = Plain.deserialize('');
- this.onChange({value});
- }
-
- focus = () => {
- this.refs.editor.focus();
- }
-
- /**
- * When a mark button is clicked, toggle the current mark.
- *
- * @param {Event} e
- * @param {String} type
- */
-
- onClickMark = (e, type) => {
-
- e.preventDefault()
- const { value } = this.state
- let { categories } = this.state
-
- let isPortalOpen = false;
-
- if (type === 'category') {
- // 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');
-
- categories = this.removeCategory(categories, key, text)
- const change = value.change().removeMark(mark)
- this.onChange(change)
- })
-
- } else {
- isPortalOpen = !this.state.isPortalOpen;
- }
- } else {
- const change = value.change().toggleMark(type)
- this.onChange(change)
- }
-
- this.setState({
- state: value.change,
- isPortalOpen: isPortalOpen,
- categories: categories
- })
- }
-
- /**
- * When a block button is clicked, toggle the block type.
- *
- * @param {Event} e
- * @param {String} type
- */
-
- onClickBlock = (e, type) => {
- e.preventDefault()
- const { value } = this.state
- const change = value.change()
- 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) {
- change
- .setBlocks(isActive ? DEFAULT_NODE : type)
- .unwrapBlock('bulleted-list')
- .unwrapBlock('numbered-list')
- }
-
- else {
- change
- .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) {
- change
- .setBlocks(DEFAULT_NODE)
- .unwrapBlock('bulleted-list')
- .unwrapBlock('numbered-list')
-
- } else if (isList) {
- change
- .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
- .wrapBlock(type)
-
- } else {
- change
- .setBlocks('list-item')
- .wrapBlock(type)
-
- }
- }
-
-
- this.onChange(change)
- }
-
- onPortalOpen = (portal) => {
- // When the portal opens, cache the menu element.
- this.setState({ hoveringMenu: portal.firstChild })
- }
-
- onPortalClose = (portal) => {
- let { value } = this.state
-
- this.setState({
- value: value.change,
- isPortalOpen: false
- })
- }
-
- onCategoryClick = (category) => {
-
- const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state;
- const change = value.change()
- let { categories } = this.state;
-
- const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
- categoryMarks.forEach(mark => change.removeMark(mark));
-
- change.addMark({
- type: 'category',
- data: {
- text: currentSelectionText,
- selection: {
- start: currentSelectionStart,
- end: currentSelectionEnd,
- },
- color: category.color,
- key: category.key
- }
- })
-
- Object.assign(category, {
- text: currentSelectionText,
- selection: {
- start: currentSelectionStart,
- end: currentSelectionEnd,
- },
- });
- categories = categories.push(category);
-
- this.onChange(change)
-
- this.setState({
- isPortalOpen: false,
- categories: categories
- });
- }
-
- onButtonClick = () => {
- if (typeof this.props.onButtonClick === 'function') {
- this.props.onButtonClick();
- }
- }
-
- onCheckboxChange = (e) => {
- if (typeof this.props.onCheckboxChange === 'function') {
- this.props.onCheckboxChange(e);
- }
- }
-
- /**
- * On key down, if it's a formatting command toggle a mark.
- *
- * @param {Event} e
- * @param {Change} change
- * @return {Change}
- */
-
- onKeyDown = (e, change) => {
-
- const {value} = this.state;
-
- // if (e.key === 'Enter' && value.document.text === '') {
- // change.removeChild()
- // }
-
- if (e.key === 'Enter' && value.document.text !== '') {
- this.setState({enterKeyValue: 1})
- }
-
- if (e.key !== 'Enter') {
- this.setState({
- enterKeyValue: 0,
- })
-
- }
-
- if (e.key === 'Enter' && !this.props.isChecked && this.state.enterKeyValue === 1 && typeof this.props.onEnterKeyDown === 'function') {
- e.preventDefault();
- this.props.onEnterKeyDown();
- this.setState({
- enterKeyValue: 0,
- })
-
-
- return change
- }
-
- 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
- // 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.renderMarkButton('category', 'label')}
-
-
- {this.renderBlockButton('numbered-list', 'format_list_numbered')}
- {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
-
- {this.renderToolbarButtons()}
- </div>
- )
- }
-
- 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"> Appuyer sur <kbd className="bg-danger text-muted ml-1">Entrée</kbd> pour ajouter une note</small>
- </label>
- </div>
- )
- }
-
- renderSaveButton = () => {
- if (this.props.note) {
- return <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold mr-2" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
- Sauvegarder</button>
- }
- }
-
- renderToolbarButtons = () => {
- return (
- <div>
- {/* <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}> */}
- {/* { this.props.note ? 'Sauvegarder' : 'Ajouter' } */}
- {this.renderSaveButton()}
- {/* </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}>
-
- <span className="material-icons">{icon}</span>
- </span>
- )
- }
-
- // Add a `renderMark` method to render marks.
-
- renderMark = props => {
- const { children, mark, attributes } = props
-
- switch (mark.type) {
- default: {
- break;
- }
- 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.
- *
- * @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 parent = value.document.getParent(value.blocks.first().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 => {
- const { attributes, children, node } = props
-
- switch (node.type) {
- default: {
- break;
- }
- 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.
- *
- * @return {Element}
- */
-
- renderEditor = () => {
- return (
- <div className="editor-slatejs p-2">
- {this.renderHoveringMenu()}
- <Editor
- ref="editor"
- spellCheck
- placeholder={'Votre espace de prise de note...'}
- schema={schema}
- plugins={plugins}
- value={this.state.value}
- onChange={this.onChange}
- onKeyDown={this.onKeyDown}
- 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 = () => {
-
- const { hoveringMenu } = this.state
-
- 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 + window.scrollY + hoveringMenu.offsetHeight}px`
- hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px`
- }
-
-}
-
-/**
- * Export.
- */
-
-export default SlateEditor