├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .storybook ├── main.js ├── preview.js └── webpack.config.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── test-compiler.js ├── test-setup.js └── webpack.config.js ├── css └── Draft.css ├── docs ├── .babelrc ├── config │ ├── server.dev.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js ├── css │ ├── carbon.css │ ├── fonts.css │ └── normalize.css ├── images │ ├── demo │ │ ├── bold.gif │ │ ├── center-align.gif │ │ ├── erase.gif │ │ ├── image.gif │ │ ├── indent.gif │ │ ├── italic.gif │ │ ├── justify.gif │ │ ├── left-align.gif │ │ ├── link.gif │ │ ├── ordered.gif │ │ ├── outdent.gif │ │ ├── redo.gif │ │ ├── right-align.gif │ │ ├── strikethrough.gif │ │ ├── subscript.gif │ │ ├── superscript.gif │ │ ├── underline.gif │ │ ├── undo.gif │ │ ├── unlink.gif │ │ └── unordered.gif │ ├── feather.svg │ ├── github.png │ └── paper_pen.svg ├── index.html ├── package.json ├── src │ ├── components │ │ ├── App │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Author │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Demo │ │ │ ├── EditorConvertToHTML │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── EditorConvertToJSON │ │ │ │ └── index.js │ │ │ ├── EditorConvertToMarkdown │ │ │ │ └── index.js │ │ │ ├── EditorCustomToolbarOption │ │ │ │ └── index.js │ │ │ ├── EditorCustomizedToolbarOption │ │ │ │ ├── ColorPic │ │ │ │ │ ├── index.js │ │ │ │ │ └── styles.css │ │ │ │ └── index.js │ │ │ ├── EditorEmbedded │ │ │ │ └── index.js │ │ │ ├── EditorI18n │ │ │ │ └── index.js │ │ │ ├── EditorImage │ │ │ │ └── index.js │ │ │ ├── EditorStyledToolbar │ │ │ │ └── index.js │ │ │ ├── EditorToolbarWhenFocused │ │ │ │ └── index.js │ │ │ ├── EditorWithMentionHashtag │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Docs │ │ │ ├── ARIASupport │ │ │ │ └── index.js │ │ │ ├── DataConversion │ │ │ │ └── index.js │ │ │ ├── Installation │ │ │ │ └── index.js │ │ │ ├── Props │ │ │ │ ├── BlockRenderingProp │ │ │ │ │ └── index.js │ │ │ │ ├── CustomStyleProp │ │ │ │ │ └── index.js │ │ │ │ ├── CustomizingToolbarProp │ │ │ │ │ ├── CustomToolbarOption │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── CustomizeToolbarOption │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── I18N │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── defaultToolbar.js │ │ │ │ │ └── index.js │ │ │ │ ├── DecoratorProp │ │ │ │ │ └── index.js │ │ │ │ ├── DraftjsProp │ │ │ │ │ └── index.js │ │ │ │ ├── EditorRef │ │ │ │ │ └── index.js │ │ │ │ ├── EditorStateProp │ │ │ │ │ └── index.js │ │ │ │ ├── EditorStyleProp │ │ │ │ │ └── index.js │ │ │ │ ├── EventCallbackProp │ │ │ │ │ └── index.js │ │ │ │ ├── HashtagProp │ │ │ │ │ └── index.js │ │ │ │ ├── MentionProp │ │ │ │ │ └── index.js │ │ │ │ ├── ReadOnlyProp │ │ │ │ │ └── index.js │ │ │ │ ├── TextAlignmentProp │ │ │ │ │ └── index.js │ │ │ │ ├── WrapperIdProp │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── Usage │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Home │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Menu │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── index.js │ ├── icons │ │ └── palette.svg │ ├── index.js │ └── util │ │ ├── sampleEditorContent.js │ │ └── uploadImageCallBack.js ├── static │ ├── bundle.js │ ├── c9203522b9cb8ed7ec319eb07e8731b1.svg │ ├── main.css │ └── main.css.map ├── template │ └── index.html └── yarn.lock ├── images ├── align-center.svg ├── align-justify.svg ├── align-left.svg ├── align-right.svg ├── bold.svg ├── color.svg ├── embedded.svg ├── emoji.svg ├── eraser.svg ├── font-size.svg ├── image.svg ├── indent.svg ├── italic.svg ├── link.svg ├── list-ordered.svg ├── list-unordered.svg ├── monospace.svg ├── openlink.svg ├── ordered.svg ├── outdent.svg ├── redo.svg ├── strikethrough.svg ├── subscript.svg ├── superscript.svg ├── underline.svg ├── undo.svg ├── unlink.svg └── unordered.svg ├── interfaces ├── CSSModule.js ├── draftjs-to-html.js ├── draftjs-utils.js ├── images.js └── mocha.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts └── publish.sh ├── src ├── Editor │ ├── __test__ │ │ └── editorTest.js │ ├── index.js │ └── styles.css ├── components │ ├── Dropdown │ │ ├── Dropdown │ │ │ ├── __test__ │ │ │ │ └── dropdownTest.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── DropdownOption │ │ │ ├── __test__ │ │ │ │ └── dropdownOptionTest.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── index.js │ ├── Option │ │ ├── __test__ │ │ │ └── optionTest.js │ │ ├── index.js │ │ └── styles.css │ └── Spinner │ │ ├── __test__ │ │ └── spinnerTest.js │ │ ├── index.js │ │ └── styles.css ├── config │ └── defaultToolbar.js ├── controls │ ├── BlockType │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── blockControlTest.js │ │ └── index.js │ ├── ColorPicker │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── colorPickerTest.js │ │ └── index.js │ ├── Embedded │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── index.js │ ├── Emoji │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── index.js │ ├── FontFamily │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── fontFamilyControlTest.js │ │ └── index.js │ ├── FontSize │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── fontSizeControlTest.js │ │ └── index.js │ ├── History │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── historyControlTest.js │ │ └── index.js │ ├── Image │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── imageControlTest.js │ │ └── index.js │ ├── Inline │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── inlineControlTest.js │ │ └── index.js │ ├── Link │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── linkControlTest.js │ │ └── index.js │ ├── List │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── listControlsTest.js │ │ └── index.js │ ├── Remove │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── index.js │ ├── TextAlign │ │ ├── Component │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── __test__ │ │ │ └── textAlignControlTest.js │ │ └── index.js │ └── index.js ├── decorators │ ├── HashTag │ │ ├── index.js │ │ └── styles.css │ ├── Link │ │ ├── __test__ │ │ │ └── linkDecoratorTest.js │ │ ├── index.js │ │ └── styles.css │ └── Mention │ │ ├── Mention │ │ ├── index.js │ │ └── styles.css │ │ ├── Suggestion │ │ ├── index.js │ │ └── styles.css │ │ ├── addMention.js │ │ └── index.js ├── event-handler │ ├── focus.js │ ├── keyDown.js │ ├── modals.js │ └── suggestions.js ├── i18n │ ├── da.js │ ├── de.js │ ├── en.js │ ├── es.js │ ├── fr.js │ ├── index.js │ ├── it.js │ ├── ja.js │ ├── ko.js │ ├── nl.js │ ├── pl.js │ ├── pt.js │ ├── ru.js │ ├── zh.js │ └── zh_tw.js ├── index.js ├── renderer │ ├── Embedded │ │ └── index.js │ ├── Image │ │ ├── __test__ │ │ │ └── imageRendererTest.js │ │ ├── index.js │ │ └── styles.css │ └── index.js ├── stories │ ├── Basic │ │ └── index.stories.js │ ├── BasicContentState │ │ └── index.stories.js │ ├── BasicControlled │ │ └── index.stories.js │ ├── ControlledSelectedOptions │ │ └── index.stories.js │ ├── ConvertFromHTML │ │ └── index.stories.js │ ├── ConvertFromRawDraftContent │ │ └── index.stories.js │ ├── ConvertToHTML │ │ └── index.stories.js │ ├── ConvertToMarkdown │ │ └── index.stories.js │ ├── ConvertToRawDraftContent │ │ └── index.stories.js │ ├── CustomToolbar │ │ ├── index.stories.js │ │ └── styles.css │ ├── Embeddable │ │ └── index.stories.js │ ├── FloatingToolbar │ │ ├── index.stories.js │ │ └── styles.css │ ├── FocusBlurCallbacks │ │ ├── index.stories.js │ │ └── styles.css │ ├── HashTag │ │ └── index.stories.js │ ├── I18n │ │ ├── index.stories.js │ │ └── styles.css │ ├── ImageDataURI │ │ └── index.stories.js │ ├── ImageUpload │ │ └── index.stories.js │ ├── Mention │ │ └── index.stories.js │ ├── ReadOnly │ │ └── index.stories.js │ ├── SelectedOptions │ │ └── index.stories.js │ ├── SpellCheck │ │ └── index.stories.js │ ├── ToolbarHidden │ │ ├── index.stories.js │ │ └── styles.css │ └── styles.css └── utils │ ├── BlockStyle.js │ ├── common.js │ ├── handlePaste.js │ ├── toolbar.js │ └── url.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["transform-flow-strip-types"], 5 | ["@babel/plugin-transform-class-properties"], 6 | [ 7 | "@babel/plugin-proposal-class-properties", 8 | { 9 | "loose": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": ["airbnb", "prettier"], 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["babel", "react"], 16 | "rules": { 17 | "import/no-unresolved": 0, 18 | "import/no-extraneous-dependencies": 0, 19 | "react/jsx-filename-extension": 0, 20 | "jsx-a11y/label-has-for": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*node_modules* 3 | .*node_modules/fbjs.* 4 | .*/__test__/* 5 | ./images/*.svg 6 | 7 | [include] 8 | .*/js/* 9 | 10 | [libs] 11 | ./interfaces/mocha.js 12 | ./interfaces/images.js 13 | ./interfaces/CSSModule.js 14 | ./interfaces/draftjs-to-html.js 15 | ./interfaces/draftjs-utils.js 16 | ./node_modules/draft-js 17 | 18 | [options] 19 | esproposal.class_static_fields=enable 20 | esproposal.class_instance_fields=enable 21 | module.name_mapper='^.*.css' -> 'CSSModule' 22 | module.name_mapper='^.*.svg' -> 'images' 23 | module.system=haste 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: jyotipuri 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | dist 5 | docs/.yalc/ 6 | docs/yalc.lock 7 | stats.json 8 | report.html -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | config 3 | css 4 | docs 5 | images 6 | interfaces 7 | scripts 8 | src 9 | stories 10 | node_modules 11 | .babelrc 12 | .eslintignore 13 | .eslintrc 14 | .flowconfig 15 | .travis.yml 16 | .gitignore 17 | postcss.config.js 18 | .npmignore 19 | yarn.lock 20 | CHANGELOG.md -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-webpack5').StorybookConfig } */ 2 | const config = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | ], 9 | framework: { 10 | name: "@storybook/react-webpack5", 11 | options: {}, 12 | }, 13 | docs: { 14 | autodocs: "tag", 15 | }, 16 | }; 17 | export default config; 18 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react').Preview } */ 2 | const preview = { 3 | parameters: { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const autoprefixer = require('autoprefixer'); 3 | // const precss = require('precss'); 4 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 5 | // This is just the basic way to add additional webpack configurations. 6 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 7 | 8 | // IMPORTANT 9 | // When you add this file, we won't add the default configurations which is similar 10 | // to "React Create App". This only has babel loader to load JavaScript. 11 | 12 | module.exports = { 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | loaders: ['style-loader', 'css-loader'], 18 | include: path.resolve(__dirname, '../'), 19 | }, 20 | { 21 | test: /\.(png|jpg)$/, 22 | loader: 'url-loader?limit=8192', 23 | include: path.resolve(__dirname, '../'), 24 | }, 25 | { 26 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 27 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml', 28 | include: path.resolve(__dirname, '../'), 29 | }, 30 | ], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - osx 4 | language: node_js 5 | node_js: 6 | - "10" 7 | script: 8 | - npm test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jyoti Puri 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /config/test-compiler.js: -------------------------------------------------------------------------------- 1 | require("@babel/core"); 2 | 3 | function noop() { 4 | return null; 5 | } 6 | require.extensions[".css"] = noop; 7 | require.extensions[".svg"] = noop; 8 | require.extensions[".png"] = noop; 9 | -------------------------------------------------------------------------------- /config/test-setup.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")(); 2 | const { configure } = require("enzyme"); 3 | const Adapter = require("@wojtekmaj/enzyme-adapter-react-17"); 4 | 5 | configure({ adapter: new Adapter() }); 6 | 7 | const jsdom = require("jsdom"); 8 | 9 | const { JSDOM } = jsdom; 10 | 11 | const { document } = new JSDOM({ 12 | url: "http://localhost", 13 | }).window; 14 | global.document = document; 15 | 16 | global.window = document.defaultView; 17 | global.HTMLElement = window.HTMLElement; 18 | global.HTMLAnchorElement = window.HTMLAnchorElement; 19 | 20 | global.navigator = { 21 | userAgent: "node.js", 22 | }; 23 | 24 | configure({ adapter: new Adapter() }); 25 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const autoprefixer = require("autoprefixer"); 5 | const precss = require("precss"); 6 | const TerserPlugin = require("terser-webpack-plugin"); 7 | 8 | module.exports = { 9 | devtool: "source-map", 10 | entry: ["./src/index"], 11 | output: { 12 | path: path.join(__dirname, "../dist"), 13 | filename: "react-draft-wysiwyg.js", 14 | library: "reactDraftWysiwyg", 15 | libraryTarget: "umd", 16 | }, 17 | externals: { 18 | react: "react", 19 | immutable: "immutable", 20 | "react-dom": "react-dom", 21 | "draft-js": "draft-js", 22 | }, 23 | optimization: { 24 | minimize: true, 25 | minimizer: [new TerserPlugin()], 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | "process.env": { 30 | NODE_ENV: JSON.stringify("production"), 31 | }, 32 | }), 33 | new MiniCssExtractPlugin({ 34 | filename: "react-draft-wysiwyg.css", 35 | chunkFilename: "[id].css", 36 | ignoreOrder: false, 37 | }), 38 | new webpack.LoaderOptionsPlugin({ 39 | options: { 40 | postcss: [autoprefixer, precss], 41 | }, 42 | }), 43 | ], 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.js$/, 48 | use: [{ loader: "babel-loader" }], 49 | exclude: /immutable\.js$|draftjs-utils\.js$/, 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: [ 54 | { 55 | loader: MiniCssExtractPlugin.loader, 56 | options: { 57 | publicPath: "../", 58 | hmr: process.env.NODE_ENV === "development", 59 | }, 60 | }, 61 | "css-loader", 62 | ], 63 | }, 64 | { test: /\.(png|jpg)$/, use: [{ loader: "url-loader?limit=8192" }] }, 65 | { 66 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 67 | use: [{ loader: "url-loader?limit=10000&mimetype=image/svg+xml" }], 68 | }, 69 | ], 70 | }, 71 | resolve: { 72 | extensions: [".js", ".json"], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["transform-flow-strip-types"], 5 | [ 6 | "@babel/plugin-proposal-class-properties", 7 | { 8 | "loose": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/config/server.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const config = require('./webpack.dev.config'); 4 | const express = require('express'); 5 | 6 | const app = express(); 7 | const compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath, 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', (req, res) => { 17 | res.sendFile(path.join(__dirname, '../index.html')); 18 | }); 19 | 20 | app.listen(3000, 'localhost', (err) => { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | console.log('Listening at http://localhost:3000'); 26 | }); 27 | -------------------------------------------------------------------------------- /docs/config/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const autoprefixer = require('autoprefixer'); 4 | const precss = require('precss'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = { 9 | devtool: 'cheap-module-eval-source-map', 10 | entry: ['webpack-hot-middleware/client', './src/index'], 11 | output: { 12 | path: path.join(__dirname, '../static'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | exclude: /react-draft-wysiwyg\.js$|immutable\.js$|draftjs-utils\.js$|draftjs-to-markdown\.js$|draftjs-to-html\.js$|lodash\.js$/, 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [ 26 | { 27 | loader: MiniCssExtractPlugin.loader, 28 | options: { 29 | publicPath: '../', 30 | hmr: process.env.NODE_ENV === 'development', 31 | }, 32 | }, 33 | 'css-loader', 34 | ], 35 | }, 36 | { 37 | test: /Draft\.css$/, 38 | use: [{ loader: 'style-loader!css-loader' }], 39 | }, 40 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192' }, 41 | { 42 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 43 | use: [{ loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }], 44 | }, 45 | { 46 | test: /\.(eot|ttf|woff|woff2)$/, 47 | use: [{ loader: 'file-loader?name=public/fonts/[name].[ext]' }], 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new MiniCssExtractPlugin({ 53 | filename: '[name].css', 54 | chunkFilename: '[id].css', 55 | ignoreOrder: false, 56 | }), 57 | new HtmlWebpackPlugin({ 58 | template: './template/index.html', 59 | inject: true, 60 | }), 61 | new webpack.HotModuleReplacementPlugin(), 62 | new webpack.NoEmitOnErrorsPlugin(), 63 | new webpack.LoaderOptionsPlugin({ 64 | options: { 65 | postcss: [autoprefixer, precss], 66 | }, 67 | }), 68 | ], 69 | resolve: { 70 | extensions: ['.js', '.json'], 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /docs/config/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const autoprefixer = require('autoprefixer'); 4 | const precss = require('precss'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 7 | 8 | module.exports = { 9 | devtool: 'source-map', 10 | entry: ['./src/index'], 11 | output: { 12 | path: path.join(__dirname, '../static'), 13 | filename: 'bundle.js', 14 | publicPath: '', 15 | }, 16 | optimization: { 17 | minimizer: [new UglifyJsPlugin()], 18 | }, 19 | plugins: [ 20 | new MiniCssExtractPlugin({ 21 | filename: '[name].css', 22 | chunkFilename: '[id].css', 23 | ignoreOrder: false, 24 | }), 25 | new webpack.DefinePlugin({ 26 | 'process.env': { 27 | NODE_ENV: JSON.stringify('production'), 28 | }, 29 | }), 30 | new webpack.LoaderOptionsPlugin({ 31 | options: { 32 | postcss: [autoprefixer, precss], 33 | }, 34 | }), 35 | ], 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.js$/, 40 | loader: 'babel-loader', 41 | exclude: /react-draft-wysiwyg\.js$|immutable\.js$|draftjs-utils\.js$|draftjs-to-markdown\.js$|draftjs-to-html\.js$|lodash\.js$/, 42 | }, 43 | { 44 | test: /\.css$/, 45 | use: [ 46 | { 47 | loader: MiniCssExtractPlugin.loader, 48 | options: { 49 | publicPath: '../', 50 | hmr: process.env.NODE_ENV === 'development', 51 | }, 52 | }, 53 | 'css-loader', 54 | ], 55 | }, 56 | { 57 | test: /Draft\.css$/, 58 | use: [{ loader: 'style-loader!css-loader' }], 59 | }, 60 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192' }, 61 | { 62 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 63 | use: [{ loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }], 64 | }, 65 | { 66 | test: /\.(eot|ttf|woff|woff2)$/, 67 | use: [{ loader: 'file-loader?name=public/fonts/[name].[ext]' }], 68 | }, 69 | ], 70 | }, 71 | resolve: { 72 | extensions: ['.js', '.json'], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /docs/css/carbon.css: -------------------------------------------------------------------------------- 1 | #carbonads { 2 | display: flex; 3 | float: right; 4 | margin: 0 0 20px 20px; 5 | max-width: 130px; 6 | border-radius: 4px; 7 | background-color: hsl(0, 0%, 98%); 8 | box-shadow: 0 0 1px hsla(0, 0%, 0%, 0.15); 9 | font-size: 11px; 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 11 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, 12 | sans-serif; 13 | } 14 | 15 | #carbonads a { 16 | color: #111; 17 | text-decoration: none; 18 | } 19 | 20 | #carbonads a:hover { 21 | color: #111; 22 | } 23 | 24 | .carbon-img { 25 | display: block; 26 | margin-bottom: 8px; 27 | max-width: 130px; 28 | line-height: 1; 29 | } 30 | 31 | .carbon-img img { 32 | display: block; 33 | margin: 0 auto; 34 | max-width: 130px; 35 | width: 130px; 36 | height: auto; 37 | } 38 | 39 | .carbon-text { 40 | display: block; 41 | padding: 0 10px 8px; 42 | text-align: left; 43 | line-height: 1.35; 44 | } 45 | 46 | .carbon-poweredby { 47 | display: block; 48 | padding: 10px; 49 | background: repeating-linear-gradient( 50 | -45deg, 51 | transparent, 52 | transparent 5px, 53 | hsla(0, 0%, 0%, 0.025) 5px, 54 | hsla(0, 0%, 0%, 0.025) 10px 55 | ) 56 | hsla(203, 11%, 95%, 0.4); 57 | text-transform: uppercase; 58 | letter-spacing: 0.5px; 59 | font-weight: 600; 60 | font-size: 8px; 61 | line-height: 0; 62 | } 63 | 64 | @media only screen and (min-width: 320px) and (max-width: 759px) { 65 | #carbonads { 66 | position: relative; 67 | float: none; 68 | margin: 20px 0; 69 | max-width: 330px; 70 | } 71 | 72 | .carbon-wrap { 73 | display: flex; 74 | flex-direction: row; 75 | } 76 | 77 | .carbon-img { 78 | margin: 0; 79 | } 80 | 81 | .carbon-text { 82 | padding: 10px 10px 0 10px; 83 | font-size: 12px; 84 | } 85 | 86 | .carbon-poweredby { 87 | position: absolute; 88 | right: 0; 89 | bottom: 0; 90 | border-radius: 0; 91 | border-top-left-radius: 3px; 92 | text-align: center; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/images/demo/bold.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/bold.gif -------------------------------------------------------------------------------- /docs/images/demo/center-align.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/center-align.gif -------------------------------------------------------------------------------- /docs/images/demo/erase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/erase.gif -------------------------------------------------------------------------------- /docs/images/demo/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/image.gif -------------------------------------------------------------------------------- /docs/images/demo/indent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/indent.gif -------------------------------------------------------------------------------- /docs/images/demo/italic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/italic.gif -------------------------------------------------------------------------------- /docs/images/demo/justify.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/justify.gif -------------------------------------------------------------------------------- /docs/images/demo/left-align.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/left-align.gif -------------------------------------------------------------------------------- /docs/images/demo/link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/link.gif -------------------------------------------------------------------------------- /docs/images/demo/ordered.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/ordered.gif -------------------------------------------------------------------------------- /docs/images/demo/outdent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/outdent.gif -------------------------------------------------------------------------------- /docs/images/demo/redo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/redo.gif -------------------------------------------------------------------------------- /docs/images/demo/right-align.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/right-align.gif -------------------------------------------------------------------------------- /docs/images/demo/strikethrough.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/strikethrough.gif -------------------------------------------------------------------------------- /docs/images/demo/subscript.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/subscript.gif -------------------------------------------------------------------------------- /docs/images/demo/superscript.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/superscript.gif -------------------------------------------------------------------------------- /docs/images/demo/underline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/underline.gif -------------------------------------------------------------------------------- /docs/images/demo/undo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/undo.gif -------------------------------------------------------------------------------- /docs/images/demo/unlink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/unlink.gif -------------------------------------------------------------------------------- /docs/images/demo/unordered.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/demo/unordered.gif -------------------------------------------------------------------------------- /docs/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpuri/react-draft-wysiwyg/4743d7387474d7f88a16e40d3c0e3a9cc9c7a3f9/docs/images/github.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | React Draft Wysiwyg 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-draft-wysiwyg-documentation", 3 | "version": "0.0.0", 4 | "description": "Documentation for React Wysiwyg Editor.", 5 | "main": "src/index.js", 6 | "devDependencies": { 7 | "@babel/core": "^7.7.5", 8 | "@babel/plugin-proposal-class-properties": "^7.7.4", 9 | "@babel/preset-env": "^7.7.6", 10 | "@babel/preset-react": "^7.7.4", 11 | "@storybook/react": "^5.2.8", 12 | "autoprefixer": "^9.7.3", 13 | "babel-eslint": "^10.0.3", 14 | "babel-loader": "^8.0.6", 15 | "babel-plugin-react-transform": "^3.0.0", 16 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "babel-preset-stage-0": "^6.24.1", 20 | "css-loader": "^3.2.1", 21 | "draftjs-to-html": "^0.9.0", 22 | "draftjs-to-markdown": "^0.6.0", 23 | "express": "^4.17.1", 24 | "file-loader": "^5.0.2", 25 | "flow-bin": "^0.113.0", 26 | "html-to-draftjs": "^1.5.0", 27 | "html-webpack-plugin": "^3.2.0", 28 | "lodash": "^4.17.13", 29 | "postcss-loader": "^3.0.0", 30 | "precss": "^4.0.0", 31 | "react": "^16.12.0", 32 | "react-dom": "^16.12.0", 33 | "react-draft-wysiwyg": "^1.14.3", 34 | "react-router": "^5.1.2", 35 | "react-transform-catch-errors": "^1.0.2", 36 | "react-transform-hmr": "^1.0.4", 37 | "redbox-react": "^1.6.0", 38 | "rimraf": "^3.0.0", 39 | "style-loader": "^1.0.1", 40 | "uglifyjs-webpack-plugin": "^2.2.0", 41 | "url-loader": "^3.0.0", 42 | "webpack": "^4.41.2", 43 | "webpack-dev-middleware": "^3.7.2", 44 | "webpack-hot-middleware": "^2.25.0" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/jpuri/react-draft-wysiwyg.git" 49 | }, 50 | "scripts": { 51 | "clean": "rimraf static", 52 | "build:webpack": "NODE_ENV=production webpack --mode production --config config/webpack.prod.config.js", 53 | "build": "npm run clean && npm run build:webpack", 54 | "start": "node config/server.dev.js" 55 | }, 56 | "author": "Jyoti Puri", 57 | "license": "MIT", 58 | "dependencies": { 59 | "babel-plugin-lodash": "^3.3.4", 60 | "classnames": "^2.2.6", 61 | "codemirror": "^5.49.2", 62 | "draft-js": "^0.11.7", 63 | "fsevents": "^2.2.1", 64 | "immutable": "^4.0.0-rc.1", 65 | "react": "^16.12.0", 66 | "react-codemirror": "^1.0.0", 67 | "react-color": "^2.17.3", 68 | "react-dom": "^16.12.0", 69 | "react-router": "^3.0.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/App/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import Menu from '../Menu'; 6 | import github from '../../../images/github.png'; 7 | import paperPen from '../../../images/paper_pen.svg'; 8 | import './styles.css'; 9 | 10 | export default class App extends Component { 11 | 12 | static propTypes = { 13 | children: PropTypes.object.isRequired, 14 | location: PropTypes.object, 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | React Draft Wysiwyg 28 | 29 | A Wysiwyg Built on ReactJS and DraftJS 30 | 31 | 32 | Fork me on GitHub 33 | 34 | 35 |
36 |
37 | 38 | {this.props.children} 39 |
40 | 41 | Made with ❤ by Jyoti 42 | 43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/components/App/styles.css: -------------------------------------------------------------------------------- 1 | .app-root { 2 | width: 100%; 3 | } 4 | .header { 5 | height: 125px; 6 | width: 100%; 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | font-family: Roboto; 11 | color: white; 12 | background: #a21717; 13 | } 14 | .header-label { 15 | color: white; 16 | text-decoration: none; 17 | } 18 | .flex-layout { 19 | display: flex; 20 | min-height: 600px; 21 | } 22 | .github { 23 | position: absolute; 24 | top: 0; 25 | right: 0; 26 | border: 0; 27 | height: 140px; 28 | } 29 | .footer { 30 | height: 100px; 31 | width: 100%; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | background: #a21717; 36 | color: white; 37 | font-size: 18px; 38 | font-family: Roboto; 39 | font-weight: 300; 40 | } 41 | .author-link { 42 | color: white; 43 | text-decoration: none; 44 | margin-left: 5px; 45 | } 46 | .header-text { 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | .header-title { 51 | font-size: 50px; 52 | } 53 | .header-subtitle { 54 | text-align: right; 55 | font-style: italic; 56 | font-size: 18px; 57 | font-weight: 300; 58 | } 59 | .header-logo { 60 | width: 70px; 61 | margin-left: 50px; 62 | cursor: pointer; 63 | } 64 | -------------------------------------------------------------------------------- /docs/src/components/Author/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import './styles.css'; 5 | 6 | export default class Demo1 extends Component { 7 | 8 | render() { 9 | return ( 10 |
11 | I am Jyoti Puri, software developer based in New Delhi. 12 | I love functional programming and solving complex problems.

13 | Original motivation and sponsorship for this work came from iPaoo. 14 | I am thankful to them for allowing the Editor to be open-sourced. 15 | I am also thankful to the developers using this Editor, their feedbacks are always so motivating. And to the awesome contributors for their great work 👍

16 | I am freelancer. You can reach me for consulting work at jyotipuri@gmail.com. 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/components/Author/styles.css: -------------------------------------------------------------------------------- 1 | .author-root { 2 | margin: 30px; 3 | width: 50%; 4 | } 5 | .photo { 6 | height: 150px; 7 | width: auto; 8 | } 9 | -------------------------------------------------------------------------------- /docs/src/components/Demo/EditorCustomizedToolbarOption/ColorPic/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { BlockPicker } from 'react-color'; 6 | 7 | import icon from '../../../../icons/palette.svg'; 8 | import './styles.css'; 9 | 10 | class ColorPic extends Component { 11 | 12 | static propTypes = { 13 | expanded: PropTypes.bool, 14 | onExpandEvent: PropTypes.func, 15 | onChange: PropTypes.func, 16 | currentState: PropTypes.object, 17 | }; 18 | 19 | stopPropagation = (event) => { 20 | event.stopPropagation(); 21 | }; 22 | 23 | onChange = (color) => { 24 | const { onChange } = this.props; 25 | onChange('color', color.hex); 26 | } 27 | 28 | renderModal = () => { 29 | const { color } = this.props.currentState; 30 | return ( 31 |
35 | 36 |
37 | ); 38 | }; 39 | 40 | render() { 41 | const { expanded, onExpandEvent } = this.props; 42 | return ( 43 |
49 |
53 | 58 |
59 | {expanded ? this.renderModal() : undefined} 60 |
61 | ); 62 | } 63 | } 64 | 65 | export default ColorPic; 66 | -------------------------------------------------------------------------------- /docs/src/components/Demo/EditorCustomizedToolbarOption/ColorPic/styles.css: -------------------------------------------------------------------------------- 1 | .demo-color-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | } 7 | .demo-icon-wrapper { 8 | border: 1px solid #F1F1F1; 9 | padding: 3px 5px; 10 | } 11 | .demo-icon { 12 | width: 20px; 13 | } 14 | .demo-color-modal { 15 | position: absolute; 16 | right: -70px; 17 | top: 135%; 18 | z-index: 10; 19 | } -------------------------------------------------------------------------------- /docs/src/components/Demo/EditorEmbedded/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { Editor } from 'react-draft-wysiwyg'; 6 | import Codemirror from 'react-codemirror'; 7 | 8 | const EditorEmbedded = () => ( 9 |
10 |

7. Editor with embedded link

11 |
12 |
13 | 27 |
28 | 38 |
39 |
40 | ); 41 | 42 | export default EditorEmbedded; 43 | -------------------------------------------------------------------------------- /docs/src/components/Demo/EditorI18n/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { Editor } from 'react-draft-wysiwyg'; 5 | import Codemirror from 'react-codemirror'; 6 | 7 | const EditorI18n = () => ( 8 |
9 |

7. Editor i18n - korean locale.

10 |
11 |
12 | 19 |
20 | (\n' + 26 | ' \n' + 33 | ');' 34 | } 35 | options={{ 36 | lineNumbers: true, 37 | mode: 'jsx', 38 | readOnly: true, 39 | }} 40 | /> 41 |
42 |
43 | ); 44 | 45 | export default EditorI18n; 46 | -------------------------------------------------------------------------------- /docs/src/components/Demo/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import draftToHtml from 'draftjs-to-html'; 5 | import { convertToRaw } from 'draft-js'; 6 | import draftToMarkdown from 'draftjs-to-markdown'; 7 | import { Editor } from 'react-draft-wysiwyg'; 8 | import EditorConvertToHTML from './EditorConvertToHTML'; 9 | import EditorConvertToJSON from './EditorConvertToJSON'; 10 | import EditorConvertToMarkdown from './EditorConvertToMarkdown'; 11 | import EditorCustomToolbarOption from './EditorCustomToolbarOption'; 12 | import EditorCustomizedToolbarOption from './EditorCustomizedToolbarOption'; 13 | import EditorToolbarWhenFocused from './EditorToolbarWhenFocused'; 14 | import EditorI18n from './EditorI18n'; 15 | import EditorWithMentionHashtag from './EditorWithMentionHashtag'; 16 | import EditorStyledToolbar from './EditorStyledToolbar'; 17 | import EditorImage from './EditorImage'; 18 | 19 | import '../../../node_modules/codemirror/lib/codemirror.css'; 20 | import './styles.css'; 21 | 22 | require('codemirror/mode/jsx/jsx'); 23 | 24 | export default class Demo extends Component { 25 | 26 | state: any = { 27 | editorContents: [], 28 | }; 29 | 30 | onEditorStateChange: Function = (index, editorContent) => { 31 | let editorContents = this.state.editorContents; 32 | editorContents[index] = editorContent; 33 | editorContents = [...editorContents]; 34 | this.setState({ 35 | editorContents, 36 | }); 37 | }; 38 | 39 | render() { 40 | const { editorContents } = this.state; 41 | return ( 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | Some more examples can be found here. 55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/src/components/Docs/ARIASupport/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js'; 6 | import draftToHtml from 'draftjs-to-html'; 7 | import htmlToDraft from 'html-to-draftjs'; 8 | import { Editor } from 'react-draft-wysiwyg'; 9 | import Codemirror from 'react-codemirror'; 10 | import sampleEditorContent from '../../../util/sampleEditorContent'; 11 | 12 | export default () => ( 13 |
14 |

ARIA Support

15 |
16 | All ARIA props supported by DraftJS editor are available in react-draft-wysiwyg Ref. 17 | In addition editor and all option in toolbar have other ARIA attributes also added with pre-configured values. 18 |
19 |
20 | ); 21 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Installation/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js'; 6 | import draftToHtml from 'draftjs-to-html'; 7 | import htmlToDraft from 'html-to-draftjs'; 8 | import { Editor } from 'react-draft-wysiwyg'; 9 | import Codemirror from 'react-codemirror'; 10 | import sampleEditorContent from '../../../util/sampleEditorContent'; 11 | 12 | const Installation = () => ( 13 |
14 |

Using editor component

15 |
Package can be installed from node package manager using npm or yarn commands.
16 | 26 |
27 |
Peer dependencies and the required versions.
28 |
    29 |
  1. draft-js: 0.10.x
  2. 30 |
  3. immutable: 3.x, 4.x
  4. 31 |
  5. react: 0.13.x, 0.14.x, ^15.0.0-0, 15.x.x
  6. 32 |
  7. react-dom: 0.13.x, 0.14.x, ^15.0.0-0, 15.x.x
  8. 33 |
34 |
35 |
36 | ); 37 | 38 | export default Installation; 39 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/BlockRenderingProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

customBlockRenderFunc

6 |
7 | Rendering of a blocks can be changed using customBlockRenderFunc. It 8 | should be a function that returns a react component. 9 |
10 | (Currently if customBlockRenderFunc is present the default renderers of 11 | the editor do not work, this will soon be fixed.) 12 |
13 |
14 | ); 15 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/CustomStyleProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from "react-codemirror"; 3 | 4 | export default () => ( 5 |
6 |

customStyleMap

7 |
8 | This is map of custom styles that be be applied in the editor. 9 |
10 | Key is name of style and value is style object. For example 11 |
12 | 28 |
29 | ); 30 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/CustomizingToolbarProp/I18N/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |
7 | Internationalizing toolbar 8 |
9 | All text used in toolbar for labels and titles etc can be localized using property 'localization'. Its is an object with 2 parameters: 10 |
    11 |
  1. 12 | locale 13 | : This can be used to pass locale. Editor has support builtin for lcoales: en, fr, zh, ru, pt, ko, it, nl, de, da, zh_tw, pl, es. 14 |
  2. 15 |
  3. 16 | translations 17 | : This can be used to override the default translations od add new ones for locales not already supported. 18 | It should be an object similar to this. 19 |
  4. 20 |
21 |
22 |
23 | (\n' + 29 | ' \n' + 36 | ');' 37 | } 38 | options={{ 39 | lineNumbers: true, 40 | mode: 'jsx', 41 | readOnly: true, 42 | }} 43 | /> 44 |
45 | ) 46 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/DecoratorProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

customDecorators

6 |
7 | Rendering of a entities can be changed using customDecorators. An array of decorators can be passed to the editor. 8 | These should be decorators exactly as in DraftJS, an object with 2 properties: strategy and component. 9 |
10 |
11 | ); 12 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/DraftjsProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

DraftJS Properties

6 |
7 | Properties provided by DraftJS like spellCheck, readOnly, tabIndex, placeholder, stripPastedStyles, aria properties are all supported by the editor. 8 | They are passed over to the underlying DraftJS editor.
9 | (ARIA properties like ariaLabel, ariaOwneeID, ariaActiveDescendantID, ariaAutoComplete, ariaDescribedBy, ariaExpanded, ariaHasPopup can be used). 10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/EditorRef/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |

Editor ref

7 |
8 | Reference of underlying DraftJS editor can be obtained using editorRef property. 9 | This can be used to raise events on editor like focus editor. 10 |
11 | {\n' + 14 | ' this.editorReferece = ref;\n' + 15 | ' ref.focus();\n' + 16 | '}\n' + 17 | '\n\n' + 18 | 'const EditorWithRef = () => (\n' + 19 | ' \n' + 22 | ')' 23 | } 24 | options={{ 25 | lineNumbers: true, 26 | mode: 'jsx', 27 | readOnly: true, 28 | }} 29 | /> 30 |
31 | ); 32 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/EditorStyleProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |

Styling the editor

7 |
8 | The editor by default will have uses DraftJS editor as it is without 9 | any styling and it will occupy 100% width of container. 10 | Some styling to add border to editor and set width will be nice. 11 |
12 |
    13 |
  1. wrapperClassName: class applied around both the editor and the toolbar
  2. 14 |
  3. editorClassName: class applied around the editor
  4. 15 |
  5. toolbarClassName: class applied around the toolbar
  6. 16 |
  7. wrapperStyle: style object applied around both the editor and the toolbar
  8. 17 |
  9. editorStyle: style object applied around the editor
  10. 18 |
  11. toolbarStyle: style object applied around the toolbar
  12. 19 |
20 | }\n' + 30 | ' editorStyle={}\n' + 31 | ' toolbarStyle={}\n' + 32 | '/>' 33 | } 34 | options={{ 35 | lineNumbers: true, 36 | mode: 'jsx', 37 | readOnly: true, 38 | }} 39 | /> 40 |
41 | Toolbar can be styles using toolbar property, detailed below.
42 | For more detailed styling, css classes in react-draft-wysiwyg.css can be overriden. 43 |
44 |
45 | ); 46 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/EventCallbackProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |

Event callbacks

7 |
8 | Editor supports 3 event callback properties: 9 |
    10 |
  1. onFocus: this callback is called when editor is focused.
  2. 11 |
  3. onBlur: this callback is called when editor is blurred.
  4. 12 |
  5. 13 | onTab 14 | : this callback is called when editor receives 'tab' keydown. Default behavior of editor on tab 15 | is to change depth of block if current block is of type list. If onTab callback returns true editor assumes 16 | that Tab has been handled by the callback and the default behavior is not executed. 17 |
  6. 18 |
19 |
20 | (\n' + 26 | ' {}}\n' + 30 | ' onBlur={(event, editorState) => {}}\n' + 31 | ' onTab={(event) => {}}\n' + 32 | ' />\n' + 33 | ')' 34 | } 35 | options={{ 36 | lineNumbers: true, 37 | mode: 'jsx', 38 | readOnly: true, 39 | }} 40 | /> 41 |
42 | ); 43 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/HashtagProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |

Enabling hashtag

7 |
8 | Hashtag can be enabled in the editor as showed in example below. 9 | separator is character that separates a mention from word preceding it, default value is space ' '. 10 | trigger is character that causes mention suggestions to appear, default value is '#'. 11 |
12 | (\n' + 18 | ' \n' + 26 | ')' 27 | } 28 | options={{ 29 | lineNumbers: true, 30 | mode: 'jsx', 31 | readOnly: true, 32 | }} 33 | /> 34 |
35 | ); 36 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/MentionProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Codemirror from 'react-codemirror'; 3 | 4 | export default () => ( 5 |
6 |

Enabling mentions

7 |
8 | Mentions can be enabled in the editor as showed in example below. 9 | separator is character that separates a mention from word preceding it, default value is space ' '. 10 | trigger is character that causes mention suggestions to appear, default value is '@'. 11 | Each suggestion has 3 peoperties: 12 |
    13 |
  1. text: this is value that is displayed in the editor.
  2. 14 |
  3. value: the filtering of suggestions is done using this value.
  4. 15 |
  5. url: mention is added as link to editor using this 'url' in href. This is optional and if not present 'value' is used instead of this.
  6. 16 |
17 |
18 | (\n' + 24 | ' \n' + 42 | ')' 43 | } 44 | options={{ 45 | lineNumbers: true, 46 | mode: 'jsx', 47 | readOnly: true, 48 | }} 49 | /> 50 |
51 | ); 52 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/ReadOnlyProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

readOnly

6 |
7 | The property can be used to render Editor in read only mode. 8 | When its true user can not change editor text or use any toolbar option. 9 |
10 |
11 | ); 12 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/TextAlignmentProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

RTL and Text Alignment

6 |
7 | DraftJS library has out of box sopport for RTL, it decides text-direction using bidi algorithm. 8 | It works at block-level.
9 | Property 'textAlignment' can be used to force text-alignment in a particular direction. 10 | It can have values 'left', 'right' and 'center'. It will over-ride the results of bidi-algorigthm. 11 | This property will be applicable to all blocks. Toobar option of text-alignment will override 'textAlignment' 12 | property also at block level. 13 |
14 |
15 | ); 16 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/WrapperIdProp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

wrapperId

6 |
7 | The library generates and adds an id to wrapper around the editor. This is required for some more complex event handling. 8 | In case of server side rendering generated id can create trouble and thus this property can be used. 9 |
10 |
11 | ); 12 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Props/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js'; 6 | import draftToHtml from 'draftjs-to-html'; 7 | import htmlToDraft from 'html-to-draftjs'; 8 | import { Editor } from 'react-draft-wysiwyg'; 9 | import Codemirror from 'react-codemirror'; 10 | import sampleEditorContent from '../../../util/sampleEditorContent'; 11 | import EditorStyleProp from './EditorStyleProp'; 12 | import EditorStateProp from './EditorStateProp'; 13 | import CustomizingToolbarProp from './CustomizingToolbarProp'; 14 | import MentionProp from './MentionProp'; 15 | import HashtagProp from './HashtagProp'; 16 | import EditorRef from './EditorRef'; 17 | import EventCallbackProp from './EventCallbackProp'; 18 | import DraftjsProp from './DraftjsProp'; 19 | import TextAlignmentProp from './TextAlignmentProp'; 20 | import ReadOnlyProp from './ReadOnlyProp'; 21 | import DecoratorProp from './DecoratorProp'; 22 | import BlockRenderingProp from './BlockRenderingProp'; 23 | import CustomStyleProp from "./CustomStyleProp"; 24 | import WrapperIdProp from './WrapperIdProp'; 25 | 26 | const Props = () => ( 27 |
28 |

Properties

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | 46 | export default Props; 47 | -------------------------------------------------------------------------------- /docs/src/components/Docs/Usage/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js'; 6 | import draftToHtml from 'draftjs-to-html'; 7 | import htmlToDraft from 'html-to-draftjs'; 8 | import { Editor } from 'react-draft-wysiwyg'; 9 | import Codemirror from 'react-codemirror'; 10 | import sampleEditorContent from '../../../util/sampleEditorContent'; 11 | 12 | const Usage = () => ( 13 |
14 |

Using editor component

15 |
Editor can be simply imported and used as a React Component. Make sure to also include react-draft-wysiwyg.css from node_modules.
16 | ' 23 | } 24 | options={{ 25 | lineNumbers: true, 26 | mode: 'jsx', 27 | readOnly: true, 28 | }} 29 | /> 30 |
31 | ); 32 | 33 | export default Usage; 34 | -------------------------------------------------------------------------------- /docs/src/components/Docs/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Installation from "./Installation"; 3 | import Usage from "./Usage"; 4 | import Props from "./Props"; 5 | import ARIASupport from "./ARIASupport"; 6 | import DataConversion from "./DataConversion"; 7 | import "./styles.css"; 8 | 9 | export default () => ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /docs/src/components/Docs/styles.css: -------------------------------------------------------------------------------- 1 | .docs-root { 2 | width: calc(100% - 150px); 3 | margin: 30px; 4 | } 5 | .docs-label { 6 | display: block; 7 | font-size: 20px; 8 | font-weight: 500; 9 | margin: 20px 0; 10 | } 11 | .docs-sub-label { 12 | display: block; 13 | font-size: 18px; 14 | font-weight: 500; 15 | margin: 10px 0; 16 | } 17 | .docs-section { 18 | margin: 20px 0 50px; 19 | } 20 | .docs-section .ReactCodeMirror { 21 | max-height: 500px; 22 | overflow: scroll; 23 | width: 75%; 24 | margin-top: 25px; 25 | margin-left: 0; 26 | height: auto !important; 27 | } 28 | .docs-section .CodeMirror { 29 | height: auto !important; 30 | } 31 | code { 32 | padding: 20px; 33 | font-size: 13px; 34 | display: inline-flex; 35 | flex-direction: column; 36 | background-color: #f1f1f1; 37 | } 38 | .code_sm { 39 | padding: 2px; 40 | } 41 | .docs-desc { 42 | width: 75%; 43 | font-size: 15px; 44 | margin-bottom: 10px; 45 | } 46 | .docs-root li { 47 | margin-bottom: 5px; 48 | } 49 | .docs-text { 50 | width: 75%; 51 | font-size: 17px; 52 | margin: 15px 0; 53 | } 54 | .top-margined { 55 | margin-top: 20px; 56 | } 57 | ol { 58 | font-size: 15px; 59 | } 60 | .docs-property { 61 | display: flex; 62 | } -------------------------------------------------------------------------------- /docs/src/components/Home/styles.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | display: flex; 4 | font-family: Roboto; 5 | align-items: center; 6 | flex-direction: column; 7 | justify-content: center; 8 | } 9 | .label { 10 | font-size: 20px; 11 | font-weight: bold; 12 | margin-bottom: 20px; 13 | } 14 | .home-list { 15 | margin-top: 20px; 16 | } 17 | .home-listItem { 18 | margin-bottom: 10px; 19 | } 20 | .home-wrapper { 21 | margin-top: 50px !important; 22 | width: 775px !important; 23 | display: block !important; 24 | height: 400px !important; 25 | } 26 | .home-editor { 27 | border: 1px solid #f1f1f1 !important; 28 | padding: 5px !important; 29 | border-radius: 2px !important; 30 | height: 80% !important; 31 | } 32 | .quote-section { 33 | margin: 50px 0 20px; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | } 38 | .quote { 39 | margin-top: 20px; 40 | display: flex; 41 | align-items: center; 42 | } 43 | .quote-text { 44 | font-size: 16px; 45 | } 46 | .quote-author { 47 | font-size: 18px; 48 | margin-left: 15px; 49 | font-style: italic; 50 | font-weight: 500; 51 | } 52 | .home-label { 53 | font-size: 25px; 54 | font-weight: 500; 55 | margin-top: 50px; 56 | } 57 | .feature-list { 58 | margin: 50px 0 50px 50px; 59 | list-style-type: decimal; 60 | } 61 | .discussion { 62 | margin-top: 15px; 63 | } 64 | .carbon_wrapper { 65 | display: flex; 66 | } 67 | .carbon_ad { 68 | margin-left: -200px; 69 | margin-right: 50px; 70 | margin-top: 50px; 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { Link } from 'react-router'; 6 | import classNames from 'classnames'; 7 | import './styles.css'; 8 | 9 | export default class Menu extends Component { 10 | 11 | static propTypes = { 12 | pathname: PropTypes.string, 13 | }; 14 | 15 | state: any = { 16 | status: false, 17 | }; 18 | 19 | changeState: any = () => { 20 | const status = !this.state.status; 21 | this.setState({ 22 | status, 23 | }); 24 | }; 25 | 26 | render() { 27 | const { pathname } = this.props; 28 | return ( 29 |
30 | 31 | Home 32 | 33 | 34 | Demo 35 | 36 | 37 | Docs 38 | 39 | 40 | Author 41 | 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/components/Menu/styles.css: -------------------------------------------------------------------------------- 1 | .menu-root { 2 | width: 120px; 3 | display: flex; 4 | flex-direction: column; 5 | padding: 30px; 6 | border-right: 1px solid #a21717; 7 | } 8 | .menu-option { 9 | margin-top: 25px; 10 | text-decoration: none; 11 | font-size: 18px; 12 | font-family: Roboto; 13 | cursor: pointer; 14 | color: black; 15 | } 16 | .menu-option:hover { 17 | color: #a21717; 18 | } 19 | .menu-option-active { 20 | color: #a21717; 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/components/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export { default as App } from './App'; 4 | export { default as Home } from './Home'; 5 | export { default as Demo } from './Demo'; 6 | export { default as Docs } from './Docs'; 7 | export { default as Author } from './Author'; 8 | -------------------------------------------------------------------------------- /docs/src/icons/palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import { Router, IndexRoute, Route, hashHistory } from "react-router"; 6 | import "../css/carbon.css"; // eslint-disable-line no-unused-vars 7 | import "../css/fonts.css"; // eslint-disable-line no-unused-vars 8 | import "../css/normalize.css"; // eslint-disable-line no-unused-vars 9 | import { App, Home, Demo, Docs, Author } from "./components"; 10 | import "../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; // eslint-disable-line no-unused-vars 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById("app") 22 | ); // eslint-disable-line no-undef 23 | -------------------------------------------------------------------------------- /docs/src/util/sampleEditorContent.js: -------------------------------------------------------------------------------- 1 | import { 2 | convertFromHTML, 3 | ContentState, 4 | EditorState, 5 | } from 'draft-js'; 6 | 7 | const contentBlocks = convertFromHTML('

Lorem ipsum ' + 8 | 'dolor sit amet, consectetur adipiscing elit. Mauris tortor felis, volutpat sit amet ' + 9 | 'maximus nec, tempus auctor diam. Nunc odio elit, ' + 10 | 'commodo quis dolor in, sagittis scelerisque nibh. ' + 11 | 'Suspendisse consequat, sapien sit amet pulvinar ' + 12 | 'tristique, augue ante dapibus nulla, eget gravida ' + 13 | 'turpis est sit amet nulla. Vestibulum lacinia mollis ' + 14 | 'accumsan. Vivamus porta cursus libero vitae mattis. ' + 15 | 'In gravida bibendum orci, id faucibus felis molestie ac. ' + 16 | 'Etiam vel elit cursus, scelerisque dui quis, auctor risus.

'); 17 | 18 | const contentState = ContentState.createFromBlockArray(contentBlocks); 19 | 20 | // const initialContentState = convertToRaw(contentState); 21 | 22 | export default EditorState.createWithContent(contentState); 23 | -------------------------------------------------------------------------------- /docs/src/util/uploadImageCallBack.js: -------------------------------------------------------------------------------- 1 | export default function uploadImageCallBack(file) { 2 | return new Promise( 3 | (resolve, reject) => { 4 | const xhr = new XMLHttpRequest(); // eslint-disable-line no-undef 5 | xhr.open('POST', 'https://api.imgur.com/3/image'); 6 | xhr.setRequestHeader('Authorization', 'Client-ID 8d26ccd12712fca'); 7 | const data = new FormData(); // eslint-disable-line no-undef 8 | data.append('image', file); 9 | xhr.send(data); 10 | xhr.addEventListener('load', () => { 11 | const response = JSON.parse(xhr.responseText); 12 | resolve(response); 13 | }); 14 | xhr.addEventListener('error', () => { 15 | const error = JSON.parse(xhr.responseText); 16 | reject(error); 17 | }); 18 | } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Draft Wysiwyg 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /images/align-center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/align-justify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/embedded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/emoji.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/eraser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/indent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/list-ordered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/list-unordered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/monospace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/openlink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/ordered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/outdent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/strikethrough.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/subscript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/superscript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/underline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/unlink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/unordered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /interfaces/CSSModule.js: -------------------------------------------------------------------------------- 1 | declare module 'CSSModule' { 2 | declare var exports: { [key: string]: string }; 3 | } 4 | -------------------------------------------------------------------------------- /interfaces/draftjs-to-html.js: -------------------------------------------------------------------------------- 1 | declare module 'draftjs-to-html' { 2 | declare var exports: { 3 | draftToHtml: () => {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /interfaces/draftjs-utils.js: -------------------------------------------------------------------------------- 1 | declare module 'draftjs-utils' { 2 | declare var exports: { 3 | changeDepth: () => {}, 4 | getSelectedBlocksType: () => {}, 5 | getEntityRange: () => {}, 6 | getSelectionText: () => {}, 7 | getSelectionEntity: () => {}, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /interfaces/images.js: -------------------------------------------------------------------------------- 1 | declare module 'images' { 2 | declare var exports: { [key: string]: string }; 3 | } 4 | -------------------------------------------------------------------------------- /interfaces/mocha.js: -------------------------------------------------------------------------------- 1 | function describe() {}; 2 | function it() {}; 3 | function xit() {}; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | git subtree split --prefix docs -b gh-pages 2 | git push -f origin gh-pages:gh-pages 3 | git branch -D gh-pages 4 | -------------------------------------------------------------------------------- /src/Editor/__test__/editorTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from "react"; 4 | import { expect, assert } from "chai"; 5 | import { shallow } from "enzyme"; 6 | import Editor from ".."; 7 | 8 | describe("Editor test suite", () => { 9 | it("should have a div when rendered", () => { 10 | expect( 11 | shallow() 12 | .childAt(0) 13 | .type() 14 | ).to.equal("div"); 15 | }); 16 | 17 | xit("should have an editorState object in state", () => { 18 | const editor = shallow(); 19 | assert.isDefined(editor.state().editorState); 20 | assert.isDefined(editor.state().editorFocused); 21 | }); 22 | 23 | it("should have toolbarHidden as false by default", () => { 24 | const editor = shallow(); 25 | expect(editor.find(".rdw-editor-toolbar")).to.have.length(1); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/Editor/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-editor-main { 2 | height: 100%; 3 | overflow: auto; 4 | box-sizing: border-box; 5 | } 6 | .rdw-editor-toolbar { 7 | padding: 6px 5px 0; 8 | border-radius: 2px; 9 | border: 1px solid #F1F1F1; 10 | display: flex; 11 | justify-content: flex-start; 12 | background: white; 13 | flex-wrap: wrap; 14 | font-size: 15px; 15 | margin-bottom: 5px; 16 | user-select: none; 17 | } 18 | .public-DraftStyleDefault-block { 19 | margin: 1em 0; 20 | } 21 | .rdw-editor-wrapper:focus { 22 | outline: none; 23 | } 24 | .rdw-editor-wrapper { 25 | box-sizing: content-box; 26 | } 27 | .rdw-editor-main blockquote { 28 | border-left: 5px solid #f1f1f1; 29 | padding-left: 5px; 30 | } 31 | .rdw-editor-main pre { 32 | background: #f1f1f1; 33 | border-radius: 3px; 34 | padding: 1px 10px; 35 | } -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown/__test__/dropdownTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | import Dropdown from '..'; 7 | import DropdownOption from '../../DropdownOption'; 8 | import ModalHandler from '../../../../event-handler/modals'; 9 | 10 | describe('Dropdown test suite', () => { 11 | it('should have a div when rendered', () => { 12 | expect(mount( 13 | 16 | test 17 | test1 18 | , 19 | ).childAt(0).type()).to.equal('div'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-dropdown-wrapper { 2 | height: 30px; 3 | background: white; 4 | cursor: pointer; 5 | border: 1px solid #F1F1F1; 6 | border-radius: 2px; 7 | margin: 0 3px; 8 | text-transform: capitalize; 9 | background: white; 10 | } 11 | .rdw-dropdown-wrapper:focus { 12 | outline: none; 13 | } 14 | .rdw-dropdown-wrapper:hover { 15 | box-shadow: 1px 1px 0px #BFBDBD; 16 | background-color: #FFFFFF; 17 | } 18 | .rdw-dropdown-wrapper:active { 19 | box-shadow: 1px 1px 0px #BFBDBD inset; 20 | } 21 | .rdw-dropdown-carettoopen { 22 | height: 0px; 23 | width: 0px; 24 | position: absolute; 25 | top: 35%; 26 | right: 10%; 27 | border-top: 6px solid black; 28 | border-left: 5px solid transparent; 29 | border-right: 5px solid transparent; 30 | } 31 | .rdw-dropdown-carettoclose { 32 | height: 0px; 33 | width: 0px; 34 | position: absolute; 35 | top: 35%; 36 | right: 10%; 37 | border-bottom: 6px solid black; 38 | border-left: 5px solid transparent; 39 | border-right: 5px solid transparent; 40 | } 41 | .rdw-dropdown-selectedtext { 42 | display: flex; 43 | position: relative; 44 | height: 100%; 45 | align-items: center; 46 | padding: 0 5px; 47 | } 48 | .rdw-dropdown-optionwrapper { 49 | z-index: 100; 50 | position: relative; 51 | border: 1px solid #F1F1F1; 52 | width: 98%; 53 | background: white; 54 | border-radius: 2px; 55 | margin: 0; 56 | padding: 0; 57 | max-height: 250px; 58 | overflow-y: scroll; 59 | } 60 | .rdw-dropdown-optionwrapper:hover { 61 | box-shadow: 1px 1px 0px #BFBDBD; 62 | background-color: #FFFFFF; 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Dropdown/DropdownOption/__test__/dropdownOptionTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { spy } from 'sinon'; 6 | import { mount } from 'enzyme'; 7 | import DropdownOption from '..'; 8 | 9 | describe('DropdownOption test suite', () => { 10 | it('should have a li when rendered', () => { 11 | expect(mount( 12 | 13 |
test
14 |
, 15 | ).childAt(0).type()).to.equal('li'); 16 | }); 17 | 18 | it('should click event should trigger onSelect function call', () => { 19 | const onSelect = spy(); 20 | const option = mount( 21 | 22 |
test
23 |
, 24 | ); 25 | option.childAt(0).simulate('click'); 26 | expect(onSelect.called).to.equal(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Dropdown/DropdownOption/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | import './styles.css'; 7 | 8 | export default class DropDownOption extends Component { 9 | static propTypes = { 10 | children: PropTypes.any, 11 | value: PropTypes.any, 12 | onClick: PropTypes.func, 13 | onSelect: PropTypes.func, 14 | setHighlighted: PropTypes.func, 15 | index: PropTypes.number, 16 | disabled: PropTypes.bool, 17 | active: PropTypes.bool, 18 | highlighted: PropTypes.bool, 19 | className: PropTypes.string, 20 | activeClassName: PropTypes.string, 21 | disabledClassName: PropTypes.string, 22 | highlightedClassName: PropTypes.string, 23 | title: PropTypes.string, 24 | }; 25 | 26 | static defaultProps = { 27 | activeClassName: '', 28 | disabledClassName: '', 29 | highlightedClassName: '' 30 | } 31 | 32 | onClick: Function = (event): void => { 33 | const { onSelect, onClick, value, disabled } = this.props; 34 | if (!disabled) { 35 | if (onSelect) { 36 | onSelect(value); 37 | } 38 | if (onClick) { 39 | event.stopPropagation(); 40 | onClick(value); 41 | } 42 | } 43 | }; 44 | 45 | setHighlighted: Function = (): void => { 46 | const { setHighlighted, index } = this.props; 47 | setHighlighted(index); 48 | }; 49 | 50 | resetHighlighted: Function = (): void => { 51 | const { setHighlighted } = this.props; 52 | setHighlighted(-1); 53 | }; 54 | 55 | render(): Object { 56 | const { 57 | children, 58 | active, 59 | disabled, 60 | highlighted, 61 | className, 62 | activeClassName, 63 | disabledClassName, 64 | highlightedClassName, 65 | title, 66 | } = this.props; 67 | return ( 68 |
  • 82 | {children} 83 |
  • 84 | ); 85 | } 86 | } 87 | // todo: review classname use above. 88 | -------------------------------------------------------------------------------- /src/components/Dropdown/DropdownOption/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-dropdownoption-default { 2 | min-height: 25px; 3 | display: flex; 4 | align-items: center; 5 | padding: 0 5px; 6 | } 7 | .rdw-dropdownoption-highlighted { 8 | background: #F1F1F1; 9 | } 10 | .rdw-dropdownoption-active { 11 | background: #f5f5f5; 12 | } 13 | .rdw-dropdownoption-disabled { 14 | opacity: 0.3; 15 | cursor: default; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export { default as Dropdown } from "./Dropdown"; 4 | export { default as DropdownOption } from "./DropdownOption"; 5 | -------------------------------------------------------------------------------- /src/components/Option/__test__/optionTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { spy } from 'sinon'; 6 | import { mount } from 'enzyme'; 7 | import Option from '..'; 8 | 9 | describe('Option test suite', () => { 10 | it('should have a span when rendered', () => { 11 | expect(mount( 12 | , 18 | ).childAt(0).type()).to.equal('div'); 19 | }); 20 | 21 | it('should have child element passed after mount', () => { 22 | const option = mount( 23 | , 29 | ); 30 | expect(option.children().length).to.equal(1); 31 | expect(option.children().type()).to.equal('div'); 32 | }); 33 | 34 | it('should execute funcion passed in onClick props when clicked', () => { 35 | const onClick = spy(); 36 | const option = mount( 37 | , 43 | ); 44 | option.children().simulate('click'); 45 | expect(onClick.calledOnce).to.equal(true); 46 | }); 47 | 48 | it('should not execute funcion passed in onClick props when clicked if disabled', () => { 49 | const onClick = spy(); 50 | const option = mount( 51 | , 58 | ); 59 | option.children().simulate('click'); 60 | expect(onClick.called).to.equal(false); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/Option/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | import './styles.css'; 7 | 8 | export default class Option extends Component { 9 | static propTypes = { 10 | onClick: PropTypes.func.isRequired, 11 | children: PropTypes.any, 12 | value: PropTypes.string, 13 | className: PropTypes.string, 14 | activeClassName: PropTypes.string, 15 | active: PropTypes.bool, 16 | disabled: PropTypes.bool, 17 | title: PropTypes.string, 18 | }; 19 | 20 | static defaultProps = { 21 | activeClassName: '', 22 | } 23 | 24 | onClick: Function = () => { 25 | const { disabled, onClick, value } = this.props; 26 | if (!disabled) { 27 | onClick(value); 28 | } 29 | }; 30 | 31 | render() { 32 | const { children, className, activeClassName, active, disabled, title } = this.props; 33 | return ( 34 |
    47 | {children} 48 |
    49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Option/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-option-wrapper { 2 | border: 1px solid #F1F1F1; 3 | padding: 5px; 4 | min-width: 25px; 5 | height: 20px; 6 | border-radius: 2px; 7 | margin: 0 4px; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | cursor: pointer; 12 | background: white; 13 | text-transform: capitalize; 14 | } 15 | .rdw-option-wrapper:hover { 16 | box-shadow: 1px 1px 0px #BFBDBD; 17 | } 18 | .rdw-option-wrapper:active { 19 | box-shadow: 1px 1px 0px #BFBDBD inset; 20 | } 21 | .rdw-option-active { 22 | box-shadow: 1px 1px 0px #BFBDBD inset; 23 | } 24 | .rdw-option-disabled { 25 | opacity: 0.3; 26 | cursor: default; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Spinner/__test__/spinnerTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | import Spinner from '..'; 7 | 8 | describe('Option test suite', () => { 9 | it('should have a span when rendered', () => { 10 | expect(mount( 11 | , 12 | ).childAt(0).type()).to.equal('div'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import './styles.css'; 5 | 6 | export default () => 7 | (
    8 |
    9 |
    10 |
    11 |
    ); 12 | -------------------------------------------------------------------------------- /src/components/Spinner/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-spinner { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | .rdw-spinner > div { 9 | width: 12px; 10 | height: 12px; 11 | background-color: #333; 12 | 13 | border-radius: 100%; 14 | display: inline-block; 15 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 16 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 17 | } 18 | .rdw-spinner .rdw-bounce1 { 19 | -webkit-animation-delay: -0.32s; 20 | animation-delay: -0.32s; 21 | } 22 | .rdw-spinner .rdw-bounce2 { 23 | -webkit-animation-delay: -0.16s; 24 | animation-delay: -0.16s; 25 | } 26 | @-webkit-keyframes sk-bouncedelay { 27 | 0%, 80%, 100% { -webkit-transform: scale(0) } 28 | 40% { -webkit-transform: scale(1.0) } 29 | } 30 | @keyframes sk-bouncedelay { 31 | 0%, 80%, 100% { 32 | -webkit-transform: scale(0); 33 | transform: scale(0); 34 | } 40% { 35 | -webkit-transform: scale(1.0); 36 | transform: scale(1.0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/controls/BlockType/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-block-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-block-dropdown { 8 | width: 110px; 9 | } 10 | -------------------------------------------------------------------------------- /src/controls/BlockType/__test__/blockControlTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | import { 7 | EditorState, 8 | convertFromHTML, 9 | ContentState, 10 | } from 'draft-js'; 11 | import Block from '..'; 12 | import defaultToolbar from '../../../config/defaultToolbar'; 13 | import ModalHandler from '../../../event-handler/modals'; 14 | import localeTranslations from '../../../i18n'; 15 | 16 | describe('Block test suite', () => { 17 | const contentBlocks = convertFromHTML('
    test
    '); 18 | const contentState = ContentState.createFromBlockArray(contentBlocks); 19 | const editorState = EditorState.createWithContent(contentState); 20 | 21 | it('should have a div at root when rendered', () => { 22 | const block = mount( 23 | {}} 25 | editorState={editorState} 26 | config={{ ...defaultToolbar.blockType, inDropdown: false }} 27 | translations={localeTranslations.en} 28 | modalHandler={new ModalHandler()} 29 | />, 30 | ); 31 | expect(block.html().startsWith(' { 35 | const block = mount( 36 | {}} 38 | editorState={editorState} 39 | config={defaultToolbar.blockType} 40 | modalHandler={new ModalHandler()} 41 | translations={localeTranslations.en} 42 | />, 43 | ); 44 | expect(block.find('Dropdown').length).to.equal(1); 45 | }); 46 | 47 | it('should have 9 child elements when inDropdown is false', () => { 48 | const block = mount( 49 | {}} 51 | editorState={editorState} 52 | config={{ ...defaultToolbar.blockType, inDropdown: false }} 53 | translations={localeTranslations.en} 54 | modalHandler={new ModalHandler()} 55 | />, 56 | ); 57 | expect(block.find('Option').length).to.equal(9); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/controls/ColorPicker/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-colorpicker-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | flex-wrap: wrap 7 | } 8 | .rdw-colorpicker-modal { 9 | position: absolute; 10 | top: 35px; 11 | left: 5px; 12 | display: flex; 13 | flex-direction: column; 14 | width: 175px; 15 | height: 175px; 16 | border: 1px solid #F1F1F1; 17 | padding: 15px; 18 | border-radius: 2px; 19 | z-index: 100; 20 | background: white; 21 | box-shadow: 3px 3px 5px #BFBDBD; 22 | } 23 | .rdw-colorpicker-modal-header { 24 | display: flex; 25 | padding-bottom: 5px; 26 | } 27 | .rdw-colorpicker-modal-style-label { 28 | font-size: 15px; 29 | width: 50%; 30 | text-align: center; 31 | cursor: pointer; 32 | padding: 0 10px 5px; 33 | } 34 | .rdw-colorpicker-modal-style-label-active { 35 | border-bottom: 2px solid #0a66b7; 36 | } 37 | .rdw-colorpicker-modal-options { 38 | margin: 5px auto; 39 | display: flex; 40 | width: 100%; 41 | height: 100%; 42 | flex-wrap: wrap; 43 | overflow: scroll; 44 | } 45 | .rdw-colorpicker-cube { 46 | width: 22px; 47 | height: 22px; 48 | border: 1px solid #F1F1F1; 49 | } 50 | .rdw-colorpicker-option { 51 | margin: 3px; 52 | padding: 0; 53 | min-height: 20px; 54 | border: none; 55 | width: 22px; 56 | height: 22px; 57 | min-width: 22px; 58 | box-shadow: 1px 2px 1px #BFBDBD inset; 59 | } 60 | .rdw-colorpicker-option:hover { 61 | box-shadow: 1px 2px 1px #BFBDBD; 62 | } 63 | .rdw-colorpicker-option:active { 64 | box-shadow: -1px -2px 1px #BFBDBD; 65 | } 66 | .rdw-colorpicker-option-active { 67 | box-shadow: 0px 0px 2px 2px #BFBDBD; 68 | } 69 | -------------------------------------------------------------------------------- /src/controls/ColorPicker/__test__/colorPickerTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { 5 | EditorState, 6 | convertFromHTML, 7 | ContentState, 8 | } from 'draft-js'; 9 | import { expect, assert } from 'chai'; 10 | import { mount } from 'enzyme'; 11 | 12 | import ColorPicker from '..'; 13 | import defaultToolbar from '../../../config/defaultToolbar'; 14 | import ModalHandler from '../../../event-handler/modals'; 15 | import localeTranslations from '../../../i18n'; 16 | 17 | describe('ColorPicker test suite', () => { 18 | const contentBlocks = convertFromHTML('
    test
    '); 19 | const contentState = ContentState.createFromBlockArray(contentBlocks); 20 | const editorState = EditorState.createWithContent(contentState); 21 | 22 | it('should have a div when rendered', () => { 23 | expect(mount( 24 | {}} 26 | editorState={editorState} 27 | config={defaultToolbar.colorPicker} 28 | translations={localeTranslations.en} 29 | modalHandler={new ModalHandler()} 30 | />, 31 | ).html().startsWith(' { 35 | const control = mount( 36 | {}} 38 | editorState={editorState} 39 | config={defaultToolbar.colorPicker} 40 | translations={localeTranslations.en} 41 | modalHandler={new ModalHandler()} 42 | />, 43 | ); 44 | const state = control.state(); 45 | assert.isNotTrue(state.expanded); 46 | assert.isUndefined(state.currentColor); 47 | assert.isUndefined(state.currentBgColor); 48 | }); 49 | 50 | it('should set variable signalExpanded to true when first child is clicked', () => { 51 | const control = mount( 52 | {}} 54 | editorState={editorState} 55 | config={defaultToolbar.colorPicker} 56 | translations={localeTranslations.en} 57 | modalHandler={new ModalHandler()} 58 | />, 59 | ); 60 | const colorPicker = control.find('ColorPicker'); 61 | assert.isNotTrue(colorPicker.instance().signalExpanded); 62 | control.find('Option').simulate('click'); 63 | assert.isTrue(colorPicker.instance().signalExpanded); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/controls/Embedded/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-embedded-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | flex-wrap: wrap 7 | } 8 | .rdw-embedded-modal { 9 | position: absolute; 10 | top: 35px; 11 | left: 5px; 12 | display: flex; 13 | flex-direction: column; 14 | width: 235px; 15 | height: 180px; 16 | border: 1px solid #F1F1F1; 17 | padding: 15px; 18 | border-radius: 2px; 19 | z-index: 100; 20 | background: white; 21 | justify-content: space-between; 22 | box-shadow: 3px 3px 5px #BFBDBD; 23 | } 24 | .rdw-embedded-modal-header { 25 | font-size: 15px; 26 | display: flex; 27 | } 28 | .rdw-embedded-modal-header-option { 29 | width: 50%; 30 | cursor: pointer; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | flex-direction: column; 35 | } 36 | .rdw-embedded-modal-header-label { 37 | width: 95px; 38 | border: 1px solid #f1f1f1; 39 | margin-top: 5px; 40 | background: #6EB8D4; 41 | border-bottom: 2px solid #0a66b7; 42 | } 43 | .rdw-embedded-modal-link-section { 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | .rdw-embedded-modal-link-input { 48 | width: 88%; 49 | height: 35px; 50 | margin: 10px 0; 51 | border: 1px solid #F1F1F1; 52 | border-radius: 2px; 53 | font-size: 15px; 54 | padding: 0 5px; 55 | } 56 | .rdw-embedded-modal-link-input-wrapper { 57 | display: flex; 58 | align-items: center; 59 | } 60 | .rdw-embedded-modal-link-input:focus { 61 | outline: none; 62 | } 63 | .rdw-embedded-modal-btn-section { 64 | display: flex; 65 | justify-content: center; 66 | } 67 | .rdw-embedded-modal-btn { 68 | margin: 0 3px; 69 | width: 75px; 70 | height: 30px; 71 | border: 1px solid #F1F1F1; 72 | border-radius: 2px; 73 | cursor: pointer; 74 | background: white; 75 | text-transform: capitalize; 76 | } 77 | .rdw-embedded-modal-btn:hover { 78 | box-shadow: 1px 1px 0px #BFBDBD; 79 | } 80 | .rdw-embedded-modal-btn:active { 81 | box-shadow: 1px 1px 0px #BFBDBD inset; 82 | } 83 | .rdw-embedded-modal-btn:focus { 84 | outline: none !important; 85 | } 86 | .rdw-embedded-modal-btn:disabled { 87 | background: #ece9e9; 88 | } 89 | .rdw-embedded-modal-size { 90 | align-items: center; 91 | display: flex; 92 | margin: 8px 0; 93 | justify-content: space-between; 94 | } 95 | .rdw-embedded-modal-size-input { 96 | width: 80%; 97 | height: 20px; 98 | border: 1px solid #F1F1F1; 99 | border-radius: 2px; 100 | font-size: 12px; 101 | } 102 | .rdw-embedded-modal-size-input:focus { 103 | outline: none; 104 | } 105 | -------------------------------------------------------------------------------- /src/controls/Embedded/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { AtomicBlockUtils } from 'draft-js'; 4 | 5 | import LayoutComponent from './Component'; 6 | 7 | class Embedded extends Component { 8 | static propTypes = { 9 | editorState: PropTypes.object.isRequired, 10 | onChange: PropTypes.func.isRequired, 11 | modalHandler: PropTypes.object, 12 | config: PropTypes.object, 13 | translations: PropTypes.object, 14 | }; 15 | 16 | state = { 17 | expanded: false, 18 | }; 19 | 20 | componentDidMount() { 21 | const { modalHandler } = this.props; 22 | modalHandler.registerCallBack(this.expandCollapse); 23 | } 24 | 25 | componentWillUnmount() { 26 | const { modalHandler } = this.props; 27 | modalHandler.deregisterCallBack(this.expandCollapse); 28 | } 29 | 30 | onExpandEvent = () => { 31 | this.signalExpanded = !this.state.expanded; 32 | }; 33 | 34 | expandCollapse = () => { 35 | this.setState({ 36 | expanded: this.signalExpanded, 37 | }); 38 | this.signalExpanded = false; 39 | }; 40 | 41 | doExpand = () => { 42 | this.setState({ 43 | expanded: true, 44 | }); 45 | }; 46 | 47 | doCollapse = () => { 48 | this.setState({ 49 | expanded: false, 50 | }); 51 | }; 52 | 53 | addEmbeddedLink = (embeddedLink, height, width) => { 54 | const { 55 | editorState, 56 | onChange, 57 | config: { embedCallback }, 58 | } = this.props; 59 | const src = embedCallback ? embedCallback(embeddedLink) : embeddedLink; 60 | const entityKey = editorState 61 | .getCurrentContent() 62 | .createEntity('EMBEDDED_LINK', 'MUTABLE', { src, height, width }) 63 | .getLastCreatedEntityKey(); 64 | const newEditorState = AtomicBlockUtils.insertAtomicBlock( 65 | editorState, 66 | entityKey, 67 | ' ' 68 | ); 69 | onChange(newEditorState); 70 | this.doCollapse(); 71 | }; 72 | 73 | render() { 74 | const { config, translations } = this.props; 75 | const { expanded } = this.state; 76 | const EmbeddedComponent = config.component || LayoutComponent; 77 | return ( 78 | 87 | ); 88 | } 89 | } 90 | 91 | export default Embedded; 92 | 93 | // todo: make default heights configurable 94 | -------------------------------------------------------------------------------- /src/controls/Emoji/Component/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | import { stopPropagation } from '../../../utils/common'; 8 | import Option from '../../../components/Option'; 9 | import './styles.css'; 10 | 11 | class LayoutComponent extends Component { 12 | static propTypes: Object = { 13 | expanded: PropTypes.bool, 14 | onExpandEvent: PropTypes.func, 15 | onChange: PropTypes.func, 16 | config: PropTypes.object, 17 | translations: PropTypes.object, 18 | }; 19 | 20 | onChange: Function = (event: Object): void => { 21 | const { onChange } = this.props; 22 | onChange(event.target.innerHTML); 23 | }; 24 | 25 | renderEmojiModal(): Object { 26 | const { config: { popupClassName, emojis } } = this.props; 27 | return ( 28 |
    32 | { 33 | emojis.map((emoji, index) => ({emoji})) 39 | } 40 |
    41 | ); 42 | } 43 | 44 | render(): Object { 45 | const { 46 | config: { icon, className, title }, 47 | expanded, 48 | onExpandEvent, 49 | translations, 50 | } = this.props; 51 | return ( 52 |
    59 | 69 | {expanded ? this.renderEmojiModal() : undefined} 70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default LayoutComponent; 76 | -------------------------------------------------------------------------------- /src/controls/Emoji/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-emoji-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | flex-wrap: wrap 7 | } 8 | .rdw-emoji-modal { 9 | overflow: auto; 10 | position: absolute; 11 | top: 35px; 12 | left: 5px; 13 | display: flex; 14 | flex-wrap: wrap; 15 | width: 235px; 16 | height: 180px; 17 | border: 1px solid #F1F1F1; 18 | padding: 15px; 19 | border-radius: 2px; 20 | z-index: 100; 21 | background: white; 22 | box-shadow: 3px 3px 5px #BFBDBD; 23 | } 24 | .rdw-emoji-icon { 25 | margin: 2.5px; 26 | height: 24px; 27 | width: 24px; 28 | cursor: pointer; 29 | font-size: 22px; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | -------------------------------------------------------------------------------- /src/controls/Emoji/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modifier, EditorState } from 'draft-js'; 4 | 5 | import LayoutComponent from './Component'; 6 | 7 | export default class Emoji extends Component { 8 | static propTypes = { 9 | editorState: PropTypes.object.isRequired, 10 | onChange: PropTypes.func.isRequired, 11 | modalHandler: PropTypes.object, 12 | config: PropTypes.object, 13 | translations: PropTypes.object, 14 | }; 15 | 16 | state = { 17 | expanded: false, 18 | }; 19 | 20 | componentDidMount() { 21 | const { modalHandler } = this.props; 22 | modalHandler.registerCallBack(this.expandCollapse); 23 | } 24 | 25 | componentWillUnmount() { 26 | const { modalHandler } = this.props; 27 | modalHandler.deregisterCallBack(this.expandCollapse); 28 | } 29 | 30 | onExpandEvent = () => { 31 | this.signalExpanded = !this.state.expanded; 32 | }; 33 | 34 | expandCollapse = () => { 35 | this.setState({ 36 | expanded: this.signalExpanded, 37 | }); 38 | this.signalExpanded = false; 39 | }; 40 | 41 | doExpand = () => { 42 | this.setState({ 43 | expanded: true, 44 | }); 45 | }; 46 | 47 | doCollapse = () => { 48 | this.setState({ 49 | expanded: false, 50 | }); 51 | }; 52 | 53 | addEmoji = emoji => { 54 | const { editorState, onChange } = this.props; 55 | const contentState = Modifier.replaceText( 56 | editorState.getCurrentContent(), 57 | editorState.getSelection(), 58 | emoji, 59 | editorState.getCurrentInlineStyle() 60 | ); 61 | onChange(EditorState.push(editorState, contentState, 'insert-characters')); 62 | this.doCollapse(); 63 | }; 64 | 65 | render() { 66 | const { config, translations } = this.props; 67 | const { expanded } = this.state; 68 | const EmojiComponent = config.component || LayoutComponent; 69 | return ( 70 | 80 | ); 81 | } 82 | } 83 | 84 | // todo: unit test cases 85 | -------------------------------------------------------------------------------- /src/controls/FontFamily/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-fontfamily-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-fontfamily-dropdown { 8 | width: 115px; 9 | } 10 | .rdw-fontfamily-placeholder { 11 | white-space: nowrap; 12 | max-width: 90px; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | } 16 | .rdw-fontfamily-optionwrapper { 17 | width: 140px; 18 | } 19 | -------------------------------------------------------------------------------- /src/controls/FontFamily/__test__/fontFamilyControlTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect, assert } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | import { 7 | EditorState, 8 | convertFromHTML, 9 | ContentState, 10 | } from 'draft-js'; 11 | 12 | import FontFamilyControl from '..'; 13 | import { Dropdown } from '../../../components/Dropdown'; 14 | import defaultToolbar from '../../../config/defaultToolbar'; 15 | import ModalHandler from '../../../event-handler/modals'; 16 | import localeTranslations from '../../../i18n'; 17 | 18 | describe('FontFamilyControl test suite', () => { 19 | const contentBlocks = convertFromHTML('
    test
    '); 20 | const contentState = ContentState.createFromBlockArray(contentBlocks); 21 | const editorState = EditorState.createWithContent(contentState); 22 | 23 | it('should have a div when rendered', () => { 24 | expect(mount( 25 | {}} 27 | editorState={editorState} 28 | config={defaultToolbar.fontFamily} 29 | modalHandler={new ModalHandler()} 30 | translations={localeTranslations.en} 31 | />, 32 | ).html().startsWith(' { 36 | const control = mount( 37 | {}} 39 | editorState={editorState} 40 | config={defaultToolbar.fontFamily} 41 | modalHandler={new ModalHandler()} 42 | translations={localeTranslations.en} 43 | />, 44 | ); 45 | assert.equal(control.find(Dropdown).length, 1); 46 | assert.equal(control.find(Dropdown).prop('children').length, 2); 47 | assert.isDefined(control.childAt(0).props().onChange); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/controls/FontSize/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-fontsize-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-fontsize-dropdown { 8 | min-width: 40px; 9 | } 10 | .rdw-fontsize-option { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /src/controls/FontSize/__test__/fontSizeControlTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect, assert } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | import { 7 | EditorState, 8 | convertFromHTML, 9 | ContentState, 10 | } from 'draft-js'; 11 | import FontSizeControl from '..'; 12 | import { Dropdown } from '../../../components/Dropdown'; 13 | import defaultToolbar from '../../../config/defaultToolbar'; 14 | import ModalHandler from '../../../event-handler/modals'; 15 | import localeTranslations from '../../../i18n'; 16 | 17 | describe('FontSizeControl test suite', () => { 18 | const contentBlocks = convertFromHTML('
    test
    '); 19 | const contentState = ContentState.createFromBlockArray(contentBlocks); 20 | const editorState = EditorState.createWithContent(contentState); 21 | 22 | it('should have a div when rendered', () => { 23 | expect(mount( 24 | {}} 26 | editorState={editorState} 27 | config={defaultToolbar.fontSize} 28 | translations={localeTranslations.en} 29 | modalHandler={new ModalHandler()} 30 | />, 31 | ).html().startsWith(' { 35 | const control = mount( 36 | {}} 38 | editorState={editorState} 39 | config={defaultToolbar.fontSize} 40 | translations={localeTranslations.en} 41 | modalHandler={new ModalHandler()} 42 | />, 43 | ); 44 | assert.equal(control.find(Dropdown).length, 1); 45 | assert.equal(control.find(Dropdown).prop('children').length, 2); 46 | assert.isDefined(control.childAt(0).props().onChange); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/controls/FontSize/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | toggleCustomInlineStyle, 5 | getSelectionCustomInlineStyle, 6 | } from 'draftjs-utils'; 7 | 8 | import LayoutComponent from './Component'; 9 | 10 | export default class FontSize extends Component { 11 | static propTypes = { 12 | onChange: PropTypes.func.isRequired, 13 | editorState: PropTypes.object, 14 | modalHandler: PropTypes.object, 15 | config: PropTypes.object, 16 | translations: PropTypes.object, 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | const { editorState, modalHandler } = props; 22 | this.state = { 23 | expanded: undefined, 24 | currentFontSize: editorState 25 | ? getSelectionCustomInlineStyle(editorState, ['FONTSIZE']).FONTSIZE 26 | : undefined, 27 | }; 28 | modalHandler.registerCallBack(this.expandCollapse); 29 | } 30 | 31 | componentDidUpdate(prevProps) { 32 | const { editorState } = this.props; 33 | if (editorState && editorState !== prevProps.editorState) { 34 | this.setState({ 35 | currentFontSize: getSelectionCustomInlineStyle(editorState, [ 36 | 'FONTSIZE', 37 | ]).FONTSIZE, 38 | }); 39 | } 40 | } 41 | 42 | componentWillUnmount() { 43 | const { modalHandler } = this.props; 44 | modalHandler.deregisterCallBack(this.expandCollapse); 45 | } 46 | 47 | onExpandEvent = () => { 48 | this.signalExpanded = !this.state.expanded; 49 | }; 50 | 51 | expandCollapse = () => { 52 | this.setState({ 53 | expanded: this.signalExpanded, 54 | }); 55 | this.signalExpanded = false; 56 | }; 57 | 58 | doExpand = () => { 59 | this.setState({ 60 | expanded: true, 61 | }); 62 | }; 63 | 64 | doCollapse = () => { 65 | this.setState({ 66 | expanded: false, 67 | }); 68 | }; 69 | 70 | toggleFontSize = fontSize => { 71 | const { editorState, onChange } = this.props; 72 | const newState = toggleCustomInlineStyle(editorState, 'fontSize', fontSize); 73 | if (newState) { 74 | onChange(newState); 75 | } 76 | }; 77 | 78 | render() { 79 | const { config, translations } = this.props; 80 | const { expanded, currentFontSize } = this.state; 81 | const FontSizeComponent = config.component || LayoutComponent; 82 | const fontSize = currentFontSize && Number(currentFontSize.substring(9)); 83 | return ( 84 | 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/controls/History/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-history-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-history-dropdownoption { 8 | height: 40px; 9 | display: flex; 10 | justify-content: center; 11 | } 12 | .rdw-history-dropdown { 13 | width: 50px; 14 | } 15 | -------------------------------------------------------------------------------- /src/controls/History/__test__/historyControlTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { 5 | EditorState, 6 | convertFromHTML, 7 | ContentState, 8 | } from 'draft-js'; 9 | import { expect } from 'chai'; 10 | import { mount } from 'enzyme'; 11 | 12 | import HistoryControl from '..'; 13 | import Option from '../../../components/Option'; 14 | import defaultToolbar from '../../../config/defaultToolbar'; 15 | import ModalHandler from '../../../event-handler/modals'; 16 | import localeTranslations from '../../../i18n'; 17 | 18 | describe('HistoryControl test suite', () => { 19 | const contentBlocks = convertFromHTML('
    test
    '); 20 | const contentState = ContentState.createFromBlockArray(contentBlocks); 21 | const editorState = EditorState.createWithContent(contentState); 22 | 23 | it('should have a div when rendered', () => { 24 | expect(mount( 25 | {}} 27 | editorState={editorState} 28 | config={defaultToolbar.history} 29 | translations={localeTranslations.en} 30 | modalHandler={new ModalHandler()} 31 | />, 32 | ).html().startsWith(' { 36 | const control = mount( 37 | {}} 39 | editorState={editorState} 40 | config={defaultToolbar.history} 41 | translations={localeTranslations.en} 42 | modalHandler={new ModalHandler()} 43 | />, 44 | ); 45 | expect(control.find(Option).length).to.equal(2); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/controls/History/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EditorState } from 'draft-js'; 4 | 5 | import LayoutComponent from './Component'; 6 | 7 | export default class History extends Component { 8 | static propTypes = { 9 | onChange: PropTypes.func.isRequired, 10 | editorState: PropTypes.object, 11 | modalHandler: PropTypes.object, 12 | config: PropTypes.object, 13 | translations: PropTypes.object, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | const state = { 19 | expanded: false, 20 | undoDisabled: false, 21 | redoDisabled: false, 22 | }; 23 | const { editorState, modalHandler } = props; 24 | if (editorState) { 25 | state.undoDisabled = editorState.getUndoStack().size === 0; 26 | state.redoDisabled = editorState.getRedoStack().size === 0; 27 | } 28 | this.state = state; 29 | modalHandler.registerCallBack(this.expandCollapse); 30 | } 31 | 32 | componentDidUpdate(prevProps) { 33 | const { editorState } = this.props; 34 | if (editorState && prevProps.editorState !== editorState) { 35 | this.setState({ 36 | undoDisabled: editorState.getUndoStack().size === 0, 37 | redoDisabled: editorState.getRedoStack().size === 0, 38 | }); 39 | } 40 | } 41 | 42 | componentWillUnmount() { 43 | const { modalHandler } = this.props; 44 | modalHandler.deregisterCallBack(this.expandCollapse); 45 | } 46 | 47 | onExpandEvent = () => { 48 | this.signalExpanded = !this.state.expanded; 49 | }; 50 | 51 | onChange = action => { 52 | const { editorState, onChange } = this.props; 53 | const newState = EditorState[action](editorState); 54 | if (newState) { 55 | onChange(newState); 56 | } 57 | }; 58 | 59 | doExpand = () => { 60 | this.setState({ 61 | expanded: true, 62 | }); 63 | }; 64 | 65 | doCollapse = () => { 66 | this.setState({ 67 | expanded: false, 68 | }); 69 | }; 70 | 71 | expandCollapse = () => { 72 | this.setState({ 73 | expanded: this.signalExpanded, 74 | }); 75 | this.signalExpanded = false; 76 | }; 77 | 78 | render() { 79 | const { config, translations } = this.props; 80 | const { undoDisabled, redoDisabled, expanded } = this.state; 81 | const HistoryComponent = config.component || LayoutComponent; 82 | return ( 83 | 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/controls/Image/__test__/imageControlTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { 5 | EditorState, 6 | convertFromHTML, 7 | ContentState, 8 | } from 'draft-js'; 9 | import { expect, assert } from 'chai'; 10 | import { mount } from 'enzyme'; 11 | 12 | import ImageControl from '..'; 13 | import defaultToolbar from '../../../config/defaultToolbar'; 14 | import ModalHandler from '../../../event-handler/modals'; 15 | import localeTranslations from '../../../i18n'; 16 | 17 | describe('ImageControl test suite', () => { 18 | const contentBlocks = convertFromHTML('
    test
    '); 19 | const contentState = ContentState.createFromBlockArray(contentBlocks); 20 | const editorState = EditorState.createWithContent(contentState); 21 | 22 | it('should have a div when rendered', () => { 23 | expect(mount( 24 | {}} 26 | editorState={editorState} 27 | config={defaultToolbar.image} 28 | translations={localeTranslations.en} 29 | modalHandler={new ModalHandler()} 30 | />, 31 | ).html().startsWith(' { 35 | const control = mount( 36 | {}} 38 | editorState={editorState} 39 | config={defaultToolbar.image} 40 | translations={localeTranslations.en} 41 | modalHandler={new ModalHandler()} 42 | />, 43 | ); 44 | expect(control.children().length).to.equal(1); 45 | }); 46 | 47 | it.skip('should set signalExpanded to true when option is clicked', () => { 48 | const control = mount( 49 | {}} 51 | editorState={editorState} 52 | config={defaultToolbar.image} 53 | translations={localeTranslations.en} 54 | modalHandler={new ModalHandler()} 55 | />, 56 | ); 57 | const imageControl = control.find('ImageControl'); 58 | assert.isNotTrue(imageControl.node.signalExpanded); 59 | control.find('Option').simulate('click'); 60 | assert.isTrue(imageControl.node.signalExpanded); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/controls/Image/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { AtomicBlockUtils } from 'draft-js'; 4 | 5 | import LayoutComponent from './Component'; 6 | 7 | class ImageControl extends Component { 8 | static propTypes = { 9 | editorState: PropTypes.object.isRequired, 10 | onChange: PropTypes.func.isRequired, 11 | modalHandler: PropTypes.object, 12 | config: PropTypes.object, 13 | translations: PropTypes.object, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | const { modalHandler } = this.props; 19 | this.state = { 20 | expanded: false, 21 | }; 22 | modalHandler.registerCallBack(this.expandCollapse); 23 | } 24 | 25 | componentWillUnmount() { 26 | const { modalHandler } = this.props; 27 | modalHandler.deregisterCallBack(this.expandCollapse); 28 | } 29 | 30 | onExpandEvent = () => { 31 | this.signalExpanded = !this.state.expanded; 32 | }; 33 | 34 | doExpand = () => { 35 | this.setState({ 36 | expanded: true, 37 | }); 38 | }; 39 | 40 | doCollapse = () => { 41 | this.setState({ 42 | expanded: false, 43 | }); 44 | }; 45 | 46 | expandCollapse = () => { 47 | this.setState({ 48 | expanded: this.signalExpanded, 49 | }); 50 | this.signalExpanded = false; 51 | }; 52 | 53 | addImage = (src, height, width, alt) => { 54 | const { editorState, onChange, config } = this.props; 55 | const entityData = { src, height, width }; 56 | if (config.alt.present) { 57 | entityData.alt = alt; 58 | } 59 | const entityKey = editorState 60 | .getCurrentContent() 61 | .createEntity('IMAGE', 'MUTABLE', entityData) 62 | .getLastCreatedEntityKey(); 63 | const newEditorState = AtomicBlockUtils.insertAtomicBlock( 64 | editorState, 65 | entityKey, 66 | ' ' 67 | ); 68 | onChange(newEditorState); 69 | this.doCollapse(); 70 | }; 71 | 72 | render() { 73 | const { config, translations } = this.props; 74 | const { expanded } = this.state; 75 | const ImageComponent = config.component || LayoutComponent; 76 | return ( 77 | 86 | ); 87 | } 88 | } 89 | 90 | export default ImageControl; 91 | -------------------------------------------------------------------------------- /src/controls/Inline/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-inline-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-inline-dropdown { 8 | width: 50px; 9 | } 10 | .rdw-inline-dropdownoption { 11 | height: 40px; 12 | display: flex; 13 | justify-content: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/controls/Link/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-link-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | flex-wrap: wrap 7 | } 8 | .rdw-link-dropdown { 9 | width: 50px; 10 | } 11 | .rdw-link-dropdownOption { 12 | height: 40px; 13 | display: flex; 14 | justify-content: center; 15 | } 16 | .rdw-link-dropdownPlaceholder { 17 | margin-left: 8px; 18 | } 19 | .rdw-link-modal { 20 | position: absolute; 21 | top: 35px; 22 | left: 5px; 23 | display: flex; 24 | flex-direction: column; 25 | width: 235px; 26 | height: 205px; 27 | border: 1px solid #F1F1F1; 28 | padding: 15px; 29 | border-radius: 2px; 30 | z-index: 100; 31 | background: white; 32 | box-shadow: 3px 3px 5px #BFBDBD; 33 | } 34 | .rdw-link-modal-label { 35 | font-size: 15px; 36 | } 37 | .rdw-link-modal-input { 38 | margin-top: 5px; 39 | border-radius: 2px; 40 | border: 1px solid #F1F1F1; 41 | height: 25px; 42 | margin-bottom: 15px; 43 | padding: 0 5px; 44 | } 45 | .rdw-link-modal-input:focus { 46 | outline: none; 47 | } 48 | .rdw-link-modal-buttonsection { 49 | margin: 0 auto; 50 | } 51 | .rdw-link-modal-target-option { 52 | margin-bottom: 20px; 53 | } 54 | .rdw-link-modal-target-option > span { 55 | margin-left: 5px; 56 | } 57 | .rdw-link-modal-btn { 58 | margin-left: 10px; 59 | width: 75px; 60 | height: 30px; 61 | border: 1px solid #F1F1F1; 62 | border-radius: 2px; 63 | cursor: pointer; 64 | background: white; 65 | text-transform: capitalize; 66 | } 67 | .rdw-link-modal-btn:hover { 68 | box-shadow: 1px 1px 0px #BFBDBD; 69 | } 70 | .rdw-link-modal-btn:active { 71 | box-shadow: 1px 1px 0px #BFBDBD inset; 72 | } 73 | .rdw-link-modal-btn:focus { 74 | outline: none !important; 75 | } 76 | .rdw-link-modal-btn:disabled { 77 | background: #ece9e9; 78 | } 79 | .rdw-link-dropdownoption { 80 | height: 40px; 81 | display: flex; 82 | justify-content: center; 83 | } 84 | .rdw-history-dropdown { 85 | width: 50px; 86 | } 87 | -------------------------------------------------------------------------------- /src/controls/List/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-list-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-list-dropdown { 8 | width: 50px; 9 | z-index: 90; 10 | } 11 | .rdw-list-dropdownOption { 12 | height: 40px; 13 | display: flex; 14 | justify-content: center; 15 | } 16 | -------------------------------------------------------------------------------- /src/controls/List/__test__/listControlsTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { 5 | EditorState, 6 | convertFromHTML, 7 | ContentState, 8 | } from 'draft-js'; 9 | import { expect, assert } from 'chai'; 10 | import { spy } from 'sinon'; 11 | import { mount } from 'enzyme'; 12 | import ListControl from '..'; 13 | import Option from '../../../components/Option'; 14 | import defaultToolbar from '../../../config/defaultToolbar'; 15 | import ModalHandler from '../../../event-handler/modals'; 16 | import localeTranslations from '../../../i18n'; 17 | 18 | describe('ListControl test suite', () => { 19 | const contentBlocks = convertFromHTML('
    test
    '); 20 | const contentState = ContentState.createFromBlockArray(contentBlocks); 21 | const editorState = EditorState.createWithContent(contentState); 22 | 23 | it('should have a div when rendered', () => { 24 | expect(mount( 25 | {}} 27 | editorState={editorState} 28 | config={defaultToolbar.list} 29 | translations={localeTranslations.en} 30 | modalHandler={new ModalHandler()} 31 | />, 32 | ).html().startsWith(' { 36 | const control = mount( 37 | {}} 39 | editorState={editorState} 40 | config={defaultToolbar.list} 41 | translations={localeTranslations.en} 42 | modalHandler={new ModalHandler()} 43 | />, 44 | ); 45 | expect(control.find(Option).length).to.equal(4); 46 | }); 47 | 48 | it('should have 1 child elements if inDropdown is true', () => { 49 | const control = mount( 50 | {}} 52 | editorState={editorState} 53 | config={{ ...defaultToolbar.list, inDropdown: true }} 54 | translations={localeTranslations.en} 55 | modalHandler={new ModalHandler()} 56 | />, 57 | ); 58 | expect(control.children().length).to.equal(1); 59 | expect(control.childAt(0).childAt(0).prop('children').length).to.equal(2); 60 | }); 61 | 62 | it('should execute onChange when create list buttons are clicked', () => { 63 | const onChange = spy(); 64 | const control = mount( 65 | , 72 | ); 73 | control.find(Option).at(0).simulate('click'); 74 | assert.isTrue(onChange.calledOnce); 75 | control.find(Option).at(0).simulate('click'); 76 | assert.equal(onChange.callCount, 2); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/controls/Remove/Component/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | import Option from '../../../components/Option'; 8 | import './styles.css'; 9 | 10 | const RemoveComponent = ({ config, onChange, translations }) => { 11 | const { icon, className, title } = config; 12 | return ( 13 |
    14 | 24 |
    25 | ); 26 | }; 27 | 28 | RemoveComponent.propTypes = { 29 | onChange: PropTypes.func, 30 | config: PropTypes.object, 31 | translations: PropTypes.object, 32 | }; 33 | 34 | export default RemoveComponent; 35 | -------------------------------------------------------------------------------- /src/controls/Remove/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-remove-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | position: relative; 6 | flex-wrap: wrap 7 | } 8 | -------------------------------------------------------------------------------- /src/controls/TextAlign/Component/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-text-align-wrapper { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 6px; 5 | flex-wrap: wrap 6 | } 7 | .rdw-text-align-dropdown { 8 | width: 50px; 9 | z-index: 90; 10 | } 11 | .rdw-text-align-dropdownOption { 12 | height: 40px; 13 | display: flex; 14 | justify-content: center; 15 | } 16 | .rdw-right-aligned-block { 17 | text-align: right; 18 | } 19 | .rdw-left-aligned-block { 20 | text-align: left !important; 21 | } 22 | .rdw-center-aligned-block { 23 | text-align: center !important; 24 | } 25 | .rdw-justify-aligned-block { 26 | text-align: justify !important; 27 | } 28 | .rdw-right-aligned-block > div { 29 | display: inline-block; 30 | } 31 | .rdw-left-aligned-block > div { 32 | display: inline-block; 33 | } 34 | .rdw-center-aligned-block > div { 35 | display: inline-block; 36 | } 37 | .rdw-justify-aligned-block > div { 38 | display: inline-block; 39 | } 40 | -------------------------------------------------------------------------------- /src/controls/TextAlign/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { getSelectedBlocksMetadata, setBlockData } from 'draftjs-utils'; 4 | 5 | import LayoutComponent from './Component'; 6 | 7 | export default class TextAlign extends Component { 8 | static propTypes = { 9 | editorState: PropTypes.object.isRequired, 10 | onChange: PropTypes.func.isRequired, 11 | modalHandler: PropTypes.object, 12 | config: PropTypes.object, 13 | translations: PropTypes.object, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | const { modalHandler } = this.props; 19 | this.state = { 20 | currentTextAlignment: undefined, 21 | }; 22 | modalHandler.registerCallBack(this.expandCollapse); 23 | } 24 | 25 | componentDidUpdate(prevProps) { 26 | const { editorState } = this.props; 27 | if (editorState !== prevProps.editorState) { 28 | this.setState({ 29 | currentTextAlignment: getSelectedBlocksMetadata(editorState).get( 30 | 'text-align' 31 | ), 32 | }); 33 | } 34 | } 35 | 36 | componentWillUnmount() { 37 | const { modalHandler } = this.props; 38 | modalHandler.deregisterCallBack(this.expandCollapse); 39 | } 40 | 41 | onExpandEvent = () => { 42 | this.signalExpanded = !this.state.expanded; 43 | }; 44 | 45 | expandCollapse = () => { 46 | this.setState({ 47 | expanded: this.signalExpanded, 48 | }); 49 | this.signalExpanded = false; 50 | }; 51 | 52 | doExpand = () => { 53 | this.setState({ 54 | expanded: true, 55 | }); 56 | }; 57 | 58 | doCollapse = () => { 59 | this.setState({ 60 | expanded: false, 61 | }); 62 | }; 63 | 64 | addBlockAlignmentData = value => { 65 | const { editorState, onChange } = this.props; 66 | const { currentTextAlignment } = this.state; 67 | if (currentTextAlignment !== value) { 68 | onChange(setBlockData(editorState, { 'text-align': value })); 69 | } else { 70 | onChange(setBlockData(editorState, { 'text-align': undefined })); 71 | } 72 | }; 73 | 74 | render() { 75 | const { config, translations } = this.props; 76 | const { expanded, currentTextAlignment } = this.state; 77 | const TextAlignmentComponent = config.component || LayoutComponent; 78 | return ( 79 | 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/controls/index.js: -------------------------------------------------------------------------------- 1 | import inline from "./Inline"; 2 | import blockType from "./BlockType"; 3 | import fontSize from "./FontSize"; 4 | import fontFamily from "./FontFamily"; 5 | import list from "./List"; 6 | import textAlign from "./TextAlign"; 7 | import colorPicker from "./ColorPicker"; 8 | import link from "./Link"; 9 | import embedded from "./Embedded"; 10 | import emoji from "./Emoji"; 11 | import image from "./Image"; 12 | import remove from "./Remove"; 13 | import history from "./History"; 14 | 15 | export default { 16 | inline, 17 | blockType, 18 | fontSize, 19 | fontFamily, 20 | list, 21 | textAlign, 22 | colorPicker, 23 | link, 24 | embedded, 25 | emoji, 26 | image, 27 | remove, 28 | history 29 | }; 30 | -------------------------------------------------------------------------------- /src/decorators/HashTag/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import "./styles.css"; 5 | 6 | class Hashtag { 7 | constructor(config) { 8 | this.className = config.className; 9 | this.hashCharacter = config.hashCharacter || "#"; 10 | this.separator = config.separator || " "; 11 | } 12 | 13 | getHashtagComponent = () => { 14 | const className = this.className; 15 | 16 | const HashtagComponent = ({ children }) => { 17 | const text = children[0].props.text; 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | }; 24 | HashtagComponent.propTypes = { 25 | children: PropTypes.object 26 | }; 27 | return HashtagComponent; 28 | }; 29 | 30 | findHashtagEntities = (contentBlock, callback) => { 31 | let text = contentBlock.getText(); 32 | let startIndex = 0; 33 | let counter = 0; 34 | 35 | for (; text.length > 0 && startIndex >= 0; ) { 36 | if (text[0] === this.hashCharacter) { 37 | startIndex = 0; 38 | counter = 0; 39 | text = text.substr(this.hashCharacter.length); 40 | } else { 41 | startIndex = text.indexOf(this.separator + this.hashCharacter); 42 | if (startIndex >= 0) { 43 | text = text.substr( 44 | startIndex + (this.separator + this.hashCharacter).length 45 | ); 46 | counter += startIndex + this.separator.length; 47 | } 48 | } 49 | if (startIndex >= 0) { 50 | const endIndex = 51 | text.indexOf(this.separator) >= 0 52 | ? text.indexOf(this.separator) 53 | : text.length; 54 | const hashtagText = text.substr(0, endIndex); 55 | if (hashtagText && hashtagText.length > 0) { 56 | callback( 57 | counter, 58 | counter + hashtagText.length + this.hashCharacter.length 59 | ); 60 | counter += this.hashCharacter.length; 61 | } 62 | } 63 | } 64 | }; 65 | 66 | getHashtagDecorator = () => ({ 67 | strategy: this.findHashtagEntities, 68 | component: this.getHashtagComponent() 69 | }); 70 | } 71 | 72 | const getDecorator = config => new Hashtag(config).getHashtagDecorator(); 73 | 74 | export default getDecorator; 75 | -------------------------------------------------------------------------------- /src/decorators/HashTag/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-hashtag-link { 2 | text-decoration: none; 3 | color: #1236ff; 4 | background-color: #f0fbff; 5 | padding: 1px 2px; 6 | border-radius: 2px; 7 | } 8 | -------------------------------------------------------------------------------- /src/decorators/Link/__test__/linkDecoratorTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { expect, assert } from 'chai'; 5 | import { shallow, mount } from 'enzyme'; 6 | import { convertFromHTML, ContentState } from 'draft-js'; 7 | import getLinkDecorator from '..'; 8 | 9 | describe('LinkDecorator test suite', () => { 10 | const LinkDecorator = getLinkDecorator({ showOpenOptionOnHover: true }); 11 | const contentBlocks = convertFromHTML('
    test
    '); 12 | const contentState = ContentState.createFromBlockArray(contentBlocks); 13 | const entityKey = contentState 14 | .createEntity('LINK', 'MUTABLE', { title: 'title', url: 'url' }) 15 | .getLastCreatedEntityKey(); 16 | const xssEntityKey = contentState 17 | .createEntity('LINK', 'MUTABLE', { title: 'title', url: 'javascript:alert(1)' }) 18 | .getLastCreatedEntityKey(); 19 | 20 | it('should have a div when rendered', () => { 21 | const Link = LinkDecorator.component; 22 | expect(mount(Link).childAt(0).type()).to.equal('span'); 23 | }); 24 | 25 | it('should have state initialized correctly', () => { 26 | const Link = LinkDecorator.component; 27 | const control = shallow(Link); 28 | assert.isNotTrue(control.state().showPopOver); 29 | }); 30 | 31 | it('should have 1 child element by default', () => { 32 | const Link = LinkDecorator.component; 33 | const control = shallow(Link); 34 | expect(control.children().length).to.equal(1); 35 | }); 36 | 37 | it('should have 2 child element when showPopOver is true', () => { 38 | const Link = LinkDecorator.component; 39 | const control = mount(Link); 40 | control.setState({ showPopOver: true }); 41 | expect(control.childAt(0).children().length).to.equal(2); 42 | }); 43 | 44 | it('should filter xss in href', () => { 45 | const Link = LinkDecorator.component; 46 | const control = mount(Link); 47 | expect(control.childAt(0).childAt(0).props().href).to.equal('#'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/decorators/Link/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ensureSafeUrl } from '../../utils/url' 4 | import openlink from '../../../images/openlink.svg'; 5 | import './styles.css'; 6 | 7 | function findLinkEntities(contentBlock, callback, contentState) { 8 | contentBlock.findEntityRanges( 9 | (character) => { 10 | const entityKey = character.getEntity(); 11 | return ( 12 | entityKey !== null && 13 | contentState.getEntity(entityKey).getType() === 'LINK' 14 | ); 15 | }, 16 | callback, 17 | ); 18 | } 19 | 20 | function getLinkComponent(config) { 21 | const showOpenOptionOnHover = config.showOpenOptionOnHover; 22 | return class Link extends Component { 23 | static propTypes = { 24 | entityKey: PropTypes.string.isRequired, 25 | children: PropTypes.array, 26 | contentState: PropTypes.object, 27 | }; 28 | 29 | state: Object = { 30 | showPopOver: false, 31 | }; 32 | 33 | openLink: Function = () => { 34 | const { entityKey, contentState } = this.props; 35 | const { url } = contentState.getEntity(entityKey).getData(); 36 | const linkTab = window.open(ensureSafeUrl(url), 'blank'); // eslint-disable-line no-undef 37 | // linkTab can be null when the window failed to open. 38 | if (linkTab) { 39 | linkTab.focus(); 40 | } 41 | }; 42 | 43 | toggleShowPopOver: Function = () => { 44 | const showPopOver = !this.state.showPopOver; 45 | this.setState({ 46 | showPopOver, 47 | }); 48 | }; 49 | 50 | render() { 51 | const { children, entityKey, contentState } = this.props; 52 | const { url, targetOption } = contentState.getEntity(entityKey).getData(); 53 | const { showPopOver } = this.state; 54 | return ( 55 | 60 | {children} 61 | {showPopOver && showOpenOptionOnHover ? 62 | 68 | : undefined 69 | } 70 | 71 | ); 72 | } 73 | }; 74 | } 75 | 76 | export default config => ({ 77 | strategy: findLinkEntities, 78 | component: getLinkComponent(config), 79 | }); 80 | -------------------------------------------------------------------------------- /src/decorators/Link/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-link-decorator-wrapper { 2 | position: relative; 3 | } 4 | .rdw-link-decorator-icon { 5 | position: absolute; 6 | left: 40%; 7 | top: 0; 8 | cursor: pointer; 9 | background-color: white; 10 | } 11 | -------------------------------------------------------------------------------- /src/decorators/Mention/Mention/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import "./styles.css"; 5 | 6 | class Mention { 7 | constructor(className) { 8 | this.className = className; 9 | } 10 | getMentionComponent = () => { 11 | const className = this.className; 12 | const MentionComponent = ({ entityKey, children, contentState }) => { 13 | const { url, value } = contentState.getEntity(entityKey).getData(); 14 | return ( 15 | 19 | {children} 20 | 21 | ); 22 | }; 23 | MentionComponent.propTypes = { 24 | entityKey: PropTypes.number, 25 | children: PropTypes.array, 26 | contentState: PropTypes.object 27 | }; 28 | return MentionComponent; 29 | }; 30 | getMentionDecorator = () => ({ 31 | strategy: this.findMentionEntities, 32 | component: this.getMentionComponent() 33 | }); 34 | } 35 | 36 | Mention.prototype.findMentionEntities = ( 37 | contentBlock, 38 | callback, 39 | contentState 40 | ) => { 41 | contentBlock.findEntityRanges(character => { 42 | const entityKey = character.getEntity(); 43 | return ( 44 | entityKey !== null && 45 | contentState.getEntity(entityKey).getType() === "MENTION" 46 | ); 47 | }, callback); 48 | }; 49 | 50 | export default Mention; 51 | -------------------------------------------------------------------------------- /src/decorators/Mention/Mention/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-mention-link { 2 | text-decoration: none; 3 | color: #1236ff; 4 | background-color: #f0fbff; 5 | padding: 1px 2px; 6 | border-radius: 2px; 7 | } 8 | -------------------------------------------------------------------------------- /src/decorators/Mention/Suggestion/styles.css: -------------------------------------------------------------------------------- 1 | .rdw-suggestion-wrapper { 2 | position: relative; 3 | } 4 | .rdw-suggestion-dropdown { 5 | position: absolute; 6 | display: flex; 7 | flex-direction: column; 8 | border: 1px solid #F1F1F1; 9 | min-width: 100px; 10 | max-height: 150px; 11 | overflow: auto; 12 | background: white; 13 | z-index: 100; 14 | } 15 | .rdw-suggestion-option { 16 | padding: 7px 5px; 17 | border-bottom: 1px solid #f1f1f1; 18 | } 19 | .rdw-suggestion-option-active { 20 | background-color: #F1F1F1; 21 | } 22 | -------------------------------------------------------------------------------- /src/decorators/Mention/addMention.js: -------------------------------------------------------------------------------- 1 | import { 2 | EditorState, 3 | Modifier, 4 | } from 'draft-js'; 5 | import { getSelectedBlock } from 'draftjs-utils'; 6 | 7 | export default function addMention( 8 | editorState: EditorState, 9 | onChange: Function, 10 | separator: string, 11 | trigger: string, 12 | suggestion: Object, 13 | ): void { 14 | const { value, url } = suggestion; 15 | const entityKey = editorState 16 | .getCurrentContent() 17 | .createEntity('MENTION', 'IMMUTABLE', { text: `${trigger}${value}`, value, url }) 18 | .getLastCreatedEntityKey(); 19 | const selectedBlock = getSelectedBlock(editorState); 20 | const selectedBlockText = selectedBlock.getText(); 21 | let focusOffset = editorState.getSelection().focusOffset; 22 | const mentionIndex = (selectedBlockText.lastIndexOf(separator + trigger, focusOffset) || 0) + 1; 23 | let spaceAlreadyPresent = false; 24 | if (selectedBlockText.length === mentionIndex + 1) { 25 | focusOffset = selectedBlockText.length; 26 | } 27 | if (selectedBlockText[focusOffset] === ' ') { 28 | spaceAlreadyPresent = true; 29 | } 30 | let updatedSelection = editorState.getSelection().merge({ 31 | anchorOffset: mentionIndex, 32 | focusOffset, 33 | }); 34 | let newEditorState = EditorState.acceptSelection(editorState, updatedSelection); 35 | let contentState = Modifier.replaceText( 36 | newEditorState.getCurrentContent(), 37 | updatedSelection, 38 | `${trigger}${value}`, 39 | newEditorState.getCurrentInlineStyle(), 40 | entityKey, 41 | ); 42 | newEditorState = EditorState.push(newEditorState, contentState, 'insert-characters'); 43 | 44 | if (!spaceAlreadyPresent) { 45 | // insert a blank space after mention 46 | updatedSelection = newEditorState.getSelection().merge({ 47 | anchorOffset: mentionIndex + value.length + trigger.length, 48 | focusOffset: mentionIndex + value.length + trigger.length, 49 | }); 50 | newEditorState = EditorState.acceptSelection(newEditorState, updatedSelection); 51 | contentState = Modifier.insertText( 52 | newEditorState.getCurrentContent(), 53 | updatedSelection, 54 | ' ', 55 | newEditorState.getCurrentInlineStyle(), 56 | undefined, 57 | ); 58 | } 59 | onChange(EditorState.push(newEditorState, contentState, 'insert-characters')); 60 | } 61 | -------------------------------------------------------------------------------- /src/decorators/Mention/index.js: -------------------------------------------------------------------------------- 1 | import Mention from "./Mention"; 2 | import Suggestion from "./Suggestion"; 3 | 4 | const getDecorators = config => [ 5 | new Mention(config.mentionClassName).getMentionDecorator(), 6 | new Suggestion(config).getSuggestionDecorator() 7 | ]; 8 | 9 | export default getDecorators; 10 | -------------------------------------------------------------------------------- /src/event-handler/focus.js: -------------------------------------------------------------------------------- 1 | export default class FocusHandler { 2 | inputFocused = false; 3 | editorMouseDown = false; 4 | 5 | onEditorMouseDown = ():void => { 6 | this.editorFocused = true; 7 | } 8 | 9 | onInputMouseDown = ():void => { 10 | this.inputFocused = true; 11 | } 12 | 13 | isEditorBlur = (event): void => { 14 | if ( 15 | (event.target.tagName === 'INPUT' || event.target.tagName === 'LABEL' || event.target.tagName === 'TEXTAREA') && 16 | !this.editorFocused 17 | ) { 18 | this.inputFocused = false; 19 | return true; 20 | } else if ( 21 | (event.target.tagName !== 'INPUT' || event.target.tagName !== 'LABEL' || event.target.tagName !== 'TEXTAREA') && 22 | !this.inputFocused 23 | ) { 24 | this.editorFocused = false; 25 | return true; 26 | } 27 | return false; 28 | }; 29 | 30 | isEditorFocused = ():void => { 31 | if (!this.inputFocused) { 32 | return true; 33 | } 34 | this.inputFocused = false; 35 | return false; 36 | } 37 | 38 | isToolbarFocused = ():void => { 39 | if (!this.editorFocused) { 40 | return true; 41 | } 42 | this.editorFocused = false; 43 | return false; 44 | } 45 | 46 | isInputFocused = ():void => this.inputFocused; 47 | } 48 | -------------------------------------------------------------------------------- /src/event-handler/keyDown.js: -------------------------------------------------------------------------------- 1 | let callBacks = []; 2 | 3 | export default { 4 | onKeyDown: (event: Object) => { 5 | callBacks.forEach((callBack) => { 6 | callBack(event); 7 | }); 8 | }, 9 | 10 | registerCallBack: (callBack): void => { 11 | callBacks.push(callBack); 12 | }, 13 | 14 | deregisterCallBack: (callBack): void => { 15 | callBacks = callBacks.filter(cb => cb !== callBack); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/event-handler/modals.js: -------------------------------------------------------------------------------- 1 | export default class ModalHandler { 2 | callBacks = []; 3 | suggestionCallback = undefined; 4 | editorFlag = false; 5 | suggestionFlag = false; 6 | 7 | closeAllModals = (event: Object) => { 8 | this.callBacks.forEach((callBack) => { 9 | callBack(event); 10 | }); 11 | }; 12 | 13 | init = (wrapperId: string) => { 14 | const wrapper = document.getElementById(wrapperId); // eslint-disable-line no-undef 15 | if (wrapper) { 16 | wrapper.addEventListener('click', () => { 17 | this.editorFlag = true; 18 | }); 19 | } 20 | if (document) { 21 | document.addEventListener('click', () => { // eslint-disable-line no-undef 22 | if (!this.editorFlag) { 23 | this.closeAllModals(); 24 | if (this.suggestionCallback) { 25 | this.suggestionCallback(); 26 | } 27 | } else { 28 | this.editorFlag = false; 29 | } 30 | }); 31 | document.addEventListener('keydown', (event) => { // eslint-disable-line no-undef 32 | if (event.key === 'Escape') { 33 | this.closeAllModals(); 34 | } 35 | }); 36 | } 37 | }; 38 | 39 | onEditorClick = () => { 40 | this.closeModals(); 41 | if (!this.suggestionFlag && this.suggestionCallback) { 42 | this.suggestionCallback(); 43 | } else { 44 | this.suggestionFlag = false; 45 | } 46 | } 47 | 48 | closeModals = (event: Object): void => { 49 | this.closeAllModals(event); 50 | }; 51 | 52 | registerCallBack = (callBack): void => { 53 | this.callBacks.push(callBack); 54 | }; 55 | 56 | deregisterCallBack = (callBack): void => { 57 | this.callBacks = this.callBacks.filter(cb => cb !== callBack); 58 | }; 59 | 60 | setSuggestionCallback = (callBack): void => { 61 | this.suggestionCallback = callBack; 62 | }; 63 | 64 | removeSuggestionCallback = (): void => { 65 | this.suggestionCallback = undefined; 66 | }; 67 | 68 | onSuggestionClick = ():void => { 69 | this.suggestionFlag = true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/event-handler/suggestions.js: -------------------------------------------------------------------------------- 1 | let suggestionDropdownOpen; 2 | 3 | export default { 4 | open: () => { 5 | suggestionDropdownOpen = true; 6 | }, 7 | 8 | close: () => { 9 | suggestionDropdownOpen = false; 10 | }, 11 | 12 | isOpen: () => suggestionDropdownOpen, 13 | }; 14 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from "./en"; 2 | import fr from "./fr"; 3 | import zh from "./zh"; 4 | import ru from "./ru"; 5 | import pt from "./pt"; 6 | import ko from "./ko"; 7 | import it from "./it"; 8 | import nl from "./nl"; 9 | import de from "./de"; 10 | import da from "./da"; 11 | import zh_tw from "./zh_tw"; 12 | import pl from "./pl"; 13 | import es from "./es"; 14 | import ja from "./ja"; 15 | 16 | export default { 17 | en, 18 | fr, 19 | zh, 20 | ru, 21 | pt, 22 | ko, 23 | it, 24 | nl, 25 | de, 26 | da, 27 | zh_tw, 28 | pl, 29 | es, 30 | ja 31 | }; 32 | -------------------------------------------------------------------------------- /src/i18n/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Generic 3 | "generic.add": "添加", 4 | "generic.cancel": "取消", 5 | 6 | // BlockType 7 | "components.controls.blocktype.h1": "标题1", 8 | "components.controls.blocktype.h2": "标题2", 9 | "components.controls.blocktype.h3": "标题3", 10 | "components.controls.blocktype.h4": "标题4", 11 | "components.controls.blocktype.h5": "标题5", 12 | "components.controls.blocktype.h6": "标题6", 13 | "components.controls.blocktype.blockquote": "引用", 14 | "components.controls.blocktype.code": "源码", 15 | "components.controls.blocktype.blocktype": "样式", 16 | "components.controls.blocktype.normal": "正文", 17 | 18 | // Color Picker 19 | "components.controls.colorpicker.colorpicker": "选色器", 20 | "components.controls.colorpicker.text": "文字", 21 | "components.controls.colorpicker.background": "背景", 22 | 23 | // Embedded 24 | "components.controls.embedded.embedded": "内嵌", 25 | "components.controls.embedded.embeddedlink": "内嵌网页", 26 | "components.controls.embedded.enterlink": "输入网页地址", 27 | 28 | // Emoji 29 | "components.controls.emoji.emoji": "表情符号", 30 | 31 | // FontFamily 32 | "components.controls.fontfamily.fontfamily": "字体", 33 | 34 | // FontSize 35 | "components.controls.fontsize.fontsize": "字号", 36 | 37 | // History 38 | "components.controls.history.history": "历史", 39 | "components.controls.history.undo": "撤销", 40 | "components.controls.history.redo": "恢复", 41 | 42 | // Image 43 | "components.controls.image.image": "图片", 44 | "components.controls.image.fileUpload": "来自文件", 45 | "components.controls.image.byURL": "在线图片", 46 | "components.controls.image.dropFileText": "点击或者拖拽文件上传", 47 | 48 | // Inline 49 | "components.controls.inline.bold": "粗体", 50 | "components.controls.inline.italic": "斜体", 51 | "components.controls.inline.underline": "下划线", 52 | "components.controls.inline.strikethrough": "删除线", 53 | "components.controls.inline.monospace": "等宽字体", 54 | "components.controls.inline.superscript": "上标", 55 | "components.controls.inline.subscript": "下标", 56 | 57 | // Link 58 | "components.controls.link.linkTitle": "超链接", 59 | "components.controls.link.linkTarget": "输入链接地址", 60 | "components.controls.link.linkTargetOption": "在新窗口中打开链接", 61 | "components.controls.link.link": "链接", 62 | "components.controls.link.unlink": "删除链接", 63 | 64 | // List 65 | "components.controls.list.list": "列表", 66 | "components.controls.list.unordered": "项目符号", 67 | "components.controls.list.ordered": "编号", 68 | "components.controls.list.indent": "增加缩进量", 69 | "components.controls.list.outdent": "减少缩进量", 70 | 71 | // Remove 72 | "components.controls.remove.remove": "清除格式", 73 | 74 | // TextAlign 75 | "components.controls.textalign.textalign": "文本对齐", 76 | "components.controls.textalign.left": "文本左对齐", 77 | "components.controls.textalign.center": "居中", 78 | "components.controls.textalign.right": "文本右对齐", 79 | "components.controls.textalign.justify": "两端对齐" 80 | }; 81 | -------------------------------------------------------------------------------- /src/i18n/zh_tw.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Generic 3 | "generic.add": "新增", 4 | "generic.cancel": "取消", 5 | 6 | // BlockType 7 | "components.controls.blocktype.h1": "標題1", 8 | "components.controls.blocktype.h2": "標題2", 9 | "components.controls.blocktype.h3": "標題3", 10 | "components.controls.blocktype.h4": "標題4", 11 | "components.controls.blocktype.h5": "標題5", 12 | "components.controls.blocktype.h6": "標題6", 13 | "components.controls.blocktype.blockquote": "引用", 14 | "components.controls.blocktype.code": "程式碼", 15 | "components.controls.blocktype.blocktype": "樣式", 16 | "components.controls.blocktype.normal": "正文", 17 | 18 | // Color Picker 19 | "components.controls.colorpicker.colorpicker": "選色器", 20 | "components.controls.colorpicker.text": "文字", 21 | "components.controls.colorpicker.background": "背景", 22 | 23 | // Embedded 24 | "components.controls.embedded.embedded": "內嵌", 25 | "components.controls.embedded.embeddedlink": "內嵌網頁", 26 | "components.controls.embedded.enterlink": "輸入網頁地址", 27 | 28 | // Emoji 29 | "components.controls.emoji.emoji": "表情符號", 30 | 31 | // FontFamily 32 | "components.controls.fontfamily.fontfamily": "字體", 33 | 34 | // FontSize 35 | "components.controls.fontsize.fontsize": "字體大小", 36 | 37 | // History 38 | "components.controls.history.history": "歷史紀錄", 39 | "components.controls.history.undo": "復原", 40 | "components.controls.history.redo": "重做", 41 | 42 | // Image 43 | "components.controls.image.image": "圖片", 44 | "components.controls.image.fileUpload": "檔案上傳", 45 | "components.controls.image.byURL": "網址", 46 | "components.controls.image.dropFileText": "點擊或拖曳檔案上傳", 47 | 48 | // Inline 49 | "components.controls.inline.bold": "粗體", 50 | "components.controls.inline.italic": "斜體", 51 | "components.controls.inline.underline": "底線", 52 | "components.controls.inline.strikethrough": "刪除線", 53 | "components.controls.inline.monospace": "等寬字體", 54 | "components.controls.inline.superscript": "上標", 55 | "components.controls.inline.subscript": "下標", 56 | 57 | // Link 58 | "components.controls.link.linkTitle": "超連結", 59 | "components.controls.link.linkTarget": "輸入連結位址", 60 | "components.controls.link.linkTargetOption": "在新視窗打開連結", 61 | "components.controls.link.link": "連結", 62 | "components.controls.link.unlink": "刪除連結", 63 | 64 | // List 65 | "components.controls.list.list": "列表", 66 | "components.controls.list.unordered": "項目符號", 67 | "components.controls.list.ordered": "編號", 68 | "components.controls.list.indent": "增加縮排", 69 | "components.controls.list.outdent": "減少縮排", 70 | 71 | // Remove 72 | "components.controls.remove.remove": "清除格式", 73 | 74 | // TextAlign 75 | "components.controls.textalign.textalign": "文字對齊", 76 | "components.controls.textalign.left": "文字向左對齊", 77 | "components.controls.textalign.center": "文字置中", 78 | "components.controls.textalign.right": "文字向右對齊", 79 | "components.controls.textalign.justify": "兩端對齊" 80 | }; 81 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export { default as Editor } from "./Editor"; 4 | -------------------------------------------------------------------------------- /src/renderer/Embedded/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Embed = ({ block, contentState }) => { 5 | const entity = contentState.getEntity(block.getEntityAt(0)); 6 | const { src, height, width } = entity.getData(); 7 | return (