diff -r 1f340f3597a8 -r ea92f4fe783d client/src/components/SlateEditor.js --- 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 => , - '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: '', - }, - ], - }, - ], - }, - ], - }, -}) - -/** - * 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 ( -
    -
    - {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.renderMarkButton('category', 'label')} - - - {this.renderBlockButton('numbered-list', 'format_list_numbered')} - {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} - - {this.renderToolbarButtons()} -
    - ) - } - - renderToolbarCheckbox = () => { - return ( -
    - -
    - ) - } - - renderSaveButton = () => { - if (this.props.note) { - return - } - } - - renderToolbarButtons = () => { - 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 ( - // - - - {icon} - - ) - } - - // Add a `renderMark` method to render marks. - - renderMark = props => { - const { children, mark, attributes } = props - - switch (mark.type) { - default: { - break; - } - case 'bold': - return {children} - case 'code': - return {children} - case 'italic': - return {children} - case 'underlined': - return {children} - } - } - /** - * 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 ( - - {icon} - - ) - } - - renderNode = props => { - const { attributes, children, node } = props - - switch (node.type) { - default: { - break; - } - case 'block-quote': - return
    {children}
    - case 'bulleted-list': - return - case 'heading-one': - return

    {children}

    - case 'heading-two': - return

    {children}

    - case 'list-item': - return
  • {children}
  • - case 'numbered-list': - return
      {children}
    - } -} - - /** - * Render the Slate editor. - * - * @return {Element} - */ - - renderEditor = () => { - return ( -
    - {this.renderHoveringMenu()} - -
    - ) - } - - renderHoveringMenu = () => { - return ( - -
    - -
    -
    - ) - } - - 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