client/src/components/SlateEditor.js
changeset 168 ea92f4fe783d
parent 167 1f340f3597a8
child 169 f98efa1bddd1
--- 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