The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .babelrc
├── .eslintignore
├── .eslintrc
├── .flowconfig
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
    ├── CNAME
    ├── css
    │   ├── demo.css
    │   └── normalize.css
    ├── demo.html
    ├── index.html
    └── react.ico
├── flow-typed
    ├── classnames.js
    └── globals.js
├── package.json
├── src
    ├── Draft.global.css
    ├── EditorDemo.js
    ├── RichTextEditor.css
    ├── RichTextEditor.js
    ├── SimpleRichTextEditor.js
    ├── __tests__
    │   └── RichTextEditor-test.js
    ├── demo.js
    ├── lib
    │   ├── EditorToolbar.css
    │   ├── EditorToolbar.js
    │   ├── EditorToolbarConfig.js
    │   ├── EditorValue.js
    │   ├── ImageDecorator.js
    │   ├── LinkDecorator.js
    │   ├── StyleButton.js
    │   ├── __tests__
    │   │   └── composite-test.js
    │   ├── blockStyleFunctions.js
    │   ├── changeBlockDepth.js
    │   ├── changeBlockType.js
    │   ├── clearEntityForRange.js
    │   ├── composite.js
    │   ├── getBlocksInSelection.js
    │   ├── getEntityAtCursor.js
    │   ├── insertBlockAfter.js
    │   └── isListItem.js
    └── ui
    │   ├── Button.css
    │   ├── Button.js
    │   ├── ButtonGroup.css
    │   ├── ButtonGroup.js
    │   ├── ButtonWrap.css
    │   ├── ButtonWrap.js
    │   ├── Dropdown.css
    │   ├── Dropdown.js
    │   ├── IconButton.css
    │   ├── IconButton.js
    │   ├── ImageSpan.css
    │   ├── ImageSpan.js
    │   ├── InputPopover.css
    │   ├── InputPopover.js
    │   └── PopoverIconButton.js
├── test
    ├── init.js
    └── mocha.opts
├── webpack.config.js
└── yarn.lock


/.babelrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "plugins": [
 3 |     "transform-runtime"
 4 |   ],
 5 |   "presets": [
 6 |     ["babel-preset-es2015", { "modules": false }],
 7 |     "babel-preset-react",
 8 |     "babel-preset-stage-2"
 9 |   ],
10 |   "env": {
11 |     "test": {
12 |       "plugins": [
13 |         "transform-es2015-modules-commonjs"
14 |       ]
15 |     }
16 |   }
17 | }
18 | 


--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | _*
2 | !__tests__
3 | /assets/dist
4 | /dist
5 | /lib
6 | /node_modules
7 | 


--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
  1 | {
  2 |   "parser": "babel-eslint",
  3 |   "plugins": [
  4 |     "babel",
  5 |     "flow-vars",
  6 |     "react"
  7 |   ],
  8 |   "env": {
  9 |     "browser": true,
 10 |     "node": true
 11 |   },
 12 |   "globals": {
 13 |     "Map": false,
 14 |     "Set": false,
 15 |     // Flow globals
 16 |     "ReactElement": false,
 17 |     "ReactNode": false
 18 |   },
 19 |   "rules": {
 20 |     "no-cond-assign": 1,                  // disallow assignment in conditional expressions
 21 |     "no-console": 0,                      // disallow use of console: should use nuclide-logging instead
 22 |     "no-constant-condition": 1,           // disallow use of constant expressions in conditions
 23 |     "comma-dangle": [                     // disallow trailing commas in object and array literals
 24 |       1, "always-multiline"
 25 |     ],
 26 |     "no-control-regex": 1,                // disallow control characters in regular expressions
 27 |     "no-debugger": 1,                     // disallow use of debugger
 28 |     "no-dupe-keys": 1,                    // disallow duplicate keys when creating object literals
 29 |     "no-dupe-args": 1,                    // disallow duplicate arguments in functions
 30 |     "no-duplicate-case": 1,               // disallow a duplicate case label
 31 |     "no-empty": 0,                        // disallow empty statements
 32 |     "no-empty-character-class": 1,        // disallow the use of empty character classes in regular expressions
 33 |     "no-ex-assign": 1,                    // disallow assigning to the exception in a catch block
 34 |     "no-extra-boolean-cast": 1,           // disallow double-negation boolean casts in a boolean context
 35 |     "no-extra-semi": 1,                   // disallow unnecessary semicolons
 36 |     "no-func-assign": 1,                  // disallow overwriting functions written as function declarations
 37 |     "no-inner-declarations": 0,           // disallow function or variable declarations in nested blocks
 38 |     "no-invalid-regexp": 1,               // disallow invalid regular expression strings in the RegExp constructor
 39 |     "no-negated-in-lhs": 1,               // disallow negation of the left operand of an in expression
 40 |     "no-obj-calls": 1,                    // disallow the use of object properties of the global object (Math and JSON) as functions
 41 |     "no-regex-spaces": 1,                 // disallow multiple spaces in a regular expression literal
 42 |     "no-reserved-keys": 0,                // disallow reserved words being used as object literal keys
 43 |     "no-sparse-arrays": 1,                // disallow sparse arrays
 44 |     "no-unreachable": 1,                  // disallow unreachable statements after a return, throw, continue, or break statement
 45 |     "use-isnan": 1,                       // disallow comparisons with the value NaN
 46 |     "valid-jsdoc": 0,                     // Ensure JSDoc comments are valid
 47 |     "valid-typeof": 1,                    // Ensure that the results of typeof are compared against a valid string
 48 | 
 49 |     // Best Practices (designed to prevent you from making mistakes)
 50 | 
 51 |     "block-scoped-var": 0,                // treat var statements as if they were block scoped
 52 |     "complexity": 0,                      // specify the maximum cyclomatic complexity allowed in a program
 53 |     "consistent-return": 0,               // require return statements to either always or never specify values
 54 |     "curly": 1,                           // specify curly brace conventions for all control statements
 55 |     "default-case": 0,                    // require default case in switch statements
 56 |     "dot-notation": 0,                    // dot notation encouraged except for foreign properties that cannot be renamed (i.e., Closure Compiler rules)
 57 |     "eqeqeq": [1, "allow-null"],          // require the use of === and !==
 58 |     "guard-for-in": 1,                    // make sure for-in loops have an if statement
 59 |     "no-alert": 1,                        // disallow the use of alert, confirm, and prompt
 60 |     "no-caller": 1,                       // disallow use of arguments.caller or arguments.callee
 61 |     "no-div-regex": 1,                    // disallow division operators explicitly at beginning of regular expression
 62 |     "no-else-return": 0,                  // disallow else after a return in an if
 63 |     "no-eq-null": 0,                      // disallow comparisons to null without a type-checking operator
 64 |     "no-eval": 1,                         // disallow use of eval()
 65 |     "no-extend-native": 1,                // disallow adding to native types
 66 |     "no-extra-bind": 1,                   // disallow unnecessary function binding
 67 |     "no-fallthrough": 1,                  // disallow fallthrough of case statements
 68 |     "no-floating-decimal": 1,             // disallow the use of leading or trailing decimal points in numeric literals
 69 |     "no-implied-eval": 1,                 // disallow use of eval()-like methods
 70 |     "no-labels": 1,                       // disallow use of labeled statements
 71 |     "no-iterator": 1,                     // disallow usage of __iterator__ property
 72 |     "no-lone-blocks": 1,                  // disallow unnecessary nested blocks
 73 |     "no-loop-func": 0,                    // disallow creation of functions within loops
 74 |     "no-multi-str": 0,                    // disallow use of multiline strings
 75 |     "no-native-reassign": 0,              // disallow reassignments of native objects
 76 |     "no-new": 1,                          // disallow use of new operator when not part of the assignment or comparison
 77 |     "no-new-func": 1,                     // disallow use of new operator for Function object
 78 |     "no-new-wrappers": 1,                 // disallows creating new instances of String,Number, and Boolean
 79 |     "no-octal": 1,                        // disallow use of octal literals
 80 |     "no-octal-escape": 1,                 // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
 81 |     "no-proto": 1,                        // disallow usage of __proto__ property
 82 |     "no-redeclare": 1,                    // disallow declaring the same variable more then once
 83 |     "no-return-assign": 1,                // disallow use of assignment in return statement
 84 |     "no-script-url": 1,                   // disallow use of javascript: urls.
 85 |     "no-self-compare": 1,                 // disallow comparisons where both sides are exactly the same
 86 |     "no-sequences": 1,                    // disallow use of comma operator
 87 |     "no-unused-expressions": 0,           // disallow usage of expressions in statement position
 88 |     "no-void": 1,                         // disallow use of void operator
 89 |     "no-warning-comments": 0,             // disallow usage of configurable warning terms in comments e.g. TODO or FIXME
 90 |     "no-with": 1,                         // disallow use of the with statement
 91 |     "radix": 1,                           // require use of the second argument for parseInt()
 92 |     "vars-on-top": 0,                     // requires to declare all vars on top of their containing scope
 93 |     "wrap-iife": 0,                       // require immediate function invocation to be wrapped in parentheses
 94 |     "yoda": 1,                            // require or disallow Yoda conditions
 95 |     "strict": 0,                          // this rule conflicts with 'use-babel' so we'll just disable it
 96 | 
 97 |     // Variables
 98 | 
 99 |     "no-catch-shadow": 1,                 // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
100 |     "no-delete-var": 1,                   // disallow deletion of variables
101 |     "no-label-var": 1,                    // disallow labels that share a name with a variable
102 |     "no-shadow": 0,                       // disallow declaration of variables already declared in the outer scope
103 |     "no-shadow-restricted-names": 1,      // disallow shadowing of names such as arguments
104 |     "no-undef": 1,                        // disallow undeclared variables
105 |     "no-undefined": 0,                    // disallow use of undefined variable
106 |     "no-undef-init": 0,                   // disallow use of undefined when initializing variables
107 |     "no-unused-vars": 1,                  // disallow declaration of variables that are not used in the code
108 |     "no-use-before-define": 0,            // disallow use of variables before they are defined
109 | 
110 |     // Node.js
111 | 
112 |     "handle-callback-err": 1,             // enforces error handling in callbacks
113 |     "no-mixed-requires": 1,               // disallow mixing regular variable and require declarations
114 |     "no-new-require": 1,                  // disallow use of new operator with the require function
115 |     "no-path-concat": 1,                  // disallow string concatenation with __dirname and __filename
116 |     "no-process-exit": 0,                 // disallow process.exit()
117 |     "no-restricted-modules": 1,           // restrict usage of specified node modules
118 |     "no-sync": 0,                         // disallow use of synchronous methods
119 | 
120 |     // React (eslint-plugin-react)
121 | 
122 |     "jsx-quotes": [1, "prefer-double"],   // <Foo bar="x" /> not <Foo bar='x' />
123 |     "react/jsx-curly-spacing": [          // Enforce or disallow spaces inside of curly braces in JSX attributes
124 |       1, "never"
125 |     ],
126 |     "react/jsx-no-undef": 1,              // Disallow undeclared variables in JSX
127 |     "react/jsx-uses-react": 1,            // Prevent React to be incorrectly marked as unused
128 |     "react/jsx-uses-vars": 1,             // Prevent variables used in JSX to be incorrectly marked as unused
129 |     "react/no-unknown-property": 1,       // Prevent usage of unknown DOM property
130 |     "react/prop-types": 1,                // Prevent missing props validation in a React component definition
131 |     "react/react-in-jsx-scope": 2,        // Prevent missing React when using JSX
132 | 
133 |     // Stylistic (these rules are purely matters of style and are quite subjective)
134 | 
135 |     "key-spacing": 1,                     // require space after colon `{a: 1}`
136 |     "comma-spacing": 1,                   // require space after comma `var a, b;`
137 |     "no-multi-spaces": 1,                 // don't allow more spaces than necessary
138 |     "brace-style": [                      // enforce one true brace style
139 |       1, "1tbs", {
140 |         "allowSingleLine": false
141 |       }
142 |     ],
143 |     "camelcase": [                        // require camel case names
144 |       1, {"properties": "never"}
145 |     ],
146 |     "consistent-this": [1, "self"],       // enforces consistent naming when capturing the current execution context
147 |     "eol-last": 1,                        // enforce newline at the end of file, with no multiple empty lines
148 |     "func-names": 0,                      // require function expressions to have a name
149 |     "func-style": 0,                      // enforces use of function declarations or expressions
150 |     "new-cap": 0,                         // require a capital letter for constructors
151 |     "new-parens": 1,                      // disallow the omission of parentheses when invoking a constructor with no arguments
152 |     "no-nested-ternary": 0,               // disallow nested ternary expressions
153 |     "no-array-constructor": 1,            // disallow use of the Array constructor
154 |     "no-lonely-if": 0,                    // disallow if as the only statement in an else block
155 |     "no-new-object": 1,                   // disallow use of the Object constructor
156 |     "no-spaced-func": 1,                  // disallow space between function identifier and application
157 |     "semi-spacing": 1,                    // disallow space before semicolon
158 |     "no-ternary": 0,                      // disallow the use of ternary operators
159 |     "no-trailing-spaces": 1,              // disallow trailing whitespace at the end of lines
160 |     "no-underscore-dangle": 0,            // disallow dangling underscores in identifiers
161 |     "no-extra-parens": [1, "functions"],  // disallow wrapping of non-IIFE statements in parens
162 |     "no-mixed-spaces-and-tabs": 1,        // disallow mixed spaces and tabs for indentation
163 |     "indent": [1, 2, {"SwitchCase": 1}],  // indentation should be two spaces
164 |     "quotes": [                           // enforce single quotes, allow double to avoid escaping ("don't escape" instead of 'don\'t escape')
165 |       1, "single", "avoid-escape"
166 |     ],
167 |     "quote-props": [1, "as-needed"],      // require quotes around object literal property names
168 |     "semi": 1,                            // require or disallow use of semicolons instead of ASI
169 |     "sort-vars": 0,                       // sort variables within the same declaration block
170 |     "keyword-spacing": 1,                 // require a space around certain keywords
171 |     "space-before-blocks": 1,             // require a space before blocks
172 |     "space-before-function-paren": [      // disallow a space before function parenthesis
173 |       1, "never"
174 |     ],
175 |     "object-curly-spacing": [             // disallow spaces inside of curly braces in object literals
176 |       1, "never"
177 |     ],
178 |     "array-bracket-spacing": [            // disallow spaces inside of curly braces in array literals
179 |       1, "never"
180 |     ],
181 |     "space-in-parens": 1,                 // require or disallow spaces inside parentheses
182 |     "space-infix-ops": 1,                 // require spaces around operators
183 |     "space-unary-ops": 1,                 // require a space around word operators such as typeof
184 |     "max-nested-callbacks": 0,            // specify the maximum depth callbacks can be nested
185 |     "one-var": [1, "never"],              // allow just one var statement per function
186 |     "wrap-regex": 0,                      // require regex literals to be wrapped in parentheses
187 | 
188 |     // ECMAScript 6/7 (2015 and above)
189 |     "arrow-parens": 1,                    // require parens in arrow function arguments
190 |     "arrow-spacing": 1,                   // require space before/after arrow function's arrow (fixable)
191 |     "constructor-super": 1,               // verify calls of super() in constructors
192 |     "generator-star-spacing": 0,          // enforce spacing around the * in generator functions (fixable)
193 |     "no-class-assign": 1,                 // disallow modifying variables of class declarations
194 |     "no-const-assign": 1,                 // disallow modifying variables that are declared using const
195 |     "no-dupe-class-members": 1,           // disallow duplicate name in class members
196 |     "no-this-before-super": 1,            // disallow use of this/super before calling super() in constructors.
197 |     "no-var": 0,                          // require let or const instead of var
198 |     "object-shorthand": 0,                // require method and property shorthand syntax for object literals
199 |     "prefer-arrow-callback": 1,           // suggest using arrow functions as callbacks
200 |     "prefer-const": 0,                    // suggest using const declaration for variables that are never modified after declared
201 |     "prefer-reflect": 0,                  // suggest using Reflect methods where applicable
202 |     "prefer-spread": 0,                   // suggest using the spread operator instead of .apply().
203 |     "prefer-template": 0,                 // suggest using template literals instead of strings concatenation
204 |     "require-yield": 0,                   // disallow generator functions that do not have yield
205 |     "babel/no-await-in-loop": 1,          // async inside a loop will run operations in serial, when often the desired behavior is to do do in parallel
206 | 
207 |     // Legacy (included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same)
208 | 
209 |     "max-depth": 0,                       // specify the maximum depth that blocks can be nested
210 |     //"max-len": [                        // specify the maximum length of a line in your program [warning level, max line length, number of characters to treat a tab as]
211 |     //  1, 100, 2, {
212 |     //    "ignoreUrls": true,
213 |     //    "ignorePattern": "^\\s*(import\\s[^{]+from|(var|const|let)\\s[^{]+=\\s*require\\s*\\()"
214 |     //  }
215 |     //],
216 |     "max-params": 0,                      // limits the number of parameters that can be used in the function declaration.
217 |     "max-statements": 0,                  // specify the maximum number of statement allowed in a function
218 |     "no-bitwise": 0,                      // disallow use of bitwise operators
219 |     "no-plusplus": 0                      // disallow use of unary operators, ++ and --
220 | 
221 |   }
222 | }
223 | 


--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
 1 | [ignore]
 2 | .*/node_modules/.*/node_modules/fbjs/.*
 3 | 
 4 | [include]
 5 | 
 6 | [libs]
 7 | 
 8 | [options]
 9 | esproposal.class_static_fields=enable
10 | module.system=haste
11 | 
12 | suppress_type=$FlowIssue
13 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
14 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | !__tests__
3 | /assets/dist
4 | /lib
5 | /dist
6 | /node_modules
7 | .idea
8 | 


--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
 1 | _*
 2 | /assets
 3 | /flow-typed
 4 | /src
 5 | /test
 6 | .babelrc
 7 | .eslintignore
 8 | .eslintrc
 9 | .flowconfig
10 | .surgeignore
11 | .travis.yml
12 | CNAME
13 | demo.html
14 | index.html
15 | webpack.config.js
16 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 |   - "6"
4 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | v0.1.17
 2 | 
 3 |   - Remove hard-coded height from RichTextEditor.css
 4 | 
 5 | v0.1.16
 6 | 
 7 |   - Update peerDependencies to allow React 0.14
 8 |   - Pass extra props to <Editor />
 9 | 
10 | v0.1.15
11 | 
12 |   - Update Flow and fix some Flow errors
13 | 
14 | v0.1.14
15 | 
16 |   - Update README about building demo
17 | 
18 | v0.1.13
19 | 
20 |   - Updates to demo including better display on mobile
21 |   - Remove console.error hack
22 |   - Minor style fixes to dropdown component
23 | 
24 | v0.1.12
25 | 
26 |   - Updates to README
27 |   - CSS fix for transparent dropdown options. Fixes #26
28 |   - Fix placeholder text that was hard-coded in component
29 |   - Update contributors
30 | 
31 | v0.1.11
32 | 
33 |   - Add onTab handler for list indent
34 |   - Refactor to use CSS Modules
35 |   - Update build system to work with CSS Modules
36 | 
37 | v0.1.10
38 | 
39 |   - [return handling] In non-list block, pressing return at end should always create a normal block
40 |   - [fix] Allow soft-linebreak when text is selected
41 |   - [fix] [parse-html] Correctly parse entities with inline style
42 | 
43 | v0.1.9
44 | 
45 |   - [demo] Add "Log State" button for debugging
46 |   - [deployment] Add travis-ci for continuous integration
47 |   - [tests] Better tests
48 |   - [docs] Update readme
49 | 
50 | v0.1.8
51 | 
52 |   - Break out import/export logic (and related deps) into separate npm packages
53 |   - Move RichTextEditor.js up one dir
54 |   - [fix] Form submit issue with InputPopover
55 |   - [docs] Update readme
56 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | ISC License
 2 | 
 3 | Copyright (c) 2016, Simon Sturmer <sstur@me.com>
 4 | 
 5 | Permission to use, copy, modify, and/or distribute this software for any
 6 | purpose with or without fee is hereby granted, provided that the above
 7 | copyright notice and this permission notice appear in all copies.
 8 | 
 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Deprecated
  2 | 
  3 | This repository is long ourdated and even [Draft.js](https://github.com/facebookarchive/draft-js) (on which this editor is built) is long outdated and has been superceded by [Lexical](https://github.com/facebook/lexical).
  4 | 
  5 | I don't recommend you use this in your project. There are many great alternatives such as [TipTap](https://tiptap.dev/).
  6 | 
  7 | # React Rich Text Editor
  8 | 
  9 | This is a UI component built completely in React that is meant to be a full-featured textarea replacement similar to [CKEditor][ckeditor], [TinyMCE][tinymce] and other [rich text "WYSIWYG" editors][rte]. It's based on the excellent, open source [Draft.js][draft-js] from Facebook which is performant and production-tested.
 10 | 
 11 | ## Demo
 12 | 
 13 | Try the editor here: [react-rte.org/demo][react-rte-demo]
 14 | 
 15 | [![Screenshot 1](https://ucassets.blob.core.windows.net/uploads/rte.png)][react-rte-demo]
 16 | 
 17 | 
 18 | ## Getting Started
 19 | 
 20 |     $ npm install --save react-rte
 21 | 
 22 | `RichTextEditor` is the main editor component. It is comprised of the Draft.js `<Editor>`, some UI components (e.g. toolbar) and some helpful abstractions around getting and setting content with HTML/Markdown.
 23 | 
 24 | `RichTextEditor` is designed to be used like a `textarea` except that instead of `value` being a string, it is an object with `toString` on it. Creating a `value` from a string is also easy using `createValueFromString(markup, 'html')`.
 25 | 
 26 | ### Browser Compatibility
 27 | 
 28 | The scripts are transpiled by Babel to ES6. Additionally, at least one of this package's dependencies does not support IE. So, for IE and back-plat support you will need to include some polyfill in your HTML (#74, #196, #203): `<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=String.prototype.startsWith,Array.from,Array.prototype.fill,Array.prototype.keys,Array.prototype.findIndex,Number.isInteger&flags=gated"></script>`
 29 | 
 30 | ### Required Webpack configuration
 31 | 
 32 | If you are not using Webpack, you can skip this section. Webpack is required for isomorphic/server-side rendering support in a Node.js environment.
 33 | 
 34 | `'react-rte'` contains a bundle that is already built (with CSS) using webpack and is not intended to be consumed again by webpack. So, if you are using webpack you must import RichTextEditor from `react-rte/lib/RichTextEditor` in order to get the un-bundled script which webpack can bundle with your app.
 35 | 
 36 | If you are using webpack you must add a css loader or else your webpack build will fail. For example:
 37 | 
 38 | ```js
 39 |   {
 40 |     test: /\.css$/,
 41 |     loaders: [
 42 |       'style-loader',
 43 |       'css-loader?modules'
 44 |     ]
 45 |   },
 46 | ```
 47 | 
 48 | ### Example Usage:
 49 | 
 50 | This example uses newer JavaScript and JSX. For an example in old JavaScript, [see below](#example-with-es5-and-no-jsx).
 51 | 
 52 | ```javascript
 53 | import React, {Component, PropTypes} from 'react';
 54 | import RichTextEditor from 'react-rte';
 55 | 
 56 | class MyStatefulEditor extends Component {
 57 |   static propTypes = {
 58 |     onChange: PropTypes.func
 59 |   };
 60 | 
 61 |   state = {
 62 |     value: RichTextEditor.createEmptyValue()
 63 |   }
 64 | 
 65 |   onChange = (value) => {
 66 |     this.setState({value});
 67 |     if (this.props.onChange) {
 68 |       // Send the changes up to the parent component as an HTML string.
 69 |       // This is here to demonstrate using `.toString()` but in a real app it
 70 |       // would be better to avoid generating a string on each change.
 71 |       this.props.onChange(
 72 |         value.toString('html')
 73 |       );
 74 |     }
 75 |   };
 76 | 
 77 |   render () {
 78 |     return (
 79 |       <RichTextEditor
 80 |         value={this.state.value}
 81 |         onChange={this.onChange}
 82 |       />
 83 |     );
 84 |   }
 85 | }
 86 | ```
 87 | 
 88 | ### Toolbar Customization
 89 | 
 90 | ```javascript
 91 | 
 92 | render() {
 93 |   // The toolbarConfig object allows you to specify custom buttons, reorder buttons and to add custom css classes.
 94 |   // Supported inline styles: https://github.com/facebook/draft-js/blob/master/docs/Advanced-Topics-Inline-Styles.md
 95 |   // Supported block types: https://github.com/facebook/draft-js/blob/master/docs/Advanced-Topics-Custom-Block-Render.md#draft-default-block-render-map
 96 |   const toolbarConfig = {
 97 |     // Optionally specify the groups to display (displayed in the order listed).
 98 |     display: ['INLINE_STYLE_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'LINK_BUTTONS', 'BLOCK_TYPE_DROPDOWN', 'HISTORY_BUTTONS'],
 99 |     INLINE_STYLE_BUTTONS: [
100 |       {label: 'Bold', style: 'BOLD', className: 'custom-css-class'},
101 |       {label: 'Italic', style: 'ITALIC'},
102 |       {label: 'Underline', style: 'UNDERLINE'}
103 |     ],
104 |     BLOCK_TYPE_DROPDOWN: [
105 |       {label: 'Normal', style: 'unstyled'},
106 |       {label: 'Heading Large', style: 'header-one'},
107 |       {label: 'Heading Medium', style: 'header-two'},
108 |       {label: 'Heading Small', style: 'header-three'}
109 |     ],
110 |     BLOCK_TYPE_BUTTONS: [
111 |       {label: 'UL', style: 'unordered-list-item'},
112 |       {label: 'OL', style: 'ordered-list-item'}
113 |     ]
114 |   };
115 |   return (
116 |     <RichTextEditor toolbarConfig={toolbarConfig} />
117 |   );
118 | }
119 | 
120 | ```
121 | 
122 | ## Motivation
123 | 
124 | In short, this is a modern approach to rich text editing built on a battle-hardened open-source framework and, importantly, we do not store document state in the DOM, eliminating entire classes of common "WYSIWYG" problems.
125 | 
126 | This editor is built on [Draft.js][draft-js] from Facebook. Draft.js is more of a low-level framework (`contentEditable` abstraction), however this component is intended to be a fully polished UI component that you can reach for when you need to replace a `<textarea/>` in your application to support bold, italic, links, lists, etc.
127 | 
128 | The data model in Draft.js allows us to represent the document in a way that is mostly agnostic to the view/render layer or the textual representation (html/markdown) you choose. This data model encapsulates the content/state of the editor and is based on [Immutable.js][immutablejs] to be both performant and easy to reason about.
129 | 
130 | ## Features
131 | 
132 |  * Pure React and fully declarative
133 |  * Supported formats: HTML and Markdown (coming soon: extensible support for custom formats)
134 |  * Document Model represents your document in a sane way that will [deterministically convert to clean markup](#deterministic-output) regardless of your format choice
135 |  * Takes full advantage of Immutable.js and the excellent performance characteristics that come with it.
136 |  * Reliable undo/redo without a large memory footprint
137 |  * Modern browser support
138 | 
139 | ## Deterministic Output
140 | 
141 | Unlike typical rich text editors (such as [CKEditor][ckeditor] and [TinyMCE][tinymce]) we keep our content state in a well-architected data model instead of in the view. One important advantage of separating our data model from our view is deterministic output.
142 | 
143 | Say, for instance, you select some text and add bold style. Then you add italic style. Or what if you add italic first and then bold. The result should be the same either way: the text range has both bold and italic style. But in the browser's view (Document Object Model) is this represented with a `<strong>` inside of an `<em>` or vice versa? Does it depend on the order in which you added the styles? In many web-based editors the HTML output *does* depend on the order of your actions. That means your output is non-deterministic. Two documents that look exactly the same in the editor will have different, sometimes unpredictable, HTML representations.
144 | 
145 | In this editor we use a pure, deterministic function to convert document state to HTML output. No matter how you *arrived at* the state, the output will be predictable. This makes everything easier to reason about. In our case, the `<strong>` will go inside the `<em>` every time.
146 | 
147 | ## API
148 | 
149 | ### Required Props
150 |   * `value`: Used to represent the content/state of the editor. Initially you will probably want to create an instance using a provided helper such as `RichTextEditor.createEmptyValue()` or `RichTextEditor.createValueFromString(markup, 'html')`.
151 |   * `onChange`: A function that will be called with the "value" of the editor whenever it is changed. The value has a `toString` method which accepts a single `format` argument (either 'html' or 'markdown').
152 | 
153 | ### Other Props
154 |   All the props you can pass to Draft.js `Editor` can be passed to `RichTextEditor` (with the exception of `editorState` which will be generated internally based on the `value` prop).
155 | 
156 |   * `autoFocus`: Setting this to true will automatically focus input into the editor when the component is mounted
157 |   * `placeholder`: A string to use as placeholder text for the `RichTextEditor`.
158 |   * `readOnly`: A boolean that determines if the `RichTextEditor` should render static html.
159 |   * `enableSoftNewLineByDefault`: Set this to `true` if you wish to use soft line breaks when only pressing the return key. By default, if you press the return/enter key it will create a new text block. If you don't set this value to `true`, the user may use one of several [designated keys](https://github.com/facebook/draft-js/blob/cf9be24e8b14419143f1f01aabc68ed1be2f95e4/src/component/utils/isSoftNewlineEvent.js#L16) while pressing the return key to create a soft new line.
160 | 
161 | ### EditorValue Class
162 | In Draft.js `EditorState` contains not only the document contents but the entire state of the editor including cursor position and selection. This is helpful for many reasons including undo/redo. To make things easier for you, we have wrapped the state of the editor in an `EditorValue` instance with helpful methods to convert to/from a HTML or Markdown. An instance of this class should be passed to `RichTextEditor` in the `value` prop.
163 | 
164 | The `EditorValue` class has certain optimizations built in. So let's say you are showing the HTML of the editor contents in your view. If you change your cursor position, that will trigger an `onChange` event (because, remember, cursor position is part of `EditorState`) and you will need to call `toString()` to render your view. However, `EditorValue` is smart enough to know that the *content* didn't actually change since last `toString()` so it will return a cached version of the HTML.
165 | 
166 | Optimization tip: Try to call `editorValue.toString()` only when you actually need to convert it to a string. If you can keep passing around the `editorValue` without calling `toString` it will be very performant.
167 | 
168 | ### Example with ES5 and no JSX
169 | ```javascript
170 | var React = require('react');
171 | var RichTextEditor = require('react-rte');
172 | 
173 | React.createClass({
174 |   propTypes: {
175 |     onChange: React.PropTypes.func
176 |   },
177 | 
178 |   getInitialState: function() {
179 |     return {
180 |       value: RichTextEditor.createEmptyValue()
181 |     };
182 |   },
183 | 
184 |   render: function() {
185 |     return React.createElement(RichTextEditor, {
186 |       value: this.state.value,
187 |       onChange: this.onChange
188 |     });
189 |   },
190 | 
191 |   onChange: function(value) {
192 |     this.setState({value: value});
193 |     if (this.props.onChange) {
194 |       // Send the changes up to the parent component as an HTML string.
195 |       // This is here to demonstrate using `.toString()` but in a real app it
196 |       // would be better to avoid generating a string on each change.
197 |       this.props.onChange(
198 |         value.toString('html')
199 |       );
200 |     }
201 |   }
202 | 
203 | });
204 | ```
205 | 
206 | ## TODO
207 | 
208 |  - Support images
209 |  - Better test coverage
210 |  - Documentation for using this editor in your projects
211 |  - Fix some issues with Markdown parsing (migrate to `remark` parser)
212 |  - Internationalization
213 |  - Better icons and overall design
214 | 
215 | ## Known Limitations
216 | 
217 | Currently the biggest limitation is that images are not supported. There is a plan to support inline images (using decorators) and eventually Medium-style block-level images (using a custom block renderer).
218 | 
219 | Other limitations include missing features such as: text-alignment and text color. These are coming soon.
220 | 
221 | React prior v15 will log the following superfluous warning:
222 | 
223 | > A component is contentEditable and contains children managed by
224 | > React. It is now your responsibility to guarantee that none of
225 | > those nodes are unexpectedly modified or duplicated. This is
226 | > probably not intentional.
227 | 
228 | As all nodes are managed internally by Draft, this is not a problem and this [warning can be safely ignored](https://github.com/facebook/draft-js/issues/53). You can suppress this warning's display completely by duck-punching `console.error` before instantiating your component:
229 | 
230 | ```javascript
231 | console.error = (function(_error) {
232 |   return function(message) {
233 |     if (typeof message !== 'string' || message.indexOf('component is `contentEditable`') === -1) {
234 |       _error.apply(console, arguments);
235 |     }
236 |   };
237 | })(console.error);
238 | ```
239 | 
240 | ## Contribute
241 | 
242 | I'm happy to take pull requests for bug-fixes and improvements (and tests). If you have a feature you want to implement it's probably a good idea to open an issue first to see if it's already being worked on. Please match the code style of the rest of the project (ESLint should enforce this) and please include tests. Thanks!
243 | 
244 | ## Run the Demo
245 | Clone this project. Run `npm install`. Run `npm run build-dist` then point the server of your choice (like [serv][serv]) to `/demo.html`.
246 | 
247 | ## License
248 | 
249 | This software is [ISC Licensed](/LICENSE).
250 | 
251 | 
252 | [ckeditor]: http://ckeditor.com/
253 | [draft-js]: https://facebook.github.io/draft-js/
254 | [immutablejs]: https://facebook.github.io/immutable-js/
255 | [react-rte-demo]: https://react-rte.org/demo
256 | [rte]: https://www.google.com/search?q=web+based+rich+text+editor
257 | [serv]: https://www.npmjs.com/package/serv
258 | [tinymce]: https://www.tinymce.com/
259 | 


--------------------------------------------------------------------------------
/assets/CNAME:
--------------------------------------------------------------------------------
1 | react-rte.org
2 | 


--------------------------------------------------------------------------------
/assets/css/demo.css:
--------------------------------------------------------------------------------
 1 | @import "/css/normalize.css";
 2 | 
 3 | html {
 4 |   height: 100%;
 5 | }
 6 | 
 7 | body {
 8 |   position: relative;
 9 |   box-sizing: border-box;
10 |   min-height: 100%;
11 | }
12 | 
13 | body::before, body::after {
14 |   content: "";
15 |   display: table;
16 | }
17 | 
18 | a {
19 |   color: #00e;
20 | }
21 | 
22 | .btn-row {
23 |   font-size: 14px;
24 |   text-align: right;
25 | }
26 | 
27 | .label {
28 |   display: inline-block;
29 |   line-height: 18px;
30 |   height: 30px;
31 |   padding: 5px 15px;
32 |   margin: 0 5px 0 0;
33 |   box-sizing: border-box;
34 | }
35 | 
36 | .btn {
37 |   display: inline-block;
38 |   line-height: 18px;
39 |   height: 30px;
40 |   padding: 5px 15px;
41 |   margin: 0 5px 0 0;
42 |   cursor: pointer;
43 |   box-sizing: border-box;
44 |   border-radius: 3px;
45 |   background: linear-gradient(to bottom, #329ced 0, #168eea 100%);
46 |   border: 1px solid #137dcf;
47 |   color: white;
48 |   text-shadow: 0 0 1px rgba(0, 0, 0, 0.3);
49 |   box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
50 |   font-family: inherit;
51 |   font-size: inherit;
52 |   font-weight: inherit;
53 |   text-decoration: none;
54 | }
55 | 
56 | .btn:last-child {
57 |   margin: 0;
58 | }
59 | 
60 | .editor-demo {
61 |   width: 560px;
62 |   margin: 10px auto;
63 | }
64 | 
65 | .editor-demo .row {
66 |   margin: 10px 0;
67 | }
68 | 
69 | .editor-demo .radio-item {
70 |   display: inline-block;
71 |   margin-right: 10px;
72 | }
73 | 
74 | .editor-demo .radio-item input {
75 |   margin-right: 5px;
76 | }
77 | 
78 | .editor-demo .source {
79 |   display: block;
80 |   margin: 0;
81 |   padding: 10px;
82 |   border: 1px solid #ddd;
83 |   border-radius: 3px;
84 |   width: 100%;
85 |   height: 130px;
86 |   box-sizing: border-box;
87 |   font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
88 |   font-size: 13px;
89 |   line-height: 18px;
90 |   resize: none;
91 |   white-space: pre;
92 |   word-wrap: normal;
93 | }
94 | 
95 | .editor-demo .react-rte-demo .public-DraftEditor-content {
96 |   min-height: 110px;
97 | }
98 | 


--------------------------------------------------------------------------------
/assets/css/normalize.css:
--------------------------------------------------------------------------------
  1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
  2 | 
  3 | /**
  4 |  * 1. Set default font family to sans-serif.
  5 |  * 2. Prevent iOS and IE text size adjust after device orientation change,
  6 |  *    without disabling user zoom.
  7 |  */
  8 | 
  9 | html {
 10 |   font-family: sans-serif; /* 1 */
 11 |   -ms-text-size-adjust: 100%; /* 2 */
 12 |   -webkit-text-size-adjust: 100%; /* 2 */
 13 | }
 14 | 
 15 | /**
 16 |  * Remove default margin.
 17 |  */
 18 | 
 19 | body {
 20 |   margin: 0;
 21 | }
 22 | 
 23 | /* HTML5 display definitions
 24 |    ========================================================================== */
 25 | 
 26 | /**
 27 |  * Correct `block` display not defined for any HTML5 element in IE 8/9.
 28 |  * Correct `block` display not defined for `details` or `summary` in IE 10/11
 29 |  * and Firefox.
 30 |  * Correct `block` display not defined for `main` in IE 11.
 31 |  */
 32 | 
 33 | article,
 34 | aside,
 35 | details,
 36 | figcaption,
 37 | figure,
 38 | footer,
 39 | header,
 40 | hgroup,
 41 | main,
 42 | menu,
 43 | nav,
 44 | section,
 45 | summary {
 46 |   display: block;
 47 | }
 48 | 
 49 | /**
 50 |  * 1. Correct `inline-block` display not defined in IE 8/9.
 51 |  * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
 52 |  */
 53 | 
 54 | audio,
 55 | canvas,
 56 | progress,
 57 | video {
 58 |   display: inline-block; /* 1 */
 59 |   vertical-align: baseline; /* 2 */
 60 | }
 61 | 
 62 | /**
 63 |  * Prevent modern browsers from displaying `audio` without controls.
 64 |  * Remove excess height in iOS 5 devices.
 65 |  */
 66 | 
 67 | audio:not([controls]) {
 68 |   display: none;
 69 |   height: 0;
 70 | }
 71 | 
 72 | /**
 73 |  * Address `[hidden]` styling not present in IE 8/9/10.
 74 |  * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
 75 |  */
 76 | 
 77 | [hidden],
 78 | template {
 79 |   display: none;
 80 | }
 81 | 
 82 | /* Links
 83 |    ========================================================================== */
 84 | 
 85 | /**
 86 |  * Remove the gray background color from active links in IE 10.
 87 |  */
 88 | 
 89 | a {
 90 |   background-color: transparent;
 91 | }
 92 | 
 93 | /**
 94 |  * Improve readability of focused elements when they are also in an
 95 |  * active/hover state.
 96 |  */
 97 | 
 98 | a:active,
 99 | a:hover {
100 |   outline: 0;
101 | }
102 | 
103 | /* Text-level semantics
104 |    ========================================================================== */
105 | 
106 | /**
107 |  * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
108 |  */
109 | 
110 | abbr[title] {
111 |   border-bottom: 1px dotted;
112 | }
113 | 
114 | /**
115 |  * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
116 |  */
117 | 
118 | b,
119 | strong {
120 |   font-weight: bold;
121 | }
122 | 
123 | /**
124 |  * Address styling not present in Safari and Chrome.
125 |  */
126 | 
127 | dfn {
128 |   font-style: italic;
129 | }
130 | 
131 | /**
132 |  * Address variable `h1` font-size and margin within `section` and `article`
133 |  * contexts in Firefox 4+, Safari, and Chrome.
134 |  */
135 | 
136 | h1 {
137 |   font-size: 2em;
138 |   margin: 0.67em 0;
139 | }
140 | 
141 | /**
142 |  * Address styling not present in IE 8/9.
143 |  */
144 | 
145 | mark {
146 |   background: #ff0;
147 |   color: #000;
148 | }
149 | 
150 | /**
151 |  * Address inconsistent and variable font size in all browsers.
152 |  */
153 | 
154 | small {
155 |   font-size: 80%;
156 | }
157 | 
158 | /**
159 |  * Prevent `sub` and `sup` affecting `line-height` in all browsers.
160 |  */
161 | 
162 | sub,
163 | sup {
164 |   font-size: 75%;
165 |   line-height: 0;
166 |   position: relative;
167 |   vertical-align: baseline;
168 | }
169 | 
170 | sup {
171 |   top: -0.5em;
172 | }
173 | 
174 | sub {
175 |   bottom: -0.25em;
176 | }
177 | 
178 | /* Embedded content
179 |    ========================================================================== */
180 | 
181 | /**
182 |  * Remove border when inside `a` element in IE 8/9/10.
183 |  */
184 | 
185 | img {
186 |   border: 0;
187 | }
188 | 
189 | /**
190 |  * Correct overflow not hidden in IE 9/10/11.
191 |  */
192 | 
193 | svg:not(:root) {
194 |   overflow: hidden;
195 | }
196 | 
197 | /* Grouping content
198 |    ========================================================================== */
199 | 
200 | /**
201 |  * Address margin not present in IE 8/9 and Safari.
202 |  */
203 | 
204 | figure {
205 |   margin: 1em 40px;
206 | }
207 | 
208 | /**
209 |  * Address differences between Firefox and other browsers.
210 |  */
211 | 
212 | hr {
213 |   box-sizing: content-box;
214 |   height: 0;
215 | }
216 | 
217 | /**
218 |  * Contain overflow in all browsers.
219 |  */
220 | 
221 | pre {
222 |   overflow: auto;
223 | }
224 | 
225 | /**
226 |  * Address odd `em`-unit font size rendering in all browsers.
227 |  */
228 | 
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 |   font-family: monospace, monospace;
234 |   font-size: 1em;
235 | }
236 | 
237 | /* Forms
238 |    ========================================================================== */
239 | 
240 | /**
241 |  * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 |  * styling of `select`, unless a `border` property is set.
243 |  */
244 | 
245 | /**
246 |  * 1. Correct color not being inherited.
247 |  *    Known issue: affects color of disabled elements.
248 |  * 2. Correct font properties not being inherited.
249 |  * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 |  */
251 | 
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 |   color: inherit; /* 1 */
258 |   font: inherit; /* 2 */
259 |   margin: 0; /* 3 */
260 | }
261 | 
262 | /**
263 |  * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 |  */
265 | 
266 | button {
267 |   overflow: visible;
268 | }
269 | 
270 | /**
271 |  * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 |  * All other form control elements do not inherit `text-transform` values.
273 |  * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 |  * Correct `select` style inheritance in Firefox.
275 |  */
276 | 
277 | button,
278 | select {
279 |   text-transform: none;
280 | }
281 | 
282 | /**
283 |  * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 |  *    and `video` controls.
285 |  * 2. Correct inability to style clickable `input` types in iOS.
286 |  * 3. Improve usability and consistency of cursor style between image-type
287 |  *    `input` and others.
288 |  */
289 | 
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 |   -webkit-appearance: button; /* 2 */
295 |   cursor: pointer; /* 3 */
296 | }
297 | 
298 | /**
299 |  * Re-set default cursor for disabled elements.
300 |  */
301 | 
302 | button[disabled],
303 | html input[disabled] {
304 |   cursor: default;
305 | }
306 | 
307 | /**
308 |  * Remove inner padding and border in Firefox 4+.
309 |  */
310 | 
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 |   border: 0;
314 |   padding: 0;
315 | }
316 | 
317 | /**
318 |  * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 |  * the UA stylesheet.
320 |  */
321 | 
322 | input {
323 |   line-height: normal;
324 | }
325 | 
326 | /**
327 |  * It's recommended that you don't attempt to style these elements.
328 |  * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 |  *
330 |  * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 |  * 2. Remove excess padding in IE 8/9/10.
332 |  */
333 | 
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 |   box-sizing: border-box; /* 1 */
337 |   padding: 0; /* 2 */
338 | }
339 | 
340 | /**
341 |  * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 |  * `font-size` values of the `input`, it causes the cursor style of the
343 |  * decrement button to change from `default` to `text`.
344 |  */
345 | 
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 |   height: auto;
349 | }
350 | 
351 | /**
352 |  * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 |  * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
354 |  */
355 | 
356 | input[type="search"] {
357 |   -webkit-appearance: textfield; /* 1 */
358 |   box-sizing: content-box; /* 2 */
359 | }
360 | 
361 | /**
362 |  * Remove inner padding and search cancel button in Safari and Chrome on OS X.
363 |  * Safari (but not Chrome) clips the cancel button when the search input has
364 |  * padding (and `textfield` appearance).
365 |  */
366 | 
367 | input[type="search"]::-webkit-search-cancel-button,
368 | input[type="search"]::-webkit-search-decoration {
369 |   -webkit-appearance: none;
370 | }
371 | 
372 | /**
373 |  * Define consistent border, margin, and padding.
374 |  */
375 | 
376 | fieldset {
377 |   border: 1px solid #c0c0c0;
378 |   margin: 0 2px;
379 |   padding: 0.35em 0.625em 0.75em;
380 | }
381 | 
382 | /**
383 |  * 1. Correct `color` not being inherited in IE 8/9/10/11.
384 |  * 2. Remove padding so people aren't caught out if they zero out fieldsets.
385 |  */
386 | 
387 | legend {
388 |   border: 0; /* 1 */
389 |   padding: 0; /* 2 */
390 | }
391 | 
392 | /**
393 |  * Remove default vertical scrollbar in IE 8/9/10/11.
394 |  */
395 | 
396 | textarea {
397 |   overflow: auto;
398 | }
399 | 
400 | /**
401 |  * Don't inherit the `font-weight` (applied by a rule above).
402 |  * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
403 |  */
404 | 
405 | optgroup {
406 |   font-weight: bold;
407 | }
408 | 
409 | /* Tables
410 |    ========================================================================== */
411 | 
412 | /**
413 |  * Remove most spacing between table cells.
414 |  */
415 | 
416 | table {
417 |   border-collapse: collapse;
418 |   border-spacing: 0;
419 | }
420 | 
421 | td,
422 | th {
423 |   padding: 0;
424 | }
425 | 


--------------------------------------------------------------------------------
/assets/demo.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html>
 3 |   <head>
 4 |     <title>React Rich Text Editor Example</title>
 5 |     <meta charset="utf-8" />
 6 |     <meta name="viewport" content="width=600, initial-scale=1" />
 7 |     <link rel="icon" type="image/x-icon" href="/react.ico" />
 8 |     <link rel="stylesheet" href="/css/demo.css" />
 9 |     <script src="/dist/demo.js"></script>
10 |   </head>
11 |   <body></body>
12 | </html>
13 | 


--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html>
 3 |   <head>
 4 |     <title>Redirecting...</title>
 5 |     <meta http-equiv="Cache-Control" content="private, no-cache, must-revalidate, max-age=0" />
 6 |     <script>
 7 |       function onload() {
 8 |         var hostname = document.location.hostname;
 9 |         var destination = (hostname === 'react-rte.org') ? '/demo' : '/demo.html';
10 |         location.replace(destination);
11 |       }
12 |     </script>
13 |   </head>
14 |   <body onload="onload()">
15 |     <noscript><p>Redirecting...</p></noscript>
16 |   </body>
17 | </html>
18 | 


--------------------------------------------------------------------------------
/assets/react.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sstur/react-rte/8e00bf68d7a9b84e2ee2f4ed6a17f6de107fb108/assets/react.ico


--------------------------------------------------------------------------------
/flow-typed/classnames.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | 
3 | declare module 'classnames' {
4 |   declare function exports(...args: Array<any>): string;
5 | }
6 | 


--------------------------------------------------------------------------------
/flow-typed/globals.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint-disable no-unused-vars, no-undef */
3 | declare var __DEV__: bool;
4 | 
5 | declare type ReactElement = React$Element<*>;
6 | declare type ReactNode = null | string | number | ReactElement | Array<string | number | ReactElement>;
7 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "react-rte",
  3 |   "version": "0.16.5",
  4 |   "description": "React Rich Text Editor",
  5 |   "main": "dist/react-rte.js",
  6 |   "files": [
  7 |     "lib",
  8 |     "dist"
  9 |   ],
 10 |   "scripts": {
 11 |     "build": "npm run build-lib && npm run build-dist",
 12 |     "build-dist": "rimraf dist && webpack -p",
 13 |     "build-lib": "rimraf lib && babel src --ignore \"_*\" --out-dir lib --copy-files",
 14 |     "lint": "eslint --max-warnings 0 .",
 15 |     "typecheck": "flow",
 16 |     "prepare": "npm run build",
 17 |     "start": "webpack-dev-server --content-base assets/",
 18 |     "test": "npm run lint && npm run typecheck && npm run test-src",
 19 |     "test-src": "BABEL_ENV=test mocha \"src/**/__tests__/*.js\""
 20 |   },
 21 |   "dependencies": {
 22 |     "babel-runtime": "^6.23.0",
 23 |     "class-autobind": "^0.1.4",
 24 |     "classnames": "^2.2.5",
 25 |     "draft-js": ">=0.10.0",
 26 |     "draft-js-export-html": ">=0.6.0",
 27 |     "draft-js-export-markdown": ">=0.3.0",
 28 |     "draft-js-import-html": ">=0.4.0",
 29 |     "draft-js-import-markdown": ">=0.3.0",
 30 |     "draft-js-utils": ">=1.4.0",
 31 |     "draft-js-import-element": ">=1.4.0",
 32 |     "immutable": "^3.8.1"
 33 |   },
 34 |   "peerDependencies": {
 35 |     "react": "0.14.x || 15.x.x || 16.x.x || 17.x.x",
 36 |     "react-dom": "0.14.x || 15.x.x || 16.x.x || 17.x.x"
 37 |   },
 38 |   "devDependencies": {
 39 |     "babel-cli": "^6.18.0",
 40 |     "babel-core": "^6.18.2",
 41 |     "babel-eslint": "^7.1.0",
 42 |     "babel-loader": "^7.1.1",
 43 |     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
 44 |     "babel-plugin-transform-runtime": "^6.23.0",
 45 |     "babel-preset-es2015": "^6.18.0",
 46 |     "babel-preset-react": "^6.11.1",
 47 |     "babel-preset-stage-2": "^6.18.0",
 48 |     "css-loader": "^0.28.4",
 49 |     "css-modules-require-hook": "^4.0.0",
 50 |     "eslint": "^3.9.1",
 51 |     "eslint-plugin-babel": "^3.3.0",
 52 |     "eslint-plugin-flow-vars": "^0.5.0",
 53 |     "eslint-plugin-react": "^6.5.0",
 54 |     "expect": "^1.20.2",
 55 |     "flow-bin": "^0.32.0",
 56 |     "mocha": "^3.1.2",
 57 |     "raw-loader": "^0.5.1",
 58 |     "react": "16.x.x",
 59 |     "react-dom": "16.x.x",
 60 |     "react-test-renderer": "^16.4.0",
 61 |     "rimraf": "^2.5.4",
 62 |     "style-loader": "^0.18.2",
 63 |     "webpack": "^3.4.0",
 64 |     "webpack-dev-server": "^2.6.1"
 65 |   },
 66 |   "repository": {
 67 |     "type": "git",
 68 |     "url": "git+https://github.com/sstur/react-rte.git"
 69 |   },
 70 |   "keywords": [
 71 |     "reactjs",
 72 |     "draftjs",
 73 |     "contenteditable",
 74 |     "wysiwyg",
 75 |     "richtext",
 76 |     "editor"
 77 |   ],
 78 |   "author": "sstur@me.com",
 79 |   "contributors": [
 80 |     {
 81 |       "name": "Adam J. McIntyre",
 82 |       "url": "https://github.com/amcintyre-cs"
 83 |     },
 84 |     {
 85 |       "name": "André Schmidt",
 86 |       "url": "https://github.com/andschdk"
 87 |     },
 88 |     {
 89 |       "name": "Ben Herila",
 90 |       "url": "https://github.com/bherila"
 91 |     },
 92 |     {
 93 |       "name": "Harris Brakmic",
 94 |       "url": "https://github.com/brakmic"
 95 |     },
 96 |     {
 97 |       "name": "Forbes Lindesay",
 98 |       "url": "https://github.com/ForbesLindesay"
 99 |     },
100 |     {
101 |       "name": "Kristopher Craw",
102 |       "url": "https://github.com/KCraw"
103 |     },
104 |     {
105 |       "name": "Rory Hunter",
106 |       "url": "https://github.com/pugnascotia"
107 |     },
108 |     {
109 |       "name": "Ralph Schindler",
110 |       "url": "https://github.com/ralphschindler"
111 |     },
112 |     {
113 |       "name": "RaoHai",
114 |       "url": "https://github.com/RaoHai"
115 |     },
116 |     {
117 |       "name": "Jordan Kohl",
118 |       "url": "https://github.com/simpixelated"
119 |     },
120 |     {
121 |       "name": "Steffen Kolmer",
122 |       "url": "https://github.com/skolmer"
123 |     },
124 |     {
125 |       "name": "Simon Sturmer",
126 |       "url": "https://github.com/sstur"
127 |     },
128 |     {
129 |       "name": "Waldir Pimenta",
130 |       "url": "https://github.com/waldyrious"
131 |     },
132 |     {
133 |       "name": "Zach Silveira",
134 |       "url": "https://github.com/zackify"
135 |     },
136 |     {
137 |       "name": "Hyunyoung Cho",
138 |       "url": "https://github.com/ZeroCho"
139 |     }
140 |   ],
141 |   "license": "ISC",
142 |   "bugs": {
143 |     "url": "https://github.com/sstur/react-rte/issues"
144 |   },
145 |   "homepage": "https://github.com/sstur/react-rte#readme"
146 | }
147 | 


--------------------------------------------------------------------------------
/src/Draft.global.css:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * We inherit the height of the container by default
  3 |  */
  4 | 
  5 | .DraftEditor-root,
  6 | .DraftEditor-editorContainer,
  7 | .public-DraftEditor-content {
  8 |   height: inherit;
  9 |   text-align: initial;
 10 | }
 11 | 
 12 | .DraftEditor-root {
 13 |   position: relative;
 14 | }
 15 | 
 16 | /**
 17 |  * Zero-opacity background used to allow focus in IE. Otherwise, clicks
 18 |  * fall through to the placeholder.
 19 |  */
 20 | 
 21 | .DraftEditor-editorContainer {
 22 |   background-color: rgba(255, 255, 255, 0);
 23 |   /* Repair mysterious missing Safari cursor */
 24 |   border: 1px solid transparent;
 25 |   position: relative;
 26 |   z-index: 1;
 27 | }
 28 | 
 29 | .public-DraftEditor-content {
 30 |   outline: none;
 31 |   white-space: pre-wrap;
 32 | }
 33 | 
 34 | .public-DraftEditor-block {
 35 |   position: relative;
 36 | }
 37 | 
 38 | .DraftEditor-alignLeft .public-DraftEditor-block {
 39 |   text-align: left;
 40 | }
 41 | 
 42 | .DraftEditor-alignLeft .public-DraftEditorPlaceholder-root {
 43 |   left: 0;
 44 |   text-align: left;
 45 | }
 46 | 
 47 | .DraftEditor-alignCenter .public-DraftEditor-block {
 48 |   text-align: center;
 49 | }
 50 | 
 51 | .DraftEditor-alignCenter .public-DraftEditorPlaceholder-root {
 52 |   margin: 0 auto;
 53 |   text-align: center;
 54 |   width: 100%;
 55 | }
 56 | 
 57 | .DraftEditor-alignRight .public-DraftEditor-block {
 58 |   text-align: right;
 59 | }
 60 | 
 61 | .DraftEditor-alignRight .public-DraftEditorPlaceholder-root {
 62 |   right: 0;
 63 |   text-align: right;
 64 | }
 65 | /**
 66 |  * @providesModule DraftEditorPlaceholder
 67 |  */
 68 | 
 69 | .public-DraftEditorPlaceholder-root {
 70 |   color: #9197a3;
 71 |   position: absolute;
 72 |   z-index: 0;
 73 | }
 74 | 
 75 | .public-DraftEditorPlaceholder-hasFocus {
 76 |   color: #bdc1c9;
 77 | }
 78 | 
 79 | .DraftEditorPlaceholder-hidden {
 80 |   display: none;
 81 | }
 82 | /**
 83 |  * @providesModule DraftStyleDefault
 84 |  */
 85 | 
 86 | .public-DraftStyleDefault-block {
 87 |   position: relative;
 88 |   white-space: pre-wrap;
 89 | }
 90 | 
 91 | /* @noflip */
 92 | 
 93 | .public-DraftStyleDefault-ltr {
 94 |   direction: ltr;
 95 |   text-align: left;
 96 | }
 97 | 
 98 | /* @noflip */
 99 | 
100 | .public-DraftStyleDefault-rtl {
101 |   direction: rtl;
102 |   text-align: right;
103 | }
104 | 
105 | /**
106 |  * These rules provide appropriate text direction for counter pseudo-elements.
107 |  */
108 | 
109 | /* @noflip */
110 | 
111 | .public-DraftStyleDefault-listLTR {
112 |   direction: ltr;
113 | }
114 | 
115 | /* @noflip */
116 | 
117 | .public-DraftStyleDefault-listRTL {
118 |   direction: rtl;
119 | }
120 | 
121 | /**
122 |  * Default spacing for list container elements. Override with CSS as needed.
123 |  */
124 | 
125 | .public-DraftStyleDefault-ul,
126 | .public-DraftStyleDefault-ol {
127 |   margin: 16px 0;
128 |   padding: 0;
129 | }
130 | 
131 | /**
132 |  * Default counters and styles are provided for five levels of nesting.
133 |  * If you require nesting beyond that level, you should use your own CSS
134 |  * classes to do so. If you care about handling RTL languages, the rules you
135 |  * create should look a lot like these.
136 |  */
137 | 
138 | /* @noflip */
139 | 
140 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR {
141 |   margin-left: 1.5em;
142 | }
143 | 
144 | /* @noflip */
145 | 
146 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL {
147 |   margin-right: 1.5em;
148 | }
149 | 
150 | /* @noflip */
151 | 
152 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR {
153 |   margin-left: 3em;
154 | }
155 | 
156 | /* @noflip */
157 | 
158 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL {
159 |   margin-right: 3em;
160 | }
161 | 
162 | /* @noflip */
163 | 
164 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR {
165 |   margin-left: 4.5em;
166 | }
167 | 
168 | /* @noflip */
169 | 
170 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL {
171 |   margin-right: 4.5em;
172 | }
173 | 
174 | /* @noflip */
175 | 
176 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR {
177 |   margin-left: 6em;
178 | }
179 | 
180 | /* @noflip */
181 | 
182 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL {
183 |   margin-right: 6em;
184 | }
185 | 
186 | /* @noflip */
187 | 
188 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR {
189 |   margin-left: 7.5em;
190 | }
191 | 
192 | /* @noflip */
193 | 
194 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL {
195 |   margin-right: 7.5em;
196 | }
197 | 
198 | /**
199 |  * Only use `square` list-style after the first two levels.
200 |  */
201 | 
202 | .public-DraftStyleDefault-unorderedListItem {
203 |   list-style-type: square;
204 |   position: relative;
205 | }
206 | 
207 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0 {
208 |   list-style-type: disc;
209 | }
210 | 
211 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1 {
212 |   list-style-type: circle;
213 | }
214 | 
215 | /**
216 |  * Ordered list item counters are managed with CSS, since all list nesting is
217 |  * purely visual.
218 |  */
219 | 
220 | .public-DraftStyleDefault-orderedListItem {
221 |   list-style-type: none;
222 |   position: relative;
223 | }
224 | 
225 | /* @noflip */
226 | 
227 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before {
228 |   left: -36px;
229 |   position: absolute;
230 |   text-align: right;
231 |   width: 30px;
232 | }
233 | 
234 | /* @noflip */
235 | 
236 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before {
237 |   position: absolute;
238 |   right: -36px;
239 |   text-align: left;
240 |   width: 30px;
241 | }
242 | 
243 | /**
244 |  * Counters are reset in JavaScript. If you need different counter styles,
245 |  * override these rules. If you need more nesting, create your own rules to
246 |  * do so.
247 |  */
248 | 
249 | .public-DraftStyleDefault-orderedListItem:before {
250 |   content: counter(ol0) ". ";
251 |   counter-increment: ol0;
252 | }
253 | 
254 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before {
255 |   content: counter(ol1) ". ";
256 |   counter-increment: ol1;
257 | }
258 | 
259 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before {
260 |   content: counter(ol2) ". ";
261 |   counter-increment: ol2;
262 | }
263 | 
264 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before {
265 |   content: counter(ol3) ". ";
266 |   counter-increment: ol3;
267 | }
268 | 
269 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before {
270 |   content: counter(ol4) ". ";
271 |   counter-increment: ol4;
272 | }
273 | 
274 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset {
275 |   counter-reset: ol0;
276 | }
277 | 
278 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset {
279 |   counter-reset: ol1;
280 | }
281 | 
282 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset {
283 |   counter-reset: ol2;
284 | }
285 | 
286 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset {
287 |   counter-reset: ol3;
288 | }
289 | 
290 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset {
291 |   counter-reset: ol4;
292 | }
293 | 


--------------------------------------------------------------------------------
/src/EditorDemo.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | import React, {Component} from 'react';
  3 | import RichTextEditor, {createEmptyValue} from './RichTextEditor';
  4 | import {convertToRaw} from 'draft-js';
  5 | import autobind from 'class-autobind';
  6 | 
  7 | import {getTextAlignBlockMetadata, getTextAlignClassName, getTextAlignStyles} from './lib/blockStyleFunctions';
  8 | import ButtonGroup from './ui/ButtonGroup';
  9 | import Dropdown from './ui/Dropdown';
 10 | import IconButton from './ui/IconButton';
 11 | 
 12 | import type {EditorValue} from './RichTextEditor';
 13 | 
 14 | type Props = {};
 15 | type State = {
 16 |   value: EditorValue;
 17 |   format: string;
 18 |   readOnly: boolean;
 19 | };
 20 | 
 21 | export default class EditorDemo extends Component {
 22 |   props: Props;
 23 |   state: State;
 24 | 
 25 |   constructor() {
 26 |     super(...arguments);
 27 |     autobind(this);
 28 |     this.state = {
 29 |       value: createEmptyValue(),
 30 |       format: 'html',
 31 |       readOnly: false,
 32 |     };
 33 |   }
 34 | 
 35 |   render() {
 36 |     let {value, format} = this.state;
 37 | 
 38 |     return (
 39 |       <div className="editor-demo">
 40 |         <div className="row">
 41 |           <p>This is a demo of the <a href="https://github.com/sstur/react-rte" target="top">react-rte</a> editor.</p>
 42 |         </div>
 43 |         <div className="row">
 44 |           <RichTextEditor
 45 |             value={value}
 46 |             onChange={this._onChange}
 47 |             className="react-rte-demo"
 48 |             placeholder="Tell a story"
 49 |             toolbarClassName="demo-toolbar"
 50 |             editorClassName="demo-editor"
 51 |             readOnly={this.state.readOnly}
 52 |             blockStyleFn={getTextAlignClassName}
 53 |             customControls={[
 54 |               // eslint-disable-next-line no-unused-vars
 55 |               (setValue, getValue, editorState) => {
 56 |                 let choices = new Map([
 57 |                   ['1', {label: '1'}],
 58 |                   ['2', {label: '2'}],
 59 |                   ['3', {label: '3'}],
 60 |                 ]);
 61 |                 return (
 62 |                   <ButtonGroup key={1}>
 63 |                     <Dropdown
 64 |                       choices={choices}
 65 |                       selectedKey={getValue('my-control-name')}
 66 |                       onChange={(value) => setValue('my-control-name', value)}
 67 |                     />
 68 |                   </ButtonGroup>
 69 |                 );
 70 |               },
 71 |               <ButtonGroup key={2}>
 72 |                 <IconButton
 73 |                   label="Remove Link"
 74 |                   iconName="remove-link"
 75 |                   focusOnClick={false}
 76 |                   onClick={() => console.log('You pressed a button')}
 77 |                 />
 78 |               </ButtonGroup>,
 79 |             ]}
 80 |           />
 81 |         </div>
 82 |         <div className="row">
 83 |           <label className="radio-item">
 84 |             <input
 85 |               type="radio"
 86 |               name="format"
 87 |               value="html"
 88 |               checked={format === 'html'}
 89 |               onChange={this._onChangeFormat}
 90 |             />
 91 |             <span>HTML</span>
 92 |           </label>
 93 |           <label className="radio-item">
 94 |             <input
 95 |               type="radio"
 96 |               name="format"
 97 |               value="markdown"
 98 |               checked={format === 'markdown'}
 99 |               onChange={this._onChangeFormat}
100 |             />
101 |             <span>Markdown</span>
102 |           </label>
103 |           <label className="radio-item">
104 |             <input
105 |               type="checkbox"
106 |               onChange={this._onChangeReadOnly}
107 |               checked={this.state.readOnly}
108 |             />
109 |             <span>Editor is read-only</span>
110 |           </label>
111 |         </div>
112 |         <div className="row">
113 |           <textarea
114 |             className="source"
115 |             placeholder="Editor Source"
116 |             value={value.toString(format, {blockStyleFn: getTextAlignStyles})}
117 |             onChange={this._onChangeSource}
118 |           />
119 |         </div>
120 |         <div className="row btn-row">
121 |           <span className="label">Debugging:</span>
122 |           <button className="btn" onClick={this._logState}>Log Content State</button>
123 |           <button className="btn" onClick={this._logStateRaw}>Log Raw</button>
124 |         </div>
125 |       </div>
126 |     );
127 |   }
128 | 
129 |   _logState() {
130 |     let editorState = this.state.value.getEditorState();
131 |     let contentState = window.contentState = editorState.getCurrentContent().toJS();
132 |     console.log(contentState);
133 |   }
134 | 
135 |   _logStateRaw() {
136 |     let editorState = this.state.value.getEditorState();
137 |     let contentState = editorState.getCurrentContent();
138 |     let rawContentState = window.rawContentState = convertToRaw(contentState);
139 |     console.log(JSON.stringify(rawContentState));
140 |   }
141 | 
142 |   _onChange(value: EditorValue) {
143 |     this.setState({value});
144 |   }
145 | 
146 |   _onChangeSource(event: Object) {
147 |     let source = event.target.value;
148 |     let oldValue = this.state.value;
149 |     this.setState({
150 |       value: oldValue.setContentFromString(source, this.state.format, {customBlockFn: getTextAlignBlockMetadata}),
151 |     });
152 |   }
153 | 
154 |   _onChangeFormat(event: Object) {
155 |     this.setState({format: event.target.value});
156 |   }
157 | 
158 |   _onChangeReadOnly(event: Object) {
159 |     this.setState({readOnly: event.target.checked});
160 |   }
161 | }
162 | 


--------------------------------------------------------------------------------
/src/RichTextEditor.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   background: #fff;
 3 |   border: 1px solid #ddd;
 4 |   border-radius: 3px;
 5 |   font-family: 'Georgia', serif;
 6 |   font-size: 14px;
 7 | }
 8 | 
 9 | .editor {
10 |   cursor: text;
11 |   font-size: 16px;
12 | }
13 | 
14 | :global(.text-align--center .public-DraftStyleDefault-ltr) {
15 |   text-align: center;
16 | }
17 | 
18 | :global(.text-align--right .public-DraftStyleDefault-ltr) {
19 |   text-align: right;
20 | }
21 | 
22 | :global(.text-align--justify .public-DraftStyleDefault-ltr) {
23 |   text-align: justify;
24 | }
25 | 
26 | .editor :global(.public-DraftEditorPlaceholder-root),
27 | .editor :global(.public-DraftEditor-content) {
28 |   margin: 0;
29 |   /* 1px is added as transparent border on .DraftEditor-editorContainer */
30 |   padding: 9px;
31 | }
32 | 
33 | .editor :global(.public-DraftEditor-content) {
34 |   overflow: auto;
35 | }
36 | 
37 | .hidePlaceholder :global(.public-DraftEditorPlaceholder-root) {
38 |   display: none;
39 | }
40 | 
41 | .editor .paragraph {
42 |   margin: 14px 0;
43 | }
44 | 
45 | /* Consecutive code blocks are nested inside a single parent <pre> (like <li>
46 |   inside <ul>). Unstyle the parent and style the children. */
47 | .editor pre {
48 |   margin: 14px 0;
49 | }
50 | 
51 | .editor .codeBlock {
52 |   background-color: #f3f3f3;
53 |   font-family: "Inconsolata", "Menlo", "Consolas", monospace;
54 |   font-size: 16px;
55 |   /* This should collapse with the margin around the parent <pre>. */
56 |   margin: 14px 0;
57 |   padding: 20px;
58 | }
59 | 
60 | /* Hacky: Remove padding from inline <code> within code block. */
61 | .editor .codeBlock span[style] {
62 |   padding: 0 !important;
63 | }
64 | 
65 | .editor .blockquote {
66 |   border-left: 5px solid #eee;
67 |   color: #666;
68 |   font-family: 'Hoefler Text', 'Georgia', serif;
69 |   font-style: italic;
70 |   margin: 16px 0;
71 |   padding: 10px 20px;
72 | }
73 | 
74 | /* There shouldn't be margin outside the first/last blocks */
75 | .editor .block:first-child,
76 | .editor pre:first-child,
77 | .editor ul:first-child,
78 | .editor ol:first-child {
79 |   margin-top: 0;
80 | }
81 | .editor .block:last-child,
82 | .editor pre:last-child,
83 | .editor ul:last-child,
84 | .editor ol:last-child {
85 |   margin-bottom: 0;
86 | }
87 | 


--------------------------------------------------------------------------------
/src/RichTextEditor.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | import React, {Component} from 'react';
  3 | import {CompositeDecorator, Editor, EditorState, Modifier, RichUtils, Entity} from 'draft-js';
  4 | import getDefaultKeyBinding from 'draft-js/lib/getDefaultKeyBinding';
  5 | import {getTextAlignBlockMetadata, getTextAlignClassName, getTextAlignStyles} from './lib/blockStyleFunctions';
  6 | import changeBlockDepth from './lib/changeBlockDepth';
  7 | import changeBlockType from './lib/changeBlockType';
  8 | import getBlocksInSelection from './lib/getBlocksInSelection';
  9 | import insertBlockAfter from './lib/insertBlockAfter';
 10 | import isListItem from './lib/isListItem';
 11 | import isSoftNewlineEvent from 'draft-js/lib/isSoftNewlineEvent';
 12 | import EditorToolbar from './lib/EditorToolbar';
 13 | import EditorValue from './lib/EditorValue';
 14 | import LinkDecorator from './lib/LinkDecorator';
 15 | import ImageDecorator from './lib/ImageDecorator';
 16 | import composite from './lib/composite';
 17 | import cx from 'classnames';
 18 | import autobind from 'class-autobind';
 19 | import EventEmitter from 'events';
 20 | import {BLOCK_TYPE} from 'draft-js-utils';
 21 | 
 22 | import './Draft.global.css';
 23 | import styles from './RichTextEditor.css';
 24 | 
 25 | import type {ContentBlock} from 'draft-js';
 26 | import type {ToolbarConfig, CustomControl} from './lib/EditorToolbarConfig';
 27 | import type {ImportOptions} from './lib/EditorValue';
 28 | 
 29 | import ButtonGroup from './ui/ButtonGroup';
 30 | import Button from './ui/Button';
 31 | import Dropdown from './ui/Dropdown';
 32 | 
 33 | const MAX_LIST_DEPTH = 2;
 34 | 
 35 | // Custom overrides for "code" style.
 36 | const styleMap = {
 37 |   CODE: {
 38 |     backgroundColor: '#f3f3f3',
 39 |     fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
 40 |     fontSize: 16,
 41 |     padding: 2,
 42 |   },
 43 | };
 44 | 
 45 | type ChangeHandler = (value: EditorValue) => any;
 46 | 
 47 | type Props = {
 48 |   className?: string;
 49 |   toolbarClassName?: string;
 50 |   editorClassName?: string;
 51 |   value: EditorValue;
 52 |   onChange?: ChangeHandler;
 53 |   placeholder?: string;
 54 |   customStyleMap?: {[style: string]: {[key: string]: any}};
 55 |   handleReturn?: (event: Object) => boolean;
 56 |   customControls?: Array<CustomControl>;
 57 |   readOnly?: boolean;
 58 |   toolbarHidden?: boolean;
 59 |   disabled?: boolean; // Alias of readOnly
 60 |   toolbarConfig?: ToolbarConfig;
 61 |   toolbarOnBottom?: boolean;
 62 |   blockStyleFn?: (block: ContentBlock) => ?string;
 63 |   autoFocus?: boolean;
 64 |   keyBindingFn?: (event: Object) => ?string;
 65 |   rootStyle?: Object;
 66 |   editorStyle?: Object;
 67 |   toolbarStyle?: Object;
 68 |   onBlur?: (event: Object) => void;
 69 | };
 70 | 
 71 | export default class RichTextEditor extends Component {
 72 |   props: Props;
 73 |   _keyEmitter: EventEmitter;
 74 |   editor: HTMLDivElement;
 75 | 
 76 |   constructor() {
 77 |     super(...arguments);
 78 |     this._keyEmitter = new EventEmitter();
 79 |     autobind(this);
 80 |   }
 81 | 
 82 |   componentDidMount() {
 83 |     const {autoFocus} = this.props;
 84 | 
 85 |     if (!autoFocus) {
 86 |       return;
 87 |     }
 88 | 
 89 |     this._focus();
 90 |   }
 91 | 
 92 |   render() {
 93 |     let {
 94 |       value,
 95 |       className,
 96 |       toolbarClassName,
 97 |       editorClassName,
 98 |       placeholder,
 99 |       customStyleMap,
100 |       readOnly,
101 |       toolbarHidden,
102 |       disabled,
103 |       toolbarConfig,
104 |       toolbarOnBottom,
105 |       blockStyleFn,
106 |       customControls,
107 |       keyBindingFn,
108 |       rootStyle,
109 |       toolbarStyle,
110 |       editorStyle,
111 |       ...otherProps // eslint-disable-line comma-dangle
112 |     } = this.props;
113 |     let editorState = value.getEditorState();
114 |     customStyleMap = customStyleMap ? {...styleMap, ...customStyleMap} : styleMap;
115 | 
116 |     // If the user changes block type before entering any text, we can either
117 |     // style the placeholder or hide it. Let's just hide it for now.
118 |     let combinedEditorClassName = cx({
119 |       [styles.editor]: true,
120 |       [styles.hidePlaceholder]: this._shouldHidePlaceholder(),
121 |     }, editorClassName);
122 |     if (readOnly == null) {
123 |       readOnly = disabled;
124 |     }
125 |     let editorToolbar;
126 | 
127 |     if (!readOnly && !toolbarHidden) {
128 |       editorToolbar = (
129 |         <EditorToolbar
130 |           rootStyle={toolbarStyle}
131 |           isOnBottom={toolbarOnBottom}
132 |           className={toolbarClassName}
133 |           keyEmitter={this._keyEmitter}
134 |           editorState={editorState}
135 |           onChange={this._onChange}
136 |           focusEditor={this._focus}
137 |           toolbarConfig={toolbarConfig}
138 |           customControls={customControls}
139 |         />
140 |       );
141 |     }
142 |     return (
143 |       <div className={cx(styles.root, className)} style={rootStyle}>
144 |         { !toolbarOnBottom && editorToolbar }
145 |         <div className={combinedEditorClassName} style={editorStyle}>
146 |           <Editor
147 |             {...otherProps}
148 |             blockStyleFn={composite(defaultBlockStyleFn, blockStyleFn)}
149 |             customStyleMap={customStyleMap}
150 |             editorState={editorState}
151 |             handleReturn={this._handleReturn}
152 |             keyBindingFn={keyBindingFn || this._customKeyHandler}
153 |             handleKeyCommand={this._handleKeyCommand}
154 |             onTab={this._onTab}
155 |             onChange={this._onChange}
156 |             placeholder={placeholder}
157 |             ariaLabel={placeholder || 'Edit text'}
158 |             ref={(el) => {
159 |               this.editor = el;
160 |             }}
161 |             spellCheck={true}
162 |             readOnly={readOnly}
163 |           />
164 |         </div>
165 |         { toolbarOnBottom && editorToolbar }
166 |       </div>
167 |     );
168 |   }
169 | 
170 |   _shouldHidePlaceholder(): boolean {
171 |     let editorState = this.props.value.getEditorState();
172 |     let contentState = editorState.getCurrentContent();
173 |     if (!contentState.hasText()) {
174 |       if (contentState.getBlockMap().first().getType() !== 'unstyled') {
175 |         return true;
176 |       }
177 |     }
178 |     return false;
179 |   }
180 | 
181 |   _handleReturn(event: Object): boolean {
182 |     let {handleReturn} = this.props;
183 |     if (handleReturn != null && handleReturn(event)) {
184 |       return true;
185 |     }
186 |     if (this._handleReturnSoftNewline(event)) {
187 |       return true;
188 |     }
189 |     if (this._handleReturnEmptyListItem()) {
190 |       return true;
191 |     }
192 |     if (this._handleReturnSpecialBlock()) {
193 |       return true;
194 |     }
195 |     return false;
196 |   }
197 | 
198 |   // `shift + return` should insert a soft newline.
199 |   _handleReturnSoftNewline(event: Object): boolean {
200 |     let editorState = this.props.value.getEditorState();
201 |     if (isSoftNewlineEvent(event)) {
202 |       let selection = editorState.getSelection();
203 |       if (selection.isCollapsed()) {
204 |         this._onChange(RichUtils.insertSoftNewline(editorState));
205 |       } else {
206 |         let content = editorState.getCurrentContent();
207 |         let newContent = Modifier.removeRange(content, selection, 'forward');
208 |         let newSelection = newContent.getSelectionAfter();
209 |         let block = newContent.getBlockForKey(newSelection.getStartKey());
210 |         newContent = Modifier.insertText(
211 |           newContent,
212 |           newSelection,
213 |           '\n',
214 |           block.getInlineStyleAt(newSelection.getStartOffset()),
215 |           null,
216 |         );
217 |         this._onChange(
218 |           EditorState.push(editorState, newContent, 'insert-fragment')
219 |         );
220 |       }
221 |       return true;
222 |     }
223 |     return false;
224 |   }
225 | 
226 |   // If the cursor is in an empty list item when return is pressed, then the
227 |   // block type should change to normal (end the list).
228 |   _handleReturnEmptyListItem(): boolean {
229 |     let editorState = this.props.value.getEditorState();
230 |     let selection = editorState.getSelection();
231 |     if (selection.isCollapsed()) {
232 |       let contentState = editorState.getCurrentContent();
233 |       let blockKey = selection.getStartKey();
234 |       let block = contentState.getBlockForKey(blockKey);
235 |       if (isListItem(block) && block.getLength() === 0) {
236 |         let depth = block.getDepth();
237 |         let newState = (depth === 0) ?
238 |           changeBlockType(editorState, blockKey, BLOCK_TYPE.UNSTYLED) :
239 |           changeBlockDepth(editorState, blockKey, depth - 1);
240 |         this._onChange(newState);
241 |         return true;
242 |       }
243 |     }
244 |     return false;
245 |   }
246 | 
247 |   // If the cursor is at the end of a special block (any block type other than
248 |   // normal or list item) when return is pressed, new block should be normal.
249 |   _handleReturnSpecialBlock(): boolean {
250 |     let editorState = this.props.value.getEditorState();
251 |     let selection = editorState.getSelection();
252 |     if (selection.isCollapsed()) {
253 |       let contentState = editorState.getCurrentContent();
254 |       let blockKey = selection.getStartKey();
255 |       let block = contentState.getBlockForKey(blockKey);
256 |       if (!isListItem(block) && block.getType() !== BLOCK_TYPE.UNSTYLED) {
257 |         // If cursor is at end.
258 |         if (block.getLength() === selection.getStartOffset()) {
259 |           let newEditorState = insertBlockAfter(
260 |             editorState,
261 |             blockKey,
262 |             BLOCK_TYPE.UNSTYLED
263 |           );
264 |           this._onChange(newEditorState);
265 |           return true;
266 |         }
267 |       }
268 |     }
269 |     return false;
270 |   }
271 | 
272 |   _onTab(event: Object): ?string {
273 |     let editorState = this.props.value.getEditorState();
274 |     let newEditorState = RichUtils.onTab(event, editorState, MAX_LIST_DEPTH);
275 |     if (newEditorState !== editorState) {
276 |       this._onChange(newEditorState);
277 |     }
278 |   }
279 | 
280 |   _customKeyHandler(event: Object): ?string {
281 |     // Allow toolbar to catch key combinations.
282 |     let eventFlags = {};
283 |     this._keyEmitter.emit('keypress', event, eventFlags);
284 |     if (eventFlags.wasHandled) {
285 |       return null;
286 |     } else {
287 |       return getDefaultKeyBinding(event);
288 |     }
289 |   }
290 | 
291 |   _handleKeyCommand(command: string): boolean {
292 |     let editorState = this.props.value.getEditorState();
293 |     let newEditorState = RichUtils.handleKeyCommand(editorState, command);
294 |     if (newEditorState) {
295 |       this._onChange(newEditorState);
296 |       return true;
297 |     } else {
298 |       return false;
299 |     }
300 |   }
301 | 
302 |   _onChange(editorState: EditorState) {
303 |     let {onChange, value} = this.props;
304 |     if (onChange == null) {
305 |       return;
306 |     }
307 |     let newValue = value.setEditorState(editorState);
308 |     let newEditorState = newValue.getEditorState();
309 |     this._handleInlineImageSelection(newEditorState);
310 |     onChange(newValue);
311 |   }
312 | 
313 |   _handleInlineImageSelection(editorState: EditorState) {
314 |     let selection = editorState.getSelection();
315 |     let blocks = getBlocksInSelection(editorState);
316 | 
317 |     const selectImage = (block, offset) => {
318 |       const imageKey = block.getEntityAt(offset);
319 |       Entity.mergeData(imageKey, {selected: true});
320 |     };
321 | 
322 |     let isInMiddleBlock = (index) => index > 0 && index < blocks.size - 1;
323 |     let isWithinStartBlockSelection = (offset, index) => (
324 |       index === 0 && offset > selection.getStartOffset()
325 |     );
326 |     let isWithinEndBlockSelection = (offset, index) => (
327 |       index === blocks.size - 1 && offset < selection.getEndOffset()
328 |     );
329 | 
330 |     blocks.toIndexedSeq().forEach((block, index) => {
331 |       ImageDecorator.strategy(
332 |         block,
333 |         (offset) => {
334 |           if (isWithinStartBlockSelection(offset, index) ||
335 |               isInMiddleBlock(index) ||
336 |               isWithinEndBlockSelection(offset, index)) {
337 |             selectImage(block, offset);
338 |           }
339 |         });
340 |     });
341 |   }
342 | 
343 |   _focus() {
344 |     this.editor.focus();
345 |   }
346 | }
347 | 
348 | function defaultBlockStyleFn(block: ContentBlock): string {
349 |   let result = styles.block;
350 |   switch (block.getType()) {
351 |     case 'unstyled':
352 |       return cx(result, styles.paragraph);
353 |     case 'blockquote':
354 |       return cx(result, styles.blockquote);
355 |     case 'code-block':
356 |       return cx(result, styles.codeBlock);
357 |     default:
358 |       return result;
359 |   }
360 | }
361 | 
362 | const decorator = new CompositeDecorator([LinkDecorator, ImageDecorator]);
363 | 
364 | function createEmptyValue(): EditorValue {
365 |   return EditorValue.createEmpty(decorator);
366 | }
367 | 
368 | function createValueFromString(markup: string, format: string, options?: ImportOptions): EditorValue {
369 |   return EditorValue.createFromString(markup, format, decorator, options);
370 | }
371 | 
372 | // $FlowIssue - This should probably not be done this way.
373 | Object.assign(RichTextEditor, {
374 |   EditorValue,
375 |   decorator,
376 |   createEmptyValue,
377 |   createValueFromString,
378 |   ButtonGroup,
379 |   Button,
380 |   Dropdown,
381 | });
382 | 
383 | export {
384 |   EditorValue,
385 |   decorator,
386 |   createEmptyValue,
387 |   createValueFromString,
388 |   getTextAlignBlockMetadata,
389 |   getTextAlignClassName,
390 |   getTextAlignStyles,
391 |   ButtonGroup,
392 |   Button,
393 |   Dropdown,
394 | };
395 | 


--------------------------------------------------------------------------------
/src/SimpleRichTextEditor.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import React, {Component} from 'react';
 3 | import RichTextEditor, {createEmptyValue} from './RichTextEditor';
 4 | import autobind from 'class-autobind';
 5 | 
 6 | import type {EditorValue} from './RichTextEditor';
 7 | 
 8 | type Props = {
 9 |   value: string;
10 |   format: string;
11 |   onChange: (value: string) => any;
12 | };
13 | type State = {
14 |   editorValue: EditorValue;
15 | };
16 | 
17 | export default class SimpleRichTextEditor extends Component {
18 |   props: Props;
19 |   state: State;
20 |   // The [format, value] of what's currently displayed in the <RichTextEditor />
21 |   _currentValue: ?[string, string];
22 | 
23 |   constructor() {
24 |     super(...arguments);
25 |     autobind(this);
26 |     this.state = {
27 |       editorValue: createEmptyValue(),
28 |     };
29 |   }
30 | 
31 |   // eslint-disable-next-line
32 |   UNSAFE_componentWillMount() {
33 |     this._updateStateFromProps(this.props);
34 |   }
35 | 
36 |   // eslint-disable-next-line
37 |   UNSAFE_componentWillReceiveProps(newProps: Props) {
38 |     this._updateStateFromProps(newProps);
39 |   }
40 | 
41 |   _updateStateFromProps(newProps: Props) {
42 |     let {value, format} = newProps;
43 |     if (this._currentValue != null) {
44 |       let [currentFormat, currentValue] = this._currentValue;
45 |       if (format === currentFormat && value === currentValue) {
46 |         return;
47 |       }
48 |     }
49 |     let {editorValue} = this.state;
50 |     this.setState({
51 |       editorValue: editorValue.setContentFromString(value, format),
52 |     });
53 |     this._currentValue = [format, value];
54 |   }
55 | 
56 |   render() {
57 |     let {value, format, onChange, ...otherProps} = this.props; // eslint-disable-line no-unused-vars
58 |     return (
59 |       <RichTextEditor
60 |         {...otherProps}
61 |         value={this.state.editorValue}
62 |         onChange={this._onChange}
63 |       />
64 |     );
65 |   }
66 | 
67 |   _onChange(editorValue: EditorValue) {
68 |     let {format, onChange} = this.props;
69 |     let oldEditorValue = this.state.editorValue;
70 |     this.setState({editorValue});
71 |     let oldContentState = oldEditorValue ? oldEditorValue.getEditorState().getCurrentContent() : null;
72 |     let newContentState = editorValue.getEditorState().getCurrentContent();
73 |     if (oldContentState !== newContentState) {
74 |       let stringValue = editorValue.toString(format);
75 |       // Optimization so if we receive new props we don't need
76 |       // to parse anything unnecessarily.
77 |       this._currentValue = [format, stringValue];
78 |       if (onChange && stringValue !== this.props.value) {
79 |         onChange(stringValue);
80 |       }
81 |     }
82 |   }
83 | }
84 | 


--------------------------------------------------------------------------------
/src/__tests__/RichTextEditor-test.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | const {describe, it} = global;
 3 | import React from 'react';
 4 | import ShallowRenderer from 'react-test-renderer/shallow';
 5 | import expect from 'expect';
 6 | import RichTextEditor, {createEmptyValue} from '../RichTextEditor';
 7 | 
 8 | describe('RichTextEditor', () => {
 9 |   it('should render', () => {
10 |     let renderer = new ShallowRenderer();
11 |     let value = createEmptyValue();
12 |     renderer.render(<RichTextEditor value={value} />);
13 |     let output = renderer.getRenderOutput();
14 |     expect(output.type).toEqual('div');
15 |     expect(output.props.className).toBeA('string');
16 |     expect(output.props.className).toInclude('RichTextEditor__root');
17 |   });
18 | });
19 | 


--------------------------------------------------------------------------------
/src/demo.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import ReactDOM from 'react-dom';
 3 | import EditorDemo from './EditorDemo';
 4 | 
 5 | document.addEventListener('DOMContentLoaded', () => {
 6 |   let rootNode = document.createElement('div');
 7 |   document.body.appendChild(rootNode);
 8 |   ReactDOM.render(
 9 |     <EditorDemo />,
10 |     rootNode,
11 |   );
12 | });
13 | 


--------------------------------------------------------------------------------
/src/lib/EditorToolbar.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   font-family: 'Helvetica', sans-serif;
 3 |   font-size: 14px;
 4 |   margin: 0 10px;
 5 |   padding: 10px 0 5px;
 6 |   border-bottom: 1px solid #ddd;
 7 |   -webkit-user-select: none;
 8 |   -moz-user-select: none;
 9 |   -ms-user-select: none;
10 |   user-select: none;
11 | }
12 | 
13 | .onBottom {
14 |   border-top: 1px solid #ddd;
15 |   border-bottom: none;
16 | }
17 | 


--------------------------------------------------------------------------------
/src/lib/EditorToolbar.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | import {hasCommandModifier} from 'draft-js/lib/KeyBindingUtil';
  3 | 
  4 | import React, {Component} from 'react';
  5 | import ReactDOM from 'react-dom';
  6 | import {EditorState, Entity, EntityDescription, RichUtils, Modifier} from 'draft-js';
  7 | import {ENTITY_TYPE} from 'draft-js-utils';
  8 | import DefaultToolbarConfig from './EditorToolbarConfig';
  9 | import StyleButton from './StyleButton';
 10 | import PopoverIconButton from '../ui/PopoverIconButton';
 11 | import ButtonGroup from '../ui/ButtonGroup';
 12 | import Dropdown from '../ui/Dropdown';
 13 | import IconButton from '../ui/IconButton';
 14 | import getEntityAtCursor from './getEntityAtCursor';
 15 | import clearEntityForRange from './clearEntityForRange';
 16 | import autobind from 'class-autobind';
 17 | import cx from 'classnames';
 18 | 
 19 | import styles from './EditorToolbar.css';
 20 | 
 21 | import type EventEmitter from 'events';
 22 | import type {ToolbarConfig, CustomControl} from './EditorToolbarConfig';
 23 | 
 24 | type ChangeHandler = (state: EditorState) => any;
 25 | 
 26 | type Props = {
 27 |   className?: string;
 28 |   editorState: EditorState;
 29 |   keyEmitter: EventEmitter;
 30 |   onChange: ChangeHandler;
 31 |   focusEditor: Function;
 32 |   toolbarConfig: ToolbarConfig;
 33 |   customControls: Array<CustomControl>;
 34 |   rootStyle?: Object;
 35 |   isOnBottom?: boolean;
 36 | };
 37 | 
 38 | type State = {
 39 |   showLinkInput: boolean;
 40 |   showImageInput: boolean;
 41 |   customControlState: {[key: string]: string};
 42 | };
 43 | 
 44 | 
 45 | export default class EditorToolbar extends Component {
 46 |   props: Props;
 47 |   state: State;
 48 | 
 49 |   constructor() {
 50 |     super(...arguments);
 51 |     autobind(this);
 52 |     this.state = {
 53 |       showLinkInput: false,
 54 |       showImageInput: false,
 55 |       customControlState: {},
 56 |     };
 57 |   }
 58 | 
 59 |   // eslint-disable-next-line
 60 |   UNSAFE_componentWillMount() {
 61 |     // Technically, we should also attach/detach event listeners when the
 62 |     // `keyEmitter` prop changes.
 63 |     this.props.keyEmitter.on('keypress', this._onKeypress);
 64 |   }
 65 | 
 66 |   componentWillUnmount() {
 67 |     this.props.keyEmitter.removeListener('keypress', this._onKeypress);
 68 |   }
 69 | 
 70 |   render() {
 71 |     let {className, toolbarConfig, rootStyle, isOnBottom} = this.props;
 72 |     if (toolbarConfig == null) {
 73 |       toolbarConfig = DefaultToolbarConfig;
 74 |     }
 75 |     let display = toolbarConfig.display || DefaultToolbarConfig.display;
 76 |     let buttonGroups = display.map((groupName) => {
 77 |       switch (groupName) {
 78 |         case 'INLINE_STYLE_BUTTONS': {
 79 |           return this._renderInlineStyleButtons(groupName, toolbarConfig);
 80 |         }
 81 |         case 'BLOCK_ALIGNMENT_BUTTONS': {
 82 |           return this._renderBlockAlignmentButtons(groupName, toolbarConfig);
 83 |         }
 84 |         case 'BLOCK_TYPE_DROPDOWN': {
 85 |           return this._renderBlockTypeDropdown(groupName, toolbarConfig);
 86 |         }
 87 |         case 'LINK_BUTTONS': {
 88 |           return this._renderLinkButtons(groupName, toolbarConfig);
 89 |         }
 90 |         case 'IMAGE_BUTTON': {
 91 |           return this._renderImageButton(groupName, toolbarConfig);
 92 |         }
 93 |         case 'BLOCK_TYPE_BUTTONS': {
 94 |           return this._renderBlockTypeButtons(groupName, toolbarConfig);
 95 |         }
 96 |         case 'HISTORY_BUTTONS': {
 97 |           return this._renderUndoRedo(groupName, toolbarConfig);
 98 |         }
 99 |       }
100 |     });
101 |     return (
102 |       <div className={cx(styles.root, (isOnBottom && styles.onBottom), className)} style={rootStyle}>
103 |         {buttonGroups}
104 |         {this._renderCustomControls()}
105 |       </div>
106 |     );
107 |   }
108 | 
109 |   _renderCustomControls() {
110 |     let {customControls, editorState} = this.props;
111 |     if (customControls == null) {
112 |       return;
113 |     }
114 |     return customControls.map((f) => {
115 |       switch (typeof f) {
116 |         case 'function': {
117 |           return f(
118 |             this._setCustomControlState,
119 |             this._getCustomControlState,
120 |             editorState
121 |           );
122 |         }
123 |         default: {
124 |           return f;
125 |         }
126 |       }
127 |     });
128 |   }
129 | 
130 |   _setCustomControlState(key: string, value: string) {
131 |     this.setState(({customControlState}) => ({
132 |       customControlState: {...customControlState, [key]: value},
133 |     }));
134 |   }
135 | 
136 |   _getCustomControlState(key: string) {
137 |     return this.state.customControlState[key];
138 |   }
139 | 
140 |   _renderBlockTypeDropdown(name: string, toolbarConfig: ToolbarConfig) {
141 |     let blockType = this._getCurrentBlockType();
142 |     let choices = new Map(
143 |       (toolbarConfig.BLOCK_TYPE_DROPDOWN || []).map((type) => [type.style, {label: type.label, className: type.className}])
144 |     );
145 |     if (!choices.has(blockType)) {
146 |       blockType = Array.from(choices.keys())[0];
147 |     }
148 |     return (
149 |       <ButtonGroup key={name}>
150 |         <Dropdown
151 |           {...toolbarConfig.extraProps}
152 |           choices={choices}
153 |           selectedKey={blockType}
154 |           onChange={this._selectBlockType}
155 |           aria-label={'Block type'}
156 |         />
157 |       </ButtonGroup>
158 |     );
159 |   }
160 | 
161 |   _renderBlockTypeButtons(name: string, toolbarConfig: ToolbarConfig) {
162 |     let blockType = this._getCurrentBlockType();
163 |     let buttons = (toolbarConfig.BLOCK_TYPE_BUTTONS || []).map((type, index) => (
164 |       <StyleButton
165 |         {...toolbarConfig.extraProps}
166 |         key={String(index)}
167 |         isActive={type.style === blockType}
168 |         label={type.label}
169 |         onToggle={this._toggleBlockType}
170 |         style={type.style}
171 |         className={type.className}
172 |       />
173 |     ));
174 |     return (
175 |       <ButtonGroup key={name}>{buttons}</ButtonGroup>
176 |     );
177 |   }
178 | 
179 |   _renderInlineStyleButtons(name: string, toolbarConfig: ToolbarConfig) {
180 |     let {editorState} = this.props;
181 |     let currentStyle = editorState.getCurrentInlineStyle();
182 |     let buttons = (toolbarConfig.INLINE_STYLE_BUTTONS || []).map((type, index) => (
183 |       <StyleButton
184 |         {...toolbarConfig.extraProps}
185 |         key={String(index)}
186 |         isActive={currentStyle.has(type.style)}
187 |         label={type.label}
188 |         onToggle={this._toggleInlineStyle}
189 |         style={type.style}
190 |         className={type.className}
191 |       />
192 |     ));
193 |     return (
194 |       <ButtonGroup key={name}>{buttons}</ButtonGroup>
195 |     );
196 |   }
197 | 
198 |   _renderBlockAlignmentButtons(name: string, toolbarConfig: ToolbarConfig) {
199 |     let {editorState} = this.props;
200 |     let content = editorState.getCurrentContent();
201 |     let selection = editorState.getSelection();
202 |     let blockKey = selection.getStartKey();
203 |     let block = content.getBlockForKey(blockKey);
204 |     let blockAlignment = block.getData().get('textAlign');
205 | 
206 |     let buttons = (toolbarConfig.BLOCK_ALIGNMENT_BUTTONS || []).map((type, index) => (
207 |       <StyleButton
208 |         {...toolbarConfig.extraProps}
209 |         key={String(index)}
210 |         isActive={blockAlignment === type.style}
211 |         label={type.label}
212 |         onToggle={this._toggleAlignment}
213 |         style={type.style}
214 |         className={type.className}
215 |       />
216 |     ));
217 |     return (
218 |       <ButtonGroup key={name}>{buttons}</ButtonGroup>
219 |     );
220 |   }
221 | 
222 |   _renderLinkButtons(name: string, toolbarConfig: ToolbarConfig) {
223 |     let {editorState} = this.props;
224 |     let selection = editorState.getSelection();
225 |     let entity = this._getEntityAtCursor();
226 |     let hasSelection = !selection.isCollapsed();
227 |     let isCursorOnLink = (entity != null && entity.type === ENTITY_TYPE.LINK);
228 |     let shouldShowLinkButton = hasSelection || isCursorOnLink;
229 |     let defaultValue = (entity && isCursorOnLink) ? entity.getData().url : '';
230 |     let config = toolbarConfig.LINK_BUTTONS || {};
231 |     let linkConfig = config.link || {};
232 |     let removeLinkConfig = config.removeLink || {};
233 |     let linkLabel = linkConfig.label || 'Link';
234 |     let removeLinkLabel = removeLinkConfig.label || 'Remove Link';
235 |     let targetBlank = (entity && isCursorOnLink) ? entity.getData().target === '_blank' : false;
236 |     let noFollow = (entity && isCursorOnLink) ? entity.getData().rel === 'nofollow' : false;
237 | 
238 |     return (
239 |       <ButtonGroup key={name}>
240 |         <PopoverIconButton
241 |           label={linkLabel}
242 |           iconName="link"
243 |           isDisabled={!shouldShowLinkButton}
244 |           showPopover={this.state.showLinkInput}
245 |           onTogglePopover={this._toggleShowLinkInput}
246 |           defaultValue={defaultValue}
247 |           onSubmit={this._setLink}
248 |           checkOptions={{
249 |             targetBlank: {label: 'Open link in new tab', defaultValue: targetBlank},
250 |             noFollow: {label: 'No follow', defaultValue: noFollow},
251 |           }}
252 |         />
253 |         <IconButton
254 |           {...toolbarConfig.extraProps}
255 |           label={removeLinkLabel}
256 |           iconName="remove-link"
257 |           isDisabled={!isCursorOnLink}
258 |           onClick={this._removeLink}
259 |           focusOnClick={false}
260 |         />
261 |       </ButtonGroup>
262 |     );
263 |   }
264 | 
265 |   _renderImageButton(name: string, toolbarConfig: ToolbarConfig) {
266 |     const config = (toolbarConfig.IMAGE_BUTTON || {});
267 |     const label = config.label || 'Image';
268 |     return (
269 |       <ButtonGroup key={name}>
270 |         <PopoverIconButton
271 |           label={label}
272 |           iconName="image"
273 |           showPopover={this.state.showImageInput}
274 |           onTogglePopover={this._toggleShowImageInput}
275 |           onSubmit={this._setImage}
276 |         />
277 |       </ButtonGroup>
278 |     );
279 |   }
280 | 
281 |   _renderUndoRedo(name: string, toolbarConfig: ToolbarConfig) {
282 |     let {editorState} = this.props;
283 |     let canUndo = editorState.getUndoStack().size !== 0;
284 |     let canRedo = editorState.getRedoStack().size !== 0;
285 |     const config = toolbarConfig.HISTORY_BUTTONS || {};
286 |     const undoConfig = config.undo || {};
287 |     const redoConfig = config.redo || {};
288 |     const undoLabel = undoConfig.label || 'Undo';
289 |     const redoLabel = redoConfig.label || 'Redo';
290 |     return (
291 |       <ButtonGroup key={name}>
292 |         <IconButton
293 |           {...toolbarConfig.extraProps}
294 |           label={undoLabel}
295 |           iconName="undo"
296 |           isDisabled={!canUndo}
297 |           onClick={this._undo}
298 |           focusOnClick={false}
299 |         />
300 |         <IconButton
301 |           {...toolbarConfig.extraProps}
302 |           label={redoLabel}
303 |           iconName="redo"
304 |           isDisabled={!canRedo}
305 |           onClick={this._redo}
306 |           focusOnClick={false}
307 |         />
308 |       </ButtonGroup>
309 |     );
310 |   }
311 | 
312 |   _onKeypress(event: Object, eventFlags: Object) {
313 |     // Catch cmd+k for use with link insertion.
314 |     if (hasCommandModifier(event) && event.keyCode === 75) {
315 |       let {editorState} = this.props;
316 |       if (!editorState.getSelection().isCollapsed()) {
317 |         this.setState({showLinkInput: true});
318 |         eventFlags.wasHandled = true;
319 |       }
320 |     }
321 |   }
322 | 
323 |   _toggleShowLinkInput(event: ?Object) {
324 |     let isShowing = this.state.showLinkInput;
325 |     // If this is a hide request, decide if we should focus the editor.
326 |     if (isShowing) {
327 |       let shouldFocusEditor = true;
328 |       if (event && event.type === 'click') {
329 |         // TODO: Use a better way to get the editor root node.
330 |         let editorRoot = ReactDOM.findDOMNode(this).parentNode;
331 |         let {activeElement} = document;
332 |         let wasClickAway = (activeElement == null || activeElement === document.body);
333 |         if (!wasClickAway && !editorRoot.contains(activeElement)) {
334 |           shouldFocusEditor = false;
335 |         }
336 |       }
337 |       if (shouldFocusEditor) {
338 |         this.props.focusEditor();
339 |       }
340 |     }
341 |     this.setState({showLinkInput: !isShowing});
342 |   }
343 | 
344 |   _toggleShowImageInput(event: ?Object) {
345 |     let isShowing = this.state.showImageInput;
346 |     // If this is a hide request, decide if we should focus the editor.
347 |     if (isShowing) {
348 |       let shouldFocusEditor = true;
349 |       if (event && event.type === 'click') {
350 |         // TODO: Use a better way to get the editor root node.
351 |         let editorRoot = ReactDOM.findDOMNode(this).parentNode;
352 |         let {activeElement} = document;
353 |         let wasClickAway = (activeElement == null || activeElement === document.body);
354 |         if (!wasClickAway && !editorRoot.contains(activeElement)) {
355 |           shouldFocusEditor = false;
356 |         }
357 |       }
358 |       if (shouldFocusEditor) {
359 |         this.props.focusEditor();
360 |       }
361 |     }
362 |     this.setState({showImageInput: !isShowing});
363 |   }
364 | 
365 |   _setImage(src: string) {
366 |     let {editorState} = this.props;
367 |     let contentState = editorState.getCurrentContent();
368 |     let selection = editorState.getSelection();
369 |     contentState = contentState.createEntity(ENTITY_TYPE.IMAGE, 'IMMUTABLE', {src});
370 |     let entityKey = contentState.getLastCreatedEntityKey();
371 |     let newContentState = Modifier.insertText(contentState, selection, ' ', null, entityKey);
372 |     this.setState({showImageInput: false});
373 |     this.props.onChange(
374 |       EditorState.push(editorState, newContentState)
375 |     );
376 |     this._focusEditor();
377 |   }
378 | 
379 |   _setLink(url: string, checkOptions: {[key: string]: boolean}) {
380 |     let {editorState} = this.props;
381 |     let contentState = editorState.getCurrentContent();
382 |     let selection = editorState.getSelection();
383 |     let origSelection = selection;
384 |     let canApplyLink = false;
385 | 
386 |     if (selection.isCollapsed()) {
387 |       let entity = this._getEntityDescriptionAtCursor();
388 |       if (entity) {
389 |         canApplyLink = true;
390 |         selection = selection.merge({
391 |           anchorOffset: entity.startOffset,
392 |           focusOffset: entity.endOffset,
393 |           isBackward: false,
394 |         });
395 |       }
396 |     } else {
397 |       canApplyLink = true;
398 |     }
399 | 
400 |     this.setState({showLinkInput: false});
401 |     if (canApplyLink) {
402 |       let target = checkOptions.targetBlank ? '_blank' : undefined;
403 |       let rel = checkOptions.noFollow ? 'nofollow' : undefined;
404 |       contentState = contentState.createEntity(ENTITY_TYPE.LINK, 'MUTABLE', {url, target, rel});
405 |       let entityKey = contentState.getLastCreatedEntityKey();
406 | 
407 |       editorState = EditorState.push(editorState, contentState);
408 |       editorState = RichUtils.toggleLink(editorState, selection, entityKey);
409 |       editorState = EditorState.acceptSelection(editorState, origSelection);
410 | 
411 |       this.props.onChange(editorState);
412 |     }
413 |     this._focusEditor();
414 |   }
415 | 
416 |   _removeLink() {
417 |     let {editorState} = this.props;
418 |     let entity = getEntityAtCursor(editorState);
419 |     if (entity != null) {
420 |       let {blockKey, startOffset, endOffset} = entity;
421 |       this.props.onChange(
422 |         clearEntityForRange(editorState, blockKey, startOffset, endOffset)
423 |       );
424 |     }
425 |   }
426 | 
427 |   _getEntityDescriptionAtCursor(): ?EntityDescription {
428 |     let {editorState} = this.props;
429 |     return getEntityAtCursor(editorState);
430 |   }
431 | 
432 |   _getEntityAtCursor(): ?Entity {
433 |     let {editorState} = this.props;
434 |     let contentState = editorState.getCurrentContent();
435 |     let entity = getEntityAtCursor(editorState);
436 |     return (entity == null) ? null : contentState.getEntity(entity.entityKey);
437 |   }
438 | 
439 |   _getCurrentBlockType(): string {
440 |     let {editorState} = this.props;
441 |     let selection = editorState.getSelection();
442 |     return editorState
443 |       .getCurrentContent()
444 |       .getBlockForKey(selection.getStartKey())
445 |       .getType();
446 |   }
447 | 
448 |   _selectBlockType() {
449 |     this._toggleBlockType(...arguments);
450 |     this._focusEditor();
451 |   }
452 | 
453 |   _toggleBlockType(blockType: string) {
454 |     this.props.onChange(
455 |       RichUtils.toggleBlockType(
456 |         this.props.editorState,
457 |         blockType
458 |       )
459 |     );
460 |   }
461 | 
462 |   _toggleInlineStyle(inlineStyle: string) {
463 |     this.props.onChange(
464 |       RichUtils.toggleInlineStyle(
465 |         this.props.editorState,
466 |         inlineStyle
467 |       )
468 |     );
469 |   }
470 | 
471 |   _toggleAlignment(textAlign: string) {
472 |     let {editorState} = this.props;
473 |     let selection = editorState.getSelection();
474 | 
475 |     let content = editorState.getCurrentContent();
476 |     let blockKey = selection.getStartKey();
477 |     let block = content.getBlockForKey(blockKey);
478 |     let blockData = block.getData();
479 | 
480 |     let newBlockData;
481 |     if (blockData.get('textAlign') === textAlign) {
482 |       newBlockData = blockData.remove('textAlign');
483 |     } else {
484 |       newBlockData = blockData.set('textAlign', textAlign);
485 |     }
486 | 
487 |     let newBlock = block.set('data', newBlockData);
488 | 
489 |     let newContent = content.merge({
490 |       blockMap: content.getBlockMap().set(blockKey, newBlock),
491 |     });
492 |     let newState = EditorState.push(
493 |       editorState,
494 |       newContent,
495 |       'change-block-data'
496 |     );
497 |     this.props.onChange(newState);
498 |   }
499 | 
500 |   _undo() {
501 |     let {editorState} = this.props;
502 |     this.props.onChange(
503 |       EditorState.undo(editorState)
504 |     );
505 |   }
506 | 
507 |   _redo() {
508 |     let {editorState} = this.props;
509 |     this.props.onChange(
510 |       EditorState.redo(editorState)
511 |     );
512 |   }
513 | 
514 |   _focusEditor() {
515 |     // Hacky: Wait to focus the editor so we don't lose selection.
516 |     setTimeout(() => {
517 |       this.props.focusEditor();
518 |     }, 50);
519 |   }
520 | }
521 | 


--------------------------------------------------------------------------------
/src/lib/EditorToolbarConfig.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import type {EditorState} from 'draft-js';
 3 | 
 4 | export type StyleConfig = {
 5 |   label: string;
 6 |   style: string;
 7 |   className?: string;
 8 | };
 9 | 
10 | type GetControlState = (key: string) => ?string;
11 | type SetControlState = (key: string, value: string) => void;
12 | 
13 | export type CustomControl = ReactNode | (set: SetControlState, get: GetControlState, state: EditorState) => ReactNode;
14 | export type CustomControlList = Array<CustomControl>;
15 | 
16 | export type StyleConfigList = Array<StyleConfig>;
17 | 
18 | export type GroupName = 'INLINE_STYLE_BUTTONS' | 'BLOCK_ALIGNMENT_BUTTONS' | 'BLOCK_TYPE_BUTTONS' | 'LINK_BUTTONS' | 'BLOCK_TYPE_DROPDOWN' | 'HISTORY_BUTTONS' | 'IMAGE_BUTTON';
19 | 
20 | export type ToolbarConfig = {
21 |   display: Array<GroupName>;
22 |   extraProps?: Object;
23 |   INLINE_STYLE_BUTTONS: StyleConfigList;
24 |   BLOCK_ALIGNMENT_BUTTONS: StyleConfigList;
25 |   BLOCK_TYPE_DROPDOWN: StyleConfigList;
26 |   BLOCK_TYPE_BUTTONS: StyleConfigList;
27 | };
28 | 
29 | export const INLINE_STYLE_BUTTONS: StyleConfigList = [
30 |   {label: 'Bold', style: 'BOLD'},
31 |   {label: 'Italic', style: 'ITALIC'},
32 |   {label: 'Strikethrough', style: 'STRIKETHROUGH'},
33 |   {label: 'Monospace', style: 'CODE'},
34 |   {label: 'Underline', style: 'UNDERLINE'},
35 | ];
36 | 
37 | export const BLOCK_ALIGNMENT_BUTTONS: StyleConfigList = [
38 |   {label: 'Align Left', style: 'ALIGN_LEFT'},
39 |   {label: 'Align Center', style: 'ALIGN_CENTER'},
40 |   {label: 'Align Right', style: 'ALIGN_RIGHT'},
41 |   {label: 'Align Justify', style: 'ALIGN_JUSTIFY'},
42 | ];
43 | 
44 | export const BLOCK_TYPE_DROPDOWN: StyleConfigList = [
45 |   {label: 'Normal', style: 'unstyled'},
46 |   {label: 'Heading Large', style: 'header-one'},
47 |   {label: 'Heading Medium', style: 'header-two'},
48 |   {label: 'Heading Small', style: 'header-three'},
49 |   {label: 'Code Block', style: 'code-block'},
50 | ];
51 | export const BLOCK_TYPE_BUTTONS: StyleConfigList = [
52 |   {label: 'UL', style: 'unordered-list-item'},
53 |   {label: 'OL', style: 'ordered-list-item'},
54 |   {label: 'Blockquote', style: 'blockquote'},
55 | ];
56 | 
57 | let EditorToolbarConfig: ToolbarConfig = {
58 |   display: ['INLINE_STYLE_BUTTONS', 'BLOCK_ALIGNMENT_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'LINK_BUTTONS', 'IMAGE_BUTTON', 'BLOCK_TYPE_DROPDOWN', 'HISTORY_BUTTONS'],
59 |   INLINE_STYLE_BUTTONS,
60 |   BLOCK_ALIGNMENT_BUTTONS,
61 |   BLOCK_TYPE_DROPDOWN,
62 |   BLOCK_TYPE_BUTTONS,
63 | };
64 | 
65 | export default EditorToolbarConfig;
66 | 


--------------------------------------------------------------------------------
/src/lib/EditorValue.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | import {ContentState, EditorState, convertToRaw, convertFromRaw} from 'draft-js';
  3 | import {stateToHTML} from 'draft-js-export-html';
  4 | import {stateFromHTML} from 'draft-js-import-html';
  5 | import {stateToMarkdown} from 'draft-js-export-markdown';
  6 | import {stateFromMarkdown} from 'draft-js-import-markdown';
  7 | 
  8 | import type {DraftDecoratorType as Decorator} from 'draft-js/lib/DraftDecoratorType';
  9 | import type {Options as ImportOptions} from 'draft-js-import-html';
 10 | import type {Options as ExportOptions} from 'draft-js-export-html';
 11 | export type {ImportOptions, ExportOptions};
 12 | 
 13 | type StringMap = {[key: string]: string};
 14 | 
 15 | export default class EditorValue {
 16 |   _editorState: EditorState;
 17 |   _cache: StringMap;
 18 | 
 19 |   constructor(editorState: EditorState, cache: StringMap = {}) {
 20 |     this._cache = cache;
 21 |     this._editorState = editorState;
 22 |   }
 23 | 
 24 |   getEditorState(): EditorState {
 25 |     return this._editorState;
 26 |   }
 27 | 
 28 |   setEditorState(editorState: EditorState): EditorValue {
 29 |     return (this._editorState === editorState) ?
 30 |       this :
 31 |       new EditorValue(editorState);
 32 |   }
 33 | 
 34 |   toString(format: string, options?: ExportOptions): string {
 35 |     let fromCache = this._cache[format];
 36 |     if (fromCache != null) {
 37 |       return fromCache;
 38 |     }
 39 |     return (this._cache[format] = toString(this.getEditorState(), format, options));
 40 |   }
 41 | 
 42 |   setContentFromString(markup: string, format: string, options?: ImportOptions): EditorValue {
 43 |     let editorState = EditorState.push(
 44 |       this._editorState,
 45 |       fromString(markup, format, options),
 46 |       'secondary-paste'
 47 |     );
 48 |     return new EditorValue(editorState, {[format]: markup});
 49 |   }
 50 | 
 51 |   static createEmpty(decorator: ?Decorator): EditorValue {
 52 |     let editorState = EditorState.createEmpty(decorator);
 53 |     return new EditorValue(editorState);
 54 |   }
 55 | 
 56 |   static createFromState(editorState: EditorState): EditorValue {
 57 |     return new EditorValue(editorState);
 58 |   }
 59 | 
 60 |   static createFromString(markup: string, format: string, decorator: ?Decorator, options?: ImportOptions): EditorValue {
 61 |     let contentState = fromString(markup, format, options);
 62 |     let editorState = EditorState.createWithContent(contentState, decorator);
 63 |     return new EditorValue(editorState, {[format]: markup});
 64 |   }
 65 | }
 66 | 
 67 | function toString(editorState: EditorState, format: string, options?: ExportOptions): string {
 68 |   let contentState = editorState.getCurrentContent();
 69 |   switch (format) {
 70 |     case 'html': {
 71 |       return stateToHTML(contentState, options);
 72 |     }
 73 |     case 'markdown': {
 74 |       return stateToMarkdown(contentState);
 75 |     }
 76 |     case 'raw': {
 77 |       return JSON.stringify(convertToRaw(contentState));
 78 |     }
 79 |     default: {
 80 |       throw new Error('Format not supported: ' + format);
 81 |     }
 82 |   }
 83 | }
 84 | 
 85 | function fromString(markup: string, format: string, options?: ImportOptions): ContentState {
 86 |   switch (format) {
 87 |     case 'html': {
 88 |       return stateFromHTML(markup, options);
 89 |     }
 90 |     case 'markdown': {
 91 |       return stateFromMarkdown(markup, options);
 92 |     }
 93 |     case 'raw': {
 94 |       return convertFromRaw(JSON.parse(markup));
 95 |     }
 96 |     default: {
 97 |       throw new Error('Format not supported: ' + format);
 98 |     }
 99 |   }
100 | }
101 | 


--------------------------------------------------------------------------------
/src/lib/ImageDecorator.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import ImageSpan from '../ui/ImageSpan';
 3 | import {ENTITY_TYPE} from 'draft-js-utils';
 4 | 
 5 | import type {ContentBlock, ContentState} from 'draft-js';
 6 | 
 7 | type EntityRangeCallback = (start: number, end: number) => void;
 8 | 
 9 | function findImageEntities(contentBlock: ContentBlock, callback: EntityRangeCallback, contentState: ?ContentState) {
10 |   contentBlock.findEntityRanges((character) => {
11 |     const entityKey = character.getEntity();
12 |     if (entityKey != null) {
13 |       let entity = contentState ? contentState.getEntity(entityKey) : null;
14 |       return entity != null && entity.getType() === ENTITY_TYPE.IMAGE;
15 |     }
16 |     return false;
17 |   }, callback);
18 | }
19 | 
20 | export default {
21 |   strategy: findImageEntities,
22 |   component: ImageSpan,
23 | };
24 | 


--------------------------------------------------------------------------------
/src/lib/LinkDecorator.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import React from 'react';
 3 | import {ENTITY_TYPE} from 'draft-js-utils';
 4 | 
 5 | import type {ContentBlock, ContentState} from 'draft-js';
 6 | 
 7 | type Props = {
 8 |   children: ReactNode,
 9 |   entityKey: string,
10 |   contentState: ContentState,
11 | };
12 | 
13 | type EntityRangeCallback = (start: number, end: number) => void;
14 | 
15 | function Link(props: Props) {
16 |   const {url} = props.contentState.getEntity(props.entityKey).getData();
17 |   return (
18 |     <a href={url}>{props.children}</a>
19 |   );
20 | }
21 | 
22 | function findLinkEntities(contentBlock: ContentBlock, callback: EntityRangeCallback, contentState: ?ContentState) {
23 |   contentBlock.findEntityRanges((character) => {
24 |     const entityKey = character.getEntity();
25 |     if (entityKey != null) {
26 |       let entity = contentState ? contentState.getEntity(entityKey) : null;
27 |       return entity != null && entity.getType() === ENTITY_TYPE.LINK;
28 |     }
29 |     return false;
30 |   }, callback);
31 | }
32 | 
33 | export default {
34 |   strategy: findLinkEntities,
35 |   component: Link,
36 | };
37 | 


--------------------------------------------------------------------------------
/src/lib/StyleButton.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import React, {Component} from 'react';
 3 | import IconButton from '../ui/IconButton';
 4 | import autobind from 'class-autobind';
 5 | 
 6 | type Props = {
 7 |   style: string;
 8 |   onToggle: (style: string) => any;
 9 | };
10 | 
11 | export default class StyleButton extends Component {
12 |   props: Props;
13 | 
14 |   constructor() {
15 |     super(...arguments);
16 |     autobind(this);
17 |   }
18 | 
19 |   render() {
20 |     let {style, onToggle, ...otherProps} = this.props; // eslint-disable-line no-unused-vars
21 |     let iconName = style.toLowerCase();
22 |     // `focusOnClick` will prevent the editor from losing focus when a control
23 |     // button is clicked.
24 |     return (
25 |       <IconButton
26 |         {...otherProps}
27 |         iconName={iconName}
28 |         onClick={this._onClick}
29 |         focusOnClick={false}
30 |         isSwitch={true}
31 |       />
32 |     );
33 |   }
34 | 
35 |   _onClick() {
36 |     this.props.onToggle(this.props.style);
37 |   }
38 | }
39 | 


--------------------------------------------------------------------------------
/src/lib/__tests__/composite-test.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | const {describe, it} = global;
 3 | 
 4 | import composite from '../composite';
 5 | import expect from 'expect';
 6 | 
 7 | describe('composite', () => {
 8 |   it('should return the composite of two functions', () => {
 9 |     let addOne = (x) => x + 1;
10 |     let addTwo = (x) => x + 2;
11 |     expect(
12 |       composite(addOne, addTwo)(5)
13 |     ).toBe(7);
14 |     expect(
15 |       composite(addOne, undefined)(5)
16 |     ).toBe(6);
17 |   });
18 | });
19 | 


--------------------------------------------------------------------------------
/src/lib/blockStyleFunctions.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import type {BlockStyleFn, CustomBlockFn} from 'draft-js-import-html';
 3 | 
 4 | export const getTextAlignClassName: BlockStyleFn = (contentBlock) => {
 5 |   switch (contentBlock.getData().get('textAlign')) {
 6 |     case 'ALIGN_LEFT':
 7 |       return 'text-align--left';
 8 | 
 9 |     case 'ALIGN_CENTER':
10 |       return 'text-align--center';
11 | 
12 |     case 'ALIGN_RIGHT':
13 |       return 'text-align--right';
14 | 
15 |     case 'ALIGN_JUSTIFY':
16 |       return 'text-align--justify';
17 | 
18 |     default:
19 |       return '';
20 |   }
21 | };
22 | 
23 | export const getTextAlignStyles: BlockStyleFn = (contentBlock) => {
24 |   switch (contentBlock.getData().get('textAlign')) {
25 |     case 'ALIGN_LEFT':
26 |       return {
27 |         style: {
28 |           textAlign: 'left',
29 |         },
30 |       };
31 | 
32 |     case 'ALIGN_CENTER':
33 |       return {
34 |         style: {
35 |           textAlign: 'center',
36 |         },
37 |       };
38 | 
39 |     case 'ALIGN_RIGHT':
40 |       return {
41 |         style: {
42 |           textAlign: 'right',
43 |         },
44 |       };
45 | 
46 |     case 'ALIGN_JUSTIFY':
47 |       return {
48 |         style: {
49 |           textAlign: 'justify',
50 |         },
51 |       };
52 | 
53 |     default:
54 |       return {};
55 |   }
56 | };
57 | 
58 | export const getTextAlignBlockMetadata: CustomBlockFn = (element) => {
59 |   switch (element.style.textAlign) {
60 |     case 'right':
61 |       return {
62 |         data: {
63 |           textAlign: 'ALIGN_RIGHT',
64 |         },
65 |       };
66 | 
67 |     case 'center':
68 |       return {
69 |         data: {
70 |           textAlign: 'ALIGN_CENTER',
71 |         },
72 |       };
73 | 
74 |     case 'justify':
75 |       return {
76 |         data: {
77 |           textAlign: 'ALIGN_JUSTIFY',
78 |         },
79 |       };
80 | 
81 |     case 'left':
82 |       return {
83 |         data: {
84 |           textAlign: 'ALIGN_LEFT',
85 |         },
86 |       };
87 | 
88 |     default:
89 |       return {};
90 |   }
91 | };
92 | 


--------------------------------------------------------------------------------
/src/lib/changeBlockDepth.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import {EditorState} from 'draft-js';
 4 | 
 5 | export default function changeBlockDepth(
 6 |   editorState: EditorState,
 7 |   blockKey: string,
 8 |   newDepth: number,
 9 | ): EditorState {
10 |   let content = editorState.getCurrentContent();
11 |   let block = content.getBlockForKey(blockKey);
12 |   let depth = block.getDepth();
13 |   if (depth === newDepth) {
14 |     return editorState;
15 |   }
16 |   let newBlock = block.set('depth', newDepth);
17 |   let newContent = content.merge({
18 |     blockMap: content.getBlockMap().set(blockKey, newBlock),
19 |   });
20 |   return EditorState.push(
21 |     editorState,
22 |     newContent,
23 |     'adjust-depth'
24 |   );
25 | }
26 | 


--------------------------------------------------------------------------------
/src/lib/changeBlockType.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import {EditorState} from 'draft-js';
 4 | 
 5 | export default function changeBlockType(
 6 |   editorState: EditorState,
 7 |   blockKey: string,
 8 |   newType: string,
 9 | ): EditorState {
10 |   let content = editorState.getCurrentContent();
11 |   let block = content.getBlockForKey(blockKey);
12 |   let type = block.getType();
13 |   if (type === newType) {
14 |     return editorState;
15 |   }
16 |   let newBlock = block.set('type', newType);
17 |   let newContent = content.merge({
18 |     blockMap: content.getBlockMap().set(blockKey, newBlock),
19 |   });
20 |   return EditorState.push(
21 |     editorState,
22 |     newContent,
23 |     'change-block-type'
24 |   );
25 | }
26 | 


--------------------------------------------------------------------------------
/src/lib/clearEntityForRange.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import {CharacterMetadata, EditorState} from 'draft-js';
 3 | 
 4 | export default function clearEntityForRange(
 5 |   editorState: EditorState,
 6 |   blockKey: string,
 7 |   startOffset: number,
 8 |   endOffset: number,
 9 | ): EditorState {
10 |   let contentState = editorState.getCurrentContent();
11 |   let blockMap = contentState.getBlockMap();
12 |   let block = blockMap.get(blockKey);
13 |   let charList = block.getCharacterList();
14 |   let newCharList = charList.map((char: CharacterMetadata, i) => {
15 |     if (i >= startOffset && i < endOffset) {
16 |       return CharacterMetadata.applyEntity(char, null);
17 |     } else {
18 |       return char;
19 |     }
20 |   });
21 |   let newBlock = block.set('characterList', newCharList);
22 |   let newBlockMap = blockMap.set(blockKey, newBlock);
23 |   let newContentState = contentState.set('blockMap', newBlockMap);
24 |   return EditorState.push(editorState, newContentState, 'apply-entity');
25 | }
26 | 


--------------------------------------------------------------------------------
/src/lib/composite.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | function composite<T, U>(
 4 |   defaultFunc: (input: T) => U,
 5 |   customFunc?: (input: T) => ?U,
 6 | ): (input: T) => U {
 7 |   return (input: T) => {
 8 |     if (customFunc) {
 9 |       let result = customFunc(input);
10 |       if (result != null) {
11 |         return result;
12 |       }
13 |     }
14 |     return defaultFunc(input);
15 |   };
16 | }
17 | 
18 | export default composite;
19 | 


--------------------------------------------------------------------------------
/src/lib/getBlocksInSelection.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import {EditorState} from 'draft-js';
 3 | import {OrderedMap} from 'immutable';
 4 | 
 5 | export default function getBlocksInSelection(
 6 |   editorState: EditorState,
 7 | ): EditorState {
 8 |   let contentState = editorState.getCurrentContent();
 9 |   let blockMap = contentState.getBlockMap();
10 |   let selection = editorState.getSelection();
11 |   if (selection.isCollapsed()) {
12 |     return new OrderedMap();
13 |   }
14 | 
15 |   let startKey = selection.getStartKey();
16 |   let endKey = selection.getEndKey();
17 |   if (startKey === endKey) {
18 |     return new OrderedMap({startKey: contentState.getBlockForKey(startKey)});
19 |   }
20 |   let blocksUntilEnd = blockMap.takeUntil((block, key) => key === endKey);
21 |   return blocksUntilEnd.skipUntil((block, key) => key === startKey);
22 | }
23 | 


--------------------------------------------------------------------------------
/src/lib/getEntityAtCursor.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import type {EditorState, ContentBlock} from 'draft-js';
 3 | 
 4 | export type EntityDescription = {
 5 |   entityKey: string;
 6 |   blockKey: string;
 7 |   startOffset: number;
 8 |   endOffset: number;
 9 | };
10 | 
11 | function getEntityAtOffset(block: ContentBlock, offset: number): ?EntityDescription {
12 |   let entityKey = block.getEntityAt(offset);
13 |   if (entityKey == null) {
14 |     return null;
15 |   }
16 |   let startOffset = offset;
17 |   while (startOffset > 0 && block.getEntityAt(startOffset - 1) === entityKey) {
18 |     startOffset -= 1;
19 |   }
20 |   let endOffset = startOffset;
21 |   let blockLength = block.getLength();
22 |   while (endOffset < blockLength && block.getEntityAt(endOffset + 1) === entityKey) {
23 |     endOffset += 1;
24 |   }
25 |   return {
26 |     entityKey,
27 |     blockKey: block.getKey(),
28 |     startOffset,
29 |     endOffset: endOffset + 1,
30 |   };
31 | }
32 | 
33 | export default function getEntityAtCursor(editorState: EditorState): ?EntityDescription {
34 |   let selection = editorState.getSelection();
35 |   let startKey = selection.getStartKey();
36 |   let startBlock = editorState.getCurrentContent().getBlockForKey(startKey);
37 |   let startOffset = selection.getStartOffset();
38 |   if (selection.isCollapsed()) {
39 |     // Get the entity before the cursor (unless the cursor is at the start).
40 |     return getEntityAtOffset(startBlock, startOffset === 0 ? startOffset : startOffset - 1);
41 |   }
42 |   if (startKey !== selection.getEndKey()) {
43 |     return null;
44 |   }
45 |   let endOffset = selection.getEndOffset();
46 |   let startEntityKey = startBlock.getEntityAt(startOffset);
47 |   for (let i = startOffset; i < endOffset; i++) {
48 |     let entityKey = startBlock.getEntityAt(i);
49 |     if (entityKey == null || entityKey !== startEntityKey) {
50 |       return null;
51 |     }
52 |   }
53 |   return {
54 |     entityKey: startEntityKey,
55 |     blockKey: startBlock.getKey(),
56 |     startOffset: startOffset,
57 |     endOffset: endOffset,
58 |   };
59 | }
60 | 


--------------------------------------------------------------------------------
/src/lib/insertBlockAfter.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import {ContentBlock, EditorState, genKey} from 'draft-js';
 4 | 
 5 | export default function insertBlockAfter(
 6 |   editorState: EditorState,
 7 |   blockKey: string,
 8 |   newType: string,
 9 | ): EditorState {
10 |   let content = editorState.getCurrentContent();
11 |   let blockMap = content.getBlockMap();
12 |   let block = blockMap.get(blockKey);
13 |   let blocksBefore = blockMap.toSeq().takeUntil((v) => (v === block));
14 |   let blocksAfter = blockMap.toSeq().skipUntil((v) => (v === block)).rest();
15 |   let newBlockKey = genKey();
16 |   let newBlock = new ContentBlock({
17 |     key: newBlockKey,
18 |     type: newType,
19 |     text: '',
20 |     characterList: block.getCharacterList().slice(0, 0),
21 |     depth: 0,
22 |   });
23 |   let newBlockMap = blocksBefore.concat(
24 |     [[blockKey, block], [newBlockKey, newBlock]],
25 |     blocksAfter,
26 |   ).toOrderedMap();
27 |   let selection = editorState.getSelection();
28 |   let newContent = content.merge({
29 |     blockMap: newBlockMap,
30 |     selectionBefore: selection,
31 |     selectionAfter: selection.merge({
32 |       anchorKey: newBlockKey,
33 |       anchorOffset: 0,
34 |       focusKey: newBlockKey,
35 |       focusOffset: 0,
36 |       isBackward: false,
37 |     }),
38 |   });
39 |   return EditorState.push(editorState, newContent, 'split-block');
40 | }
41 | 


--------------------------------------------------------------------------------
/src/lib/isListItem.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import {BLOCK_TYPE} from 'draft-js-utils';
 4 | 
 5 | import type {ContentBlock} from 'draft-js';
 6 | 
 7 | 
 8 | export default function isListItem(block: ContentBlock): boolean {
 9 |   let blockType = block.getType();
10 |   return (
11 |     blockType === BLOCK_TYPE.UNORDERED_LIST_ITEM ||
12 |     blockType === BLOCK_TYPE.ORDERED_LIST_ITEM
13 |   );
14 | }
15 | 


--------------------------------------------------------------------------------
/src/ui/Button.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   display: inline-block;
 3 |   margin: 0 5px 0 0;
 4 |   padding: 3px 8px;
 5 |   height: 30px;
 6 |   line-height: 22px;
 7 |   box-sizing: border-box;
 8 |   background: none #fdfdfd;
 9 |   background: linear-gradient(to bottom, #fdfdfd 0%,#f6f7f8 100%);
10 |   border: 1px solid #999;
11 |   border-radius: 2px;
12 |   color: #333;
13 |   text-decoration: none;
14 |   font-size: inherit;
15 |   font-family: inherit;
16 |   cursor: pointer;
17 |   white-space: nowrap;
18 | }
19 | 
20 | .root:disabled {
21 |   cursor: not-allowed;
22 |   background: none transparent;
23 | }
24 | 
25 | .root:disabled > * {
26 |   opacity: .5;
27 | }
28 | 


--------------------------------------------------------------------------------
/src/ui/Button.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import React, {Component} from 'react';
 4 | import cx from 'classnames';
 5 | import autobind from 'class-autobind';
 6 | 
 7 | import styles from './Button.css';
 8 | 
 9 | type EventHandler = (event: Event) => any;
10 | 
11 | type Props = {
12 |   children?: ReactNode;
13 |   className?: string;
14 |   focusOnClick?: boolean;
15 |   formSubmit?: boolean;
16 |   isDisabled?: boolean;
17 |   onMouseDown?: EventHandler;
18 | };
19 | 
20 | export default class Button extends Component {
21 |   props: Props;
22 | 
23 |   constructor() {
24 |     super(...arguments);
25 |     autobind(this);
26 |   }
27 | 
28 |   render() {
29 |     let {props} = this;
30 |     let {className, isDisabled, focusOnClick, formSubmit, ...otherProps} = props;
31 |     className = cx(className, styles.root);
32 |     let onMouseDown = (focusOnClick === false) ? this._onMouseDownPreventDefault : props.onMouseDown;
33 |     let type = formSubmit ? 'submit' : 'button';
34 |     return (
35 |       <button type={type} {...otherProps} onMouseDown={onMouseDown} className={className} disabled={isDisabled}>
36 |         {props.children}
37 |       </button>
38 |     );
39 |   }
40 | 
41 |   _onMouseDownPreventDefault(event: Event) {
42 |     event.preventDefault();
43 |     let {onMouseDown} = this.props;
44 |     if (onMouseDown != null) {
45 |       onMouseDown(event);
46 |     }
47 |   }
48 | }
49 | 


--------------------------------------------------------------------------------
/src/ui/ButtonGroup.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   display: inline-block;
 3 |   vertical-align: top;
 4 |   margin: 0 5px 5px 0;
 5 |   white-space: nowrap;
 6 | }
 7 | 
 8 | .root:last-child {
 9 |   margin-right: 0;
10 | }
11 | 
12 | /* TODO: remove all this child selector and tag selector stuff. */
13 | 
14 | .root > div > button {
15 |   margin-right: 0;
16 |   border-radius: 0;
17 | }
18 | 
19 | .root > div > button:focus {
20 |   position: relative;
21 |   z-index: 1;
22 | }
23 | 
24 | .root > div:first-child > button {
25 |   border-top-left-radius: 2px;
26 |   border-bottom-left-radius: 2px;
27 | }
28 | 
29 | .root > div + div > button {
30 |   border-left-width: 0;
31 | }
32 | 
33 | .root > div:last-child > button {
34 |   border-top-right-radius: 2px;
35 |   border-bottom-right-radius: 2px;
36 | }
37 | 


--------------------------------------------------------------------------------
/src/ui/ButtonGroup.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import React from 'react';
 4 | import cx from 'classnames';
 5 | 
 6 | import styles from './ButtonGroup.css';
 7 | 
 8 | type Props = {
 9 |   className?: string;
10 | };
11 | 
12 | export default function ButtonGroup(props: Props) {
13 |   let className = cx(props.className, styles.root);
14 |   return (
15 |     <div {...props} className={className} />
16 |   );
17 | }
18 | 


--------------------------------------------------------------------------------
/src/ui/ButtonWrap.css:
--------------------------------------------------------------------------------
1 | .root {
2 |   display: inline-block;
3 |   position: relative;
4 |   /* This ensures the popover will show on top of the editor. */
5 |   z-index: 10;
6 | }
7 | 


--------------------------------------------------------------------------------
/src/ui/ButtonWrap.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import React from 'react';
 4 | import cx from 'classnames';
 5 | 
 6 | import styles from './ButtonWrap.css';
 7 | 
 8 | type Props = {
 9 |   className?: string;
10 | };
11 | 
12 | export default function ButtonWrap(props: Props) {
13 |   let className = cx(props.className, styles.root);
14 |   return <div {...props} className={className} />;
15 | }
16 | 


--------------------------------------------------------------------------------
/src/ui/Dropdown.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   display: inline-block;
 3 |   position: relative;
 4 |   line-height: 22px;
 5 |   vertical-align: top;
 6 |   -webkit-user-select: none;
 7 |   -moz-user-select: none;
 8 |   -ms-user-select: none;
 9 |   user-select: none;
10 | }
11 | 
12 | .root select {
13 |   position: relative;
14 |   z-index: 2;
15 |   display: inline-block;
16 |   box-sizing: border-box;
17 |   height: 30px;
18 |   line-height: inherit;
19 |   font-family: inherit;
20 |   font-size: inherit;
21 |   color: inherit;
22 |   margin: 0;
23 |   padding: 0;
24 |   border: 4px solid transparent;
25 |   border-right-width: 10px;
26 |   border-left-width: 5px;
27 |   background: none transparent;
28 |   opacity: 0;
29 |   cursor: pointer;
30 | }
31 | 
32 | .root .value {
33 |   display: block;
34 |   position: absolute;
35 |   z-index: 1;
36 |   left: 0;
37 |   top: 0;
38 |   right: 0;
39 |   bottom: 0;
40 |   line-height: 23px;
41 |   border: 1px solid #999;
42 |   border-radius: 2px;
43 |   padding: 3px;
44 |   padding-right: 33px;
45 |   padding-left: 12px;
46 |   white-space: nowrap;
47 |   text-overflow: ellipsis;
48 | }
49 | 
50 | .root .value::before,
51 | .root .value::after {
52 |   display: block;
53 |   content: "";
54 |   position: absolute;
55 |   top: 50%;
56 |   right: 10px;
57 |   width: 0;
58 |   height: 0;
59 |   border: 4px solid transparent;
60 | }
61 | 
62 | .root .value::before {
63 |   margin-top: -10px;
64 |   border-bottom-color: #555;
65 | }
66 | 
67 | .root .value::after {
68 |   margin-top: 1px;
69 |   border-top-color: #555;
70 | }
71 | 
72 | .root select:focus + .value {
73 |   border-color: #66afe9;
74 | }
75 | 
76 | /* On Webkit we can style <select> to be transparant without turning off the
77 |    default focus styles. This is better for accessibility. */
78 | @media screen and (-webkit-min-device-pixel-ratio:0) {
79 |   .root select {
80 |     opacity: 1;
81 |     color: inherit;
82 |     -webkit-appearance: none;
83 |     border-left-width: 12px;
84 |     border-right-width: 35px;
85 |   }
86 | 
87 |   .root select + .value {
88 |     color: transparent;
89 |   }
90 | 
91 |   .root select:focus + .value {
92 |     border-color: #999;
93 |   }
94 | }
95 | 


--------------------------------------------------------------------------------
/src/ui/Dropdown.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | import React, {Component} from 'react';
 3 | import autobind from 'class-autobind';
 4 | import cx from 'classnames';
 5 | 
 6 | import styles from './Dropdown.css';
 7 | 
 8 | type Choice = {
 9 |   label: string;
10 |   className?: string;
11 | };
12 | 
13 | type Props = {
14 |   choices: Map<string, Choice>;
15 |   selectedKey: ?string;
16 |   onChange: (selectedKey: string) => any;
17 |   className?: string;
18 | };
19 | 
20 | export default class Dropdown extends Component {
21 |   props: Props;
22 | 
23 |   constructor() {
24 |     super(...arguments);
25 |     autobind(this);
26 |   }
27 | 
28 |   render() {
29 |     let {choices, selectedKey, className, ...otherProps} = this.props;
30 |     className = cx(className, styles.root);
31 |     let selectedItem = (selectedKey == null) ? null : choices.get(selectedKey);
32 |     let selectedValue = selectedItem && selectedItem.label || '';
33 |     return (
34 |       <span className={className} title={selectedValue}>
35 |         <select {...otherProps} value={selectedKey} onChange={this._onChange}>
36 |           {this._renderChoices()}
37 |         </select>
38 |         <span className={styles.value}>{selectedValue}</span>
39 |       </span>
40 |     );
41 |   }
42 | 
43 |   _onChange(event: Object) {
44 |     let value: string = event.target.value;
45 |     this.props.onChange(value);
46 |   }
47 | 
48 |   _renderChoices() {
49 |     let {choices} = this.props;
50 |     let entries = Array.from(choices.entries());
51 |     return entries.map(([key, {label, className}]) => (
52 |       <option key={key} value={key} className={className}>{label}</option>
53 |     ));
54 |   }
55 | }
56 | 


--------------------------------------------------------------------------------
/src/ui/IconButton.css:
--------------------------------------------------------------------------------
  1 | .root {
  2 |   padding-left: 3px;
  3 |   padding-right: 3px;
  4 | }
  5 | 
  6 | .icon {
  7 |   display: inline-block;
  8 |   width: 22px;
  9 |   height: 22px;
 10 |   background-position: center center;
 11 |   background-repeat: no-repeat;
 12 |   background-size: 18px;
 13 | }
 14 | 
 15 | .isActive {
 16 |   background: none #d8d8d8;
 17 | }
 18 | 
 19 | .icon-undo {
 20 |   composes: icon;
 21 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTcuODU2IDI0YzIuNjY1LTQuODMgMy4xMTUtMTIuMTk1LTcuMzU2LTExLjk1VjE4bC05LTkgOS05djUuODJDMjMuMDM4IDUuNDk1IDI0LjQzNSAxNi44OSAxNy44NTYgMjR6Ii8+PC9zdmc+");
 22 |   background-size: 14px;
 23 | }
 24 | .icon-redo {
 25 |   composes: icon;
 26 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMuNSA1LjgyVjBsOSA5LTkgOXYtNS45NUMzLjAzIDExLjgwNiAzLjQ3OCAxOS4xNyA2LjE0NCAyNC0uNDM2IDE2Ljg5Ljk2MiA1LjQ5NCAxMy41IDUuODJ6Ii8+PC9zdmc+");
 27 |   background-size: 14px;
 28 | }
 29 | 
 30 | .icon-unordered-list-item {
 31 |   composes: icon;
 32 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNNC42NTYgMy4zNDRIMTR2MS4zMTNINC42NTZWMy4zNDR6bTAgNS4zMTJWNy4zNDNIMTR2MS4zMTNINC42NTZ6bTAgNHYtMS4zMTNIMTR2MS4zMTNINC42NTZ6bS0yLTEuNTNxLjM3NSAwIC42NC4yNXQuMjY3LjYyNC0uMjY2LjYyNS0uNjQuMjUtLjYyNi0uMjVUMS43OCAxMnQuMjUtLjYyNS42MjYtLjI1em0wLTguMTI2cS40MDYgMCAuNzAzLjI4dC4yOTYuNzItLjI5Ny43Mi0uNzA0LjI4LS43MDMtLjI4VDEuNjU2IDR0LjI5Ny0uNzIuNzAzLS4yOHptMCA0cS40MDYgMCAuNzAzLjI4dC4yOTYuNzItLjI5Ny43Mi0uNzA0LjI4LS43MDMtLjI4VDEuNjU2IDh0LjI5Ny0uNzIuNzAzLS4yOHoiLz48L3N2Zz4=");
 33 | }
 34 | .icon-ordered-list-item {
 35 |   composes: icon;
 36 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNNC42NTYgOC42NTZWNy4zNDNIMTR2MS4zMTNINC42NTZ6bTAgNHYtMS4zMTNIMTR2MS4zMTNINC42NTZ6bTAtOS4zMTJIMTR2MS4zMTNINC42NTZWMy4zNDR6bS0zLjMxMiA0di0uNjg4aDJ2LjYyNWwtMS4yMiAxLjM3NmgxLjIydi42ODhoLTJWOC43MmwxLjE4OC0xLjM3NkgxLjM0NHptLjY1Ni0ydi0yaC0uNjU2di0uNjg4aDEuMzEzdjIuNjg4SDJ6bS0uNjU2IDZ2LS42ODhoMnYyLjY4OGgtMnYtLjY4OGgxLjMxM3YtLjMxM0gydi0uNjg4aC42NTd2LS4zMTNIMS4zNDR6Ii8+PC9zdmc+");
 37 | }
 38 | .icon-blockquote {
 39 |   composes: icon;
 40 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNOS4zNDQgMTEuMzQ0bDEuMzEzLTIuNjg4aC0ydi00aDR2NGwtMS4zMTMgMi42ODhoLTJ6bS01LjM0NCAwbDEuMzQ0LTIuNjg4aC0ydi00aDR2NEw2IDExLjM0NEg0eiIvPjwvc3ZnPg==");
 41 | }
 42 | 
 43 | .icon-bold {
 44 |   composes: icon;
 45 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNOSAxMC4zNDRxLjQzOCAwIC43Mi0uMjk3dC4yOC0uNzAzLS4yOC0uNzAzVDkgOC4zNDVINi42NTZ2Mkg5em0tMi4zNDQtNnYyaDJxLjQwNiAwIC43MDMtLjI5N3QuMjk2LS43MDMtLjI5Ny0uNzAzLS43MDQtLjI5NmgtMnptMy43NSAyLjg0NHExLjQzOC42NTYgMS40MzggMi4yOCAwIDEuMDY0LS43MDMgMS43OThUOS4zNzYgMTJoLTQuNzJWMi42NTZoNC4xOXExLjEyNCAwIDEuODkuNzh0Ljc2NiAxLjkwNy0xLjA5MyAxLjg0NHoiLz48L3N2Zz4=");
 46 | }
 47 | .icon-italic {
 48 |   composes: icon;
 49 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNNi42NTYgMi42NTZIMTJ2MmgtMS44NzVMNy44NzUgMTBoMS40N3YySDR2LTJoMS44NzVsMi4yNS01LjM0NGgtMS40N3YtMnoiLz48L3N2Zz4=");
 50 | }
 51 | .icon-underline {
 52 |   composes: icon;
 53 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNMy4zNDQgMTIuNjU2aDkuMzEzVjE0SDMuMzQ0di0xLjM0NHpNOCAxMS4zNDRxLTEuNjU2IDAtMi44MjgtMS4xNzJUNCA3LjM0NFYyaDEuNjU2djUuMzQ0cTAgLjk3LjY4OCAxLjY0VDggOS42NTh0MS42NTYtLjY3Mi42ODgtMS42NFYySDEydjUuMzQ0UTEyIDkgMTAuODI4IDEwLjE3MlQ4IDExLjM0NHoiLz48L3N2Zz4=");
 54 | }
 55 | .icon-strikethrough {
 56 |   composes: icon;
 57 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjMuNTcgMTJxLjE5IDAgLjMxLjEydC4xMi4zMXYuODU2cTAgLjE4OC0uMTIuMzA4dC0uMzEuMTJILjQzcS0uMTg4IDAtLjMwOC0uMTJUMCAxMy4yODZ2LS44NTdxMC0uMTkuMTItLjMxVC40MjggMTJIMjMuNTd6bS0xNy4xLS44NTdxLS4zNzYtLjQ3LS42ODQtMS4wNy0uNjQzLTEuMy0uNjQzLTIuNTIgMC0yLjQyMyAxLjc5NS00LjEzNyAxLjc4LTEuNyA1LjI2My0xLjcuNjcgMCAyLjIzOC4yNTMuODg0LjE2IDIuMzcuNjQyLjEzNS41MS4yODIgMS41OC4xODggMS42NDcuMTg4IDIuNDUgMCAuMjQyLS4wNjcuNjA0bC0uMTYuMDQtMS4xMjUtLjA4LS4xODgtLjAyN3EtLjY3LTEuOTk3LTEuMzgtMi43NDctMS4xNzgtMS4yMi0yLjgxMi0xLjIyLTEuNTI3IDAtMi40MzguNzktLjg5Ny43NzgtLjg5NyAxLjk1NiAwIC45NzcuODg0IDEuODc0dDMuNzM3IDEuNzI4cS45MjUuMjY4IDIuMzE4Ljg4NC43NzcuMzc1IDEuMjcyLjY5Nkg2LjQ3em02Ljc5IDMuNDI4aDUuNTAzcS4wOTQuNTIzLjA5NCAxLjIzMyAwIDEuNDg3LS41NSAyLjg0LS4zMDcuNzM2LS45NSAxLjM5Mi0uNDk2LjQ3LTEuNDYgMS4wODUtMS4wNy42NDMtMi4wNS44ODQtMS4wNy4yOC0yLjcxOC4yOC0xLjUyOCAwLTIuNjEzLS4zMDdsLTEuODc1LS41MzZxLS43NjMtLjIxMy0uOTY0LS4zNzQtLjEwNy0uMTA3LS4xMDctLjI5NXYtLjE3M3EwLTEuNDQ2LS4wMjYtMi4wOS0uMDEzLS40IDAtLjkxbC4wMjctLjQ5NnYtLjU4OGwxLjM2Ny0uMDI3cS4yLjQ1NS40MDIuOTV0LjMuNzUuMTY3LjM2M3EuNDcuNzYzIDEuMDcgMS4yNi41NzcuNDggMS40MDcuNzYyLjc5LjI5NSAxLjc2OC4yOTUuODU3IDAgMS44NjItLjM2MiAxLjAzLS4zNDggMS42MzQtMS4xNTIuNjMtLjgxNi42My0xLjcyNyAwLTEuMTI1LTEuMDg2LTIuMTAzLS40NTUtLjM4OC0xLjgzNS0uOTV6Ii8+PC9zdmc+");
 58 |   background-size: 14px;
 59 | }
 60 | .icon-code {
 61 |   composes: icon;
 62 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTE2IDExNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEuNDE0Ij48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0yMi40NjQgMjguNDhjMCAyLjg5NS4zNDQgNS45MDUuODA2IDkuMDIuMzQyIDMuMDEuNjkgNi4wMi42OSA4LjkxNyAwIDMuNTYyLS45MTcgNy43OS04Ljk1NSA3LjkxMnY3LjIzNmM4LjAzNi4xMTUgOC45NTYgNC42NzIgOC45NTYgNy45MTIgMCAyLjg4Ni0uMzQ4IDUuNzgzLS42OSA4Ljc4Ny0uNDYyIDMuMDEzLS44MDYgNi4xMzQtLjgwNiA4LjkyIDAgMTEuMjM4IDcuMTA2IDE1LjI1MiAxNy4wODcgMTUuMjUyaDMuMzJ2LTcuOTEyaC0yLjA2MmMtNS43MjYgMC04LjAyNS0zLjIzMy04LjAyNS04Ljc5NiAwLTIuMjM2LjM0NC00LjU3LjgwNi03LjAyMy4yMjctMi40MzguNjg0LTUuMTIuNjg0LTguMTIuMTE1LTcuNzkyLTMuMzItMTEuMjUzLTkuMTc0LTEyLjU4NnYtLjIyNWM1Ljg1NC0xLjMzMiA5LjI5My00LjY3NiA5LjE3LTEyLjQ3IDAtMi44OTUtLjQ1Ny01LjU2NS0uNjg0LTguMDI0LS40NjItMi40NC0uODA3LTQuNzc3LS44MDctNy4wMTIgMC01LjQ1IDIuMDU4LTguNjg4IDguMDI0LTguNjg4aDIuMDY2di04LjAxNGgtMy4zMmMtMTAuMjA1LS4wMDMtMTcuMDg2IDQuNDQ0LTE3LjA4NiAxNC45MTV6TTkyLjA2IDQ2LjQxN2MwLTIuODkzLjQ1My01LjkwMy44MDMtOC45MTguMzQzLTMuMTE0Ljc5Ny02LjEyLjc5Ny05LjAyIDAtMTAuNDctNi44NzUtMTQuOTE3LTE3LjA4LTE0LjkxN2gtMy4zMjd2OC4wMTdoMi4wNmM1Ljg1Mi4xMTQgNy45MSAzLjIzMyA3LjkxIDguNjg4IDAgMi4yMy0uMzQyIDQuNTY1LS42ODUgNy4wMTItLjM1IDIuNDU1LS42OTIgNS4xMjYtLjY5MiA4LjAyNC0uMTA1IDcuNzk3IDMuMzI3IDExLjEzNiA5LjA1NiAxMi40N3YuMjIyYy01LjcyIDEuMzMzLTkuMTYgNC43OTYtOS4wNTYgMTIuNTg3IDAgMyAuMzQyIDUuNjg2LjY5MiA4LjEyLjM0MyAyLjQ1NS42ODYgNC43OS42ODYgNy4wMjUgMCA1LjU1NC0yLjE4IDguNjgtNy45MTIgOC43ODhoLTIuMDZ2Ny45MTJoMy4zMjVjOS45NzUgMCAxNy4wNzYtNC4wMSAxNy4wNzYtMTUuMjUgMC0yLjc4My0uNDU0LTUuOS0uNzk2LTguOTE0LS4zNDctMy4wMS0uODA1LTUuOS0uODA1LTguNzk1IDAtMy4yMzMgMS4wMzUtNy43OSA4Ljk0My03LjkxM1Y1NC4zMmMtNy45MDQtLjExMi04LjkzNS00LjM0LTguOTM1LTcuOTAzeiIvPjwvZz48L3N2Zz4=");
 63 | }
 64 | 
 65 | .icon-link {
 66 |   composes: icon;
 67 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMiIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDIyIDI0Ij48cGF0aCBkPSJNMTkuNSAxNi4yODZxMC0uNTM2LS4zNzUtLjkxbC0yLjc4Ni0yLjc4N3EtLjM3Ni0uMzc2LS45MTItLjM3Ni0uNTYzIDAtLjk2NC40M2wuMjU0LjI0N3EuMjE0LjIwOC4yODguMjl0LjIuMjUzLjE3NS4zNDIuMDQ4LjM2OHEwIC41MzYtLjM3NS45MXQtLjkxLjM3NnEtLjIwMiAwLS4zNy0uMDQ4dC0uMzQtLjE3NC0uMjU1LS4yLS4yODgtLjI5LS4yNDgtLjI1M3EtLjQ0Mi40MTUtLjQ0Mi45NzggMCAuNTM2LjM3NS45MWwyLjc2IDIuNzczcS4zNi4zNjIuOTEuMzYyLjUzNiAwIC45MS0uMzQ4bDEuOTctMS45NTVxLjM3NS0uMzc1LjM3NS0uODk3em0tOS40MTUtOS40NDJxMC0uNTM2LS4zNzUtLjkxTDYuOTUgMy4xNnEtLjM3NC0uMzc0LS45MS0uMzc0LS41MjIgMC0uOTEuMzYyTDMuMTYgNS4xMDNxLS4zNzUuMzc1LS4zNzUuODk3IDAgLjUzNi4zNzUuOTFsMi43ODYgMi43ODdxLjM2Mi4zNjIuOTEuMzYyLjU2NCAwIC45NjUtLjQxNmwtLjI1My0uMjQ4cS0uMjEzLS4yMDgtLjI4OC0uMjg4dC0uMjAyLS4yNTQtLjE3NC0uMzQyLS4wNDctLjM2OHEwLS41MzYuMzc1LS45MXQuOTEtLjM3NnEuMjAyIDAgLjM3LjA0N3QuMzQuMTc0LjI1NS4yLjI4OC4yODguMjQ4LjI1NHEuNDQyLS40MTUuNDQyLS45Nzh6bTExLjk4NiA5LjQ0MnEwIDEuNjA3LTEuMTM3IDIuNzJsLTEuOTcgMS45NTRxLTEuMTEgMS4xMTItMi43MTggMS4xMTItMS42MiAwLTIuNzMyLTEuMTM4bC0yLjc2LTIuNzcycS0xLjExLTEuMTEyLTEuMTEtMi43MiAwLTEuNjQ2IDEuMTc4LTIuNzk4bC0xLjE3OC0xLjE4cS0xLjE1MiAxLjE4LTIuNzg2IDEuMTgtMS42MDcgMC0yLjczMi0xLjEyNUwxLjMzOCA4LjczMlEuMjEzIDcuNjA4LjIxMyA2VDEuMzUgMy4yODNsMS45Ny0xLjk1NVE0LjQzMi4yMTUgNi4wNC4yMTVxMS42MiAwIDIuNzMgMS4xMzhsMi43NiAyLjc3MnExLjExMiAxLjExMiAxLjExMiAyLjcyIDAgMS42NDYtMS4xOCAyLjc5OGwxLjE4IDEuMThxMS4xNTItMS4xOCAyLjc4Ni0xLjE4IDEuNjA3IDAgMi43MzIgMS4xMjVsMi43ODYgMi43ODZxMS4xMjUgMS4xMjUgMS4xMjUgMi43MzJ6Ii8+PC9zdmc+");
 68 |   background-size: 14px;
 69 | }
 70 | 
 71 | .icon-remove-link {
 72 |   composes: icon;
 73 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMiIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDIyIDI0Ij48cGF0aCBkPSJNNS44OCAxNy4wMjJsLTMuNDMgMy40M3EtLjEzNC4xMi0uMzA4LjEyLS4xNiAwLS4zMDgtLjEyLS4xMi0uMTM1LS4xMi0uMzF0LjEyLS4zMDdsMy40My0zLjQzcS4xMzMtLjEyLjMwNy0uMTJ0LjMxLjEycS4xMi4xMzUuMTIuMzF0LS4xMi4zMDd6bTIuMjYzLjU1djQuMjg1cTAgLjE4OC0uMTIuMzA4dC0uMzEuMTItLjMwNy0uMTItLjEyLS4zMDhWMTcuNTdxMC0uMTg3LjEyLS4zMDd0LjMwOC0uMTIuMzA4LjEyLjEyLjMwOHptLTMtM3EwIC4xODctLjEyLjMwN3QtLjMxLjEySC40M3EtLjE4OCAwLS4zMDgtLjEyVDAgMTQuNTd0LjEyLS4zMDcuMzA4LS4xMmg0LjI4NnEuMTg4IDAgLjMwOC4xMnQuMTIuMzA4em0xNi45MjggMS43MTRxMCAxLjYwNy0xLjEzNyAyLjcybC0xLjk3IDEuOTU0cS0xLjExIDEuMTEyLTIuNzE4IDEuMTEyLTEuNjIgMC0yLjczMi0xLjEzOEw5LjA0IDE2LjQ0N3EtLjI4LS4yOC0uNTYzLS43NWwzLjItLjI0IDMuNjU3IDMuNjdxLjM2Mi4zNi45MS4zNjd0LjkxMi0uMzU1bDEuOTctMS45NTZxLjM3NC0uMzc1LjM3NC0uODk3IDAtLjUzNi0uMzc1LS45MWwtMy42Ny0zLjY4NC4yNC0zLjJxLjQ3LjI4Ljc1LjU2Mmw0LjUgNC41cTEuMTI2IDEuMTUyIDEuMTI2IDIuNzMyek0xMy44MSA2LjU5bC0zLjIuMjRMNi45NSAzLjE2cS0uMzc0LS4zNzUtLjkxLS4zNzUtLjUyMiAwLS45MS4zNjJMMy4xNiA1LjEwMnEtLjM3NS4zNzUtLjM3NS44OTcgMCAuNTM1LjM3NS45MWwzLjY3IDMuNjctLjI0IDMuMjE0cS0uNDctLjI4LS43NS0uNTYzbC00LjUtNC41US4yMTMgNy41OC4yMTMgNnEwLTEuNjA4IDEuMTM4LTIuNzJsMS45Ny0xLjk1NVE0LjQzLjIxMyA2LjA0LjIxM3ExLjYyIDAgMi43MzIgMS4xMzhsNC40NzMgNC40ODhxLjI4LjI4LjU2My43NXptOC40NzggMS4xMjRxMCAuMTg4LS4xMi4zMDh0LS4zMS4xMmgtNC4yODVxLS4xODcgMC0uMzA3LS4xMnQtLjEyLS4zMDguMTItLjMwOC4zMDgtLjEyaDQuMjg3cS4xODggMCAuMzA4LjEydC4xMi4zMDh6TTE1IC40M3Y0LjI4NXEwIC4xODgtLjEyLjMwOHQtLjMxLjEyLS4zMDctLjEyLS4xMi0uMzA4Vi40M3EwLS4xOS4xMi0uMzFUMTQuNTcgMHQuMzEuMTIuMTIuMzF6bTUuNDUgMi4wMmwtMy40MjggMy40M3EtLjE0Ny4xMi0uMzA4LjEydC0uMzA4LS4xMnEtLjEyLS4xMzQtLjEyLS4zMDh0LjEyLS4zMDhsMy40My0zLjQzcS4xMzMtLjEyLjMwNy0uMTJ0LjMwOC4xMnEuMTIyLjEzNS4xMjIuMzF0LS4xMi4zMDd6Ii8+PC9zdmc+");
 74 |   background-size: 14px;
 75 | }
 76 | 
 77 | .icon-image {
 78 |   composes: icon;
 79 |   background-image: url("data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4IiB2aWV3Qm94PSIwIDAgNTMzLjMzMyA1MzMuMzM0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MzMuMzMzIDUzMy4zMzQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8cGF0aCBkPSJNNDY2LjY2NywxMDBoLTQwMHYzMzMuMzMzaDQwMFYxMDB6IE01MzMuMzMzLDMzLjMzM0w1MzMuMzMzLDMzLjMzM1Y1MDBIMFYzMy4zMzNINTMzLjMzM3ogTTQzMy4zMzMsNDAwSDEwMHYtNjYuNjY3ICAgbDEwMC0xNjYuNjY3bDEzNi45NzksMTY2LjY2N2w5Ni4zNTQtNjYuNjY2VjMwMFY0MDB6IE0zMzMuMzMzLDE4My4zMzNjMCwyNy42MTQsMjIuMzg2LDUwLDUwLDUwczUwLTIyLjM4Niw1MC01MHMtMjIuMzg2LTUwLTUwLTUwICAgUzMzMy4zMzMsMTU1LjcxOSwzMzMuMzMzLDE4My4zMzN6IiBmaWxsPSIjMDAwMDAwIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==");
 80 |   background-size: 14px;
 81 | }
 82 | 
 83 | .icon-cancel {
 84 |   composes: icon;
 85 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjMuNzggMTkuMjhMMTYuNSAxMmw3LjI4LTcuMjhhLjc0OC43NDggMCAwIDAgMC0xLjA2TDIwLjM0LjIxOGEuNzUuNzUgMCAwIDAtMS4wNi0uMDAyTDEyIDcuNDk4IDQuNzE3LjIyYS43NDguNzQ4IDAgMCAwLTEuMDYgMEwuMjE3IDMuNjZhLjc1Ljc1IDAgMCAwIDAgMS4wNkw3LjQ5NyAxMmwtNy4yOCA3LjI4YS43NDguNzQ4IDAgMCAwIDAgMS4wNmwzLjQ0IDMuNDRhLjc1Ljc1IDAgMCAwIDEuMDYuMDAybDcuMjgtNy4yOCA3LjI4MiA3LjI4Yy4wNzguMDc4LjE3LjEzNS4yNjguMTcuMjY3LjEuNTguMDQ0Ljc5My0uMTdsMy40NC0zLjQ0YS43NS43NSAwIDAgMCAwLTEuMDZ6Ii8+PC9zdmc+");
 86 |   background-size: 13px;
 87 | }
 88 | 
 89 | .icon-accept {
 90 |   composes: icon;
 91 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjAuMjUgM0w5IDE0LjI1IDMuNzUgOSAwIDEyLjc1bDkgOSAxNS0xNXoiLz48L3N2Zz4=");
 92 |   background-size: 13px;
 93 | }
 94 | 
 95 | .icon-align_left {
 96 |   composes: icon;
 97 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KPHBhdGggZD0iTTkuOSwxMGgtOFY4LjdoOFYxMHogTTkuOSwzLjNoLTh2MS4zaDhWMy4zeiBNMS44LDEyLjdoMTIuM3YtMS4zSDEuOFYxMi43eiBNMS44LDZ2MS4zaDEyLjNWNkgxLjh6Ii8+Cjwvc3ZnPgo=");
 98 | }
 99 | 
100 | .icon-align_center {
101 |   composes: icon;
102 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KPHBhdGggZD0iTTEyLDQuN0g0VjMuM2g4VjQuN3ogTTEyLDguN0g0VjEwaDhWOC43eiBNMS44LDZ2MS4zaDEyLjNWNkgxLjh6IE0xLjgsMTIuN2gxMi4zdi0xLjNIMS44VjEyLjd6Ii8+Cjwvc3ZnPgo=");
103 | }
104 | 
105 | .icon-align_right {
106 |   composes: icon;
107 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KPHBhdGggZD0iTTEuOCwxMS4zaDEyLjN2MS4zSDEuOFYxMS4zeiBNMTQuMiwzLjNoLTh2MS4zaDhWMy4zeiBNNi4xLDguN1YxMGg4VjguN0g2LjF6IE0xLjgsNnYxLjNoMTIuM1Y2SDEuOHoiLz4KPC9zdmc+Cg==");
108 | }
109 | 
110 | .icon-align_justify {
111 |   composes: icon;
112 |   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KPHBhdGggZD0iTTEuOCw4LjdoMTIuM1YxMEgxLjhWOC43eiBNMS44LDEyLjdoMTIuM3YtMS4zSDEuOFYxMi43eiBNMS44LDcuM2gxMi4zVjZIMS44VjcuM3ogTTEuOCwzLjN2MS4zaDEyLjNWMy4zSDEuOHoiLz4KPC9zdmc+Cg==");
113 | }


--------------------------------------------------------------------------------
/src/ui/IconButton.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import React, {Component} from 'react';
 4 | import cx from 'classnames';
 5 | import Button from './Button';
 6 | import ButtonWrap from './ButtonWrap';
 7 | 
 8 | import styles from './IconButton.css';
 9 | 
10 | type Props = {
11 |   iconName: string;
12 |   isActive?: boolean;
13 |   children?: ReactNode;
14 |   className?: string;
15 |   label?: string;
16 |   isSwitch?: boolean;
17 | };
18 | 
19 | export default class IconButton extends Component {
20 |   props: Props;
21 | 
22 |   render() {
23 |     let {props} = this;
24 |     let {className, iconName, label, children, isActive, isSwitch, ...otherProps} = props;
25 |     className = cx(className, {
26 |       [styles.root]: true,
27 |       [styles.isActive]: isActive,
28 |     });
29 |     return (
30 |       <ButtonWrap>
31 |         <Button {...otherProps} title={label} className={className} role={isSwitch && 'switch'} aria-checked={isActive}>
32 |           <span className={styles['icon-' + iconName]} />
33 |           {/* TODO: add text label here with aria-hidden */}
34 |         </Button>
35 |         {children}
36 |       </ButtonWrap>
37 |     );
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/src/ui/ImageSpan.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   background-repeat: no-repeat;
 3 |   display: inline-block;
 4 |   overflow: hidden;
 5 |   cursor: pointer;
 6 | }
 7 | 
 8 | .resize {
 9 |   border: 1px dashed #78a300;
10 |   position: relative;
11 |   max-width: 100%;
12 |   display: inline-block;
13 |   line-height: 0;
14 |   top: -1px;
15 |   left: -1px;
16 | }
17 | 
18 | .resizeHandle {
19 |   cursor: nwse-resize;
20 |   position: absolute;
21 |   z-index: 2;
22 |   line-height: 1;
23 |   bottom: -4px;
24 |   right: -5px;
25 |   border: 1px solid white;
26 |   background-color: #78a300;
27 |   width: 8px;
28 |   height: 8px;
29 | }
30 | 


--------------------------------------------------------------------------------
/src/ui/ImageSpan.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | 
  3 | import autobind from 'class-autobind';
  4 | import cx from 'classnames';
  5 | import React, {Component} from 'react';
  6 | import {Entity} from 'draft-js';
  7 | 
  8 | import styles from './ImageSpan.css';
  9 | 
 10 | import type {ContentState} from 'draft-js';
 11 | 
 12 | // TODO: Use a more specific type here.
 13 | type ReactNode = any;
 14 | 
 15 | type Props = {
 16 |   children: ReactNode;
 17 |   entityKey: string;
 18 |   contentState: ContentState,
 19 |   className?: string;
 20 | };
 21 | 
 22 | type State = {
 23 |   width: string;
 24 |   height: string;
 25 | };
 26 | 
 27 | export default class ImageSpan extends Component {
 28 |   props: Props;
 29 |   state: State;
 30 | 
 31 |   constructor(props: Props) {
 32 |     super(props);
 33 |     autobind(this);
 34 |     const entity = props.contentState.getEntity(props.entityKey);
 35 |     const {width, height} = entity.getData();
 36 |     this._setSize(width, height);
 37 |   }
 38 | 
 39 |   componentDidMount() {
 40 |     const {width, height} = this.state;
 41 |     const entity = this.props.contentState.getEntity(this.props.entityKey);
 42 |     const image = new Image();
 43 |     const {src} = entity.getData();
 44 |     image.src = src;
 45 |     image.onload = () => {
 46 |       if (width == null || height == null) {
 47 |         // TODO: isMounted?
 48 |         this._setSize({width: image.width, height: image.height});
 49 |         Entity.mergeData(
 50 |           this.props.entityKey,
 51 |           {
 52 |             width: image.width,
 53 |             height: image.height,
 54 |             originalWidth: image.width,
 55 |             originalHeight: image.height,
 56 |           }
 57 |         );
 58 |       }
 59 |     };
 60 |   }
 61 | 
 62 |   render() {
 63 |     const {width, height} = this.state;
 64 |     let {className} = this.props;
 65 |     const entity = this.props.contentState.getEntity(this.props.entityKey);
 66 |     const {src} = entity.getData();
 67 | 
 68 |     className = cx(className, styles.root);
 69 |     const imageStyle = {
 70 |       verticalAlign: 'bottom',
 71 |       backgroundImage: `url("${src}")`,
 72 |       backgroundSize: `${width} ${height}`,
 73 |       lineHeight: `${height}`,
 74 |       fontSize: `${height}`,
 75 |       width,
 76 |       height,
 77 |       letterSpacing: width,
 78 |     };
 79 | 
 80 |     return (
 81 |       <span
 82 |         className={className}
 83 |         style={imageStyle}
 84 |         onClick={this._onClick}
 85 |       >
 86 |         {this.props.children}
 87 |       </span>
 88 |     );
 89 |   }
 90 | 
 91 |   _onClick() {
 92 |     console.log('image clicked');
 93 |   }
 94 | 
 95 |   _handleResize(event: Object, data: Object) {
 96 |     const {width, height} = data.size;
 97 |     this._setSize(width, height);
 98 |     Entity.mergeData(
 99 |       this.props.entityKey,
100 |       {width, height}
101 |     );
102 |   }
103 | 
104 |   _setSize(width: string | number, height: string | number) {
105 |     if (isFinite(width)) {
106 |       width = `${width}px`;
107 |     }
108 |     if (isFinite(height)) {
109 |       height = `${height}px`;
110 |     }
111 |     this.setState({width, height});
112 |   }
113 | }
114 | 


--------------------------------------------------------------------------------
/src/ui/InputPopover.css:
--------------------------------------------------------------------------------
 1 | .root {
 2 |   position: absolute;
 3 |   top: calc(100% + 5px);
 4 |   left: 0;
 5 |   width: 260px;
 6 |   background: none #fdfdfd;
 7 |   background: linear-gradient(to bottom, #fdfdfd 0%,#f6f7f8 100%);
 8 |   border: 1px solid #999;
 9 |   border-radius: 2px;
10 |   box-sizing: border-box;
11 |   padding: 4px;
12 | }
13 | 
14 | .root:before {
15 |   content: "";
16 |   display: block;
17 |   position: absolute;
18 |   width: 0;
19 |   height: 0;
20 |   top: -10px;
21 |   left: 10px;
22 |   border: 5px solid transparent;
23 |   border-bottom-color: #999;
24 | }
25 | 
26 | .root:after {
27 |   content: "";
28 |   display: block;
29 |   position: absolute;
30 |   width: 0;
31 |   height: 0;
32 |   top: -9px;
33 |   left: 10px;
34 |   border: 5px solid transparent;
35 |   border-bottom-color: #fdfdfd;
36 | }
37 | 
38 | .inner {
39 |   display: flex;
40 | }
41 | 
42 | .input {
43 |   display: block;
44 |   flex: 1 0 auto;
45 |   height: 30px;
46 |   background: none white;
47 |   border: 1px solid #999;
48 |   border-radius: 2px;
49 |   box-sizing: border-box;
50 |   padding: 2px 6px;
51 |   font-family: inherit;
52 |   font-size: inherit;
53 |   line-height: 24px;
54 | }
55 | 
56 | .inner .buttonGroup {
57 |   flex: 0 1 auto;
58 |   margin-left: 4px;
59 |   margin-bottom: 0;
60 | }
61 | 
62 | .checkOption {
63 |   margin: 8px 2px;
64 | }
65 | 
66 | .checkOption input {
67 |   margin-right: 8px;
68 | }
69 | 
70 | .checkOption input {
71 |   cursor: pointer;
72 | }
73 | 


--------------------------------------------------------------------------------
/src/ui/InputPopover.js:
--------------------------------------------------------------------------------
  1 | /* @flow */
  2 | import React, {Component} from 'react';
  3 | import ReactDOM from 'react-dom';
  4 | import IconButton from './IconButton';
  5 | import ButtonGroup from './ButtonGroup';
  6 | import autobind from 'class-autobind';
  7 | import cx from 'classnames';
  8 | 
  9 | import styles from './InputPopover.css';
 10 | 
 11 | type CheckOptionValues = {
 12 |   [key: string]: boolean;
 13 | };
 14 | 
 15 | type Props = {
 16 |   className?: string;
 17 |   defaultValue?: string;
 18 |   checkOptions?: {
 19 |     [key: string]: { label: string, defaultValue: boolean };
 20 |   };
 21 |   onCancel: () => any;
 22 |   onSubmit: (value: string, checkOptionValues: CheckOptionValues) => any;
 23 | };
 24 | 
 25 | type State = {
 26 |   checkOptionValues: CheckOptionValues
 27 | };
 28 | 
 29 | export default class InputPopover extends Component {
 30 |   props: Props;
 31 |   state: State;
 32 |   _inputRef: ?Object;
 33 | 
 34 |   constructor() {
 35 |     super(...arguments);
 36 |     autobind(this);
 37 |     let {checkOptions} = this.props;
 38 |     let checkOptionValues: CheckOptionValues = {};
 39 |     if (checkOptions) {
 40 |       for (let key of Object.keys(checkOptions)) {
 41 |         let {defaultValue} = checkOptions[key];
 42 |         checkOptionValues[key] = defaultValue;
 43 |       }
 44 |     }
 45 |     this.state = {
 46 |       checkOptionValues,
 47 |     };
 48 |   }
 49 | 
 50 |   componentDidMount() {
 51 |     document.addEventListener('click', this._onDocumentClick, true);
 52 |     document.addEventListener('keydown', this._onDocumentKeydown);
 53 |     if (this._inputRef) {
 54 |       this._inputRef.focus();
 55 |     }
 56 |   }
 57 | 
 58 |   componentWillUnmount() {
 59 |     document.removeEventListener('click', this._onDocumentClick, true);
 60 |     document.removeEventListener('keydown', this._onDocumentKeydown);
 61 |   }
 62 | 
 63 |   render() {
 64 |     let {props} = this;
 65 |     let className = cx(props.className, styles.root);
 66 |     return (
 67 |       <div className={className}>
 68 |         <div className={styles.inner}>
 69 |           <input
 70 |             ref={this._setInputRef}
 71 |             defaultValue={props.defaultValue}
 72 |             type="text"
 73 |             placeholder="https://example.com/"
 74 |             className={styles.input}
 75 |             onKeyPress={this._onInputKeyPress}
 76 |           />
 77 |           <ButtonGroup className={styles.buttonGroup}>
 78 |             <IconButton
 79 |               label="Cancel"
 80 |               iconName="cancel"
 81 |               onClick={props.onCancel}
 82 |             />
 83 |             <IconButton
 84 |               label="Submit"
 85 |               iconName="accept"
 86 |               onClick={this._onSubmit}
 87 |             />
 88 |           </ButtonGroup>
 89 |         </div>
 90 |         {this._renderCheckOptions()}
 91 |       </div>
 92 |     );
 93 |   }
 94 | 
 95 |   _renderCheckOptions() {
 96 |     if (!this.props.checkOptions) {
 97 |       return null;
 98 |     }
 99 |     let {checkOptions} = this.props;
100 |     return Object.keys(checkOptions).map((key) => {
101 |       let label = checkOptions && checkOptions[key] ? checkOptions[key].label : '';
102 |       return (
103 |         <div key={key} className={styles.checkOption}>
104 |           <label>
105 |             <input
106 |               type="checkbox"
107 |               checked={this.state.checkOptionValues[key]}
108 |               onChange={() => this._onCheckOptionPress(key)}
109 |             />
110 |             <span>{label}</span>
111 |           </label>
112 |         </div>
113 |       );
114 |     });
115 |   }
116 | 
117 |   _setInputRef(inputElement: Object) {
118 |     this._inputRef = inputElement;
119 |   }
120 | 
121 |   _onCheckOptionPress(key: string) {
122 |     let {checkOptionValues} = this.state;
123 |     let oldValue = Boolean(checkOptionValues[key]);
124 |     let newCheckOptionValues = {...checkOptionValues, [key]: !oldValue};
125 |     this.setState({
126 |       checkOptionValues: newCheckOptionValues,
127 |     });
128 |   }
129 | 
130 |   _onInputKeyPress(event: Object) {
131 |     if (event.which === 13) {
132 |       // Avoid submitting a <form> somewhere up the element tree.
133 |       event.preventDefault();
134 |       this._onSubmit();
135 |     }
136 |   }
137 | 
138 |   _onSubmit() {
139 |     let value = this._inputRef ? this._inputRef.value : '';
140 |     this.props.onSubmit(value, this.state.checkOptionValues);
141 |   }
142 | 
143 |   _onDocumentClick(event: Object) {
144 |     let rootNode = ReactDOM.findDOMNode(this);
145 |     if (!rootNode.contains(event.target)) {
146 |       // Here we pass the event so the parent can manage focus.
147 |       this.props.onCancel(event);
148 |     }
149 |   }
150 | 
151 |   _onDocumentKeydown(event: Object) {
152 |     if (event.keyCode === 27) {
153 |       this.props.onCancel();
154 |     }
155 |   }
156 | }
157 | 


--------------------------------------------------------------------------------
/src/ui/PopoverIconButton.js:
--------------------------------------------------------------------------------
 1 | /* @flow */
 2 | 
 3 | import React, {Component} from 'react';
 4 | import IconButton from './IconButton';
 5 | import InputPopover from './InputPopover';
 6 | import autobind from 'class-autobind';
 7 | 
 8 | type Props = {
 9 |   iconName: string;
10 |   showPopover: boolean,
11 |   defaultValue?: string,
12 |   checkOptions?: {
13 |     [key: string]: { label: string, defaultValue: boolean };
14 |   };
15 |   onTogglePopover: Function,
16 |   onSubmit: Function;
17 | };
18 | 
19 | export default class PopoverIconButton extends Component {
20 |   props: Props;
21 | 
22 |   constructor() {
23 |     super(...arguments);
24 |     autobind(this);
25 |   }
26 | 
27 |   render() {
28 |     let {onTogglePopover, showPopover, checkOptions, ...props} = this.props; // eslint-disable-line no-unused-vars
29 |     return (
30 |       <IconButton {...props} onClick={onTogglePopover}>
31 |         {this._renderPopover()}
32 |       </IconButton>
33 |     );
34 |   }
35 | 
36 |   _renderPopover() {
37 |     if (!this.props.showPopover) {
38 |       return null;
39 |     }
40 |     return (
41 |       <InputPopover
42 |         defaultValue={this.props.defaultValue}
43 |         checkOptions={this.props.checkOptions}
44 |         onSubmit={this._onSubmit}
45 |         onCancel={this._hidePopover}
46 |       />
47 |     );
48 |   }
49 | 
50 |   _onSubmit() {
51 |     this.props.onSubmit(...arguments);
52 |   }
53 | 
54 |   _hidePopover() {
55 |     if (this.props.showPopover) {
56 |       this.props.onTogglePopover(...arguments);
57 |     }
58 |   }
59 | }
60 | 


--------------------------------------------------------------------------------
/test/init.js:
--------------------------------------------------------------------------------
1 | require('css-modules-require-hook')({
2 |   generateScopedName: '[name]__[local]___[hash:base64:5]',
3 | });
4 | 


--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --compilers js:babel-core/register
2 | --require ./test/init.js
3 | 


--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
 1 | /*eslint-env node */
 2 | var path = require('path');
 3 | var webpack = require('webpack');
 4 | 
 5 | var rules = [
 6 |   {
 7 |     test: /\.js$/,
 8 |     use: ['babel-loader'],
 9 |     exclude: /node_modules/,
10 |   },
11 |   {
12 |     test: /\.css$/,
13 |     exclude: /\.global\.css$/,
14 |     use: [
15 |       {
16 |         loader: 'style-loader',
17 |         options: {sourceMap: true},
18 |       },
19 |       {
20 |         loader: 'css-loader',
21 |         options: {
22 |           modules: true,
23 |           importLoaders: true,
24 |           localIdentName: '[name]__[local]___[hash:base64:5]',
25 |         },
26 |       },
27 |     ],
28 |   },
29 |   {
30 |     test: /\.global\.css$/,
31 |     use: ['style-loader', 'raw-loader'],
32 |   },
33 | ];
34 | 
35 | module.exports = [{
36 |   entry: './src/RichTextEditor.js',
37 |   output: {
38 |     path: path.join(__dirname, 'dist'),
39 |     filename: 'react-rte.js',
40 |     libraryTarget: 'commonjs2',
41 |   },
42 |   externals: {
43 |     react: 'react',
44 |     'react-dom': 'react-dom',
45 |   },
46 |   module: {
47 |     rules: rules,
48 |   },
49 |   plugins: [
50 |     new webpack.DefinePlugin({
51 |       'process.env': {
52 |         NODE_ENV: JSON.stringify('production'),
53 |       },
54 |     }),
55 |     new webpack.optimize.UglifyJsPlugin(),
56 |     new webpack.optimize.ModuleConcatenationPlugin(),
57 |   ],
58 | }, {
59 |   entry: './src/demo.js',
60 |   output: {
61 |     path: path.join(__dirname, 'dist'),
62 |     publicPath: '/dist/',
63 |     filename: 'demo.js',
64 |   },
65 |   module: {
66 |     rules: rules,
67 |   },
68 | }];
69 | 


--------------------------------------------------------------------------------