diff --git a/examples/with-draft-js/README.md b/examples/with-draft-js/README.md new file mode 100644 index 00000000..5a9c90ac --- /dev/null +++ b/examples/with-draft-js/README.md @@ -0,0 +1,41 @@ +[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-draft-js) +# DraftJS Medium editor inspiration + +## How to use + +### Using `create-next-app` + +Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: + +``` +npm i -g create-next-app +create-next-app --example with-draft-js +``` + +### Download manually + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-draft-js +cd with-draft-js +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)): + +```bash +now +``` + +## The idea behind the example + +Have you ever wanted to have an editor like medium.com in your Next.js app? DraftJS is avalaible for SSR, but some plugins like the toolbar are using `window`, which does not work when doing SSR. + +This example aims to provides a fully customizable example of the famous medium editor with DraftJS. The goal was to get it as customizable as possible, and fully working with Next.js without using the react-no-ssr package. diff --git a/examples/with-draft-js/package.json b/examples/with-draft-js/package.json new file mode 100644 index 00000000..fdb9eccd --- /dev/null +++ b/examples/with-draft-js/package.json @@ -0,0 +1,17 @@ +{ + "name": "with-draft-js", + "version": "1.0.0", + "author": "Asten Mies", + "license": "ISC", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "draft-js": "0.10.5", + "next": "5.0.0", + "react": "16.2.0", + "react-dom": "16.2.0" + } +} diff --git a/examples/with-draft-js/pages/index.js b/examples/with-draft-js/pages/index.js new file mode 100644 index 00000000..286b3305 --- /dev/null +++ b/examples/with-draft-js/pages/index.js @@ -0,0 +1,270 @@ +import React from 'react' +import { + Editor, + EditorState, + RichUtils, + convertToRaw, + convertFromRaw + } from 'draft-js' + +export default class App extends React.Component { + constructor (props) { + super(props) + this.state = { + editorState: EditorState.createWithContent(convertFromRaw(initialData)), + showToolbar: false, + windowWidth: 0, + toolbarMeasures: { + w: 0, + h: 0 + }, + selectionMeasures: { + w: 0, + h: 0 + }, + selectionCoordinates: { + x: 0, + y: 0 + }, + toolbarCoordinates: { + x: 0, + y: 0 + }, + showRawData: false + } + + this.focus = () => this.editor.focus() + this.onChange = (editorState) => this.setState({editorState}) + } + + onClickEditor = () => { + this.focus() + this.checkSelectedText() + } + + // 1- Check if some text is selected + checkSelectedText = () => { + if (typeof window !== 'undefined') { + const text = window.getSelection().toString() + if (text !== '') { + // 1-a Define the selection coordinates + this.setSelectionXY() + } else { + // Hide the toolbar if nothing is selected + this.setState({ + showToolbar: false + }) + } + } + } + + // 2- Identify the selection coordinates + setSelectionXY = () => { + var r = window.getSelection().getRangeAt(0).getBoundingClientRect() + var relative = document.body.parentNode.getBoundingClientRect() + // 2-a Set the selection coordinates in the state + this.setState({ + selectionCoordinates: r, + windowWidth: relative.width, + selectionMeasures: { + w: r.width, + h: r.height + } + }, () => this.showToolbar()) + } + + // 3- Show the toolbar + showToolbar = () => { + this.setState({ + showToolbar: true + }, () => this.measureToolbar()) + } + + // 4- The toolbar was hidden until now + measureToolbar = () => { + // 4-a Define the toolbar width and height, as it is now visible + this.setState({ + toolbarMeasures: { + w: this.elemWidth, + h: this.elemHeight + } + }, () => this.setToolbarXY()) + } + + // 5- Now that we have all measures, define toolbar coordinates + setToolbarXY = () => { + let coordinates = {} + + const { selectionMeasures, selectionCoordinates, toolbarMeasures, windowWidth } = this.state + + const hiddenTop = selectionCoordinates.y < toolbarMeasures.h + const hiddenRight = windowWidth - selectionCoordinates.x < toolbarMeasures.w / 2 + const hiddenLeft = selectionCoordinates.x < toolbarMeasures.w / 2 + + const normalX = selectionCoordinates.x - (toolbarMeasures.w / 2) + (selectionMeasures.w / 2) + const normalY = selectionCoordinates.y - toolbarMeasures.h + + const invertedY = selectionCoordinates.y + selectionMeasures.h + const moveXToLeft = windowWidth - toolbarMeasures.w + const moveXToRight = 0 + + coordinates = { + x: normalX, + y: normalY + } + + if (hiddenTop) { + coordinates.y = invertedY + } + + if (hiddenRight) { + coordinates.x = moveXToLeft + } + + if (hiddenLeft) { + coordinates.x = moveXToRight + } + + this.setState({ + toolbarCoordinates: coordinates + }) + } + + handleKeyCommand = (command) => { + const {editorState} = this.state + const newState = RichUtils.handleKeyCommand(editorState, command) + if (newState) { + this.onChange(newState) + return true + } + return false + } + + toggleToolbar = (inlineStyle) => { + this.onChange( + RichUtils.toggleInlineStyle( + this.state.editorState, + inlineStyle + ) + ) + } + + render () { + const {editorState} = this.state + // Make sure we're not on the ssr + if (typeof window !== 'undefined') { + // Let's stick the toolbar to the selection + // when the window is resized + window.addEventListener('resize', this.checkSelectedText) + } + + const toolbarStyle = { + display: this.state.showToolbar ? 'block' : 'none', + backgroundColor: 'black', + color: 'white', + position: 'absolute', + left: this.state.toolbarCoordinates.x, + top: this.state.toolbarCoordinates.y, + zIndex: 999, + padding: 10 + } + return ( +