Skip to content
Browse files

Open source Draft.js ๐ŸŽ‰

  • Loading branch information...
0 parents commit 342576bf7186d07c82a41d9ca8169130669747d6 @hellendag hellendag committed with zpao
Showing with 9,776 additions and 0 deletions.
  1. +6 โˆ’0 .eslintignore
  2. +15 โˆ’0 .eslintrc.js
  3. +8 โˆ’0 .gitignore
  4. +14 โˆ’0 .travis.yml
  5. +40 โˆ’0 CONTRIBUTING.md
  6. +31 โˆ’0 LICENSE
  7. +11 โˆ’0 LICENSE-examples
  8. +33 โˆ’0 PATENTS
  9. +53 โˆ’0 README.md
  10. +157 โˆ’0 docs/APIReference-CharacterMetadata.md
  11. +246 โˆ’0 docs/APIReference-ContentBlock.md
  12. +265 โˆ’0 docs/APIReference-ContentState.md
  13. +46 โˆ’0 docs/APIReference-Data-Conversion.md
  14. +193 โˆ’0 docs/APIReference-Editor.md
  15. +481 โˆ’0 docs/APIReference-EditorState.md
  16. +117 โˆ’0 docs/APIReference-Entity.md
  17. +207 โˆ’0 docs/APIReference-Modifier.md
  18. +324 โˆ’0 docs/APIReference-SelectionState.md
  19. +120 โˆ’0 docs/Advanced-Topics-Block-Components.md
  20. +59 โˆ’0 docs/Advanced-Topics-Block-Styling.md
  21. +152 โˆ’0 docs/Advanced-Topics-Decorators.md
  22. +136 โˆ’0 docs/Advanced-Topics-Entities.md
  23. +112 โˆ’0 docs/Advanced-Topics-Inline-Styles.md
  24. +75 โˆ’0 docs/Advanced-Topics-Issues-and-Pitfalls.md
  25. +100 โˆ’0 docs/Advanced-Topics-Key-Bindings.md
  26. +40 โˆ’0 docs/Advanced-Topics-Managing-Focus.md
  27. +22 โˆ’0 docs/Advanced-Topics-Nested-Lists.md
  28. +35 โˆ’0 docs/Advanced-Topics-Text-Direction.md
  29. +45 โˆ’0 docs/Overview.md
  30. +75 โˆ’0 docs/QuickStart-API-Basics.md
  31. +112 โˆ’0 docs/QuickStart-Rich-Styling.md
  32. +219 โˆ’0 examples/color/color.html
  33. +223 โˆ’0 examples/entity/entity.html
  34. +178 โˆ’0 examples/link/link.html
  35. +89 โˆ’0 examples/plaintext/plaintext.html
  36. +62 โˆ’0 examples/rich/RichEditor.css
  37. +218 โˆ’0 examples/rich/rich.html
  38. +172 โˆ’0 examples/tex/.eslintrc
  39. +25 โˆ’0 examples/tex/js/app.js
  40. +176 โˆ’0 examples/tex/js/components/TeXBlock.js
  41. +113 โˆ’0 examples/tex/js/components/TeXEditorExample.js
  42. +68 โˆ’0 examples/tex/js/data/content.js
  43. +98 โˆ’0 examples/tex/js/modifiers/insertTeXBlock.js
  44. +39 โˆ’0 examples/tex/js/modifiers/removeTeXBlock.js
  45. +22 โˆ’0 examples/tex/package.json
  46. +100 โˆ’0 examples/tex/public/TeXEditor.css
  47. +15 โˆ’0 examples/tex/public/index.html
  48. +53 โˆ’0 examples/tex/server.js
  49. +144 โˆ’0 examples/tweet/tweet.html
  50. +134 โˆ’0 gulpfile.js
  51. +85 โˆ’0 package.json
  52. +20 โˆ’0 scripts/babel/default-options.js
  53. +31 โˆ’0 scripts/jest/preprocessor.js
  54. +17 โˆ’0 src/.flowconfig
  55. +53 โˆ’0 src/Draft.js
  56. +67 โˆ’0 src/component/base/DraftEditor.css
  57. +448 โˆ’0 src/component/base/DraftEditor.react.js
  58. +17 โˆ’0 src/component/base/DraftEditorPlaceholder.css
  59. +64 โˆ’0 src/component/base/DraftEditorPlaceholder.react.js
  60. +116 โˆ’0 src/component/base/DraftEditorProps.js
  61. +18 โˆ’0 src/component/base/DraftScrollPosition.js
  62. +15 โˆ’0 src/component/base/DraftTextAlignment.js
  63. +224 โˆ’0 src/component/contents/DraftEditorBlock.react.js
  64. +232 โˆ’0 src/component/contents/DraftEditorContents.react.js
  65. +158 โˆ’0 src/component/contents/DraftEditorLeaf.react.js
  66. +96 โˆ’0 src/component/contents/DraftEditorTextNode.react.js
  67. +555 โˆ’0 src/component/contents/__tests__/DraftEditorBlock.react-test.js
  68. +212 โˆ’0 src/component/contents/__tests__/DraftEditorTextNode-test.js
  69. +43 โˆ’0 src/component/handlers/DraftEditorModes.js
  70. +183 โˆ’0 src/component/handlers/composition/DraftEditorCompositionHandler.js
  71. +156 โˆ’0 src/component/handlers/drag/DraftEditorDragHandler.js
  72. +43 โˆ’0 src/component/handlers/edit/DraftEditorEditHandler.js
  73. +80 โˆ’0 src/component/handlers/edit/commands/SecondaryClipboard.js
  74. +55 โˆ’0 src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js
  75. +54 โˆ’0 src/component/handlers/edit/commands/keyCommandBackspaceWord.js
  76. +52 โˆ’0 src/component/handlers/edit/commands/keyCommandDeleteWord.js
  77. +26 โˆ’0 src/component/handlers/edit/commands/keyCommandInsertNewline.js
  78. +39 โˆ’0 src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js
  79. +39 โˆ’0 src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
  80. +55 โˆ’0 src/component/handlers/edit/commands/keyCommandPlainBackspace.js
  81. +56 โˆ’0 src/component/handlers/edit/commands/keyCommandPlainDelete.js
  82. +90 โˆ’0 src/component/handlers/edit/commands/keyCommandTransposeCharacters.js
  83. +52 โˆ’0 src/component/handlers/edit/commands/keyCommandUndo.js
  84. +58 โˆ’0 src/component/handlers/edit/commands/moveSelectionBackward.js
  85. +50 โˆ’0 src/component/handlers/edit/commands/moveSelectionForward.js
  86. +51 โˆ’0 src/component/handlers/edit/commands/removeTextWithStrategy.js
  87. +163 โˆ’0 src/component/handlers/edit/editOnBeforeInput.js
  88. +44 โˆ’0 src/component/handlers/edit/editOnBlur.js
  89. +29 โˆ’0 src/component/handlers/edit/editOnCompositionStart.js
  90. +35 โˆ’0 src/component/handlers/edit/editOnCopy.js
  91. +71 โˆ’0 src/component/handlers/edit/editOnCut.js
  92. +24 โˆ’0 src/component/handlers/edit/editOnDragOver.js
  93. +23 โˆ’0 src/component/handlers/edit/editOnDragStart.js
  94. +36 โˆ’0 src/component/handlers/edit/editOnFocus.js
  95. +128 โˆ’0 src/component/handlers/edit/editOnInput.js
  96. +135 โˆ’0 src/component/handlers/edit/editOnKeyDown.js
Sorry, we could not display the entire diff because it was too big.
6 .eslintignore
@@ -0,0 +1,6 @@
+lib/
+dist/
+docs/
+examples/
+node_modules/
+website/
15 .eslintrc.js
@@ -0,0 +1,15 @@
+
+module.exports = {
+ parser: 'babel-eslint',
+
+ extends: './node_modules/fbjs-scripts/eslint/.eslintrc.js',
+
+ plugins: [
+ 'react',
+ ],
+
+ rules: {
+ 'react/jsx-uses-react': 1,
+ 'react/react-in-jsx-scope': 1,
+ }
+};
8 .gitignore
@@ -0,0 +1,8 @@
+/dist/
+/lib/
+/node_modules/
+/examples/tex/node_modules/
+/website/build/
+/website/node_modules/
+/website/src/draft-js/lib/
+npm-debug.log
14 .travis.yml
@@ -0,0 +1,14 @@
+language: node_js
+node_js:
+- 5
+sudo: false
+cache:
+ directories:
+ - node_modules
+before_install:
+- |
+ npm install -g npm@latest-2
+ npm --version
+script:
+- |
+ npm test
40 CONTRIBUTING.md
@@ -0,0 +1,40 @@
+# Contributing to Draft.js
+We want to make contributing to this project as easy and transparent as
+possible.
+
+## Our Development Process
+We use GitHub to sync code to and from our internal repository. We'll use GitHub
+to track issues and feature requests, as well as accept pull requests.
+
+## Pull Requests
+We actively welcome your pull requests.
+
+1. Fork the repo and create your branch from `master`.
+2. If you've added code that should be tested, add tests.
+3. If you've changed APIs, update the documentation.
+4. Ensure the test suite passes.
+5. Make sure your code lints.
+6. If you haven't already, complete the Contributor License Agreement ("CLA").
+
+## Contributor License Agreement ("CLA")
+In order to accept your pull request, we need you to submit a CLA. You only need
+to do this once to work on any of Facebook's open source projects.
+
+Complete your CLA here: <https://code.facebook.com/cla>
+
+## Issues
+We use GitHub issues to track public bugs. Please ensure your description is
+clear and has sufficient instructions to be able to reproduce the issue.
+
+Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
+disclosure of security bugs. In those cases, please go through the process
+outlined on that page and do not file a public issue.
+
+## Coding Style
+* 2 spaces for indentation rather than tabs
+* 80 character line length
+* Run `npm run lint` to conform to our lint rules
+
+## License
+By contributing to Draft.js, you agree that your contributions will be licensed
+under its BSD license.
31 LICENSE
@@ -0,0 +1,31 @@
+BSD License
+
+For Draft.js software
+
+Copyright (c) 2013-present, Facebook, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11 LICENSE-examples
@@ -0,0 +1,11 @@
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+The examples provided by Facebook are for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 PATENTS
@@ -0,0 +1,33 @@
+Additional Grant of Patent Rights Version 2
+
+"Software" means the Draft.js software distributed by Facebook, Inc.
+
+Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software
+("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
+(subject to the termination provision below) license under any Necessary
+Claims, to make, have made, use, sell, offer to sell, import, and otherwise
+transfer the Software. For avoidance of doubt, no license is granted under
+Facebook's rights in any patent claims that are infringed by (i) modifications
+to the Software made by you or any third party or (ii) the Software in
+combination with any software or other technology.
+
+The license granted hereunder will terminate, automatically and without notice,
+if you (or any of your subsidiaries, corporate affiliates or agents) initiate
+directly or indirectly, or take a direct financial interest in, any Patent
+Assertion: (i) against Facebook or any of its subsidiaries or corporate
+affiliates, (ii) against any party if such Patent Assertion arises in whole or
+in part from any software, technology, product or service of Facebook or any of
+its subsidiaries or corporate affiliates, or (iii) against any party relating
+to the Software. Notwithstanding the foregoing, if Facebook or any of its
+subsidiaries or corporate affiliates files a lawsuit alleging patent
+infringement against you in the first instance, and you respond by filing a
+patent infringement counterclaim in that lawsuit against that party that is
+unrelated to the Software, the license granted hereunder will not terminate
+under section (i) of this paragraph due to such counterclaim.
+
+A "Necessary Claim" is a claim of a patent owned by Facebook that is
+necessarily infringed by the Software standing alone.
+
+A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
+or contributory infringement or inducement to infringe any patent, including a
+cross-claim or counterclaim.
53 README.md
@@ -0,0 +1,53 @@
+# [Draft.js](https://facebook.github.io/draft-js/)
+
+Draft.js is a JavaScript rich text editor framework, built for React and
+backed by an immutable model.
+
+- **Extensible and Customizable:** We provide the building blocks to enable
+the creation of a broad variety of rich text composition experiences, from
+simple text styles to embedded media.
+- **Declarative Rich Text:** Draft.js fits seamlessly into
+[React](http://facebook.github.io/react/) applications,
+abstracting away the details of rendering, selection, and input behavior with a
+familiar declarative API.
+- **Immutable Editor State:** The Draft.js model is built
+with [immutable-js](https://facebook.github.io/immutable-js/), offering
+an API with functional state updates and aggressively leveraging data persistence
+for scalable memory usage.
+
+[Learn how to use Draft.js in your own project.](https://facebook.github.io/draft-js/docs/quickstart-getting-started.html)
+
+## Examples
+
+Visit https://facebook.github.io/draft-js/ to try out a simple rich editor example.
+
+The repository includes a variety of different editor examples to demonstrate
+some of the features offered by the framework.
+
+To run the examples, first build Draft.js locally:
+
+```
+git clone https://github.com/facebook/draft-js.git
+cd draft-js
+npm install
+npm run build
+```
+
+then open the example HTML files in your browser.
+
+Draft.js is used in production on Facebook, including status and
+comment inputs, [Notes](https://www.facebook.com/notes/), and
+[messenger.com](https://www.messenger.com).
+
+## Contribute
+
+We actively welcome pull requests. Learn how to
+[contribute](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md).
+
+## License
+
+Draft.js is [BSD Licensed](https://github.com/facebook/draft-js/blob/master/LICENSE).
+We also provide an additional [patent grant](https://github.com/facebook/draft-js/blob/master/PATENTS).
+
+Examples provided in this repository and in the documentation are separately
+licensed.
157 docs/APIReference-CharacterMetadata.md
@@ -0,0 +1,157 @@
+---
+id: api-reference-character-metadata
+title: CharacterMetadata
+layout: docs
+category: API Reference
+next: api-reference-entity
+permalink: docs/api-reference-character-metadata.html
+---
+
+`CharacterMetadata` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents inline style and entity information for a single character.
+
+`CharacterMetadata` objects are aggressively pooled and shared. If two characters
+have the same inline style and entity, they are represented with the same
+`CharacterMetadata` object. We therefore need only as many objects as combinations
+of utilized inline style sets with entity keys, keeping our memory footprint
+small even as the contents grow in size and complexity.
+
+To that end, you should create or apply changes to `CharacterMetadata` objects
+via the provided set of static methods, which will ensure that pooling is utilized.
+
+Most Draft use cases are unlikely to use these static methods, since most common edit
+operations are already implemented and available via utility modules. The getter
+methods, however, may come in handy at render time.
+
+See the API reference on
+[ContentBlock](/draft-js/docs/api-reference-content-block.html#representing-styles-and-entities)
+for information on how `CharacterMetadata` is used within `ContentBlock`.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#create">
+ <pre>static create(...): CharacterMetadata</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#applystyle">
+ <pre>static applyStyle(...): CharacterMetadata</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#removestyle">
+ <pre>static removeStyle(...): CharacterMetadata</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#applyentity">
+ <pre>static applyEntity(...): CharacterMetadata</pre>
+ </a>
+ </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#getstyle">
+ <pre>getStyle(): DraftInlineStyle</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#hasstyle">
+ <pre>hasStyle(style: string): boolean</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getentity">
+ <pre>getEntity(): ?string</pre>
+ </a>
+ </li>
+</ul>
+
+## Static Methods
+
+Under the hood, these methods will utilize pooling to return a matching object,
+or return a new object if none exists.
+
+### create
+
+```
+static create(config?: CharacterMetadataConfig): CharacterMetadata
+```
+Generates a `CharacterMetadata` object from the provided configuration. This
+function should be used in lieu of a constructor.
+
+The configuration will be used to check whether a pooled match for this
+configuration already exists. If so, the pooled object will be returned.
+Otherwise, a new `CharacterMetadata` will be pooled for this configuration,
+and returned.
+
+### applyStyle
+
+```
+static applyStyle(
+ record: CharacterMetadata,
+ style: string
+): CharacterMetadata
+```
+Apply an inline style to this `CharacterMetadata`.
+
+### removeStyle
+
+```
+static removeStyle(
+ record: CharacterMetadata,
+ style: string
+): CharacterMetadata
+```
+Remove an inline style from this `CharacterMetadata`.
+
+### applyEntity
+
+```
+static applyEntity(
+ record: CharacterMetadata,
+ entityKey: ?string
+): CharacterMetadata
+```
+
+Apply an entity key -- or provide `null` to remove an entity key -- on this
+`CharacterMetadata`.
+
+## Methods
+
+### getStyle
+
+```
+getStyle(): DraftInlineStyle
+```
+Returns the `DraftInlineStyle` for this character, an `OrderedSet` of strings
+that represents the inline style to apply for the character at render time.
+
+### hasStyle
+
+```
+hasStyle(style: string): boolean
+```
+Returns whether this character has the specified style.
+
+### getEntity
+
+```
+getEntity(): ?string
+```
+Returns the entity key (if any) for this character, as mapped to the global set of
+entities tracked by the [`Entity`](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntity.js)
+module.
+
+By tracking a string key here, we can keep the corresponding metadata separate
+from the character representation.
+
+If null, no entity is applied for this character.
246 docs/APIReference-ContentBlock.md
@@ -0,0 +1,246 @@
+---
+id: api-reference-content-block
+title: ContentBlock
+layout: docs
+category: API Reference
+next: api-reference-character-metadata
+permalink: docs/api-reference-content-block.html
+---
+
+`ContentBlock` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of a single block of editor content, including:
+
+ - Plain text contents of the block
+ - Type, e.g. paragraph, header, list item
+ - Entity, inline style, and depth information
+
+A `ContentState` object contains an `OrderedMap` of these `ContentBlock` objects,
+which together comprise the full contents of the editor.
+
+`ContentBlock` objects are largely analogous to block-level HTML elements like
+paragraphs and list items.
+
+New `ContentBlock` objects may be created directly using the constructor.
+Expected Record values are detailed below.
+
+### Representing styles and entities
+
+The `characterList` field is an immutable `List` containing a `CharacterMetadata`
+object for every character in the block. This is how we encode styles and
+entities for a given block.
+
+By making heavy use of immutability and data persistence for these lists and
+`CharacterMetadata` objects, edits to the content generally have little impact
+on the memory footprint of the editor.
+
+By encoding inline styles and entities together in this way, a function that
+performs edits on a `ContentBlock` can perform slices, concats, and other List
+methods on a single `List` object.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#getkey">
+ <pre>getKey(): string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#gettype">
+ <pre>getType(): DraftBlockType</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#gettext">
+ <pre>getText(): string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getcharacterlist">
+ <pre>getCharacterList(): List<CharacterMetadata></pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getlength">
+ <pre>getLength(): number</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getdepth">
+ <pre>getDepth(): number</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getinlinestyleat">
+ <pre>getInlineStyleAt(offset: number): DraftInlineStyle</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getentityat">
+ <pre>getEntityAt(offset: number): ?string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#findstyleranges">
+ <pre>findStyleRanges(filterFn: Function, callback: Function): void</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#findentityranges">
+ <pre>findEntityRanges(filterFn: Function, callback: Function): void</pre>
+ </a>
+ </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the `ContentBlock` constructor or to set properties.
+
+<ul class="apiIndex">
+ <li>
+ <a href="#key">
+ <pre>key: string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#type">
+ <pre>type: DraftBlockType</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#text">
+ <pre>text: string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#characterlist">
+ <pre>characterList: List<CharacterMetadata></pre>
+ </a>
+ </li>
+ <li>
+ <a href="#depth">
+ <pre>depth: number</pre>
+ </a>
+ </li>
+</ul>
+
+## Methods
+
+### getKey()
+
+```
+getKey(): string
+```
+Returns the string key for this `ContentBlock`.
+
+### getType()
+
+```
+getType(): DraftBlockType
+```
+Returns the type for this `ContentBlock`. Type values are largely analogous to
+block-level HTML elements.
+
+### getText()
+
+```
+getText(): string
+```
+Returns the full plaintext for this `ContentBlock`. This value does not contain
+any styling, decoration, or HTML information.
+
+### getCharacterList()
+
+```
+getCharacterList(): List<CharacterMetadata>
+```
+Returns an immutable `List` of `CharacterMetadata` objects, one for each
+character in the `ContentBlock`. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+This `List` contains all styling and entity information for the block.
+
+### getLength()
+
+```
+getLength(): number
+```
+Returns the length of the plaintext for the `ContentBlock`.
+
+This value uses the standard JavaScript `length` property for the string, and
+is therefore not Unicode-aware -- surrogate pairs will be counted as two
+characters.
+
+### getDepth()
+
+```
+getDepth(): number
+```
+Returns the depth value for this block, if any. This is currently used only
+for list items.
+
+### getInlineStyleAt()
+
+```
+getInlineStyleAt(offset: number): DraftInlineStyle
+```
+Returns the `DraftInlineStyle` value (an `OrderedSet<string>`) at a given offset
+within this `ContentBlock`.
+
+### getEntityAt()
+
+```
+getEntityAt(offset: number): ?string
+```
+Returns the entity key value (or `null` if none) at a given offset within this
+`ContentBlock`.
+
+### findStyleRanges()
+
+```
+findStyleRanges(
+ filterFn: (value: CharacterMetadata) => boolean,
+ callback: (start: number, end: number) => void
+): void
+```
+Executes a callback for each contiguous range of styles within this
+`ContentBlock`.
+
+### findEntityRanges()
+
+```
+findEntityRanges(
+ filterFn: (value: CharacterMetadata) => boolean,
+ callback: (start: number, end: number) => void
+): void
+```
+Executes a callback for each contiguous range of entities within this
+`ContentBlock`.
+
+## Properties
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the `ContentBlock` constructor or to set properties.
+
+### key
+See `getKey()`.
+
+### text
+See `getText()`.
+
+### type
+See `getType()`.
+
+### characterList
+See `getCharacterList()`.
+
+### depth
+See `getDepth()`.
265 docs/APIReference-ContentState.md
@@ -0,0 +1,265 @@
+---
+id: api-reference-content-state
+title: ContentState
+layout: docs
+category: API Reference
+next: api-reference-content-block
+permalink: docs/api-reference-content-state.html
+---
+
+`ContentState` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the `ContentState` object is via `EditorState.getCurrentContent()`,
+which provides the `ContentState` currently being rendered in the editor.
+
+An `EditorState` object maintains undo and redo stacks comprised of `ContentState`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#createfromtext">
+ <pre>static createFromText(text: string): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#createfromblockarray">
+ <pre>static createFromBlockArray(blocks: Array<ContentBlock>): ContentState</pre>
+ </a>
+ </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#getblockmap">
+ <pre>getBlockMap()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getselectionbefore">
+ <pre>getSelectionBefore()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getselectionafter">
+ <pre>getSelectionAfter()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getblockforkey">
+ <pre>getBlockForKey(key)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getkeybefore">
+ <pre>getKeyBefore(key)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getkeyafter">
+ <pre>getKeyAfter(key)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getblockbefore">
+ <pre>getBlockBefore(key)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getblockafter">
+ <pre>getBlockAfter(key)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getblocksasarray">
+ <pre>getBlocksAsArray()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getlastblock">
+ <pre>getLastBlock()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getplaintext">
+ <pre>getPlainText(delimiter)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#hastext">
+ <pre>hasText()</pre>
+ </a>
+ </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+ <li>
+ <a href="#blockmap">
+ <pre>blockMap</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#selectionbefore">
+ <pre>selectionBefore</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#selectionafter">
+ <pre>selectionAfter</pre>
+ </a>
+ </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+```
+static createFromText(
+ text: string,
+ delimiter?: string
+): ContentState
+```
+Generates a `ContentState` from a string, with a delimiter to split the string
+into `ContentBlock` objects. If no delimiter is provided, '\n' is used.
+
+### createFromBlockArray
+
+```
+static createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+```
+Generates a `ContentState` from an array of `ContentBlock` objects. The default
+`selectionBefore` and `selectionAfter` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+```
+getBlockMap(): BlockMap
+```
+Returns the full ordered map of `ContentBlock` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific `ContentBlock` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+```
+getSelectionBefore(): SelectionState
+```
+Returns the `SelectionState` displayed in the editor before rendering `blockMap`.
+
+When performing an `undo` action in the editor, the `selectionBefore` of the current
+`ContentState` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+```
+getSelectionAfter(): SelectionState
+```
+Returns the `SelectionState` displayed in the editor after rendering `blockMap`.
+
+When performing any action in the editor that leads to this `blockMap` being rendered,
+the selection range will be placed in the `selectionAfter` position.
+
+### getBlockForKey
+
+```
+getBlockForKey(key: string): ContentBlock
+```
+Returns the `ContentBlock` corresponding to the given block key.
+
+#### Example
+
+```
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+ .getCurrentContent()
+ .getBlockForKey(startKey)
+ .getType();
+```
+
+### getKeyBefore()
+
+```
+getKeyBefore(key: string): ?string
+```
+Returns the key before the specified key in `blockMap`, or null if this is the first key.
+
+### getKeyAfter()
+
+```
+getKeyAfter(key: string): ?string
+```
+Returns the key after the specified key in `blockMap`, or null if this is the last key.
+
+### getBlockBefore()
+
+```
+getBlockBefore(key: string): ?ContentBlock
+```
+Returns the `ContentBlock` before the specified key in `blockMap`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+```
+getBlockAfter(key: string): ?ContentBlock
+```
+Returns the `ContentBlock` after the specified key in `blockMap`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+```
+getBlocksAsArray(): Array<ContentBlock>
+```
+Returns the values of `blockMap` as an array.
+
+### getPlainText()
+
+```
+getPlainText(delimiter?: string): string
+```
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (`\u000A`) is used.
+
+### hasText()
+
+```
+hasText(): boolean
+```
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See `getBlockMap()`.
+
+### selectionBefore
+See `getSelectionBefore()`.
+
+### selectionAfter
+See `getSelectionAfter()`.
46 docs/APIReference-Data-Conversion.md
@@ -0,0 +1,46 @@
+---
+id: api-reference-data-conversion
+title: Data Conversion
+layout: docs
+category: API Reference
+next: api-reference-modifier
+permalink: docs/api-reference-data-conversion.html
+---
+
+Because a text editor doesn't exist in a vacuum and it's important to save
+contents for storage or transmission, you will want to be able to
+convert a `ContentState` into raw JS, and vice versa.
+
+To that end, we provide a couple of utility functions that allow you to perform
+these conversions.
+
+Note that the Draft library does not currently provide utilities to convert to
+and from markdown or markup, since different clients may have different requirements
+for these formats. We instead provide JavaScript objects that can be converted
+to other formats as needed.
+
+The Flow type [`RawDraftContentState`](https://github.com/facebook/draft-js/blob/master/src/model/encoding/RawDraftContentState.js)
+denotes the expected structure of the raw format of the contents. The raw state
+contains a list of content blocks, as well as a map of all relevant entity
+objects.
+
+## Functions
+
+### convertFromRaw
+
+```
+convertFromRaw(rawState: RawDraftContentState): ContentState
+```
+
+Given a raw state, convert it to `ContentState` object. This is useful when
+restoring contents to use within a Draft editor.
+
+### convertToRaw
+
+```
+convertToRaw(contentState: ContentState): RawDraftContentState
+```
+
+Given a `ContentState` object, convert it to a raw JS structure. This is useful
+when saving an editor state for storage, conversion to other formats, or
+other usage within an application.
193 docs/APIReference-Editor.md
@@ -0,0 +1,193 @@
+---
+id: api-reference-editor
+title: Editor Component
+layout: docs
+category: API Reference
+next: api-reference-editor-state
+permalink: docs/api-reference-editor.html
+---
+
+This article discusses the API and props of the core controlled contentEditable
+component itself, `Editor`. Props are defined within
+[`DraftEditorProps`](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js).
+
+## Props
+
+### Basics
+
+See [API Basics](/draft-js/docs/quickstart-api-basics.html) for an introduction.
+
+#### editorState
+```
+editorState: EditorState
+```
+The `EditorState` object to be rendered by the `Editor`.
+
+#### onChange
+```
+onChange: (editorState: EditorState) => void
+```
+The `onChange` function to be executed by the `Editor` when edits and selection
+changes occur.
+
+### Presentation (Optional)
+
+#### placeholder
+```
+placeholder?: string
+```
+Optional placeholder string to display when the editor is empty.
+
+Note: You can use CSS to style or hide your placeholder as needed. For instance,
+in the [rich editor example](https://github.com/facebook/draft-js/tree/master/examples/rich),
+the placeholder is hidden when the user changes block styling in an empty editor.
+This is because the placeholder may not line up with the cursor when the style
+is changed.
+
+#### textAlignment
+```
+textAlignment?: DraftTextAlignment
+```
+Optionally set the overriding text alignment for this editor. This alignment value will
+apply to the entire contents, regardless of default text direction for input text.
+
+You may use this if you wish to center your text or align it flush in one direction
+to fit it within your UI design.
+
+If this value is not set, text alignment will be based on the characters within
+the editor, on a per-block basis.
+
+#### blockRendererFn
+```
+blockRendererFn?: (block: ContentBlock) => ?Object
+```
+Optionally set a function to define custom block rendering. See
+[Advanced Topics: Block Components](/draft-js/docs/advanced-topics-block-components.html)
+for details on usage.
+
+#### blockStyleFn
+```
+blockStyleFn?: (block: ContentBlock) => string
+```
+Optionally set a function to define class names to apply to the given block
+when it is rendered. See
+[Advanced Topics: Block Styling](/draft-js/docs/advanced-topics-block-styling.html)
+for details on usage.
+
+#### customStyleMap
+```
+customStyleMap?: Object
+```
+Optionally define a map of inline styles to apply to spans of text with the specified
+style. See
+[Advanced Topics: Inline Styles](/draft-js/docs/advanced-topics-inline-styles.html)
+for details on usage.
+
+### Behavior (Optional)
+
+#### readOnly
+```
+readOnly?: boolean
+```
+Set whether the editor should be rendered as static DOM, with all editability
+disabled.
+
+This is useful when supporting interaction within
+[custom block components](/draft-js/docs/advanced-topics-block-components.html)
+or if you just want to display content for a static use case.
+
+Default is `false`.
+
+#### spellCheck
+```
+spellCheck?: boolean
+```
+Set whether spellcheck is turned on for your editor.
+
+Note that in OSX Safari, enabling spellcheck also enables autocorrect, if the user
+has it turned on. Also note that spellcheck is always disabled in IE, since the events
+needed to observe spellcheck events are not fired in IE.
+
+Default is `false`.
+
+#### stripPastedStyles
+```
+stripPastedStyles?: boolean
+```
+Set whether to remove all information except plaintext from pasted content.
+
+This should be used if your editor does not support rich styles.
+
+Default is `false`.
+
+### DOM and Accessibility (Optional)
+
+#### tabIndex
+#### ARIA props
+
+These props allow you to set accessibility properties on your editor. See
+[DraftEditorProps](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js) for the exhaustive list of supported attributes.
+
+### Cancelable Handlers (Optional)
+
+These prop functions are provided to allow custom event handling for a small
+set of useful events. By returning true from your handler, you indicate that
+the event is handled and the Draft core should do nothing more with it. By returning
+false, you defer to Draft to handle the event.
+
+#### handleReturn
+```
+handleReturn?: (e: SyntheticKeyboardEvent) => boolean
+```
+Handle a `RETURN` keydown event. Example usage: Choosing a mention tag from a
+rendered list of results to trigger applying the mention entity to your content.
+
+#### handleKeyCommand
+```
+handleKeyCommand?: (command: string) => boolean
+```
+Handle the named editor command. See
+[Advanced Topics: Key Bindings](/draft-js/docs/advanced-topics-key-bindings.html)
+for details on usage.
+
+#### handleBeforeInput
+```
+handleBeforeInput?: (e: SyntheticInputEvent) => boolean
+```
+Handle a `beforeInput` event before character insertion occurs within the editor.
+Example usage: After a user has typed `- ` at the start of a new block, you might
+convert that `ContentBlock` into an `unordered-list-item`.
+
+At Facebook, we also use this to convert typed ASCII quotes into "smart" quotes,
+and to convert typed emoticons into images.
+
+#### handlePastedFiles
+```
+handlePastedFiles?: (files: Array<Blob>) => boolean
+```
+Handle files that have been pasted directly into the editor.
+
+### Key Handlers (Optional)
+
+These prop functions expose common useful key events. Example: at Facebook, these are
+used to provide keyboard interaction for mention results in inputs.
+
+#### onEscape
+```
+onEscape?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onTab
+```
+onTab?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onUpArrow
+```
+onUpArrow?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onDownArrow
+```
+onDownArrow?: (e: SyntheticKeyboardEvent) => void
+```
481 docs/APIReference-EditorState.md
@@ -0,0 +1,481 @@
+---
+id: api-reference-editor-state
+title: EditorState
+layout: docs
+category: API Reference
+next: api-reference-content-state
+permalink: docs/api-reference-editor-state.html
+---
+
+`EditorState` is the top-level state object for the editor.
+
+It is an Immutable [Record](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+that represents the entire state of a Draft editor, including:
+
+ - The current text content state
+ - The current selection state
+ - The fully decorated representation of the contents
+ - Undo/redo stacks
+ - The most recent type of change made to the contents
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods below.
+
+## Overview
+
+*Common instance methods*
+
+The list below includes the most commonly used instance methods for `EditorState` objects.
+
+<ul class="apiIndex">
+ <li>
+ <a href="#getcurrentcontent">
+ <pre>getCurrentContent(): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getselection">
+ <pre>getSelection(): SelectionState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getcurrentinlinestyle">
+ <pre>getCurrentInlineStyle(): DraftInlineStyle</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getblocktree">
+ <pre>getBlockTree(): OrderedMap</pre>
+ </a>
+ </li>
+</ul>
+
+*Static Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#createempty">
+ <pre>static createEmpty(?decorator): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#createwithcontent">
+ <pre>static createWithContent(contentState, ?decorator): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#create">
+ <pre>static create(config): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#push">
+ <pre>static push(editorState, contentState, changeType): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#undo">
+ <pre>static undo(editorState): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#redo">
+ <pre>static redo(editorState): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#acceptselection">
+ <pre>static acceptSelection(editorState, selectionState): EditorState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#forceselection">
+ <pre>static forceSelection(editorState, selectionState): EditorState</pre>
+ </a>
+ </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use the static `EditorState` methods to set properties, rather than using
+> the Immutable API directly.
+
+<ul class="apiIndex">
+ <li>
+ <a href="#allowundo">
+ <pre>allowUndo</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#currentcontent">
+ <pre>currentContent</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#decorator">
+ <pre>decorator</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#directionMap">
+ <pre>directionMap</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#forceselection">
+ <pre>forceSelection</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#incompositionmode">
+ <pre>inCompositionMode</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#inlinestyleoverride">
+ <pre>inlineStyleOverride</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#lastchangetype">
+ <pre>lastChangeType</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#nativelyrenderedcontent">
+ <pre>nativelyRenderedContent</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#redostack">
+ <pre>redoStack</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#selection">
+ <pre>selection</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#treemap">
+ <pre>treeMap</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#undostack">
+ <pre>undoStack</pre>
+ </a>
+ </li>
+</ul>
+
+## Common Instance Methods
+
+### getCurrentContent
+
+```
+getCurrentContent(): ContentState
+```
+Returns the current contents of the editor.
+
+### getSelection
+
+```
+getSelection(): SelectionState
+```
+Returns the current cursor/selection state of the editor.
+
+### getCurrentInlineStyle
+
+```
+getCurrentInlineStyle(): DraftInlineStyle
+```
+Returns an `OrderedSet<string>` that represents the "current" inline style
+for the editor.
+
+This is the inline style value that would be used if a character were inserted
+for the current `ContentState` and `SelectionState`, and takes into account
+any inline style overrides that should be applied.
+
+### getBlockTree
+
+```
+getBlockTree(blockKey: string): List;
+```
+Returns an Immutable `List` of decorated and styled ranges. This is used for
+rendering purposes, and is generated based on the `currentContent` and
+`decorator`.
+
+At render time, this object is used to break the contents into the appropriate
+block, decorator, and styled range components.
+
+## Static Methods
+
+### createEmpty
+
+```
+static createEmpty(decorator?: DraftDecoratorType): EditorState
+```
+Returns a new `EditorState` object with an empty `ContentState` and default
+configuration.
+
+### createWithContent
+
+```
+static createWithContent(
+ contentState: ContentState,
+ decorator?: DraftDecoratorType
+): EditorState
+```
+Returns a new `EditorState` object based on the `ContentState` and decorator
+provided.
+
+### create
+
+```
+static create(config: EditorStateCreationConfig): EditorState
+```
+Returns a new `EditorState` object based on a configuration object. Use this
+if you need custom configuration not available via the methods above.
+
+### push
+
+```
+static push(
+ editorState: EditorState,
+ contentState: ContentState,
+ changeType: EditorChangeType
+): EditorState
+```
+Returns a new `EditorState` object with the specified `ContentState` applied
+as the new `currentContent`. Based on the `changeType`, this `ContentState`
+may be regarded as a boundary state for undo/redo behavior. See
+[Undo/Redo](/draft-js/docs/advanced-undo-redo.html) discussion for details.
+
+All content changes must be applied to the EditorState with this method.
+
+_To be renamed._
+
+### undo
+
+```
+static undo(editorState: EditorState): EditorState
+```
+Returns a new `EditorState` object with the top of the undo stack applied
+as the new `currentContent`.
+
+The existing `currentContent` is pushed onto the `redo` stack.
+
+### redo
+
+```
+static redo(editorState: EditorState): EditorState
+```
+Returns a new `EditorState` object with the top of the redo stack applied
+as the new `currentContent`.
+
+The existing `currentContent` is pushed onto the `undo` stack.
+
+### acceptSelection
+
+```
+static acceptSelection(
+ editorState: EditorState,
+ selectionState: SelectionState
+): EditorState
+```
+Returns a new `EditorState` object with the specified `SelectionState` applied,
+but without requiring the selection to be rendered.
+
+For example, this is useful when the DOM selection has changed outside of our
+control, and no re-renders are necessary.
+
+### forceSelection
+
+```
+static forceSelection(
+ editorState: EditorState,
+ selectionState: SelectionState
+): EditorState
+```
+Returns a new `EditorState` object with the specified `SelectionState` applied,
+forcing the selection to be rendered.
+
+This is useful when the selection should be manually rendered in the correct
+location to maintain control of the rendered output.
+
+## Properties and Getters
+
+In most cases, the instance and static methods above should be sufficient to
+manage the state of your Draft editor.
+
+Below is the full list of properties tracked by an `EditorState`, as well as
+their corresponding getter methods, to better provide detail on the scope of the
+state tracked in this object.
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods above.
+
+### allowUndo
+
+```
+allowUndo: boolean;
+getAllowUndo()
+```
+Whether to allow undo/redo behavior in this editor. Default is `true`.
+
+Since the undo/redo stack is the major source of memory retention, if you have
+an editor UI that does not require undo/redo behavior, you might consider
+setting this to `false`.
+
+### currentContent
+
+```
+currentContent: ContentState;
+getCurrentContent()
+```
+The currently rendered `ContentState`. See [getCurrentContent()](#getcurrentcontent).
+
+### decorator
+
+```
+decorator: ?DraftDecoratorType;
+getDecorator()
+```
+The current decorator object, if any.
+
+Note that the `ContentState` is independent of your decorator. If a decorator
+is provided, it will be used to decorate ranges of text for rendering.
+
+### directionMap
+
+```
+directionMap: BlockMap;
+getDirectionMap()
+```
+A map of each block and its text direction, as determined by UnicodeBidiService.
+
+You should not manage this manually.
+
+### forceSelection
+
+```
+forceSelection: boolean;
+mustForceSelection()
+```
+Whether to force the current `SelectionState` to be rendered.
+
+You should not set this property manually -- see
+[forceSelection()](#forceselection).
+
+### inCompositionMode
+
+```
+inCompositionMode: boolean;
+isInCompositionMode()
+```
+Whether the user is in IME composition mode. This is useful for rendering the
+appropriate UI for IME users, even when no content changes have been committed
+to the editor. You should not set this property manually.
+
+### inlineStyleOverride
+
+```
+inlineStyleOverride: DraftInlineStyle;
+getInlineStyleOverride()
+```
+An inline style value to be applied to the next inserted characters. This is
+used when keyboard commands or style buttons are used to apply an inline style
+for a collapsed selection range.
+
+`DraftInlineStyle` is a type alias for an immutable `OrderedSet` of strings,
+each of which corresponds to an inline style.
+
+### lastChangeType
+
+```
+lastChangeType: EditorChangeType;
+getLastChangeType()
+```
+The type of content change that took place in order to bring us to our current
+`ContentState`. This is used when determining boundary states for undo/redo.
+
+### nativelyRenderedContent
+
+```
+nativelyRenderedContent: ?ContentState;
+getNativelyRenderedContent()
+```
+During edit behavior, the editor may allow certain actions to render natively.
+For instance, during normal typing behavior in the contentEditable-based component,
+we can typically allow key events to fall through to print characters in the DOM.
+In doing so, we can avoid extra re-renders and preserve spellcheck highlighting.
+
+When allowing native rendering behavior, it is appropriate to use the
+`nativelyRenderedContent` property to indicate that no re-render is necessary
+for this `EditorState`.
+
+### redoStack
+
+```
+redoStack: Stack<ContentState>;
+getRedoStack()
+```
+An immutable stack of `ContentState` objects that can be resurrected for redo
+operations. When performing an undo operation, the current `ContentState` is
+pushed onto the `redoStack`.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the `allowUndo` property.
+
+See also [undoStack](#undostack).
+
+### selection
+
+```
+selection: SelectionState;
+getSelection()
+```
+The currently rendered `SelectionState`. See [acceptSelection()](#acceptselection)
+and [forceSelection()](#forceselection).
+
+You should not manage this property manually.
+
+### treeMap
+
+```
+treeMap: OrderedMap<string, List>;
+```
+The fully decorated and styled tree of ranges to be rendered in the editor
+component. The `treeMap` object is generated based on a `ContentState` and an
+optional decorator (`DraftDecoratorType`).
+
+At render time, components should iterate through the `treeMap` object to
+render decorated ranges and styled ranges, using the [getBlockTree()](#getblocktree)
+method.
+
+You should not manage this property manually.
+
+### undoStack
+
+```
+undoStack: Stack<ContentState>;
+getUndoStack()
+```
+An immutable stack of `ContentState` objects that can be restored for undo
+operations.
+
+When performing operations that modify contents, we determine whether the
+current `ContentState` should be regarded as a "boundary" state that the user
+can reach by performing an undo operation. If so, the `ContentState` is pushed
+onto the `undoStack`. If not, the outgoing `ContentState` is simply discarded.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the `allowUndo` property.
+
+See also [redoStack](#redostack).
117 docs/APIReference-Entity.md
@@ -0,0 +1,117 @@
+---
+id: api-reference-entity
+title: Entity
+layout: docs
+category: API Reference
+next: api-reference-selection-state
+permalink: docs/api-reference-entity.html
+---
+
+`Entity` is a static module containing the API for creating, retrieving, and
+updating entity objects, which are used for annotating text ranges with metadata.
+This module also houses the single store used to maintain entity data.
+
+This article is dedicated to covering the details of the API. See the
+[advanced topics article on entities](/draft-js/docs/advanced-topics-entities.html)
+for more detail on how entities may be used.
+
+Entity objects returned by `Entity` methods are represented as
+[DraftEntityInstance](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntityInstance.js) immutable records. These have a simple set of getter functions and should
+be used only for retrieval.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#create">
+ <pre>create(...): DraftEntityInstance</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#add">
+ <pre>add(instance: DraftEntityInstance): string</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#get">
+ <pre>get(key: string): DraftEntityInstance</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#mergedata">
+ <pre>mergeData(...): DraftEntityInstance</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#replacedata">
+ <pre>replaceData(...): DraftEntityInstance</pre>
+ </a>
+ </li>
+</ul>
+
+## Methods
+
+### create
+
+```
+create(
+ type: DraftEntityType,
+ mutability: DraftEntityMutability,
+ data?: Object
+): string
+```
+The `create` method should be used to generate a new entity object with the
+supplied properties.
+
+Note that a string is returned from this function. This is because entities
+are referenced by their string key in `ContentState`. The string value should
+be used within `CharacterMetadata` objects to track the entity for annotated
+characters.
+
+### add
+
+```
+add(instance: DraftEntityInstance): string
+```
+In most cases, you will use `Entity.create()`. This is a convenience method
+that you probably will not need in typical Draft usage.
+
+The `add` function is useful in cases where the instances have already been
+created, and now need to be added to the `Entity` store. This may occur in cases
+where a vanilla JavaScript representation of a `ContentState` is being revived
+for editing.
+
+### get
+
+```
+get(key: string): DraftEntityInstance
+```
+Returns the `DraftEntityInstance` for the specified key. Throws if no instance
+exists for that key.
+
+### mergeData
+
+```
+mergeData(
+ key: string,
+ toMerge: {[key: string]: any}
+): DraftEntityInstance
+```
+Since `DraftEntityInstance` objects are immutable, you cannot update an entity's
+metadata through typical mutative means.
+
+The `mergeData` method allows you to apply updates to the specified entity.
+
+### replaceData
+
+```
+replaceData(
+ key: string,
+ newData: {[key: string]: any}
+): DraftEntityInstance
+```
+The `replaceData` method is similar to the `mergeData` method, except it will
+totally discard the existing `data` value for the instance and replace it with
+the specified `newData`.
207 docs/APIReference-Modifier.md
@@ -0,0 +1,207 @@
+---
+id: api-reference-modifier
+title: Modifier
+layout: docs
+category: API Reference
+permalink: docs/api-reference-modifier.html
+---
+
+The `Modifier` module is a static set of utility functions that encapsulate common
+edit operations on `ContentState` objects. It is highly recommended that you use
+these methods for edit operations.
+
+These methods also take care of removing or modifying entity ranges appropriately,
+given the mutability types of any affected entities.
+
+In each case, these methods accept `ContentState` objects with relevant
+parameters and return `ContentState` objects. The returned `ContentState`
+will be the same as the input object if no edit was actually performed.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#replacetext">
+ <pre>replaceText(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#inserttext">
+ <pre>insertText(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#movetext">
+ <pre>moveText(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#replacewithfragment">
+ <pre>replaceWithFragment(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#removerange">
+ <pre>removeRange(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#splitblock">
+ <pre>splitBlock(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#applyinlinestyle">
+ <pre>applyInlineStyle(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#removeinlinestyle">
+ <pre>removeInlineStyle(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#setblocktype">
+ <pre>setBlockType(...): ContentState</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#applyentity">
+ <pre>applyEntity(...): ContentState</pre>
+ </a>
+ </li>
+</ul>
+
+## Static Methods
+
+### replaceText
+
+```
+replaceText(
+ contentState: ContentState,
+ rangeToReplace: SelectionState,
+ text: string,
+ inlineStyle?: DraftInlineStyle,
+ entityKey?: ?string
+): ContentState
+```
+Replaces the specified range of this `ContentState` with the supplied string,
+with the inline style and entity key applied to the entire inserted string.
+
+Example: On Facebook, when replacing `@abraham lincoln` with a mention of
+Abraham Lincoln, the entire old range is the target to replace and the mention
+entity should be applied to the inserted string.
+
+### insertText
+
+```
+insertText(
+ contentState: ContentState,
+ targetRange: SelectionState,
+ text: string,
+ inlineStyle?: DraftInlineStyle,
+ entityKey?: ?string
+): ContentState
+```
+Identical to `replaceText`, but enforces that the target range is collapsed
+so that no characters are replaced. This is just for convenience, since text
+edits are so often insertions rather than replacements.
+
+### moveText
+
+```
+moveText(
+ contentState: ContentState,
+ removalRange: SelectionState,
+ targetRange: SelectionState
+): ContentState
+```
+Moves the "removal" range to the "target" range, replacing the target text.
+
+### replaceWithFragment
+
+```
+replaceWithFragment(
+ contentState: ContentState,
+ targetRange: SelectionState,
+ fragment: BlockMap
+): ContentState
+```
+A "fragment" is a section of a block map, effectively just an
+`OrderedMap<string, ContentBlock>` much the same as the full block map of a
+`ContentState` object.
+
+This method will replace the "target" range with the fragment.
+
+Example: When pasting content, we convert the paste into a fragment to be inserted
+into the editor, then use this method to add it.
+
+### removeRange
+
+```
+removeRange(
+ contentState: ContentState,
+ rangeToRemove: SelectionState,
+ removalDirection: DraftRemovalDirection
+): ContentState
+```
+Remove an entire range of text from the editor. The removal direction is important
+for proper entity deletion behavior.
+
+### splitBlock
+
+```
+splitBlock(
+ contentState: ContentState,
+ selectionState: SelectionState
+): ContentState
+```
+Split the selected block into two blocks. This should only be used if the
+selection is collapsed.
+
+### applyInlineStyle
+
+```
+applyInlineStyle(
+ contentState: ContentState,
+ selectionState: SelectionState,
+ inlineStyle: string
+): ContentState
+```
+Apply the specified inline style to the entire selected range.
+
+### removeInlineStyle
+
+```
+removeInlineStyle(
+ contentState: ContentState,
+ selectionState: SelectionState,
+ inlineStyle: string
+): ContentState
+```
+Remove the specified inline style from the entire selected range.
+
+### setBlockType
+
+```
+setBlockType(
+ contentState: ContentState,
+ selectionState: SelectionState,
+ blockType: DraftBlockType
+): ContentState
+```
+Set the block type for all selected blocks.
+
+### applyEntity
+
+```
+applyEntity(
+ contentState: ContentState,
+ selectionState: SelectionState,
+ entityKey: ?string
+): ContentState
+```
+Apply an entity to the entire selected range, or remove all entities from the
+range if `entityKey` is `null`.
324 docs/APIReference-SelectionState.md
@@ -0,0 +1,324 @@
+---
+id: api-reference-selection-state
+title: SelectionState
+layout: docs
+category: API Reference
+next: api-reference-data-conversion
+permalink: docs/api-reference-selection-state.html
+---
+
+`SelectionState` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents a selection range in the editor.
+
+The most common use for the `SelectionState` object is via `EditorState.getSelection()`,
+which provides the `SelectionState` currently being rendered in the editor.
+
+### Keys and Offsets
+
+A selection range has two points: an **anchor** and a **focus**. (Read more on
+[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary)).
+
+The native DOM approach represents each point as a Node/offset pair, where the offset
+is a number corresponding either to a position within a Node's `childNodes` or, if the
+Node is a text node, a character offset within the text contents.
+
+Since Draft maintains the contents of the editor using `ContentBlock` objects,
+we can use our own model to represent these points. Thus, selection points are
+tracked as key/offset pairs, where the `key` value is the key of the `ContentBlock`
+where the point is positioned and the `offset` value is the character offset
+within the block.
+
+### Start/End vs. Anchor/Focus
+
+The concept of **anchor** and **focus** is very useful when actually rendering
+a selection state in the browser, as it allows us to use forward and backward
+selection as needed. For editing operations, however, the direction of the selection
+doesn't matter. In this case, it is more appropriate to think in terms of
+**start** and **end** points.
+
+The `SelectionState` therefore exposes both anchor/focus values and
+start/end values. When managing selection behavior, we recommend that
+you work with _anchor_ and _focus_ values to maintain selection direction.
+When managing content operations, however, we recommend that you use _start_
+and _end_ values.
+
+For instance, when extracting a slice of text from a block based on a
+`SelectionState`, it is irrelevant whether the selection is backward:
+
+```
+var start = selectionState.getStartOffset();
+var end = selectionState.getEndOffset();
+var selectedText = myContentBlock.getText().slice(start, end);
+```
+
+Note that `SelectionState` itself tracks only _anchor_ and _focus_ values.
+_Start_ and _end_ values are derived.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#createempty">
+ <pre>static createEmpty(blockKey)</pre>
+ </a>
+ </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+ <li>
+ <a href="#getstartkey">
+ <pre>getStartKey()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getstartoffset">
+ <pre>getStartOffset()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getendkey">
+ <pre>getEndKey()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getendoffset">
+ <pre>getEndOffset()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getanchorkey">
+ <pre>getAnchorKey()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getanchoroffset">
+ <pre>getAnchorOffset()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getfocuskey">
+ <pre>getFocusKey()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getfocusoffset">
+ <pre>getFocusOffset()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#getisbackward">
+ <pre>getIsBackward()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#gethasfocus">
+ <pre>getHasFocus()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#iscollapsed">
+ <pre>isCollapsed()</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#hasedgewithin">
+ <pre>hasEdgeWithin(blockKey, start, end)</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#serialize">
+ <pre>serialize()</pre>
+ </a>
+ </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+ <li>
+ <a href="#anchorkey">
+ <pre>anchorKey</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#anchoroffset">
+ <pre>anchorOffset</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#focuskey">
+ <pre>focusKey</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#focusoffset">
+ <pre>focusOffset</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#isbackward">
+ <pre>isBackward</pre>
+ </a>
+ </li>
+ <li>
+ <a href="#hasfocus">
+ <pre>hasFocus</pre>
+ </a>
+ </li>
+</ul>
+
+## Static Methods
+
+### createEmpty()
+
+```
+createEmpty(blockKey: string): SelectionState
+```
+Create a `SelectionState` object at the zero offset of the provided block key
+and `hasFocus` set to false.
+
+## Methods
+
+### getStartKey()
+
+```
+getStartKey(): string
+```
+Returns the key of the block containing the start position of the selection range.
+
+### getStartOffset()
+
+```
+getStartOffset(): number
+```
+Returns the block-level character offset of the start position of the selection range.
+
+### getEndKey()
+
+```
+getEndKey(): string
+```
+Returns the key of the block containing the end position of the selection range.
+
+### getEndOffset()
+
+```
+getEndOffset(): number
+```
+Returns the block-level character offset of the end position of the selection range.
+
+### getAnchorKey()
+
+```
+getAnchorKey(): string
+```
+Returns the key of the block containing the anchor position of the selection range.
+
+### getAnchorOffset()
+
+```
+getAnchorOffset(): number
+```
+Returns the block-level character offset of the anchor position of the selection range.
+
+### getFocusKey()
+
+```
+getFocusKey(): string
+```
+Returns the key of the block containing the focus position of the selection range.
+
+### getFocusOffset()
+
+```
+getFocusOffset(): number
+```
+Returns the block-level character offset of the focus position of the selection range.
+
+### getIsBackward()
+
+```
+getIsBackward(): boolean
+```
+Returns whether the focus position is before the anchor position in the document.
+
+This must be derived from the key order of the active `ContentState`, or if the selection
+range is entirely within one block, a comparison of the anchor and focus offset values.
+
+### getHasFocus()
+
+```
+getHasFocus(): boolean
+```
+Returns whether the editor has focus.
+
+### isCollapsed()
+
+```
+isCollapsed(): boolean
+```
+Returns whether the selection range is collapsed, i.e. a caret. This is true
+when the anchor and focus keys are the same /and/ the anchor and focus offsets
+are the same.
+
+### hasEdgeWithin()
+
+```
+hasEdgeWithin(blockKey: string, start: number, end: number): boolean
+```
+Returns whether the selection range has an edge that overlaps with the specified
+start/end range within a given block.
+
+This is useful when setting DOM selection within a block after contents are
+rendered.
+
+### serialize()
+
+```
+serialize(): string
+```
+Returns a serialized version of the `SelectionState`. Useful for debugging.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+```
+var selectionState = SelectionState.createEmpty('foo');
+var updatedSelection = selectionState.merge({
+ focusKey: 'bar',
+ focusOffset: 0,
+});
+var anchorKey = updatedSelection.getAnchorKey(); // 'foo'
+var focusKey = updatedSelection.getFocusKey(); // 'bar'
+```
+
+### anchorKey
+The block containing the anchor end of the selection range.
+
+### anchorOffset
+The offset position of the anchor end of the selection range.
+
+### focusKey
+The block containing the focus end of the selection range.
+
+### focusOffset
+The offset position of the focus end of the selection range.
+
+### isBackward
+If the anchor position is lower in the document than the focus position, the
+selection is backward. Note: The `SelectionState` is an object with
+no knowledge of the `ContentState` structure. Therefore, when updating
+`SelectionState` values, you are responsible for updating `isBackward` as well.
+
+### hasFocus
+Whether the editor currently has focus.
120 docs/Advanced-Topics-Block-Components.md
@@ -0,0 +1,120 @@
+---
+id: advanced-topics-block-components
+title: Custom Block Components
+layout: docs
+category: Advanced Topics
+next: advanced-topics-inline-styles
+permalink: docs/advanced-topics-block-components.html
+---
+
+Draft is designed to solve problems for straightforward rich text interfaces
+like comments and chat messages, but it also powers richer editor experiences
+like [Facebook Notes](https://www.facebook.com/notes/).
+
+Users can embed images within their Notes, either loading from their existing
+Facebook photos or by uploading new images from the desktop. To that end,
+the Draft framework supports custom rendering at the block level, to render
+content like rich media in place of plain text.
+
+The [TeX editor](https://github.com/facebook/draft-js/tree/master/examples/tex)
+in the Draft repository provides a live example of custom block rendering, with
+TeX syntax translated on the fly into editable embedded formula rendering via the
+[KaTeX library](https://khan.github.io/KaTeX/).
+
+By using a custom block renderer, it is possible to introduce complex rich
+interactions within the frame of your editor.
+
+## Custom Block Components
+
+Within the `Editor` component, one may specify the `blockRendererFn` prop.
+This prop function allows a higher-level component to define custom React
+rendering for `ContentBlock` objects, based on block type, text, or other
+criteria.
+
+For instance, we may wish to render `ContentBlock` objects of type `'media'` using
+a custom `MediaComponent`.
+
+```
+function myBlockRenderer(contentBlock) {
+ const type = contentBlock.getType();
+ if (type === 'media') {
+ return {
+ component: MediaComponent,
+ props: {
+ foo: 'bar',
+ },
+ };
+ }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithMedia = React.createClass({
+ ...
+ render() {
+ return <Editor ... blockRendererFn={myBlockRenderer} />;
+ }
+});
+```
+
+If no custom renderer object is returned by the `blockRendererFn` function,
+`Editor` will render the default `DraftEditorBlock` text block component.
+
+The `component` property defines the component to be used, while the optional
+`props` object includes props that will be passed through to the rendered
+custom component.
+
+By defining this function within the context of a higher-level component,
+the props for this custom component may be bound to that component, allowing
+instance methods for custom component props.
+
+## Defining custom block components
+
+Within `MediaComponent`, the most likely use case is that you will want to
+retrieve entity metadata to render your custom block. You may apply an entity
+key to the text within a `'media'` block during `EditorState` management,
+then retrieve the metadata for that key in your custom component `render()`
+code.
+
+```
+import {Entity} from 'draft-js';
+const MediaComponent = React.createClass({
+ render() {
+ const {block, foo} = this.props;
+ const data = Entity.get(block.getEntityAt(0)).getData();
+ // Return a <figure> or some other content using this data.
+ }
+});
+```
+
+The `ContentBlock` object is made available within the custom component, along
+with the props defined at the top level. By extracting entity information from
+the `ContentBlock` and the `Entity` map, you can obtain the metadata required to
+render your custom component.
+
+_Retrieving the entity from the block is admittedly a bit of an awkward API,
+and is worth revisiting._
+
+## Recommendations and other notes
+
+If your custom block renderer requires mouse interaction, it is often wise
+to temporarily set your `Editor` to `readOnly={true}` during this
+interaction. In this way, the user does not trigger any selection changes within
+the editor while interacting with the custom block. This should not be a problem
+with respect to editor behavior, since interacting with your custom block
+component is most likely mutually exclusive from text changes within the editor.
+
+The recommendation above is especially important for custom block renderers
+that involve text input, like the TeX editor example.
+
+It is also worth noting that within the Facebook Notes editor, we have not
+tried to perform any special SelectionState rendering or management on embedded
+media, such as rendering a highlight on an embedded photo when selecting it.
+This is in part because of the rich interaction provided on the media
+itself, with resize handles and other controls exposed to mouse behavior.
+
+Since an engineer using Draft has full awareness of the selection state
+of the editor and full control over native Selection APIs, it would be possible
+to build selection behavior on static embedded media if desired. So far, though,
+we have not tried to solve this at Facebook, so we have not packaged solutions
+for this use case into the Draft project at this time.
59 docs/Advanced-Topics-Block-Styling.md
@@ -0,0 +1,59 @@
+---
+id: advanced-topics-block-styling
+title: Block Styling
+layout: docs
+category: Advanced Topics
+next: advanced-topics-block-components
+permalink: docs/advanced-topics-block-styling.html
+---
+
+Within `Editor`, some block types are given default CSS styles to limit the amount
+of basic configuration required to get engineers up and running with custom
+editors.
+
+By defining a `blockStyleFn` prop function for a `Editor`, it is possible
+to specify classes that should be applied to blocks at render time.
+
+## DraftStyleDefault.css
+
+The Draft library includes default block CSS styles within
+[DraftStyleDefault.css](https://github.com/facebook/draft-js/blob/master/src/component/utils/DraftStyleDefault.css). _(Note that the annotations on the CSS class names are
+artifacts of Facebook's internal CSS module management system._
+
+These CSS rules are largely devoted to providing default styles for list items,
+without which callers would be responsible for managing their own default list
+styles.
+
+## blockStyleFn
+
+The `blockStyleFn` prop on `Editor` allows you to define CSS classes to
+style blocks at render time. For instance, you may wish to style `'blockquote'`
+type blocks with fancy italic text.
+
+```
+function myBlockStyleFn(contentBlock) {
+ const type = contentBlock.getType();
+ if (type === 'blockquote') {
+ return 'superFancyBlockquote';
+ }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithFancyBlockquotes = React.createClass({
+ render() {
+ return <Editor ... blockStyleFn={myBlockStyleFn} />;
+ }
+});
+```
+
+Then in your own CSS:
+
+```
+.superFancyBlockquote {
+ color: #999;
+ font-family: 'Hoefler Text', Georgia, serif;
+ font-style: italic;
+ text-align: center;
+}
+```
152 docs/Advanced-Topics-Decorators.md
@@ -0,0 +1,152 @@
+---
+id: advanced-topics-decorators
+title: Decorators
+layout: docs
+category: Advanced Topics
+next: advanced-topics-key-bindings
+permalink: docs/advanced-topics-decorators.html
+---
+
+Inline and block styles aren't the only kind of rich styling that we might
+want to add to our editor. The Facebook comment input, for example, provides
+blue background highlights for mentions and hashtags.
+
+To support flexibility for custom rich text, Draft provides a "decorator"
+system. The [tweet example](https://github.com/facebook/draft-js/tree/master/examples/tweet)
+offers a live example of decorators in action.
+
+## CompositeDecorator
+
+The decorator concept is based on scanning the contents of a given
+[ContentBlock](/draft-js/docs/api-reference-content-block.html)
+for ranges of text that match a defined strategy, then rendering them
+with a specified React component.
+
+You can use the `CompositeDecorator` class to define your desired
+decorator behavior. This class allows you to supply multiple `DraftDecorator`
+objects, and will search through a block of text with each strategy in turn.
+
+Decorators are stored within the `EditorState` record. When creating a new
+`EditorState` object, e.g. via `EditorState.createEmpty()`, a decorator may
+optionally be provided.
+
+> Under the hood
+>
+> When contents change in a Draft editor, the resulting `EditorState` object
+> will evaluate the new `ContentState` with its decorator, and identify ranges
+> to be decorated. A complete tree of blocks, decorators, and inline styles is
+> formed at this time, and serves as the basis for our rendered output.
+>
+> In this way, we always ensure that as contents change, rendered decorations
+> are in sync with our `EditorState`.
+
+In the "Tweet" editor example, for instance, we use a `CompositeDecorator` that
+searches for @-handle strings as well as hashtag strings:
+
+```
+const compositeDecorator = new CompositeDecorator([
+ {
+ strategy: handleStrategy,
+ component: HandleSpan,
+ },
+ {
+ strategy: hashtagStrategy,
+ component: HashtagSpan,
+ },
+]);
+```
+
+This composite decorator will first scan a given block of text for @-handle
+matches, then for hashtag matches.
+
+```
+// Note: these aren't very good regexes, don't use them!
+const HANDLE_REGEX = /\@[\w]+/g;
+const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
+
+function handleStrategy(contentBlock, callback) {
+ findWithRegex(HANDLE_REGEX, contentBlock, callback);
+}
+
+function hashtagStrategy(contentBlock, callback) {
+ findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+}
+
+function findWithRegex(regex, contentBlock, callback) {
+ const text = contentBlock.getText();
+ let matchArr, start;
+ while ((matchArr = regex.exec(text)) !== null) {
+ start = matchArr.index;
+ callback(start, start + matchArr[0].length);
+ }
+}
+```
+
+The strategy functions execute the provided callback with the `start` and
+`end` values of the matching range of text.
+
+## Decorator Components
+
+For your decorated ranges of text, you must define a React component to use
+to render them. These tend to be simple `span` elements with CSS classes or
+styles applied to them.
+
+In our current example, the `CompositeDecorator` object names `HandleSpan` and
+`HashtagSpan` as the components to use for decoration. These are just basic
+stateless components:
+
+```
+const HandleSpan = (props) => {
+ return <span {...props} style={styles.handle}>{props.children}</span>;
+};
+
+const HashtagSpan = (props) => {
+ return <span {...props} style={styles.hashtag}>{props.children}</span>;
+};
+```
+
+Note that `props.children` is passed through to the rendered output. This is
+done to ensure that the text is rendered within the decorated `span`.
+
+You can use the same approach for links, as demonstrated in our
+[link example](https://github.com/facebook/draft-js/tree/master/examples/link).
+
+### Beyond CompositeDecorator
+
+The decorator object supplied to an `EditorState` need only match the expectations
+of the
+[DraftDecoratorType](https://github.com/facebook/draft-js/blob/master/src/model/decorators/DraftDecoratorType.js)
+Flow type definition, which means that you can create any decorator classes
+you wish, as long as they match the expected type -- you are not bound by
+`CompositeDecorator`.
+
+## Setting new decorators
+
+Further, it is acceptable to set a new `decorator` value on the `EditorState`
+on the fly, during normal state propagation -- through immutable means, of course.
+
+This means that during your app workflow, if your decorator becomes invalid or
+requires a modification, you can create a new decorator object (or use
+`null` to remove all decorations) and `EditorState.set()` to make use of the new
+decorator setting.
+
+For example, if for some reason we wished to disable the creation of @-handle
+decorations while the user interacts with the editor, it would be fine to do the
+following:
+
+```
+function turnOffHandleDecorations(editorState) {
+ const onlyHashtags = new CompositeDecorator([{
+ strategy: hashtagStrategy,
+ component: HashtagSpan,
+ }]);
+ return EditorState.set(editorState, {decorator: onlyHashtags});
+}
+```
+
+The `ContentState` for this `editorState` will be re-evaluated with the new
+decorator, and @-handle decorations will no longer be present in the next
+render pass.
+
+Again, this remains memory-efficient due to data persistence across immutable
+objects.
136 docs/Advanced-Topics-Entities.md
@@ -0,0 +1,136 @@
+---
+id: advanced-topics-entities
+title: Entities
+layout: docs
+category: Advanced Topics
+next: advanced-topics-decorators
+permalink: docs/advanced-topics-entities.html
+---
+
+This article discusses the Entity system, which Draft uses for annotating
+ranges of text with metadata. Entities enable engineers to introduce levels of
+richness beyond styled text to their editors. Links, mentions, and embedded
+content can all be implemented using entities.
+
+In the Draft repository, the
+[link editor](https://github.com/facebook/draft-js/tree/master/examples/link)
+and
+[entity demo](https://github.com/facebook/draft-js/tree/master/examples/entity)
+provide live code examples to help clarify how entities can be used, as well
+as their built-in behavior.
+
+The [Entity API Reference](/draf-js/docs/api-reference-entity.html) provides
+details on the static methods to be used when creating, retrieving, or updating
+entity objects.
+
+## Introduction
+
+An entity is an object that represents metadata for a range of text within a
+Draft editor. It has three properties:
+
+- **type**: A string that indicates what kind of entity it is, e.g. `'LINK'`,
+`'MENTION'`, `'PHOTO'`.
+- **mutability**: Not to be confused with immutability a la `immutable-js`, this
+property denotes the behavior of a range of text annotated with this entity
+object when editing the text range within the editor. This is addressed in
+greater detail below.
+- **data**: An optional object containing metadata for the entity. For instance,
+a `'LINK'` entity might contain a `data` object that contains the `href` value
+for that link.
+
+All entities are stored in a single object store within the `Entity` module,
+and are referenced by key within `ContentState` and React components used to
+decorate annotated ranges. _(We are considering future changes to bring
+the entity store into `EditorState` or `ContentState`.)_
+
+Using [decorators](/draft-js/docs/advanced-topics-decorators.html) or
+[custom block components](docs/advanced-topics-block-components.html), you can
+add rich rendering to your editor based on entity metadata.
+
+## Creating and Retrieving Entities
+
+Entities should be created using `Entity.create`, which accepts the three
+properties above as arguments. This method returns a string key, which can then
+be used to refer to the entity.
+
+This key is the value that should be used when applying entities to your
+content. For instance, the `Modifier` module contains an `applyEntity` method:
+
+```
+const key = Entity.create('LINK', 'MUTABLE', {href: 'http://www.zombo.com'});
+const contentStateWithLink = Modifier.applyEntity(
+ contentState,
+ targetRange,
+ key
+);
+```
+
+For a given range of text, then, you can extract its associated entity key by using
+the `getEntityAt()` method on a `ContentBlock` object, passing in the target
+offset value.
+
+```
+const blockWithLinkAtBeginning = contentState.getBlockForKey('...');
+const linkKey = blockWithLinkAtBeginning.getEntityAt(0);
+const linkInstance = Entity.get(linkKey);
+const {href} = linkInstance.getData();
+```
+
+## "Mutability"
+
+Entities may have one of three "mutability" values. The difference between them
+is the way they behave when the user makes edits to them.
+
+Note that `DraftEntityInstance` objects are always immutable Records, and this
+property is meant only to indicate how the annotated text may be "mutated" within
+the editor. _(Future changes may rename this property to ward off potential
+confusion around naming.)_
+
+### Immutable
+
+This text cannot be altered without removing the entity annotation
+from the text. Entities with this mutability type are effectively atomic.
+
+For instance, in a Facebook input, add a mention for a Page (i.e. Barack Obama).
+Then, either add a character within the mentioned text, or try to delete a character.
+Note that when adding characters, the entity is removed, and when deleting charaters,
+the entire entity is removed.
+
+This mutability value is useful in cases where the text absolutely must match
+its relevant metadata, and may not be altered.
+
+### Mutable
+
+This text may be altered freely. For instance, link text is
+generally intended to be "mutable" since the href and linkified text are not
+tightly coupled.
+
+### Segmented
+
+Entities that are "segmented" are tightly coupled to their text in much the
+same way as "immutable" entities, but allow customization via deletion.
+
+For instance, in a Facebook input, add a mention for a friend. Then, add a
+character to the text. Note that the entity is removed from the entire string,
+since your mentioned friend may not have their name altered in your text.
+
+Next, try deleting a character or word within the mention. Note that only the
+section of the mention that you have deleted is removed. In this way, we can
+allow short names for mentions.
+
+## Modifying Entities
+
+Since `DraftEntityInstance` records are immutable, you may not update the `data`
+property on an instance directly.
+
+Instead, two `Entity` methods are available to modify entities: `mergeData` and
+`replaceData`. The former allows updating data by passing in an object to merge,
+while the latter completely swaps in the new data object.
+
+## Using Entities for Rich Content
+
+The next article in this section covers the usage of decorator objects, which
+can be used to retrieve entities for rendering purposes.
+
+The [link editor example](https://github.com/facebook/draft-js/tree/master/examples/link)
+provides a working example of entity creation and decoration in use.
112 docs/Advanced-Topics-Inline-Styles.md
@@ -0,0 +1,112 @@
+---
+id: advanced-topics-inline-styles
+title: Complex Inline Styles
+layout: docs
+category: Advanced Topics
+next: advanced-topics-nested-lists
+permalink: docs/advanced-topics-inline-styles.html
+---
+
+Within your editor, you may wish to provide a wide variety of inline style
+behavior that goes well beyond the bold/italic/underline basics. For instance,
+you may want to support variety with color, font families, font sizes, and more.
+Further, your desired styles may overlap or be mutually exclusive.
+
+The [Rich Editor](http://github.com/facebook/draft-js/examples/rich) and
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+examples demonstrate complex inline style behavior in action.
+
+### Model
+
+Within the Draft model, inline styles are represented at the character level,
+using an immutable `OrderedSet` to define the list of styles to be applied to
+each character. These styles are identified by string. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+For example, consider the text "Hello **world**". The first six characters of
+the string are represented by the empty set, `OrderedSet()`. The final five
+characters are represented by `OrderedSet.of('BOLD')`. For convenience, we can
+think of these `OrderedSet` objects as arrays, though in reality we aggressively
+reuse identical immutable objects.
+
+In essence, our styles are:
+
+```
+[
+ [], // H
+ [], // e
+ ...
+ ['BOLD'], // w
+ ['BOLD'], // o
+ // etc.
+]
+```
+
+### Overlapping Styles
+
+Now let's say that we wish to make the middle range of characters italic as well:
+"He_llo **wo**_**rld**". This operation can be performed via the
+[Modifier](/draft-js/docs/api-reference-modifier.html) API.
+
+The end result will accommodate the overlap by including `'ITALIC'` in the
+relevant `OrderedSet` objects as well.
+
+```
+[
+ [], // H
+ [], // e
+ ['ITALIC'], // l
+ ...
+ ['BOLD', 'ITALIC'], // w
+ ['BOLD', 'ITALIC'], // o
+ ['BOLD'], // r
+ // etc.
+]
+```
+
+When determining how to render inline-styled text, Draft will identify
+contiguous ranges of identically styled characters and render those characters
+together in styled `span` nodes.
+
+### Mapping a style string to CSS
+
+By default, `Editor` provides support for a basic list of inline styles:
+`'BOLD'`, `'ITALIC'`, `'UNDERLINE'`, and `'CODE'`. These are mapped to simple CSS
+style objects, which are used to apply styles to the relevant ranges.
+
+For your editor, you may define custom style strings to include with these
+defaults, or you may override the default style objects for the basic styles.
+
+Within your `Editor` use case, you may provide the `customStyleMap` prop
+to define your style objects. (See
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+for a live example.)
+
+For example, you may want to add a `'STRIKETHROUGH'` style. To do so, define a
+custom style map:
+
+```
+import {Editor} from 'draft-js';
+
+const styleMap = {
+ 'STRIKETHROUGH': {
+ textDecoration: 'line-through',
+ },
+};
+
+class MyEditor extends React.Component {
+ // ...
+ render() {
+ return (
+ <Editor
+ customStyleMap={styleMap}
+ editorState={this.state.editorState}
+ ...
+ />
+ );
+ }
+}
+```
+
+When rendered, the `textDecoration: line-through` style will be applied to all
+character ranges with the `STRIKETHROUGH` style.
75 docs/Advanced-Topics-Issues-and-Pitfalls.md
@@ -0,0 +1,75 @@
+---
+id: advanced-topics-issues-and-pitfalls
+title: Issues and Pitfalls
+layout: docs
+category: Advanced Topics
+next: api-reference-editor
+permalink: docs/advanced-topics-issues-and-pitfalls.html
+---
+
+This article addresses some known issues with the Draft editor framework, as
+well as some common pitfalls that we have encountered while using the framework
+at Facebook.
+
+## Common Pitfalls
+
+### Delayed state updates
+
+A common pattern for unidirectional data management is to batch or otherwise
+delay updates to data stores, using a setTimeout or another mechanism. Stores are
+updated, then emit changes to the relevant React components to propagate
+re-rendering.
+
+When delays are introduced to a React application with a Draft editor, however,
+it is possible to cause significant interaction problems. This is because the
+editor expects immediate updates and renders that stay in sync with the user's typing
+behavior. Delays can prevent updates from being propagated through the editor
+component tree, which can cause a disconnect between keystrokes and updates.
+
+To avoid this while still using a delaying or batching mechanism, you should
+separate the delay behavior from your `Editor` state propagation. That is,
+you must always allow your `EditorState` to propagate to your `Editor`
+component without delay, and independently perform batched updates that do
+not affect the state of your `Editor` component.
+
+## Known Issues
+
+### React ContentEditable Warning
+
+Within the React core, a warning is used to ward off engineers who wish to
+use ContentEditable within their components, since by default the
+browser-controlled nature of ContentEditable does not mesh with strict React
+control over the DOM. The Draft editor resolves this issue, so for our case,
+the warning is noise. You can ignore it for now.
+
+We are currently looking into removing or replacing the warning to alleviate
+the irritation it may cause: https://github.com/facebook/react/issues/6081
+
+### Custom OSX Keybindings
+
+Because the browser has no access to OS-level custom keybindings, it is not
+possible to intercept edit intent behaviors that do not map to default system
+key bindings.
+
+The result of this is that users who use custom keybindings may encounter
+issues with Draft editors, since their key commands may not behave as expected.
+
+### Browser plugins/extensions
+
+As with any React application, browser plugins and extensions that modify the
+DOM can cause Draft editors to break.
+
+Grammar checkers, for instance, may modify the DOM within contentEditable
+elements, adding styles like underlines and backgrounds. Since React cannot
+reconcile the DOM if the browser does not match its expectations,
+the editor state may fail to remain in sync with the DOM.
+
+Certain old ad blockers are also known to break the native DOM Selection
+API -- a bad idea no matter what! -- and since Draft depends on this API to
+maintain controlled selection state, this can cause trouble for editor
+interaction.
+
+### IME and Internet Explorer
+
+As of IE11, Internet Explorer demonstrates notable issues with certain international
+input methods, most significantly Korean input.
100 docs/Advanced-Topics-Key-Bindings.md
@@ -0,0 +1,100 @@
+---
+id: advanced-topics-key-bindings
+title: Key Bindings
+layout: docs
+category: Advanced Topics
+next: advanced-topics-managing-focus
+permalink: docs/advanced-topics-key-bindings.html
+---
+
+The `Editor` component offers flexibility to define custom key bindings
+for your editor, via the `keyBindingFn` prop. This allows you to match key
+commands to behaviors in your editor component.
+
+### Defaults
+
+The default key binding function is `getDefaultKeyBinding`.
+
+Since the Draft framework maintains tight control over DOM rendering and
+behavior, basic editing commands must be captured and routed through the key
+binding system.
+
+`getDefaultKeyBinding` maps known OS-level editor commands to `DraftEditorCommand`
+strings, which then correspond to behaviors within component handlers.
+
+For instance, `Ctrl+Z` (Win) and `Cmd+Z` (OSX) map to the `'undo'` command,
+which then routes our handler to perform an `EditorState.undo()`.
+
+### Customization
+
+You may provide your own key binding function to supply custom command strings.
+
+It is recommended that your function use `getDefaultKeyBinding` as a
+fall-through case, so that your editor may benefit from default commands.
+
+With your custom command string, you may then implement the `handleKeyCommand`
+prop function, which allows you to map that command string to your desired
+behavior. If `handleKeyCommand` returns `true`, the command is considered
+handled. If it returns `false`, the command will fall through
+
+### Example
+
+Let's say we have an editor that should have a "Save" mechanism to periodically
+write your contents to the server as a draft copy.
+
+First, let's define our key binding function.
+
+```
+import {KeyBindingUtil} from 'draft-js';
+const {hasCommandModifier} = KeyBindingUtil;
+
+function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
+ if (e.keyCode === 83 /* `S` key */ && hasCommandModifier(e)) {
+ return 'myeditor-save';
+ }
+ return getDefaultKeyBinding(e);
+}
+```
+
+Our function receives a key event, and we check whether it matches our criteria:
+it must be an `S` key, and it must have a command modifier, i.e. the command
+key for OSX, or the control key otherwise.
+
+If the command is a match, return a string that names the command. Otherwise,
+fall through to the default key bindings.
+
+In our editor component, we can then make use of the command via the
+`handleKeyCommand` prop:
+
+```
+import {Editor} from 'draft-js';
+class MyEditor extends React.Component {
+ // ...
+
+ handleKeyCommand(command: string): boolean {
+ if (command === 'myeditor-save') {
+ // Perform a request to save your contents, set
+ // a new `editorState`, etc.
+ return true;
+ }
+ return false;
+ }
+
+ render() {
+ return (
+ <Editor
+ editorState={this.state.editorState}
+ handleKeyCommand={this.handleKeyCommand.bind(this)}
+ ...
+ />
+ );
+ }
+}
+```
+
+The `'myeditor-save'` command can be used for our custom behavior, and returning
+true instructs the editor that the command has been handled and no more work
+is required.
+
+By returning false in all other cases, default commands are able to fall
+through to default handler behavior.
40 docs/Advanced-Topics-Managing-Focus.md
@@ -0,0 +1,40 @@
+---
+id: advanced-topics-managing-focus
+title: Managing Focus
+layout: docs
+category: Advanced Topics
+next: advanced-topics-block-styling
+permalink: docs/advanced-topics-managing-focus.html
+---
+
+Managing text input focus can be a tricky task within React components. The browser
+focus/blur API is imperative, so setting or removing focus via declarative means
+purely through `render()` tends to feel awkward and incorrect, and it requires
+challenging attempts at controlling focus state.
+
+With that in mind, at Facebook we often choose to expose `focus()` methods
+on components that wrap text inputs. This breaks the declarative paradigm,
+but it also simplifies the work needed for engineers to successfully manage
+focus behavior within their apps.
+
+The `Editor` component follows this pattern, so there is a public `focus()`
+method available on the component. This allows you to use a ref within your
+higher-level component to call `focus()` directly on the component when needed.
+
+The event listeners within the component will observe focus changes and
+propagate them through `onChange` as expected, so state and DOM will remain
+correctly in sync.
+
+## Translating container clicks to focus
+
+Your higher-level component will most likely wrap the `Editor` component in a
+container of some kind, perhaps with padding to style it to match your app.
+
+By default, if a user clicks within this container but outside of the rendered
+`Editor` while attempting to focus the editor, the editor will have no awareness
+of the click event. It is therefore recommended that you use a click listener
+on your container component, and use the `focus()` method described above to
+apply focus to your editor.
+
+The [plaintext editor example](https://github.com/facebook/draft-js/tree/master/examples/plaintext),
+for instance, uses this pattern.
22 docs/Advanced-Topics-Nested-Lists.md
@@ -0,0 +1,22 @@
+---
+id: advanced-topics-nested-lists
+title: Nested Lists
+layout: docs
+category: Advanced Topics
+next: advanced-topics-text-direction
+permalink: docs/advanced-topics-nested-lists.html
+---
+
+The Draft framework provides support for nested lists, as demonstrated in the
+Facebook Notes editor. There, you can use `Tab` and `Shift+Tab` to add or remove
+depth to a list item.
+
+The `RichUtils` module provides a handy `onTab` method that manages this
+behavior, and should be sufficient for most nested list needs. You can use
+the `onTab` prop on your `Editor` to make use of this utility.
+
+By default, styling is applied to list items to set appropriate spacing and
+list style behavior, via `DraftStyleDefault.css`.
+
+Note that there is currently no support for handling depth for blocks of any type
+except `'ordered-list-item'` and `'unordered-list-item'`.
35 docs/Advanced-Topics-Text-Direction.md
@@ -0,0 +1,35 @@
+---
+id: advanced-topics-text-direction
+title: Text Direction
+layout: docs
+category: Advanced Topics
+next: advanced-topics-issues-and-pitfalls
+permalink: docs/advanced-topics-text-direction.html
+---
+
+Facebook supports dozens of languages, which means that our text inputs need
+to be flexible enough to handle considerable variety.
+
+For example, we want input behavior for RTL languages such as Arabic and Hebrew
+to meet users' expectations. We also want to be able to support editor contents
+with a mixture of LTR and RTL text.
+
+To that end, Draft uses a bidi direction algorithm to determine appropriate
+text alignment and direction on a per-block basis.
+
+Text is rendered with an LTR or RTL direction automatically as the user types.
+You should not need to do anything to set direction yourself.
+
+## Text Alignment
+
+While languages are automatically aligned to the left or right during composition,
+as defined by the content characters, it is also possible for engineers to
+manually set the text alignment for an editor's contents.
+
+This may be useful, for instance, if an editor requires strictly centered
+contents, or needs to keep text aligned flush against another UI element.
+
+The `Editor` component therefore provides a `textAlignment` prop, with a
+simple set of values: `'left'`, `'center'`, and `'right'`. Using these values,
+the contents of your editor will be aligned to the specified direction regardless
+of language and character set.
45 docs/Overview.md
@@ -0,0 +1,45 @@
+---
+id: getting-started
+title: Overview
+layout: docs
+category: Quick Start
+next: quickstart-api-basics
+permalink: docs/overview.html
+---
+
+Draft.js is a framework for building rich text editors in React, powered by an immutable model and abstracting over cross-browser differences.
+
+Draft.js makes it easy to build any type of rich text input, whether you're just looking to support a few inline text styles or building a complex text editor for composing long-form articles.
+
+### Installation
+
+Currently Draft.js is distributed via npm. It depends on React and React DOM which must also be installed.
+
+```sh
+npm install --save draft-js react react-dom
+```
+
+### Usage
+
+```js
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Editor} from 'draft-js';
+
+class MyEditor extends React.Component {
+ onChange(editorState) {
+ this.setState({editorState});
+ },
+ render() {
+ const {editorState} = this.state;
+ return <Editor editorState={editorState} onChange={this.onChange} />;
+ }
+}
+
+ReactDOM.render(
+ <MyEditor />,
+ document.getElementById('container')
+);
+```
+
+Next, let's go into the basics of the API and learn what else you can do with Draft.js.
75 docs/QuickStart-API-Basics.md
@@ -0,0 +1,75 @@
+---
+id: quickstart-api-basics
+title: API Basics
+layout: docs
+category: Quick Start
+next: quickstart-rich-styling
+permalink: docs/quickstart-api-basics.html
+---
+
+This document provides an overview of the basics of the `Draft` API. A
+[working example](https://github.com/facebook/draft-js/tree/master/examples/plaintext)
+is also available to follow along.
+
+## Controlled Inputs
+
+The `Editor` React component is built as a controlled ContentEditable component,
+with the goal of providing a top-level API modeled on the familiar React
+*controlled input* API.
+
+As a brief refresher, controlled inputs involve two key pieces:
+
+1. A _value_ to represent the state of the input
+2. An _onChange_ prop function to receive updates to the input
+
+This approach allows the component that composes the input to have strict
+control over the state of the input, while still allowing updates to the DOM
+to provide information about the text that the user has written.
+
+```
+const MyInput = React.createClass({
+ onChange(evt) {
+ this.setState({value: evt.target.value});
+ },
+ render() {
+ return <input value={this.state.value} onChange={this.onChange} />;
+ }
+});
+```
+
+The top-level component can maintain control over the input state via this
+`value` state property.
+
+## Controlling Rich Text
+
+In a React rich text scenario, however, there are two clear problems:
+
+1. A string of plaintext is insufficient to represent the complex state of
+a rich editor.
+2. There is no such `onChange` event available for a ContentEditable element.
+
+State is therefore represented as a single immutable
+[EditorState](/draft-js/docs/api-reference-editor-state.html) object, and
+`onChange` is implemented within the `Editor` core to provide this state
+value to the top level.
+
+The `EditorState` object is a complete snapshot of the state of the editor,
+including contents, cursor, and undo/redo history. All changes to content and
+selection within the editor will create new `EditorState` objects. Note that
+this remains efficient due to data persistence across immutable objects.
+
+```
+import {Editor} from 'draft-js';
+const MyEditor = React.createClass({
+ onChange(editorState) {
+ this.setState({editorState});
+ },
+ render() {
+ const {editorState} = this.state;
+ return <Editor editorState={editorState} onChange={this.onChange} />;
+ }
+});
+```
+
+For any edits or selection changes that occur in the editor DOM, your `onChange`
+handler will execute with the latest `EditorState` object based on those changes.
112 docs/QuickStart-Rich-Styling.md
@@ -0,0 +1,112 @@
+---
+id: quickstart-rich-styling
+title: Rich Styling
+layout: docs
+category: Quick Start
+next: advanced-topics-entities
+permalink: docs/quickstart-rich-styling.html
+---
+
+Now that we have established the basics of the top-level API, we can go a step
+further and examine how basic rich styling can be added to a `Draft` editor.
+
+A [rich text example](https://github.com/facebook/draft-js/tree/master/examples/rich)
+is also available to follow along.
+
+## EditorState: Yours to Command
+
+The previous article introduced the `EditorState` object as a snapshot of the
+full state of the editor, as provided by the `Editor` core via the
+`onChange` prop.
+
+However, since your top-level React component is responsible for maintaining the
+state, you also have the freedom to apply changes to that `EditorState` object
+in any way you see fit.
+
+For inline and block style behavior, for example, the `RichUtils` module
+provides a number of useful functions to help manipulate state.
+
+Similarly, the [Modifier](/draft-js/docs/api-reference-modifier.html) module also provides a
+number of common operations that allow you to apply edits, including changes
+to text, styles, and more. This module is a suite of edit functions that
+compose simpler, smaller edit functions to return the desired `EditorState`
+object.
+
+For this example, we'll stick with `RichUtils` to demonstrate how to apply basic
+rich styling within the top-level component.
+
+## RichUtils and Key Commands
+
+`RichUtils` has information about the core key commands available to web editors,
+such as Cmd+B (bold), Cmd+I (italic), and so on.
+
+We can observe and handle key commands via the `handleKeyCommand` prop, and
+hook these into `RichUtils` to apply or remove the desired style.
+
+```
+import {Editor, RichUtils} from 'draft-js';
+const MyEditor = React.createClass({
+ onChange(editorState) {
+ this.setState({editorState});
+ },
+ handleKeyCommand(command) {
+ const {editorState} = this.state;
+ const newState = RichUtils.handleKeyCommand(editorState, command);
+ if (newState) {
+ this.onChange(newState);
+ return true;
+ }
+ return false;
+ },
+ render() {
+ const {editorState} = this.state;
+ return (
+ <Editor
+ editorState={editorState}
+ handleKeyCommand={this.handleKeyCommand}
+ onChange={this.onChange}
+ />
+ );
+ }
+});
+```
+
+> handleKeyCommand
+>
+> The `command` argument supplied to `handleKeyCommand` is a string value, the
+> name of the command to be executed. This is mapped from a DOM key event. See
+> [Advanced Topics - Key Binding](/draft-js/docs/advanced-topics-key-bindings.html) for more
+> on this, as well as details on why the function returns a boolean.
+
+## Styling Controls in UI
+
+Within your React component, you can add buttons or other controls to allow
+the user to modify styles within the editor. In the example above, we are using
+known key commands, but we can add more complex UI to provide these rich
+features.
+
+Here's a super-basic example with a "Bold" button to toggle the `BOLD` style.
+
+```
+const MyEditor = React.createClass({
+ ...
+
+ _onBoldClick() {
+ this.onChange(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
+ }
+
+ render() {
+ const {editorState} = this.state;
+ return (
+ <div>
+ <button onClick={this._onBoldClick}>Bold</button>
+ <Editor
+ editorState={editorState}
+ handleKeyCommand={this.handleKeyCommand}
+ onChange={this.onChange}
+ />
+ </div>
+ );
+ }
+});
+```
219 examples/color/color.html
@@ -0,0 +1,219 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Color</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ </head>
+ <body>
+ <div style="font-family: Georgia, serif; font-size: 15px; padding: 20px; width: 600px; line-height: 24px;">
+ This example demonstrates how custom inline styles can be used to create
+ editors with an unlimited range of style combinations. Note also that
+ the state of the editor can be manipulated to enforce that only one
+ color may be active at any time.
+ </div>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.min.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {Editor, EditorState, Modifier, RichUtils} = Draft;
+
+ class ColorfulEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {editorState: EditorState.createEmpty()};
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+ this.toggleColor = (toggledColor) => this._toggleColor(toggledColor);
+ }
+
+ _toggleColor(toggledColor) {
+ const {editorState} = this.state;
+ const selection = editorState.getSelection();
+
+ // Let's just allow one color at a time. Turn off all active colors.
+ const nextContentState = Object.keys(colorStyleMap)
+ .reduce((contentState, color) => {
+ return Modifier.removeInlineStyle(contentState, selection, color)
+ }, editorState.getCurrentContent());
+
+ let nextEditorState = EditorState.push(
+ editorState,
+ nextContentState,
+ 'change-inline-style'
+ );
+
+ const currentStyle = editorState.getCurrentInlineStyle();
+
+ // Unset style override for current color.
+ if (selection.isCollapsed()) {
+ nextEditorState = currentStyle.reduce((state, color) => {
+ return RichUtils.toggleInlineStyle(state, color);
+ }, nextEditorState);
+ }
+
+ // If the color is being toggled on, apply it.
+ if (!currentStyle.has(toggledColor)) {
+ nextEditorState = RichUtils.toggleInlineStyle(
+ nextEditorState,
+ toggledColor
+ );
+ }
+
+ this.onChange(nextEditorState);
+ }
+
+ render() {
+ const {editorState} = this.state;
+ return (
+ <div style={styles.root}>
+ <ColorControls
+ editorState={editorState}
+ onToggle={this.toggleColor}
+ />
+ <div style={styles.editor} onClick={this.focus}>
+ <Editor
+ customStyleMap={colorStyleMap}
+ editorState={editorState}
+ onChange={this.onChange}
+ placeholder="Write something colorful..."
+ ref="editor"
+ />
+ </div>
+ </div>
+ );
+ }
+ }
+
+ class StyleButton extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onToggle = (e) => {
+ e.preventDefault();
+ this.props.onToggle(this.props.style);
+ };
+ }
+
+ render() {
+ let style;
+ if (this.props.active) {
+ style = {...styles.styleButton, ...colorStyleMap[this.props.style]};
+ } else {
+ style = styles.styleButton;
+ }
+
+ return (
+ <span style={style} onMouseDown={this.onToggle}>
+ {this.props.label}
+ </span>
+ );
+ }
+ }
+
+ var COLORS = [
+ {label: 'Red', style: 'red'},
+ {label: 'Orange', style: 'orange'},
+ {label: 'Yellow', style: 'yellow'},
+ {label: 'Green', style: 'green'},
+ {label: 'Blue', style: 'blue'},
+ {label: 'Indigo', style: 'indigo'},
+ {label: 'Violet', style: 'violet'},
+ ];
+
+ const ColorControls = (props) => {
+ var currentStyle = props.editorState.getCurrentInlineStyle();
+ return (
+ <div style={styles.controls}>
+ {COLORS.map(type =>
+ <StyleButton
+ active={currentStyle.has(type.style)}
+ label={type.label}
+ onToggle={props.onToggle}
+ style={type.style}
+ />
+ )}
+ </div>
+ );
+ };
+
+ // This object provides the styling information for our custom color
+ // styles.
+ const colorStyleMap = {
+ red: {
+ color: 'rgba(255, 0, 0, 1.0)',
+ },
+ orange: {
+ color: 'rgba(255, 127, 0, 1.0)',
+ },
+ yellow: {
+ color: 'rgba(180, 180, 0, 1.0)',
+ },
+ green: {
+ color: 'rgba(0, 180, 0, 1.0)',
+ },
+ blue: {
+ color: 'rgba(0, 0, 255, 1.0)',
+ },
+ indigo: {
+ color: 'rgba(75, 0, 130, 1.0)',
+ },
+ violet: {
+ color: 'rgba(127, 0, 255, 1.0)',
+ },
+ };
+
+ const styles = {
+ root: {
+ fontFamily: '\'Georgia\', serif',
+ fontSize: 14,
+ padding: 20,
+ width: 600,
+ },
+ editor: {
+ borderTop: '1px solid #ddd',
+ cursor: 'text',
+ fontSize: 16,
+ marginTop: 20,
+ minHeight: 400,
+ paddingTop: 20,
+ },
+ controls: {
+ fontFamily: '\'Helvetica\', sans-serif',
+ fontSize: 14,
+ marginBottom: 10,
+ userSelect: 'none',
+ },
+ styleButton: {
+ color: '#999',
+ cursor: 'pointer',
+ marginRight: 16,
+ padding: '2px 0',
+ },
+ };
+
+ ReactDOM.render(
+ <ColorfulEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
223 examples/entity/entity.html
@@ -0,0 +1,223 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Plain Text Editor</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {
+ convertFromRaw,
+ convertToRaw,
+ CompositeDecorator,
+ ContentState,
+ Editor,
+ EditorState,
+ Entity,
+ } = Draft;
+
+ const rawContent = {
+ blocks: [
+ {
+ text: (
+ 'This is an "immutable" entity: Superman. Deleting any ' +
+ 'characters will delete the entire entity. Adding characters ' +
+ 'will remove the entity from the range.'
+ ),
+ type: 'unstyled',
+ entityRanges: [{offset: 31, length: 8, key: 'first'}],
+ },
+ {
+ text: '',
+ type: 'unstyled',
+ },
+ {
+ text: (
+ 'This is a "mutable" entity: Batman. Characters may be added ' +
+ 'and removed.'
+ ),
+ type: 'unstyled',
+ entityRanges: [{offset: 28, length: 6, key: 'second'}],
+ },
+ {
+ text: '',
+ type: 'unstyled',
+ },
+ {
+ text: (
+ 'This is a "segmented" entity: Green Lantern. Deleting any ' +
+ 'characters will delete the current "segment" from the range. ' +
+ 'Adding characters will remove the entire entity from the range.'
+ ),
+ type: 'unstyled',
+ entityRanges: [{offset: 30, length: 13, key: 'third'}],
+ },
+ ],
+
+ entityMap: {
+ first: {
+ type: 'TOKEN',
+ mutability: 'IMMUTABLE',
+ },
+ second: {
+ type: 'TOKEN',
+ mutability: 'MUTABLE',
+ },
+ third: {
+ type: 'TOKEN',
+ mutability: 'SEGMENTED',
+ },
+ },
+ };
+
+ class EntityEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const decorator = new CompositeDecorator([
+ {
+ strategy: getEntityStrategy('IMMUTABLE'),
+ component: TokenSpan,
+ },
+ {
+ strategy: getEntityStrategy('MUTABLE'),
+ component: TokenSpan,
+ },
+ {
+ strategy: getEntityStrategy('SEGMENTED'),
+ component: TokenSpan,
+ },
+ ]);
+
+ const blocks = convertFromRaw(rawContent);
+
+ this.state = {
+ editorState: EditorState.createWithContent(
+ ContentState.createFromBlockArray(blocks),
+ decorator
+ ),
+ };
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+ this.logState = () => {
+ console.log(convertToRaw(this.state.editorState.toJS()));
+ };
+ }
+
+ render() {
+ return (
+ <div style={styles.root}>
+ <div style={styles.editor} onClick={this.focus}>
+ <Editor
+ editorState={this.state.editorState}
+ onChange={this.onChange}
+ placeholder="Enter some text..."
+ ref="editor"
+ />
+ </div>
+ <input
+ onClick={this.logState}
+ style={styles.button}
+ type="button"
+ value="Log State"
+ />
+ </div>
+ );
+ }
+ }
+
+ function getEntityStrategy(mutability) {
+ return function(contentBlock, callback) {
+ contentBlock.findEntityRanges(
+ (character) => {
+ const entityKey = character.getEntity();
+ if (entityKey === null) {
+ return false;
+ }
+ return Entity.get(entityKey).getMutability() === mutability;
+ },
+ callback
+ );
+ };
+ }
+
+ function getDecoratedStyle(mutability) {
+ switch (mutability) {
+ case 'IMMUTABLE': return styles.immutable;
+ case 'MUTABLE': return styles.mutable;
+ case 'SEGMENTED': return styles.segmented;
+ default: return null;
+ }
+ }
+
+ const TokenSpan = (props) => {
+ const style = getDecoratedStyle(
+ Entity.get(props.entityKey).getMutability()
+ );
+ return (
+ <span {...props} style={style}>
+ {props.children}
+ </span>
+ );
+ };
+
+ const styles = {
+ root: {
+ fontFamily: '\'Helvetica\', sans-serif',
+ padding: 20,
+ width: 600,
+ },
+ editor: {
+ border: '1px solid #ccc',
+ cursor: 'text',
+ minHeight: 80,
+ padding: 10,
+ },
+ button: {
+ marginTop: 10,
+ textAlign: 'center',
+ },
+ immutable: {
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
+ padding: '2px 0',
+ },
+ mutable: {
+ backgroundColor: 'rgba(204, 204, 255, 1.0)',
+ padding: '2px 0',
+ },
+ segmented: {
+ backgroundColor: 'rgba(248, 222, 126, 1.0)',
+ padding: '2px 0',
+ },
+ };
+
+ ReactDOM.render(
+ <EntityEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
178 examples/link/link.html
@@ -0,0 +1,178 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Link Editor</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {
+ CompositeDecorator,
+ ContentState,
+ Editor,
+ EditorState,
+ Entity,
+ RichUtils,
+ } = Draft;
+
+ class LinkEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const decorator = new CompositeDecorator([
+ {
+ strategy: findLinkEntities,
+ component: Link,
+ },
+ ]);
+
+ this.state = {
+ editorState: EditorState.createEmpty(decorator),
+ };
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+ this.logState = () => {
+ console.log(convertToRaw(this.state.editorState.toJS()));
+ };
+
+ this.addLink = this._addLink.bind(this);
+ this.removeLink = this._removeLink.bind(this);
+ }
+
+ _addLink(e) {
+ const {editorState} = this.state;
+ const selection = editorState.getSelection();
+ if (selection.isCollapsed()) {
+ return;
+ }
+ const href = window.prompt('Enter a URL');
+ const entityKey = Entity.create('link', 'MUTABLE', {href});
+ const content = editorState.getCurrentContent();
+ this.setState({
+ editorState: RichUtils.toggleLink(editorState, selection, entityKey),
+ });
+ }
+
+ _removeLink(e) {
+ const {editorState} = this.state;
+ const selection = editorState.getSelection();
+ if (selection.isCollapsed()) {
+ return;
+ }
+ const content = editorState.getCurrentContent();
+ this.setState({
+ editorState: RichUtils.toggleLink(editorState, selection, null),
+ });
+ }
+
+ render() {
+ return (
+ <div style={styles.root}>
+ <div style={{marginBottom: 10}}>
+ Select some text, then use the buttons to add or remove links
+ on the selected text.
+ </div>
+ <div style={styles.buttons}>
+ <button onMouseDown={this.addLink} style={{marginRight: 10}}>
+ Add Link
+ </button>
+ <button onMouseDown={this.removeLink}>
+ Remove Link
+ </button>
+ </div>
+ <div style={styles.editor} onClick={this.focus}>
+ <Editor
+ editorState={this.state.editorState}
+ onChange={this.onChange}
+ placeholder="Enter some text..."
+ ref="editor"
+ />
+ </div>
+ <input
+ onClick={this.logState}
+ style={styles.button}
+ type="button"
+ value="Log State"
+ />
+ </div>
+ );
+ }
+ }
+
+ function findLinkEntities(contentBlock, callback) {
+ contentBlock.findEntityRanges(
+ (character) => {
+ const entityKey = character.getEntity();
+ return (
+ entityKey !== null &&
+ Entity.get(entityKey).getType() === 'link'
+ );
+ },
+ callback
+ );
+ }
+
+ const Link = (props) => {
+ const {href} = Entity.get(props.entityKey).getData();
+ return (
+ <a href={href} style={styles.link}>
+ {props.children}
+ </a>
+ );
+ };
+
+ const styles = {
+ root: {
+ fontFamily: '\'Georgia\', serif',
+ padding: 20,
+ width: 600,
+ },
+ buttons: {
+ marginBottom: 10,
+ },
+ editor: {
+ border: '1px solid #ccc',
+ cursor: 'text',
+ minHeight: 80,
+ padding: 10,
+ },
+ button: {
+ marginTop: 10,
+ textAlign: 'center',
+ },
+ link: {
+ color: '#3b5998',
+ textDecoration: 'underline',
+ },
+ };
+
+ ReactDOM.render(
+ <LinkEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
89 examples/plaintext/plaintext.html
@@ -0,0 +1,89 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Plain Text Editor</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {Editor, EditorState} = Draft;
+
+ class PlainTextEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {editorState: EditorState.createEmpty()};
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+ this.logState = () => console.log(this.state.editorState.toJS());
+ }
+
+ render() {
+ return (
+ <div style={styles.root}>
+ <div style={styles.editor} onClick={this.focus}>
+ <Editor
+ editorState={this.state.editorState}
+ onChange={this.onChange}
+ placeholder="Enter some text..."
+ ref="editor"
+ />
+ </div>
+ <input
+ onClick={this.logState}
+ style={styles.button}
+ type="button"
+ value="Log State"
+ />
+ </div>
+ );
+ }
+ }
+
+ const styles = {
+ root: {
+ fontFamily: '\'Helvetica\', sans-serif',
+ padding: 20,
+ width: 600,
+ },
+ editor: {
+ border: '1px solid #ccc',
+ cursor: 'text',
+ minHeight: 80,
+ padding: 10,
+ },
+ button: {
+ marginTop: 10,
+ textAlign: 'center',
+ },
+ };
+
+ ReactDOM.render(
+ <PlainTextEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
62 examples/rich/RichEditor.css
@@ -0,0 +1,62 @@
+.RichEditor-root {
+ background: #fff;
+ border: 1px solid #ddd;
+ font-family: 'Georgia', serif;
+ font-size: 14px;
+ padding: 15px;
+}
+
+.RichEditor-editor {
+ border-top: 1px solid #ddd;
+ cursor: text;
+ font-size: 16px;
+ margin-top: 10px;
+}
+
+.RichEditor-editor .public-DraftEditorPlaceholder-root,
+.RichEditor-editor .public-DraftEditor-content {
+ margin: 0 -15px -15px;
+ padding: 15px;
+}
+
+.RichEditor-editor .public-DraftEditor-content {
+ min-height: 100px;
+}
+
+.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {
+ display: none;
+}
+
+.RichEditor-editor .RichEditor-blockquote {
+ border-left: 5px solid #eee;
+ color: #666;
+ font-family: 'Hoefler Text', 'Georgia', serif;
+ font-style: italic;
+ margin: 16px 0;
+ padding: 10px 20px;
+}
+
+.RichEditor-editor .public-DraftStyleDefault-pre {
+ background-color: rgba(0, 0, 0, 0.05);
+ font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
+ font-size: 16px;
+ padding: 20px;
+}
+
+.RichEditor-controls {
+ font-family: 'Helvetica', sans-serif;
+ font-size: 14px;
+ margin-bottom: 5px;
+ user-select: none;
+}
+
+.RichEditor-styleButton {
+ color: #999;
+ cursor: pointer;
+ margin-right: 16px;
+ padding: 2px 0;
+}
+
+.RichEditor-activeButton {
+ color: #5890ff;
+}
218 examples/rich/rich.html
@@ -0,0 +1,218 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Rich Text</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ <link rel="stylesheet" href="RichEditor.css" />
+ <style>
+ #target { width: 600px; }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.min.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {Editor, EditorState, RichUtils} = Draft;
+
+ class RichEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {editorState: EditorState.createEmpty()};
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+
+ this.handleKeyCommand = (command) => this._handleKeyCommand(command);
+ this.toggleBlockType = (type) => this._toggleBlockType(type);
+ this.toggleInlineStyle = (style) => this._toggleInlineStyle(style);
+ }
+
+ _handleKeyCommand(command) {
+ const {editorState} = this.state;
+ const newState = RichUtils.handleKeyCommand(editorState, command);
+ if (newState) {
+ this.onChange(newState);
+ return true;
+ }
+ return false;
+ }
+
+ _toggleBlockType(blockType) {
+ this.onChange(
+ RichUtils.toggleBlockType(
+ this.state.editorState,
+ blockType
+ )
+ );
+ }
+
+ _toggleInlineStyle(inlineStyle) {
+ this.onChange(
+ RichUtils.toggleInlineStyle(
+ this.state.editorState,
+ inlineStyle
+ )
+ );
+ }
+
+ render() {
+ const {editorState} = this.state;
+
+ // If the user changes block type before entering any text, we can
+ // either style the placeholder or hide it. Let's just hide it now.
+ let className = 'RichEditor-editor';
+ var contentState = editorState.getCurrentContent();
+ if (!contentState.hasText()) {
+ if (contentState.getBlockMap().first().getType() !== 'unstyled') {
+ className += ' RichEditor-hidePlaceholder';
+ }
+ }
+
+ return (
+ <div className="RichEditor-root">
+ <BlockStyleControls
+ editorState={editorState}
+ onToggle={this.toggleBlockType}
+ />
+ <InlineStyleControls
+ editorState={editorState}
+ onToggle={this.toggleInlineStyle}
+ />
+ <div className={className} onClick={this.focus}>
+ <Editor
+ blockStyleFn={getBlockStyle}
+ customStyleMap={styleMap}
+ editorState={editorState}
+ handleKeyCommand={this.handleKeyCommand}
+ onChange={this.onChange}
+ placeholder="Tell a story..."
+ ref="editor"
+ spellCheck={true}
+ />
+ </div>
+ </div>
+ );
+ }
+ }
+
+ // Custom overrides for "code" style.
+ const styleMap = {
+ CODE: {
+ backgroundColor: 'rgba(0, 0, 0, 0.05)',
+ fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
+ fontSize: 16,
+ padding: 2,
+ },
+ };
+
+ function getBlockStyle(block) {
+ switch (block.getType()) {
+ case 'blockquote': return 'RichEditor-blockquote';
+ default: return null;
+ }
+ }
+
+ class StyleButton extends React.Component {
+ constructor() {
+ super();
+ this.onToggle = (e) => {
+ e.preventDefault();
+ this.props.onToggle(this.props.style);
+ };
+ }
+
+ render() {
+ let className = 'RichEditor-styleButton';
+ if (this.props.active) {
+ className += ' RichEditor-activeButton';
+ }
+
+ return (
+ <span className={className} onMouseDown={this.onToggle}>
+ {this.props.label}
+ </span>
+ );
+ }
+ }
+
+ const BLOCK_TYPES = [
+ {label: 'H1', style: 'header-one'},
+ {label: 'H2', style: 'header-two'},
+ {label: 'Blockquote', style: 'blockquote'},
+ {label: 'UL', style: 'unordered-list-item'},
+ {label: 'OL', style: 'ordered-list-item'},
+ {label: 'Code Block', style: 'code-block'},
+ ];
+
+ const BlockStyleControls = (props) => {
+ const {editorState} = props;
+ const selection = editorState.getSelection();
+ const blockType = editorState
+ .getCurrentContent()
+ .getBlockForKey(selection.getStartKey())
+ .getType();
+
+ return (
+ <div className="RichEditor-controls">
+ {BLOCK_TYPES.map((type) =>
+ <StyleButton
+ active={type.style === blockType}
+ label={type.label}
+ onToggle={props.onToggle}
+ style={type.style}
+ />
+ )}
+ </div>
+ );
+ };
+
+ var INLINE_STYLES = [
+ {label: 'Bold', style: 'BOLD'},
+ {label: 'Italic', style: 'ITALIC'},
+ {label: 'Underline', style: 'UNDERLINE'},
+ {label: 'Monospace', style: 'CODE'},
+ ];
+
+ const InlineStyleControls = (props) => {
+ var currentStyle = props.editorState.getCurrentInlineStyle();
+ return (
+ <div className="RichEditor-controls">
+ {INLINE_STYLES.map(type =>
+ <StyleButton
+ active={currentStyle.has(type.style)}
+ label={type.label}
+ onToggle={props.onToggle}
+ style={type.style}
+ />
+ )}
+ </div>
+ );
+ };
+
+ ReactDOM.render(
+ <RichEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
172 examples/tex/.eslintrc
@@ -0,0 +1,172 @@
+---
+parser: babel-eslint
+
+plugins:
+ - react
+
+env:
+ node: true
+ es6: true
+
+globals:
+ document: false
+ Immutable: false
+ React: false
+
+arrowFunctions: true
+blockBindings: true
+classes: true
+defaultParams: true
+destructuring: true
+forOf: true
+generators: true
+modules: true
+objectLiteralComputedProperties: true
+objectLiteralShorthandMethods: true
+objectLiteralShorthandProperties: true
+spread: true
+templateStrings: true
+
+rules:
+ # ERRORS
+ brace-style: [2, 1tbs, allowSingleLine: true]
+ camelcase: [2, properties: always]
+ comma-style: [2, last]
+ curly: [2, all]
+ eol-last: 2
+ eqeqeq: 2
+ guard-for-in: 2
+ handle-callback-err: [2, error]
+ indent: [2, 2, SwitchCase: 1]
+ key-spacing: [2, {beforeColon: false, afterColon: true}]
+ max-len: [2, 80, 4, ignorePattern: "^(\\s*var\\s.+=\\s*require\\s*\\(|import )"]
+ new-parens: 2
+ no-alert: 2
+ no-array-constructor: 2
+ no-caller: 2
+ no-cond-assign: 2
+ no-constant-condition: 2
+ no-delete-var: 2
+ no-div-regex: 2
+ no-dupe-args: 2
+ no-dupe-keys: 2
+ no-duplicate-case: 2
+ no-empty-character-class: 2
+ no-empty-label: 2
+ no-empty: 2
+ no-eval: 2
+ no-ex-assign: 2
+ no-extend-native: 2
+ no-extra-bind: 2
+ no-extra-boolean-cast: 2
+ no-extra-semi: 2
+ no-fallthrough: 2
+ no-floating-decimal: 2
+ no-func-assign: 2
+ no-implied-eval: 2
+ no-inner-declarations: [2, functions]
+ no-invalid-regexp: 2
+ no-irregular-whitespace: 2
+ no-iterator: 2
+ no-label-var: 2
+ no-lonely-if: 2
+ no-mixed-requires: [2, true]
+ no-mixed-spaces-and-tabs: 2
+ no-multi-spaces: 2
+ no-multi-str: 2
+ no-negated-in-lhs: 2
+ no-new-object: 2
+ no-new-require: 2
+ no-new-wrappers: 2
+ no-new: 2
+ no-obj-calls: 2
+ no-octal-escape: 2
+ no-octal: 2
+ no-param-reassign: 2
+ no-path-concat: 2
+ no-proto: 2
+ no-redeclare: 2
+ no-regex-spaces: 2
+ no-return-assign: 2
+ no-script-url: 2
+ no-sequences: 2
+ no-shadow-restricted-names: 2
+ no-shadow: 2
+ no-spaced-func: 2
+ no-sparse-arrays: 2
+ no-sync: 2
+ no-throw-literal: 2
+ no-trailing-spaces: 2
+ no-undef-init: 2
+ no-undef: 2
+ no-unreachable: 2
+ no-unused-expressions: 2
+ no-unused-vars: [2, {vars: all, args: after-used}]
+ no-void: 2
+ no-with: 2
+ one-var: [2, never]
+ operator-assignment: [2, always]
+ quote-props: [2, as-needed]
+ quotes: [2, single]
+ radix: 2
+ semi-spacing: [2, {before: false, after: true}]
+ semi: [2, always]
+ space-after-keywords: [2, always]
+ space-before-blocks: [2, always]
+ space-before-function-paren: [2, {anonymous: always, named: never}]
+ space-infix-ops: [2, int32Hint: false]
+ space-return-throw-case: 2
+ space-unary-ops: [2, {words: true, nonwords: false}]
+ spaced-comment: [2, always]
+ use-isnan: 2
+ valid-typeof: 2
+ wrap-iife: 2
+ yoda: [2, never, exceptRange: true]
+
+ # WARNINGS
+
+ # DISABLED
+ block-scoped-var: 0
+ comma-dangle: 0
+ complexity: 0
+ consistent-return: 0
+ consistent-this: 0
+ default-case: 0
+ dot-notation: 0
+ func-names: 0
+ func-style: 0
+ max-nested-callbacks: 0
+ new-cap: 0
+ newline-after-var: 0
+ no-catch-shadow: 0
+ no-console: 0
+ no-control-regex: 0
+ no-debugger: 0
+ no-eq-null: 0
+ no-inline-comments: 0
+ no-labels: 0
+ no-lone-blocks: 0
+ no-loop-func: 0
+ no-multiple-empty-lines: 0
+ no-native-reassign: 0
+ no-nested-ternary: 0
+ no-new-func: 0
+ no-process-env: 0
+ no-process-exit: 0
+ no-reserved-keys: 0
+ no-restricted-modules: 0
+ no-self-compare: 0
+ no-ternary: 0
+ no-undefined: 0
+ no-underscore-dangle: 0
+ no-use-before-define: 0
+ no-var: 0
+ no-warning-comments: 0
+ padded-blocks: 0
+ sort-vars: 0
+ space-in-brackets: 0
+ space-in-parens: 0
+ strict: 0
+ valid-jsdoc: 0
+ vars-on-top: 0
+ wrap-regex: 0
25 examples/tex/js/app.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import 'babel/polyfill';
+import TeXEditorExample from './components/TeXEditorExample';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+ReactDOM.render(
+ <TeXEditorExample />,
+ document.getElementById('target')
+);
176 examples/tex/js/components/TeXBlock.js
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import katex from 'katex';
+import React from 'react';
+import {Entity} from 'draft-js';
+
+class KatexOutput extends React.Component {
+ constructor(props) {
+ super(props);
+ this._timer = null;
+ }
+
+ _update() {
+ if (this._timer) {
+ clearTimeout(this._timer);
+ }
+
+ this._timer = setTimeout(() => {
+ katex.render(
+ this.props.content,
+ this.refs.container,
+ {displayMode: true}
+ );
+ }, 0);
+ }
+
+ componentDidMount() {
+ this._update();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.content !== this.props.content) {
+ this._update();
+ }
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this._timer);
+ this._timer = null;
+ }
+
+ render() {
+ return <div ref="container" onClick={this.props.onClick} />;
+ }
+}
+
+export default class TeXBlock extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {editMode: false};
+
+ this._onClick = () => {
+ if (this.state.editMode) {
+ return;
+ }
+
+ this.setState({
+ editMode: true,
+ texValue: this._getValue(),
+ }, () => {
+ this._startEdit();
+ });
+ };
+
+ this._onValueChange = evt => {
+ var value = evt.target.value;
+ var invalid = false;
+ try {
+ katex.__parse(value);
+ } catch (e) {
+ invalid = true;
+ } finally {
+ this.setState({
+ invalidTeX: invalid,
+ texValue: value,
+ });
+ }
+ };
+
+ this._save = () => {
+ var entityKey = this.props.block.getEntityAt(0);
+ Entity.mergeData(entityKey, {content: this.state.texValue});
+ this.setState({
+ invalidTeX: false,
+ editMode: false,
+ texValue: null,
+ }, this._finishEdit);
+ };
+
+ this._remove = () => {
+ this.props.blockProps.onRemove(this.props.block.getKey());
+ };
+ this._startEdit = () => {
+ this.props.blockProps.onStartEdit(this.props.block.getKey());
+ };
+ this._finishEdit = () => {
+ this.props.blockProps.onFinishEdit(this.props.block.getKey());
+ };
+ }
+
+ _getValue() {
+ return Entity
+ .get(this.props.block.getEntityAt(0))
+ .getData()['content'];
+ }
+
+ render() {
+ var texContent = null;
+ if (this.state.editMode) {
+ if (this.state.invalidTeX) {
+ texContent = '';
+ } else {
+ texContent = this.state.texValue;
+ }
+ } else {
+ texContent = this._getValue();
+ }
+
+ var className = 'TeXEditor-tex';
+ if (this.state.editMode) {
+ className += ' TeXEditor-activeTeX';
+ }
+
+ var editPanel = null;
+ if (this.state.editMode) {
+ var buttonClass = 'TeXEditor-saveButton';
+ if (this.state.invalidTeX) {
+ buttonClass += ' TeXEditor-invalidButton';
+ }
+
+ editPanel =
+ <div className="TeXEditor-panel">
+ <textarea
+ className="TeXEditor-texValue"
+ onChange={this._onValueChange}
+ ref="textarea"
+ value={this.state.texValue}
+ />
+ <div className="TeXEditor-buttons">
+ <button
+ className={buttonClass}
+ disabled={this.state.invalidTeX}
+ onClick={this._save}>
+ {this.state.invalidTeX ? 'Invalid TeX' : 'Done'}
+ </button>
+ <button className="TeXEditor-removeButton" onClick={this._remove}>
+ Remove
+ </button>
+ </div>
+ </div>;
+ }
+
+ return (
+ <figure
+ contentEditable={false}
+ className={className}>
+ <KatexOutput content={texContent} onClick={this._onClick} />
+ {editPanel}
+ </figure>
+ );
+ }
+}
113 examples/tex/js/components/TeXEditorExample.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import Draft from 'draft-js';
+import {Map} from 'immutable';
+import React from 'react';
+
+import TeXBlock from './TeXBlock';
+import {content} from '../data/content';
+import {insertTeXBlock} from '../modifiers/insertTeXBlock';
+import {removeTeXBlock} from '../modifiers/removeTeXBlock';
+
+var {ContentState, Editor, EditorState, RichUtils} = Draft;
+
+export default class TeXEditorExample extends React.Component {
+ constructor(props) {
+ super(props);
+ const contentState = ContentState.createFromBlockArray(content);
+ this.state = {
+ editorState: EditorState.createWithContent(contentState),
+ liveTeXEdits: Map(),
+ };
+
+ this._blockRenderer = (block) => {
+ if (block.getType() === 'media') {
+ return {
+ component: TeXBlock,
+ props: {
+ onStartEdit: (blockKey) => {
+ var {liveTeXEdits} = this.state;
+ this.setState({liveTeXEdits: liveTeXEdits.set(blockKey, true)});
+ },
+ onFinishEdit: (blockKey) => {
+ var {liveTeXEdits} = this.state;
+ this.setState({liveTeXEdits: liveTeXEdits.remove(blockKey)});
+ },
+ onRemove: (blockKey) => this._removeTeX(blockKey),
+ },
+ };
+ }
+ return null;
+ };
+
+ this._focus = () => this.refs.editor.focus();
+ this._onChange = (editorState) => this.setState({editorState});
+
+ this._handleKeyCommand = command => {
+ var {editorState} = this.state;
+ var newState = RichUtils.handleKeyCommand(editorState, command);
+ if (newState) {
+ this._onChange(newState);
+ return true;
+ }
+ return false;
+ };
+
+ this._removeTeX = (blockKey) => {
+ var {editorState, liveTeXEdits} = this.state;
+ this.setState({
+ liveTeXEdits: liveTeXEdits.remove(blockKey),
+ editorState: removeTeXBlock(editorState, blockKey),
+ });
+ };
+
+ this._insertTeX = () => {
+ this.setState({
+ liveTeXEdits: Map(),
+ editorState: insertTeXBlock(this.state.editorState),
+ });
+ };
+ }
+
+ /**
+ * While editing TeX, set the Draft editor to read-only. This allows us to
+ * have a textarea within the DOM.
+ */
+ render() {
+ return (
+ <div className="TexEditor-container">
+ <div className="TeXEditor-root">
+ <div className="TeXEditor-editor" onClick={this._focus}>
+ <Editor
+ blockRendererFn={this._blockRenderer}
+ editorState={this.state.editorState}
+ handleKeyCommand={this._handleKeyCommand}
+ onChange={this._onChange}
+ placeholder="Start a document..."
+ readOnly={this.state.liveTeXEdits.count()}
+ ref="editor"
+ spellCheck={true}
+ />
+ </div>
+ </div>
+ <button onClick={this._insertTeX} className="TeXEditor-insert">
+ {'Insert new TeX'}
+ </button>
+ </div>
+ );
+ }
+}
68 examples/tex/js/data/content.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import {convertFromRaw} from 'draft-js';
+
+var rawContent = {
+ blocks: [
+ {
+ text: 'This is a Draft-based editor that supports TeX rendering.',
+ type: 'unstyled',
+ },
+ {
+ text: '',
+ type: 'unstyled',
+ },
+ {
+ text: (
+ 'Each TeX block below is represented as a DraftEntity object and ' +
+ 'rendered using Khan Academy\'s KaTeX library.'
+ ),
+ type: 'unstyled',
+ },
+ {
+ text: '',
+ type: 'unstyled',
+ },
+ {
+ text: 'Click any TeX block to edit.',
+ type: 'unstyled',
+ },
+ {
+ text: ' ',
+ type: 'media',
+ entityRanges: [{offset: 0, length: 1, key: 'first'}],
+ },
+ {
+ text: 'You can also insert a new TeX block at the cursor location.',
+ type: 'unstyled',
+ },
+ ],
+
+ entityMap: {
+ first: {
+ type: 'TOKEN',
+ mutability: 'IMMUTABLE',
+ data: {
+ content: (
+ '\\left( \\sum_{k=1}^n a_k b_k \\right)^{\\!\\!2} \\leq\n' +
+ '\\left( \\sum_{k=1}^n a_k^2 \\right)\n' +
+ '\\left( \\sum_{k=1}^n b_k^2 \\right)'
+ ),
+ },
+ }
+ }
+};
+
+export var content = convertFromRaw(rawContent);
98 examples/tex/js/modifiers/insertTeXBlock.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import {List, Repeat} from 'immutable';
+import {
+ BlockMapBuilder,
+ CharacterMetadata,
+ ContentBlock,
+ EditorState,
+ Entity,
+ Modifier,
+ genKey,
+} from 'draft-js';
+
+var count = 0;
+var examples = [
+ '\\int_a^bu\\frac{d^2v}{dx^2}\\,dx\n' +
+ '=\\left.u\\frac{dv}{dx}\\right|_a^b\n' +
+ '-\\int_a^b\\frac{du}{dx}\\frac{dv}{dx}\\,dx',
+
+ 'P(E) = {n \\choose k} p^k (1-p)^{ n-k} ',
+
+ '\\tilde f(\\omega)=\\frac{1}{2\\pi}\n' +
+ '\\int_{-\\infty}^\\infty f(x)e^{-i\\omega x}\\,dx',
+
+ '\\frac{1}{(\\sqrt{\\phi \\sqrt{5}}-\\phi) e^{\\frac25 \\pi}} =\n' +
+ '1+\\frac{e^{-2\\pi}} {1+\\frac{e^{-4\\pi}} {1+\\frac{e^{-6\\pi}}\n' +
+ '{1+\\frac{e^{-8\\pi}} {1+\\ldots} } } }',
+];
+
+export function insertTeXBlock(editorState) {
+ var contentState = editorState.getCurrentContent();
+ var selectionState = editorState.getSelection();
+
+ var afterRemoval = Modifier.removeRange(
+ contentState,
+ selectionState,
+ 'backward'
+ );
+
+ var targetSelection = afterRemoval.getSelectionAfter();
+ var afterSplit = Modifier.splitBlock(afterRemoval, targetSelection);
+ var insertionTarget = afterSplit.getSelectionAfter();
+
+ var asMedia = Modifier.setBlockType(afterSplit, insertionTarget, 'media');
+ var nextFormula = count++ % examples.length;
+
+ var entityKey = Entity.create(
+ 'TOKEN',
+ 'IMMUTABLE',
+ {content: examples[nextFormula]}
+ );
+
+ var charData = CharacterMetadata.create({entity: entityKey});
+
+ var fragmentArray = [
+ new ContentBlock({
+ key: genKey(),
+ type: 'media',
+ text: ' ',
+ characterList: List(Repeat(charData, 1)),
+ }),
+ new ContentBlock({
+ key: genKey(),
+ type: 'unstyled',
+ text: '',
+ characterList: List(),
+ }),
+ ];
+
+ var fragment = BlockMapBuilder.createFromArray(fragmentArray);
+
+ var withMedia = Modifier.replaceWithFragment(
+ asMedia,
+ insertionTarget,
+ fragment
+ );
+
+ var newContent = withMedia.merge({
+ selectionBefore: selectionState,
+ selectionAfter: withMedia.getSelectionAfter().set('hasFocus', true),
+ });
+
+ return EditorState.push(editorState, newContent, 'insert-fragment');
+}
39 examples/tex/js/modifiers/removeTeXBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import {EditorState, Modifier, SelectionState} from 'draft-js';
+
+export function removeTeXBlock(editorState, blockKey) {
+ var content = editorState.getCurrentContent();
+ var block = content.getBlockForKey(blockKey);
+
+ var targetRange = new SelectionState({
+ anchorKey: blockKey,
+ anchorOffset: 0,
+ focusKey: blockKey,
+ focusOffset: block.getLength(),
+ });
+
+ var withoutTeX = Modifier.removeRange(content, targetRange, 'backward');
+ var resetBlock = Modifier.setBlockType(
+ withoutTeX,
+ withoutTeX.getSelectionAfter(),
+ 'unstyled'
+ );
+
+ var newState = EditorState.push(editorState, resetBlock, 'remove-range');
+ return EditorState.forceSelection(newState, resetBlock.getSelectionAfter());
+}
22 examples/tex/package.json
@@ -0,0 +1,22 @@
+{
+ "private": true,
+ "scripts": {
+ "start": "babel-node ./server.js",
+ "postinstall": "npm install ../.."
+ },
+ "dependencies": {
+ "babel": "5.8.23",
+ "babel-eslint": "^4.1.3",
+ "babel-loader": "5.3.2",
+ "classnames": "^2.1.3",
+ "eslint": "^1.0.0",
+ "eslint-loader": "^1.0.0",
+ "eslint-plugin-react": "^3.2.0",
+ "express": "^4.13.1",
+ "immutable": "^3.7.4",
+ "katex": "^0.5.1",
+ "react": "^0.14.0",
+ "webpack": "^1.10.5",
+ "webpack-dev-server": "^1.10.1"
+ }
+}
100 examples/tex/public/TeXEditor.css
@@ -0,0 +1,100 @@
+.TeXEditor-root {
+ font-family: 'Century Schoolbook', serif;
+ -webkit-font-smoothing: antialiased;
+ margin: 40px auto;
+ width: 900px;
+}
+
+.TeXEditor-editor {
+ cursor: text;
+ font-size: 18px;
+ min-height: 40px;
+ padding: 30px;
+}
+
+.TeXEditor-button {
+ margin-top: 10px;
+ text-align: center;
+}
+
+.TeXEditor-handle {
+ color: rgba(98, 177, 254, 1.0);
+ direction: ltr;
+ unicode-bidi: bidi-override;
+}
+
+.TeXEditor-hashtag {
+ color: rgba(95, 184, 138, 1.0);
+}
+
+.TeXEditor-tex {
+ background-color: #fff;
+ cursor: pointer;
+ margin: 20px auto;
+ padding: 20px;
+ -webkit-transition: background-color 0.2s fade-in-out;
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+.TeXEditor-activeTeX {
+ color: #888;
+}
+
+.TeXEditor-panel {
+ font-family: 'Helvetica', sans-serif;
+ font-weight: 200;
+}
+
+.TeXEditor-panel .TeXEditor-texValue {
+ border: 1px solid #e1e1e1;
+ display: block;
+ font-family: 'Inconsolata', 'Menlo', monospace;
+ font-size: 14px;
+ height: 110px;
+ margin: 20px auto 10px;
+ outline: none;
+ padding: 14px;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ width: 500px;
+}
+
+.TeXEditor-buttons {
+ text-align: center;
+}
+
+.TeXEditor-saveButton,
+.TeXEditor-removeButton {
+ background-color: #fff;
+ border: 1px solid #0a0;
+ cursor: pointer;
+ font-family: 'Helvetica', 'Arial', sans-serif;
+ font-size: 16px;
+ font-weight: 200;
+ margin: 10px auto;
+ padding: 6px;
+ -webkit-border-radius: 3px;
+ width: 100px;
+}
+
+.TeXEditor-removeButton {
+ border-color: #aaa;
+ color: #999;
+ margin-left: 8px;
+}
+
+.TeXEditor-invalidButton {
+ background-color: #eee;
+ border-color: #a00;
+ color: #666;
+}
+
+.TeXEditor-insert {
+ background-color: #f1f1f1;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ bottom: 30px;
+ position: fixed;
+ right: 30px;
+}
15 examples/tex/public/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข TeX</title>
+ <link rel="stylesheet" href="TeXEditor.css" />
+ <link rel="stylesheet" href="node_modules/draft-js/dist/Draft.css" />
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css">
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="http://localhost:3000/webpack-dev-server.js"></script>
+ <script src="js/app.js"></script>
+ </body>
+</html>
53 examples/tex/server.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import express from 'express';
+import path from 'path';
+import webpack from 'webpack';
+import WebpackDevServer from 'webpack-dev-server';
+
+const APP_PORT = 3000;
+
+// Serve the TeX Editor app
+var compiler = webpack({
+ entry: path.resolve(__dirname, 'js', 'app.js'),
+ eslint: {
+ configFile: '.eslintrc'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel',
+ },
+ {
+ test: /\.js$/,
+ loader: 'eslint'
+ }
+ ]
+ },
+ output: {filename: 'app.js', path: '/'}
+});
+var app = new WebpackDevServer(compiler, {
+ contentBase: '/public/',
+ publicPath: '/js/',
+ stats: {colors: true}
+});
+// Serve static resources
+app.use('/', express.static('public'));
+app.use('/node_modules', express.static('node_modules'));
+app.listen(APP_PORT, () => {
+ console.log(`TeX Editor is now running on http://localhost:${APP_PORT}`);
+});
144 examples/tweet/tweet.html
@@ -0,0 +1,144 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Draft โ€ข Decorators</title>
+ <link rel="stylesheet" href="../../dist/Draft.css" />
+ </head>
+ <body>
+ <div id="target"></div>
+ <script src="../../node_modules/react/dist/react.js"></script>
+ <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+ <script src="../../node_modules/immutable/dist/immutable.js"></script>
+ <script src="../../node_modules/babel-core/browser.js"></script>
+ <script src="../../dist/Draft.js"></script>
+ <script type="text/babel">
+ 'use strict';
+
+ const {CompositeDecorator, Editor, EditorState} = Draft;
+
+ class TweetEditorExample extends React.Component {
+ constructor() {
+ super();
+ const compositeDecorator = new CompositeDecorator([
+ {
+ strategy: handleStrategy,
+ component: HandleSpan,
+ },
+ {
+ strategy: hashtagStrategy,
+ component: HashtagSpan,
+ },
+ ]);
+
+ this.state = {
+ editorState: EditorState.createEmpty(compositeDecorator),
+ };
+
+ this.focus = () => this.refs.editor.focus();
+ this.onChange = (editorState) => this.setState({editorState});
+ this.logState = () => console.log(this.state.editorState.toJS());
+ }
+
+ render() {
+ return (
+ <div style={styles.root}>
+ <div style={styles.editor} onClick={this.focus}>
+ <Editor
+ editorState={this.state.editorState}
+ onChange={this.onChange}
+ placeholder="Write a tweet..."
+ ref="editor"
+ spellCheck={true}
+ />
+ </div>
+ <input
+ onClick={this.logState}
+ style={styles.button}
+ type="button"
+ value="Log State"
+ />
+ </div>
+ );
+ }
+ }
+
+ /**
+ * Super simple decorators for handles and hashtags, for demonstration
+ * purposes only. Don't reuse these regexes.
+ */
+ const HANDLE_REGEX = /\@[\w]+/g;
+ const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
+
+ function handleStrategy(contentBlock, callback) {
+ findWithRegex(HANDLE_REGEX, contentBlock, callback);
+ }
+
+ function hashtagStrategy(contentBlock, callback) {
+ findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+ }
+
+ function findWithRegex(regex, contentBlock, callback) {
+ const text = contentBlock.getText();
+ let matchArr, start;
+ while ((matchArr = regex.exec(text)) !== null) {
+ start = matchArr.index;
+ callback(start, start + matchArr[0].length);
+ }
+ }
+
+ const HandleSpan = (props) => {
+ return <span {...props} style={styles.handle}>{props.children}</span>;
+ };
+
+ const HashtagSpan = (props) => {
+ return <span {...props} style={styles.hashtag}>{props.children}</span>;
+ };
+
+ const styles = {
+ root: {
+ fontFamily: '\'Helvetica\', sans-serif',
+ padding: 20,
+ width: 600,
+ },
+ editor: {
+ border: '1px solid #ddd',
+ cursor: 'text',
+ fontSize: 16,
+ minHeight: 40,
+ padding: 10,
+ },
+ button: {
+ marginTop: 10,
+ textAlign: 'center',
+ },
+ handle: {
+ color: 'rgba(98, 177, 254, 1.0)',
+ direction: 'ltr',
+ unicodeBidi: 'bidi-override',
+ },
+ hashtag: {
+ color: 'rgba(95, 184, 138, 1.0)',
+ },
+ };
+
+ ReactDOM.render(
+ <TweetEditorExample />,
+ document.getElementById('target')
+ );
+ </script>
+ </body>
+</html>
134 gulpfile.js
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+var babel = require('gulp-babel');
+var del = require('del');
+var concatCSS = require('gulp-concat-css');
+var derequire = require('gulp-derequire');
+var flatten = require('gulp-flatten');
+var gulp = require('gulp');
+var gulpUtil = require('gulp-util');
+var runSequence = require('run-sequence');
+var webpackStream = require('webpack-stream');
+
+var babelOpts = require('./scripts/babel/default-options');
+var babelPluginDEV = require('fbjs-scripts/babel/dev-expression');
+var gulpCheckDependencies = require('fbjs-scripts/gulp/check-dependencies');
+
+var paths = {
+ dist: 'dist',
+ lib: 'lib',
+ src: [
+ 'src/**/*.js',
+ '!src/**/__tests__/**/*.js',
+ '!src/**/__mocks__/**/*.js'
+ ],
+ css: [
+ 'src/**/*.css'
+ ]
+};
+
+// Ensure that we use another plugin that isn't specified in the default Babel
+// options, converting __DEV__.
+babelOpts.plugins.push(babelPluginDEV);
+
+var buildDist = function(opts) {
+ var webpackOpts = {
+ debug: opts.debug,
+ externals: {
+ immutable: 'Immutable',
+ react: 'React',
+ 'react-dom': 'ReactDOM',
+ },
+ output: {
+ filename: opts.output,
+ libraryTarget: 'var',
+ library: 'Draft'
+ },
+ plugins: [
+ new webpackStream.webpack.optimize.OccurenceOrderPlugin(),
+ new webpackStream.webpack.optimize.DedupePlugin()
+ ]
+ };
+ if (!opts.debug) {
+ webpackOpts.plugins.push(
+ new webpackStream.webpack.optimize.UglifyJsPlugin({
+ compress: {
+ hoist_vars: true,
+ screw_ie8: true,
+ warnings: false
+ }
+ })
+ );
+ }
+ return webpackStream(webpackOpts, null, function(err, stats) {
+ if (err) {
+ throw new gulpUtil.PluginError('webpack', err);
+ }
+ if (stats.compilation.errors.length) {
+ gulpUtil.log('webpack', '\n' + stats.toString({colors: true}));
+ }
+ });
+};
+
+gulp.task('clean', function() {
+ return del([paths.dist, paths.lib]);
+});
+
+gulp.task('modules', function() {
+ return gulp
+ .src(paths.src)
+ .pipe(babel(babelOpts))
+ .pipe(flatten())
+ .pipe(gulp.dest(paths.lib));
+});
+
+gulp.task('css', function() {
+ return gulp
+ .src(paths.css)
+ .pipe(concatCSS('Draft.css'))
+ .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('dist', ['modules', 'css'], function() {
+ var opts = {
+ debug: true,
+ output: 'Draft.js'
+ };
+ return gulp.src('./lib/Draft.js')
+ .pipe(buildDist(opts))
+ .pipe(derequire())
+ .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('dist:min', ['modules'], function() {
+ var opts = {
+ debug: false,
+ output: 'Draft.min.js',
+ };
+ return gulp.src('./lib/Draft.js')
+ .pipe(buildDist(opts))
+ .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('check-dependencies', function() {
+ return gulp
+ .src('package.json')
+ .pipe(gulpCheckDependencies());
+});
+
+gulp.task('watch', function() {
+ gulp.watch(paths.src, ['modules']);
+});
+
+gulp.task('default', function(cb) {
+ runSequence('check-dependencies', 'clean', 'modules', ['dist', 'dist:min'], cb);
+});
85 package.json
@@ -0,0 +1,85 @@
+{
+ "name": "draft-js",
+ "private": true,
+ "description": "A React framework for building text editors.",
+ "version": "0.1.0",
+ "keywords": [
+ "draftjs",
+ "editor",
+ "react",
+ "richtext"
+ ],
+ "homepage": "https://facebook.github.io/draft-js",
+ "bugs": "https://github.com/facebook/draft-js/issues",
+ "files": [
+ "dist/",
+ "lib/",
+ "LICENSE",
+ "PATENTS"
+ ],
+ "main": "lib/Draft.js",
+ "repository": "facebook/draft-js",
+ "license": "BSD-3-Clause",
+ "scripts": {
+ "build": "gulp",
+ "lint": "eslint .",
+ "test": "NODE_ENV=test jest"
+ },
+ "dependencies": {
+ "fbjs": "^0.8.0-alpha.1",
+ "immutable": "^3.7.4"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0",
+ "react-dom": "^0.14.0"
+ },
+ "devDependencies": {
+ "babel-core": "^5.8.35",
+ "babel-eslint": "^4.1.3",
+ "del": "^2.2.0",
+ "envify": "^3.4.0",
+ "eslint": "^1.5.1",
+ "eslint-plugin-react": "^3.2.2",
+ "fbjs-scripts": "^0.6.0-alpha.1",
+ "gulp": "^3.9.0",
+ "gulp-babel": "^5.1.0",
+ "gulp-browserify-thin": "^0.1.5",
+ "gulp-concat-css": "^2.2.0",
+ "gulp-derequire": "^2.1.0",
+ "gulp-flatten": "^0.2.0",
+ "gulp-uglify": "^1.2.0",
+ "gulp-util": "^3.0.6",
+ "jest-cli": "^0.9.0-fb1",
+ "object-assign": "^4.0.1",
+ "react": "^0.14.0",
+ "react-dom": "^0.14.0",
+ "run-sequence": "^1.1.2",
+ "vinyl-buffer": "^1.0.0",
+ "webpack-stream": "^3.0.0"
+ },
+ "jest": {
+ "rootDir": "",
+ "scriptPreprocessor": "scripts/jest/preprocessor.js",
+ "setupEnvScriptFile": "node_modules/fbjs-scripts/jest/environment.js",
+ "persistModuleRegistryBetweenSpecs": true,
+ "modulePathIgnorePatterns": [
+ "<rootDir>/lib/",
+ "<rootDir>/node_modules/"
+ ],
+ "preprocessorIgnorePatterns": [
+ "<rootDir>/node_modules/"
+ ],
+ "testPathDirs": [
+ "<rootDir>/src/"
+ ],
+ "testRunner": "node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js",
+ "unmockedModulePathPatterns": [
+ "<rootDir>/node_modules/fbjs/node_modules/",
+ "<rootDir>/node_modules/fbjs/lib/(?!(UserAgent.js$|UserAgentData.js$))",
+ "<rootDir>/node_modules/fbjs-scripts/",
+ "<rootDir>/node_modules/immutable/",
+ "<rootDir>/node_modules/react/",
+ "<rootDir>/node_modules/react-dom/"
+ ]
+ }
+}
20 scripts/babel/default-options.js
@@ -0,0 +1,20 @@
+var assign = require('object-assign');
+var babelPluginModules = require('fbjs-scripts/babel/rewrite-modules');
+
+module.exports = {
+ blacklist: [
+ 'es6.regex.unicode',
+ ],
+ nonStandard: true,
+ optional: [
+ 'es7.trailingFunctionCommas',
+ 'es7.classProperties',
+ ],
+ stage: 1,
+ plugins: [babelPluginModules],
+ _moduleMap: assign({}, require('fbjs/module-map'), {
+ immutable: 'immutable',
+ React: 'react',
+ ReactDOM: 'react-dom',
+ }),
+};
31 scripts/jest/preprocessor.js
@@ -0,0 +1,31 @@
+var babel = require('babel-core');
+var assign = require('object-assign');
+var babelOpts = require('../babel/default-options');
+var babelInlineRequires = require('fbjs-scripts/babel/inline-requires');
+var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
+var path = require('path');
+
+// Modify babelOpts to account for our needs. Namely we want to retain line
+// numbers for better stack traces in tests.
+babelOpts.retainLines = true;
+babelOpts._moduleMap = assign(babelOpts._moduleMap, {
+ ReactTestUtils: 'react/lib/ReactTestUtils',
+ reactComponentExpect: 'react/lib/reactComponentExpect',
+});
+babelOpts.plugins.push({
+ position: 'after',
+ transformer: babelInlineRequires,
+});
+
+var cacheKeyFunction = createCacheKeyFunction([
+ __filename,
+ path.join(__dirname, '..', '..', 'node_modules', 'fbjs', 'package.json'),
+ path.join(__dirname, '..', '..', 'node_modules', 'fbjs-scripts', 'package.json'),
+]);
+
+module.exports = {
+ process: function(src, path) {
+ return babel.transform(src, assign({filename: path}, babelOpts)).code;
+ },
+ getCacheKey: cacheKeyFunction,
+};
17 src/.flowconfig
@@ -0,0 +1,17 @@
+[ignore]
+.*/__tests__.*
+.*/react/node_modules/.*
+.*/fbjs/node_modules/.*
+
+[include]
+../node_modules/fbjs/lib/
+../node_modules/immutable
+../node_modules/react
+
+[libs]
+../node_modules/fbjs/flow/lib
+
+[options]
+module.system=haste
+esproposal.class_static_fields=enable
+suppress_type=$FlowIssue
53 src/Draft.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Draft
+ */
+
+'use strict';
+
+const BlockMapBuilder = require('BlockMapBuilder');
+const CharacterMetadata = require('CharacterMetadata');
+const CompositeDraftDecorator = require('CompositeDraftDecorator');
+const ContentBlock = require('ContentBlock');
+const ContentState = require('ContentState');
+const DraftEditor = require('DraftEditor.react');
+const DraftModifier = require('DraftModifier');
+const DraftEntity = require('DraftEntity');
+const DraftEntityInstance = require('DraftEntityInstance');
+const EditorState = require('EditorState');
+const RichTextEditorUtil = require('RichTextEditorUtil');
+const SelectionState = require('SelectionState');
+
+const convertFromDraftStateToRaw = require('convertFromDraftStateToRaw');
+const convertFromRawToDraftState = require('convertFromRawToDraftState');
+const generateBlockKey = require('generateBlockKey');
+
+var DraftPublic = {
+ Editor: DraftEditor,
+ EditorState,
+
+ CompositeDecorator: CompositeDraftDecorator,
+ Entity: DraftEntity,
+ EntityInstance: DraftEntityInstance,
+
+ BlockMapBuilder,
+ CharacterMetadata,
+ ContentBlock,
+ ContentState,
+ SelectionState,
+
+ Modifier: DraftModifier,
+ RichUtils: RichTextEditorUtil,
+
+ convertFromRaw: convertFromRawToDraftState,
+ convertToRaw: convertFromDraftStateToRaw,
+ genKey: generateBlockKey,
+};
+
+module.exports = DraftPublic;
67 src/component/base/DraftEditor.css
@@ -0,0 +1,67 @@
+/**
+ * @providesModule DraftEditor
+ * @permanent
+ */
+
+/**
+ * We inherit the height of the container by default
+ */
+.DraftEditor-root,
+.DraftEditor-editorContainer,
+.public-DraftEditor-content {
+ height: inherit;
+ text-align: initial;
+}
+
+.DraftEditor-root {
+ position: relative;
+}
+
+/**
+ * Zero-opacity background used to allow focus in IE. Otherwise, clicks
+ * fall through to the placeholder.
+ */
+.DraftEditor-editorContainer {
+ background-color: rgba(255, 255, 255, 0);
+ /* Repair mysterious missing Safari cursor */
+ border-left: 0.1px solid transparent;
+ position: relative;
+ z-index: 1;
+}
+
+.public-DraftEditor-content {
+ outline: none;
+ white-space: pre-wrap;
+}
+
+.public-DraftEditor-block {
+ position: relative;
+}
+
+.DraftEditor-alignLeft .public-DraftEditor-block {
+ text-align: left;
+}
+
+.DraftEditor-alignLeft .public-DraftEditorPlaceholder/root {
+ left: 0;
+ text-align: left;
+}
+
+.DraftEditor-alignCenter .public-DraftEditor-block {
+ text-align: center;
+}
+
+.DraftEditor-alignCenter .public-DraftEditorPlaceholder/root {
+ margin: 0 auto;
+ text-align: center;
+ width: 100%;
+}
+
+.DraftEditor-alignRight .public-DraftEditor-block {
+ text-align: right;
+}
+
+.DraftEditor-alignRight .public-DraftEditorPlaceholder/root {
+ right: 0;
+ text-align: right;
+}
448 src/component/base/DraftEditor.react.js
@@ -0,0 +1,448 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditor.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DefaultDraftInlineStyle = require('DefaultDraftInlineStyle');
+const DraftEditorCompositionHandler = require('DraftEditorCompositionHandler');
+const DraftEditorContents = require('DraftEditorContents.react');
+const DraftEditorDragHandler = require('DraftEditorDragHandler');
+const DraftEditorEditHandler = require('DraftEditorEditHandler');
+const DraftEditorPlaceholder = require('DraftEditorPlaceholder.react');
+const EditorState = require('EditorState');
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const Scroll = require('Scroll');
+const Style = require('Style');
+const UserAgent = require('UserAgent');
+
+const cx = require('cx');
+const emptyFunction = require('emptyFunction');
+const getDefaultKeyBinding = require('getDefaultKeyBinding');
+const nullthrows = require('nullthrows');
+const getScrollPosition = require('getScrollPosition');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentBlock from 'ContentBlock';
+import type {DraftEditorModes} from 'DraftEditorModes';
+import type {DraftEditorProps} from 'DraftEditorProps';
+import type {DraftScrollPosition} from 'DraftScrollPosition';
+
+const isIE = UserAgent.isBrowser('IE');
+
+// IE does not support the `input` event on contentEditable, so we can't
+// observe spellcheck behavior.
+const allowSpellCheck = !isIE;
+
+// Define a set of handler objects to correspond to each possible `mode`
+// of editor behavior.
+const handlerMap = {
+ 'edit': DraftEditorEditHandler,
+ 'composite': DraftEditorCompositionHandler,
+ 'drag': DraftEditorDragHandler,
+ 'cut': null,
+ 'render': null,
+};
+
+type DefaultProps = {
+ blockRendererFn: (block: ContentBlock) => ?Object;
+ blockStyleFn: (type: number) => string,
+ keyBindingFn: (e: SyntheticKeyboardEvent) => ?string,
+ readOnly: boolean,
+ spellCheck: boolean,
+ stripPastedStyles: boolean,
+};
+
+type State = {
+ containerKey: number,
+};
+
+/**
+ * `DraftEditor` is the root editor component. It composes a `contentEditable`
+ * div, and provides a wide variety of useful function props for managing the
+ * state of the editor. See `DraftEditorProps` for details.
+ */
+class DraftEditor
+ extends React.Component<DefaultProps, DraftEditorProps, State> {
+ state: State;
+
+ static defaultProps = {
+ blockRendererFn: emptyFunction.thatReturnsNull,
+ blockStyleFn: emptyFunction.thatReturns(''),
+ keyBindingFn: getDefaultKeyBinding,
+ readOnly: false,
+ spellCheck: false,
+ stripPastedStyles: false,
+ };
+
+ _blockSelectEvents: boolean;
+ _clipboard: ?BlockMap;
+ _guardAgainstRender: boolean;
+ _handler: ?Object;
+ _dragCount: number;
+
+ /**
+ * Define proxies that can route events to the current handler.
+ */
+ _onBeforeInput: Function;
+ _onBlur: Function;
+ _onCharacterData: Function;
+ _onCompositionEnd: Function;
+ _onCompositionStart: Function;
+ _onCopy: Function;
+ _onCut: Function;
+ _onDragEnd: Function;
+ _onDragOver: Function;
+ _onDragStart: Function;
+ _onDrop: Function;
+ _onInput: Function;
+ _onFocus: Function;
+ _onKeyDown: Function;
+ _onKeyPress: Function;
+ _onKeyUp: Function;
+ _onMouseDown: Function;
+ _onMouseUp: Function;
+ _onPaste: Function;
+ _onSelect: Function;
+
+ focus: () => void;
+ blur: () => void;
+ setMode: (mode: DraftEditorModes) => void;
+ exitCurrentMode: () => void;
+ restoreEditorDOM: (scrollPosition: DraftScrollPosition) => void;
+ setRenderGuard: () => void;
+ removeRenderGuard: () => void;
+ setClipboard: (clipboard?: BlockMap) => void;
+ getClipboard: () => ?BlockMap;
+ update: (editorState: EditorState) => void;
+ onDragEnter: () => void;
+ onDragLeave: () => void;
+
+ constructor(props: DraftEditorProps) {
+ super(props);
+
+ this._blockSelectEvents = false;
+ this._clipboard = null;
+ this._guardAgainstRender = false;
+ this._handler = null;
+ this._dragCount = 0;
+
+ this._onBeforeInput = this._buildHandler('onBeforeInput');
+ this._onBlur = this._buildHandler('onBlur');
+ this._onCharacterData = this._buildHandler('onCharacterData');
+ this._onCompositionEnd = this._buildHandler('onCompositionEnd');
+ this._onCompositionStart = this._buildHandler('onCompositionStart');
+ this._onCopy = this._buildHandler('onCopy');
+ this._onCut = this._buildHandler('onCut');
+ this._onDragEnd = this._buildHandler('onDragEnd');
+ this._onDragOver = this._buildHandler('onDragOver');
+ this._onDragStart = this._buildHandler('onDragStart');
+ this._onDrop = this._buildHandler('onDrop');
+ this._onInput = this._buildHandler('onInput');
+ this._onFocus = this._buildHandler('onFocus');
+ this._onKeyDown = this._buildHandler('onKeyDown');
+ this._onKeyPress = this._buildHandler('onKeyPress');
+ this._onKeyUp = this._buildHandler('onKeyUp');
+ this._onMouseDown = this._buildHandler('onMouseDown');
+ this._onMouseUp = this._buildHandler('onMouseUp');
+ this._onPaste = this._buildHandler('onPaste');
+ this._onSelect = this._buildHandler('onSelect');
+
+ // Manual binding for public and internal methods.
+ this.focus = this._focus.bind(this);
+ this.blur = this._blur.bind(this);
+ this.setMode = this._setMode.bind(this);
+ this.exitCurrentMode = this._exitCurrentMode.bind(this);
+ this.restoreEditorDOM = this._restoreEditorDOM.bind(this);
+ this.setRenderGuard = this._setRenderGuard.bind(this);
+ this.removeRenderGuard = this._removeRenderGuard.bind(this);
+ this.setClipboard = this._setClipboard.bind(this);
+ this.getClipboard = this._getClipboard.bind(this);
+ this.update = this._update.bind(this);
+ this.onDragEnter = this._onDragEnter.bind(this);
+ this.onDragLeave = this._onDragLeave.bind(this);
+
+ // See `_restoreEditorDOM()`.
+ this.state = {containerKey: 0};
+ }
+
+ /**
+ * Build a method that will pass the event to the specified handler method.
+ * This allows us to look up the correct handler function for the current
+ * editor mode, if any has been specified.
+ */
+ _buildHandler(eventName: string): Function {
+ return (e) => {
+ if (!this.props.readOnly) {
+ const method = this._handler && this._handler[eventName];
+ method && method.call(this, e);
+ }
+ };
+ }
+
+ _renderPlaceholder(): ?ReactElement {
+ const content = this.props.editorState.getCurrentContent();
+ const showPlaceholder = (
+ this.props.placeholder &&
+ !this.props.editorState.isInCompositionMode() &&
+ !content.hasText()
+ );
+
+ if (showPlaceholder) {
+ return (
+ <DraftEditorPlaceholder
+ text={nullthrows(this.props.placeholder)}
+ editorState={this.props.editorState}
+ textAlignment={this.props.textAlignment}
+ />
+ );
+ }
+ }
+
+ render(): ReactElement {
+ const {readOnly, textAlignment} = this.props;
+ const rootClass = cx({
+ 'DraftEditor/root': true,
+ 'DraftEditor/alignLeft': textAlignment === 'left',
+ 'DraftEditor/alignRight': textAlignment === 'right',
+ 'DraftEditor/alignCenter': textAlignment === 'center',
+ });
+ const hasContent = this.props.editorState.getCurrentContent().hasText();
+
+ return (
+ <div className={rootClass}>
+ {this._renderPlaceholder()}
+ <div
+ className={cx('DraftEditor/editorContainer')}
+ key={'editor' + this.state.containerKey}
+ ref="editorContainer">
+ <div
+ aria-activedescendant={
+ readOnly ? null : this.props.ariaActiveDescendantID
+ }
+ aria-autocomplete={readOnly ? null : this.props.ariaAutoComplete}
+ aria-describedby={this.props.ariaDescribedBy}
+ aria-expanded={readOnly ? null : this.props.ariaExpanded}
+ aria-haspopup={readOnly ? null : this.props.ariaHasPopup}
+ aria-label={this.props.ariaLabel}
+ aria-owns={readOnly ? null : this.props.ariaOwneeID}
+ className={cx('public/DraftEditor/content')}
+ contentEditable={!readOnly}
+ data-testid={this.props.webDriverTestID}
+ onBeforeInput={this._onBeforeInput}
+ onBlur={this._onBlur}
+ onCompositionEnd={this._onCompositionEnd}
+ onCompositionStart={this._onCompositionStart}
+ onCopy={this._onCopy}
+ onCut={this._onCut}
+ onDragEnd={this._onDragEnd}
+ onDragEnter={this.onDragEnter}
+ onDragLeave={this.onDragLeave}
+ onDragOver={this._onDragOver}
+ onDragStart={this._onDragStart}
+ onDrop={this._onDrop}
+ onFocus={this._onFocus}
+ onInput={this._onInput}
+ onKeyDown={this._onKeyDown}
+ onKeyPress={this._onKeyPress}
+ onKeyUp={this._onKeyUp}
+ onMouseUp={this._onMouseUp}
+ onPaste={this._onPaste}
+ onSelect={this._onSelect}
+ ref="editor"
+ role={readOnly ? null : (this.props.role || 'textbox')}
+ spellCheck={allowSpellCheck && this.props.spellCheck}
+ tabIndex={this.props.tabIndex}
+ title={hasContent ? null : this.props.placeholder}>
+ <DraftEditorContents
+ blockRendererFn={nullthrows(this.props.blockRendererFn)}
+ blockStyleFn={nullthrows(this.props.blockStyleFn)}
+ customStyleMap={
+ {...DefaultDraftInlineStyle, ...this.props.customStyleMap}
+ }
+ editorState={this.props.editorState}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ componentDidMount(): void {
+ this.setMode('edit');
+
+ /**
+ * IE has a hardcoded "feature" that attempts to convert link text into
+ * anchors in contentEditable DOM. This breaks the editor's expectations of
+ * the DOM, and control is lost. Disable it to make IE behave.
+ * See: http://blogs.msdn.com/b/ieinternals/archive/2010/09/15/
+ * ie9-beta-minor-change-list.aspx
+ */
+ if (isIE) {
+ document.execCommand('AutoUrlDetect', false, false);
+ }
+ }
+
+ /**
+ * Prevent selection events from affecting the current editor state. This
+ * is mostly intended to defend against IE, which fires off `selectionchange`
+ * events regardless of whether the selection is set via the browser or
+ * programmatically. We only care about selection events that occur because
+ * of browser interaction, not re-renders and forced selections.
+ */
+ componentWillUpdate(): void {
+ this._blockSelectEvents = true;
+ }
+
+ componentDidUpdate(): void {
+ this._blockSelectEvents = false;
+ }
+
+ /**
+ * Used via `this.focus()`.
+ *
+ * Force focus back onto the editor node.
+ *
+ * Forcing focus causes the browser to scroll to the top of the editor, which
+ * may be undesirable when the editor is taller than the viewport. To solve
+ * this, either use a specified scroll position (in cases like `cut` behavior
+ * where it should be restored to a known position) or store the current
+ * scroll state and put it back in place after focus has been forced.
+ */
+ _focus(scrollPosition?: DraftScrollPosition): void {
+ const {editorState} = this.props;
+ const alreadyHasFocus = editorState.getSelection().getHasFocus();
+ const editorNode = ReactDOM.findDOMNode(this.refs.editor);
+
+ const scrollParent = Style.getScrollParent(editorNode);
+ const {x, y} = scrollPosition || getScrollPosition(scrollParent);
+
+ editorNode.focus();
+ if (scrollParent === window) {
+ window.scrollTo(x, y);
+ } else {
+ Scroll.setTop(scrollParent, y);
+ }
+
+ // On Chrome and Safari, calling focus on contenteditable focuses the
+ // cursor at the first character. This is something you don't expect when
+ // you're clicking on an input element but not directly on a character.
+ // Put the cursor back where it was before the blur.
+ if (!alreadyHasFocus) {
+ this.update(
+ EditorState.forceSelection(
+ editorState,
+ editorState.getSelection()
+ )
+ );
+ }
+ }
+
+ _blur(): void {
+ ReactDOM.findDOMNode(this.refs.editor).blur();
+ }
+
+ /**
+ * Used via `this.setMode(...)`.
+ *
+ * Set the behavior mode for the editor component. This switches the current
+ * handler module to ensure that DOM events are managed appropriately for
+ * the active mode.
+ */
+ _setMode(mode: DraftEditorModes): void {
+ this._handler = handlerMap[mode];
+ }
+
+ _exitCurrentMode(): void {
+ this.setMode('edit');
+ }
+
+ /**
+ * Used via `this.restoreEditorDOM()`.
+ *
+ * Force a complete re-render of the editor based on the current EditorState.
+ * This is useful when we know we are going to lose control of the DOM
+ * state (cut command, IME) and we want to make sure that reconciliation
+ * occurs on a version of the DOM that is synchronized with our EditorState.
+ */
+ _restoreEditorDOM(scrollPosition?: DraftScrollPosition): void {
+ this.setState({containerKey: this.state.containerKey + 1}, () => {
+ this._focus(scrollPosition);
+ });
+ }
+
+ /**
+ * Guard against rendering. Intended for use when we need to manually
+ * reset editor contents, to ensure that no outside influences lead to
+ * React reconciliation when we are in an uncertain state.
+ */
+ _setRenderGuard(): void {
+ this._guardAgainstRender = true;
+ }
+
+ _removeRenderGuard(): void {
+ this._guardAgainstRender = false;
+ }
+
+ /**
+ * Used via `this.setClipboard(...)`.
+ *
+ * Set the clipboard state for a cut/copy event.
+ */
+ _setClipboard(clipboard: ?BlockMap): void {
+ this._clipboard = clipboard;
+ }
+
+ /**
+ * Used via `this.getClipboard()`.
+ *
+ * Retrieve the clipboard state for a cut/copy event.
+ */
+ _getClipboard(): ?BlockMap {
+ return this._clipboard;
+ }
+
+ /**
+ * Used via `this.update(...)`.
+ *
+ * Propagate a new `EditorState` object to higher-level components. This is
+ * the method by which event handlers inform the `DraftEditor` component of
+ * state changes. A component that composes a `DraftEditor` **must** provide
+ * an `onChange` prop to receive state updates passed along from this
+ * function.
+ */
+ _update(editorState: EditorState): void {
+ this.props.onChange(editorState);
+ }
+
+ /**
+ * Used in conjunction with `_onDragLeave()`, by counting the number of times
+ * a dragged element enters and leaves the editor (or any of its children),
+ * to determine when the dragged element absolutely leaves the editor.
+ */
+ _onDragEnter(): void {
+ this._dragCount++;
+ }
+
+ /**
+ * See `_onDragEnter()`.
+ */
+ _onDragLeave(): void {
+ this._dragCount--;
+ if (this._dragCount === 0) {
+ this.exitCurrentMode();
+ }
+ }
+}
+
+module.exports = DraftEditor;
17 src/component/base/DraftEditorPlaceholder.css
@@ -0,0 +1,17 @@
+/**
+ * @providesModule DraftEditorPlaceholder
+ */
+
+.public-DraftEditorPlaceholder-root {
+ color: #9197a3;
+ position: absolute;
+ z-index: 0;
+}
+
+.public-DraftEditorPlaceholder-hasFocus {
+ color: #bdc1c9;
+}
+
+.DraftEditorPlaceholder-hidden {
+ display: none;
+}
64 src/component/base/DraftEditorPlaceholder.react.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorPlaceholder.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const React = require('React');
+
+const cx = require('cx');
+
+import type {DraftTextAlignment} from 'DraftTextAlignment';
+import type EditorState from 'EditorState';
+
+type Props = {
+ editorState: EditorState,
+ text: string,
+ textAlignment: DraftTextAlignment,
+};
+
+/**
+ * This component is responsible for rendering placeholder text for the
+ * `DraftEditor` component.
+ *
+ * Override placeholder style via CSS.
+ */
+class DraftEditorPlaceholder extends React.Component {
+ shouldComponentUpdate(nextProps: Props): boolean {
+ return (
+ this.props.text !== nextProps.text ||
+ (
+ this.props.editorState.getSelection().getHasFocus() !==
+ nextProps.editorState.getSelection().getHasFocus()
+ )
+ );
+ }
+
+ render(): ReactElement {
+ const hasFocus = this.props.editorState.getSelection().getHasFocus();
+
+ const className = cx({
+ 'public/DraftEditorPlaceholder/root': true,
+ 'public/DraftEditorPlaceholder/hasFocus': hasFocus,
+ });
+
+ return (
+ <div className={className}>
+ <div className={cx('public/DraftEditorPlaceholder/inner')}>
+ {this.props.text}
+ </div>
+ </div>
+ );
+ }
+}
+
+module.exports = DraftEditorPlaceholder;
116 src/component/base/DraftEditorProps.js
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorProps
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+import type {DraftTextAlignment} from 'DraftTextAlignment';
+import type EditorState from 'EditorState';
+
+export type DraftEditorProps = {
+ /**
+ * The two most critical props are `editorState` and `onChange`.
+ *
+ * The `editorState` prop defines the entire state of the editor, while the
+ * `onChange` prop is the method in which all state changes are propagated
+ * upward to higher-level components.
+ *
+ * These props are analagous to `value` and `onChange` in controlled React
+ * text inputs.
+ */
+ editorState: EditorState,
+ onChange: (editorState: EditorState) => void,
+
+ placeholder?: string,
+
+ // Specify whether text alignment should be forced in a direction
+ // regardless of input characters.
+ textAlignment?: DraftTextAlignment,
+
+ // For a given `ContentBlock` object, return an object that specifies
+ // a custom block component and/or props. If no object is returned,
+ // the default `TextEditorBlock` is used.
+ blockRendererFn?: (block: ContentBlock) => ?Object,
+
+ // Function that returns a cx map corresponding to block-level styles.
+ blockStyleFn?: (type: number) => string,
+
+ // A function that accepts a synthetic key event and returns
+ // the matching DraftEditorCommand constant, or null if no command should
+ // be invoked.
+ keyBindingFn?: (e: SyntheticKeyboardEvent) => ?string,
+
+ // Set whether the `DraftEditor` component should be editable. Useful for
+ // temporarily disabling edit behavior or allowing `DraftEditor` rendering
+ // to be used for consumption purposes.
+ readOnly?: boolean,
+
+ // Note: spellcheck is always disabled for IE. If enabled in Safari, OSX
+ // autocorrect is enabled as well.
+ spellCheck?: boolean,
+
+ // Set whether to remove all style information from pasted content. If your
+ // use case should not have any block or inline styles, it is recommended
+ // that you set this to `true`.
+ stripPastedStyles?: boolean,
+
+ tabIndex?: number,
+
+ ariaActiveDescendantID?: string,
+ ariaAutoComplete?: string,
+ ariaDescribedBy?: string,
+ ariaExpanded?: boolean,
+ ariaHasPopup?: boolean,
+ ariaLabel?: string,
+ ariaOwneeID?: string,
+
+ webDriverTestID?: string,
+
+ /**
+ * Cancelable event handlers, handled from the top level down. A handler
+ * that returns true will be the last handler to execute for that event.
+ */
+
+ // Useful for managing special behavior for pressing the `Return` key. E.g.
+ // removing the style from an empty list item.
+ handleReturn?: (e: SyntheticKeyboardEvent) => boolean,
+
+ // Map a key command string provided by your key binding function to a
+ // specified behavior.
+ handleKeyCommand?: (command: DraftEditorCommand) => boolean,
+
+ // Handle intended text insertion before the insertion occurs. This may be
+ // useful in cases where the user has entered characters that you would like
+ // to trigger some special behavior. E.g. immediately converting `:)` to an
+ // emoji Unicode character, or replacing ASCII quote characters with smart
+ // quotes.
+ handleBeforeInput?: (e: SyntheticInputEvent) => boolean,
+
+ handlePastedFiles?: (files: Array<Blob>) => boolean,
+
+ /**
+ * Non-cancelable event triggers.
+ */
+ onEscape?: (e: SyntheticKeyboardEvent) => void,
+ onTab?: (e: SyntheticKeyboardEvent) => void,
+ onUpArrow?: (e: SyntheticKeyboardEvent) => void,
+ onDownArrow?: (e: SyntheticKeyboardEvent) => void,
+ onPasteRawText?: (text: string) => void,
+
+ onBlur?: (e: SyntheticEvent) => void,
+ onFocus?: (e: SyntheticEvent) => void,
+
+ // Provide a map of inline style names corresponding to CSS style objects
+ // that will be rendered for matching ranges.
+ customStyleMap?: Object,
+};
18 src/component/base/DraftScrollPosition.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftScrollPosition
+ * @flow
+ */
+
+'use strict';
+
+export type DraftScrollPosition = {
+ x: number,
+ y: number,
+};
15 src/component/base/DraftTextAlignment.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftTextAlignment
+ * @flow
+ */
+
+'use strict';
+
+export type DraftTextAlignment = 'left' | 'center' | 'right';
224 src/component/contents/DraftEditorBlock.react.js
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorBlock.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const ContentBlock = require('ContentBlock');
+const DraftEditorLeaf = require('DraftEditorLeaf.react');
+const DraftOffsetKey = require('DraftOffsetKey');
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const Scroll = require('Scroll');
+const SelectionState = require('SelectionState');
+const Style = require('Style');
+const UnicodeBidi = require('UnicodeBidi');
+const UnicodeBidiDirection = require('UnicodeBidiDirection');
+
+const cx = require('cx');
+const getElementPosition = require('getElementPosition');
+const getScrollPosition = require('getScrollPosition');
+const getViewportDimensions = require('getViewportDimensions');
+const nullthrows = require('nullthrows');
+
+import type {BidiDirection} from 'UnicodeBidiDirection';
+import type {DraftDecoratorType} from 'DraftDecoratorType';
+import type {List} from 'immutable';
+
+const SCROLL_BUFFER = 10;
+
+type Props = {
+ block: ContentBlock,
+ customStyleMap: Object,
+ tree: List,
+ selection: SelectionState,
+ decorator: DraftDecoratorType,
+ forceSelection: boolean,
+ direction: BidiDirection,
+ blockProps?: Object,
+ startIndent?: boolean,
+ blockStyleFn: Function,
+};
+
+/**
+ * The default block renderer for a `DraftEditor` component.
+ *
+ * A `DraftEditorBlock` is able to render a given `ContentBlock` to its
+ * appropriate decorator and inline style components.
+ */
+class DraftEditorBlock extends React.Component {
+ shouldComponentUpdate(nextProps: Props): boolean {
+ return (
+ this.props.block !== nextProps.block ||
+ this.props.tree !== nextProps.tree ||
+ this.props.direction !== nextProps.direction ||
+ (
+ isBlockOnSelectionEdge(
+ nextProps.selection,
+ nextProps.block.getKey()
+ ) &&
+ nextProps.forceSelection
+ )
+ );
+ }
+
+ /**
+ * When a block is mounted and overlaps the selection state, we need to make
+ * sure that the cursor is visible to match native behavior. This may not
+ * be the case if the user has pressed `RETURN` or pasted some content, since
+ * programatically creating these new blocks and setting the DOM selection
+ * will miss out on the browser natively scrolling to that position.
+ *
+ * To replicate native behavior, if the block overlaps the selection state
+ * on mount, force the scroll position. Check the scroll state of the scroll
+ * parent, and adjust it to align the entire block to the bottom of the
+ * scroll parent.
+ */
+ componentDidMount(): void {
+ var selection = this.props.selection;
+ var endKey = selection.getEndKey();
+ if (!selection.getHasFocus() || endKey !== this.props.block.getKey()) {
+ return;
+ }
+
+ var blockNode = ReactDOM.findDOMNode(this);
+ var scrollParent = Style.getScrollParent(blockNode);
+ var scrollPosition = getScrollPosition(scrollParent);
+ var scrollDelta;
+
+ if (scrollParent === window) {
+ var nodePosition = getElementPosition(blockNode);
+ var nodeBottom = nodePosition.y + nodePosition.height;
+ var viewportHeight = getViewportDimensions().height;
+ scrollDelta = nodeBottom - viewportHeight;
+ if (scrollDelta > 0) {
+ window.scrollTo(
+ scrollPosition.x,
+ scrollPosition.y + scrollDelta + SCROLL_BUFFER
+ );
+ }
+ } else {
+ var blockBottom = blockNode.offsetHeight + blockNode.offsetTop;
+ var scrollBottom = scrollParent.offsetHeight + scrollPosition.y;
+ scrollDelta = blockBottom - scrollBottom;
+ if (scrollDelta > 0) {
+ Scroll.setTop(
+ scrollParent,
+ Scroll.getTop(scrollParent) + scrollDelta + SCROLL_BUFFER
+ );
+ }
+ }
+ }
+
+ _renderChildren(): Array<ReactElement> {
+ var block = this.props.block;
+ var blockKey = block.getKey();
+ var text = block.getText();
+ var lastLeafSet = this.props.tree.size - 1;
+ var hasSelection = isBlockOnSelectionEdge(this.props.selection, blockKey);
+
+ return this.props.tree.map((leafSet, ii) => {
+ var leavesForLeafSet = leafSet.get('leaves');
+ var lastLeaf = leavesForLeafSet.size - 1;
+ var leaves = leavesForLeafSet.map((leaf, jj) => {
+ var offsetKey = DraftOffsetKey.encode(blockKey, ii, jj);
+ var start = leaf.get('start');
+ var end = leaf.get('end');
+ return (
+ <DraftEditorLeaf
+ key={offsetKey}
+ offsetKey={offsetKey}
+ blockKey={blockKey}
+ start={start}
+ selection={hasSelection ? this.props.selection : undefined}
+ forceSelection={this.props.forceSelection}
+ text={text.slice(start, end)}
+ styleSet={block.getInlineStyleAt(start)}
+ customStyleMap={this.props.customStyleMap}
+ isLast={ii === lastLeafSet && jj === lastLeaf}
+ />
+ );
+ }).toArray();
+
+ var decoratorKey = leafSet.get('decoratorKey');
+ if (decoratorKey == null) {
+ return leaves;
+ }
+
+ if (!this.props.decorator) {
+ return leaves;
+ }
+
+ var decorator = nullthrows(this.props.decorator);
+
+ var DecoratorComponent = decorator.getComponentForKey(decoratorKey);
+ if (!DecoratorComponent) {
+ return leaves;
+ }
+
+ var decoratorProps = decorator.getPropsForKey(decoratorKey);
+ var decoratorOffsetKey = DraftOffsetKey.encode(blockKey, ii, 0);
+ var decoratedText = text.slice(
+ leavesForLeafSet.first().get('start'),
+ leavesForLeafSet.last().get('end')
+ );
+
+ // Resetting dir to the same value on a child node makes Chrome/Firefox
+ // confused on cursor movement. See http://jsfiddle.net/d157kLck/3/
+ var dir = UnicodeBidiDirection.getHTMLDirIfDifferent(
+ UnicodeBidi.getDirection(decoratedText),
+ this.props.direction,
+ );
+
+ return (
+ <DecoratorComponent
+ {...decoratorProps}
+ dir={dir}
+ key={decoratorOffsetKey}
+ entityKey={block.getEntityAt(leafSet.get('start'))}
+ offsetKey={decoratorOffsetKey}>
+ {leaves}
+ </DecoratorComponent>
+ );
+ }).toArray();
+ }
+
+ render(): ReactElement {
+ const {direction, offsetKey} = this.props;
+ const className = cx({
+ 'public/DraftStyleDefault/block': true,
+ 'public/DraftStyleDefault/ltr': direction === 'LTR',
+ 'public/DraftStyleDefault/rtl': direction === 'RTL',
+ });
+
+ return (
+ <div data-offset-key={offsetKey} className={className}>
+ {this._renderChildren()}
+ </div>
+ );
+ }
+}
+
+/**
+ * Return whether a block overlaps with either edge of the `SelectionState`.
+ */
+function isBlockOnSelectionEdge(
+ selection: SelectionState,
+ key: string
+): boolean {
+ return (
+ selection.getAnchorKey() === key ||
+ selection.getFocusKey() === key
+ );
+}
+
+module.exports = DraftEditorBlock;
232 src/component/contents/DraftEditorContents.react.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorContents.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DraftEditorBlock = require('DraftEditorBlock.react');
+const DraftOffsetKey = require('DraftOffsetKey');
+const EditorState = require('EditorState');
+const React = require('React');
+
+const cx = require('cx');
+const getElementForBlockType = require('getElementForBlockType');
+const getWrapperTemplateForBlockType = require('getWrapperTemplateForBlockType');
+const joinClasses = require('joinClasses');
+const nullthrows = require('nullthrows');
+
+import type {BidiDirection} from 'UnicodeBidiDirection';
+import type ContentBlock from 'ContentBlock';
+
+type Props = {
+ blockRendererFn: Function,
+ blockStyleFn: (block: ContentBlock) => string,
+ editorState: EditorState,
+};
+
+/**
+ * `DraftEditorContents` is the container component for all block components
+ * rendered for a `DraftEditor`. It is optimized to aggressively avoid
+ * re-rendering blocks whenever possible.
+ *
+ * This component is separate from `DraftEditor` because certain props
+ * (for instance, ARIA props) must be allowed to update without affecting
+ * the contents of the editor.
+ */
+class DraftEditorContents extends React.Component {
+ shouldComponentUpdate(nextProps: Props): boolean {
+ const prevEditorState = this.props.editorState;
+ const nextEditorState = nextProps.editorState;
+
+ const prevDirectionMap = prevEditorState.getDirectionMap();
+ const nextDirectionMap = nextEditorState.getDirectionMap();
+
+ // Text direction has changed for one or more blocks. We must re-render.
+ if (prevDirectionMap !== nextDirectionMap) {
+ return true;
+ }
+
+ const didHaveFocus = prevEditorState.getSelection().getHasFocus();
+ const nowHasFocus = nextEditorState.getSelection().getHasFocus();
+
+ if (didHaveFocus !== nowHasFocus) {
+ return true;
+ }
+
+ const nextNativeContent = nextEditorState.getNativelyRenderedContent();
+
+ const wasComposing = prevEditorState.isInCompositionMode();
+ const nowComposing = nextEditorState.isInCompositionMode();
+
+ // If the state is unchanged or we're currently rendering a natively
+ // rendered state, there's nothing new to be done.
+ if (
+ prevEditorState === nextEditorState ||
+ (
+ nextNativeContent !== null &&
+ nextEditorState.getCurrentContent() === nextNativeContent
+ ) ||
+ (wasComposing && nowComposing)
+ ) {
+ return false;
+ }
+
+ const prevContent = prevEditorState.getCurrentContent();
+ const nextContent = nextEditorState.getCurrentContent();
+ const prevDecorator = prevEditorState.getDecorator();
+ const nextDecorator = nextEditorState.getDecorator();
+ return (
+ wasComposing !== nowComposing ||
+ prevContent !== nextContent ||
+ prevDecorator !== nextDecorator ||
+ nextEditorState.mustForceSelection()
+ );
+ }
+
+ render(): ReactElement {
+ const {blockRendererFn, customStyleMap, editorState} = this.props;
+ const content = editorState.getCurrentContent();
+ const selection = editorState.getSelection();
+ const forceSelection = editorState.mustForceSelection();
+ const decorator = editorState.getDecorator();
+ const directionMap = nullthrows(editorState.getDirectionMap());
+
+ const blocksAsArray = content.getBlocksAsArray();
+ const blocks = [];
+ let currentWrapperElement = null;
+ let currentWrapperTemplate = null;
+ let currentDepth = null;
+ let currentWrappedBlocks;
+ let block, key, blockType, child, wrapperTemplate;
+
+ for (let ii = 0; ii < blocksAsArray.length; ii++) {
+ block = blocksAsArray[ii];
+ key = block.getKey();
+ blockType = block.getType();
+
+ const customRenderer = blockRendererFn(block);
+ let CustomComponent, customProps;
+ if (customRenderer) {
+ CustomComponent = customRenderer.component;
+ customProps = customRenderer.props;
+ }
+
+ const direction = directionMap.get(key);
+ const offsetKey = DraftOffsetKey.encode(key, 0, 0);
+ const componentProps = {
+ block,
+ blockProps: customProps,
+ customStyleMap,
+ decorator,
+ direction,
+ forceSelection,
+ key,
+ offsetKey,
+ selection,
+ tree: editorState.getBlockTree(key),
+ };
+
+ wrapperTemplate = getWrapperTemplateForBlockType(blockType);
+ const useNewWrapper = wrapperTemplate !== currentWrapperTemplate;
+
+ if (CustomComponent) {
+ child = <CustomComponent {...componentProps} />;
+ } else {
+ const Element = getElementForBlockType(blockType);
+ const depth = block.getDepth();
+ let className = this.props.blockStyleFn(block);
+
+ // List items are special snowflakes, since we handle nesting and
+ // counters manually.
+ if (Element === 'li') {
+ const shouldResetCount = (
+ useNewWrapper ||
+ currentDepth === null ||
+ depth > currentDepth
+ );
+ className = joinClasses(
+ className,
+ getListItemClasses(blockType, depth, shouldResetCount, direction)
+ );
+ }
+
+ /* $FlowFixMe - Support DOM elements in React.createElement */
+ child = React.createElement(
+ Element,
+ {
+ className,
+ 'data-block': true,
+ 'data-offset-key': offsetKey,
+ key,
+ },
+ <DraftEditorBlock {...componentProps} />,
+ );
+ }
+
+ if (wrapperTemplate) {
+ if (useNewWrapper) {
+ currentWrappedBlocks = [];
+ currentWrapperElement = React.cloneElement(
+ wrapperTemplate,
+ {
+ key: key + '-wrap',
+ 'data-offset-key': offsetKey,
+ },
+ currentWrappedBlocks
+ );
+ currentWrapperTemplate = wrapperTemplate;
+ blocks.push(currentWrapperElement);
+ }
+ currentDepth = block.getDepth();
+ nullthrows(currentWrappedBlocks).push(child);
+ } else {
+ currentWrappedBlocks = null;
+ currentWrapperElement = null;
+ currentWrapperTemplate = null;
+ currentDepth = null;
+ blocks.push(child);
+ }
+ }
+
+ return <div data-contents="true">{blocks}</div>;
+ }
+}
+
+/**
+ * Provide default styling for list items. This way, lists will be styled with
+ * proper counters and indentation even if the caller does not specify
+ * their own styling at all. If more than five levels of nesting are needed,
+ * the necessary CSS classes can be provided via `blockStyleFn` configuration.
+ */
+function getListItemClasses(
+ type: string,
+ depth: number,
+ shouldResetCount: boolean,
+ direction: BidiDirection
+): string {
+ return cx({
+ 'public/DraftStyleDefault/unorderedListItem':
+ type === 'unordered-list-item',
+ 'public/DraftStyleDefault/orderedListItem':
+ type === 'ordered-list-item',
+ 'public/DraftStyleDefault/reset': shouldResetCount,
+ 'public/DraftStyleDefault/depth0': depth === 0,
+ 'public/DraftStyleDefault/depth1': depth === 1,
+ 'public/DraftStyleDefault/depth2': depth === 2,
+ 'public/DraftStyleDefault/depth3': depth === 3,
+ 'public/DraftStyleDefault/depth4': depth === 4,
+ 'public/DraftStyleDefault/listLTR': direction === 'LTR',
+ 'public/DraftStyleDefault/listRTL': direction === 'RTL',
+ });
+}
+
+module.exports = DraftEditorContents;
158 src/component/contents/DraftEditorLeaf.react.js
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorLeaf.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftEditorTextNode = require('DraftEditorTextNode.react');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var SelectionState = require('SelectionState');
+
+var setDraftEditorSelection = require('setDraftEditorSelection');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+type Props = {
+ // A function passed through from the the top level to define a cx
+ // style map for the provided style value.
+ blockKey: string,
+
+ // Mapping of style names to CSS declarations.
+ customStyleMap: Object,
+
+ // Whether to force the DOM selection after render.
+ forceSelection: boolean,
+
+ // Whether this leaf is the last in its block. Used for a DOM hack.
+ isLast: boolean,
+
+ offsetKey: string,
+
+ // The current `SelectionState`, used to
+ selection: SelectionState,
+
+ // The offset of this string within its block.
+ start: number,
+
+ // The set of style(s) names to apply to the node.
+ styleSet: DraftInlineStyle,
+
+ // The full text to be rendered within this node.
+ text: string,
+};
+
+/**
+ * All leaf nodes in the editor are spans with single text nodes. Leaf
+ * elements are styled based on the merging of an optional custom style map
+ * and a default style map.
+ *
+ * `DraftEditorLeaf` also provides a wrapper for calling into the imperative
+ * DOM Selection API. In this way, top-level components can declaratively
+ * maintain the selection state.
+ */
+class DraftEditorLeaf extends React.Component {
+ /**
+ * By making individual leaf instances aware of their context within
+ * the text of the editor, we can set our selection range more
+ * easily than we could in the non-React world.
+ *
+ * Note that this depends on our maintaining tight control over the
+ * DOM structure of the TextEditor component. If leaves had multiple
+ * text nodes, this would be harder.
+ */
+ _setSelection(): void {
+ const {selection} = this.props;
+
+ // If selection state is irrelevant to the parent block, no-op.
+ if (selection == null || !selection.getHasFocus()) {
+ return;
+ }
+
+ const {blockKey, start, text} = this.props;
+ const end = start + text.length;
+ if (!selection.hasEdgeWithin(blockKey, start, end)) {
+ return;
+ }
+
+ // Determine the appropriate target node for selection. If the child
+ // is not a text node, it is a <br /> spacer. In this case, use the
+ // <span> itself as the selection target.
+ const node = ReactDOM.findDOMNode(this);
+ const child = node.firstChild;
+ let targetNode;
+
+ if (child.nodeType === Node.TEXT_NODE) {
+ targetNode = child;
+ } else if (child.tagName === 'BR') {
+ targetNode = node;
+ } else {
+ targetNode = child.firstChild;
+ }
+
+ setDraftEditorSelection(selection, targetNode, blockKey, start, end);
+ }
+
+ shouldComponentUpdate(nextProps: Props): boolean {
+ return (
+ ReactDOM.findDOMNode(this.refs.leaf).textContent !== nextProps.text ||
@sstur
sstur added a note

Wouldn't this be the same as ReactDOM.findDOMNode(this)? Why the ref?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ nextProps.forceSelection
+ );
+ }
+
+ componentDidUpdate(): void {
+ this._setSelection();
+ }
+
+ componentDidMount(): void {
+ this._setSelection();
+ }
+
+ render(): ReactElement {
+ let {text} = this.props;
+
+ // If the leaf is at the end of its block and ends in a soft newline, append
+ // an extra line feed character. Browsers collapse trailing newline
+ // characters, which leaves the cursor in the wrong place after a
+ // shift+enter. The extra character repairs this.
+ if (text.endsWith('\n') && this.props.isLast) {
+ text += '\n';
+ }
+
+ const {customStyleMap, offsetKey, styleSet} = this.props;
+ const styleObj = styleSet.reduce((map, styleName) => {
+ const mergedStyles = {};
+ const style = customStyleMap[styleName];
+
+ if (
+ style !== undefined &&
+ map.textDecoration !== style.textDecoration
+ ) {
+ mergedStyles.textDecoration =
+ [map.textDecoration, style.textDecoration].join(' ');
+ }
+
+ return Object.assign(map, style, mergedStyles);
+ }, {});
+
+ return (
+ <span
+ data-offset-key={offsetKey}
+ ref="leaf"
+ style={styleObj}>
+ <DraftEditorTextNode>{text}</DraftEditorTextNode>
+ </span>
+ );
+ }
+}
+
+module.exports = DraftEditorLeaf;
96 src/component/contents/DraftEditorTextNode.react.js
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorTextNode.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const UserAgent = require('UserAgent');
+
+// In IE, spans with <br> tags render as two newlines. By rendering a span
+// with only a newline character, we can be sure to render a single line.
+const useNewlineChar = UserAgent.isBrowser('IE <= 11');
+
+/**
+ * Check whether the node should be considered a newline.
+ */
+function isNewline(node: Element): boolean {
+ return useNewlineChar ? node.textContent === '\n' : node.tagName === 'BR';
+}
+
+/**
+ * Placeholder elements for empty text content.
+ *
+ * What is this `data-text` attribute, anyway? It turns out that we need to
+ * put an attribute on the lowest-level text node in order to preserve correct
+ * spellcheck handling. If the <span> is naked, Chrome and Safari may do
+ * bizarre things to do the DOM -- split text nodes, create extra spans, etc.
+ * If the <span> has an attribute, this appears not to happen.
+ * See http://jsfiddle.net/9khdavod/ for the failure case, and
+ * http://jsfiddle.net/7pg143f7/ for the fixed case.
+ */
+const NEWLINE_A = useNewlineChar ?
+ <span key="A" data-text="true">{'\n'}</span> :
+ <br key="A" data-text="true" />;
+
+const NEWLINE_B = useNewlineChar ?
+ <span key="B" data-text="true">{'\n'}</span> :
+ <br key="B" data-text="true" />;
+
+type Props = {
+ children: string,
+};
+
+/**
+ * The lowest-level component in a `DraftEditor`, the text node component
+ * replaces the default React text node implementation. This allows us to
+ * perform custom handling of newline behavior and avoid re-rendering text
+ * nodes with DOM state that already matches the expectations of our immutable
+ * editor state.
+ */
+class DraftEditorTextNode extends React.Component {
+ _forceFlag: boolean;
+
+ constructor(props: Props) {
+ super(props);
+ this._forceFlag = false;
+ }
+
+ shouldComponentUpdate(nextProps: Props): boolean {
+ const node = ReactDOM.findDOMNode(this);
+ const shouldBeNewline = nextProps.children === '';
+ if (shouldBeNewline) {
+ return !isNewline(node);
+ }
+ return node.textContent !== nextProps.children;
+ }
+
+ componentWillUpdate(): void {
+ // By flipping this flag, we also keep flipping keys which forces
+ // React to remount this node every time it rerenders.
+ this._forceFlag = !this._forceFlag;
+ }
+
+ render(): ReactElement {
+ if (this.props.children === '') {
+ return this._forceFlag ? NEWLINE_A : NEWLINE_B;
+ }
+ return (
+ <span key={this._forceFlag ? 'A' : 'B'} data-text="true">
+ {this.props.children}
+ </span>
+ );
+ }
+}
+
+module.exports = DraftEditorTextNode;
555 src/component/contents/__tests__/DraftEditorBlock.react-test.js
@@ -0,0 +1,555 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ * @typechecks
+ */
+
+'use strict';
+
+jest.autoMockOff()
+ .mock('Style')
+ .mock('getElementPosition')
+ .mock('getScrollPosition')
+ .mock('getViewportDimensions');
+
+var BlockTree = require('BlockTree');
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var ReactTestUtils = require('ReactTestUtils');
+var SampleDraftInlineStyle = require('SampleDraftInlineStyle');
+var SelectionState = require('SelectionState');
+var Style = require('Style');
+var UnicodeBidiDirection = require('UnicodeBidiDirection');
+
+var getElementPosition = require('getElementPosition');
+var getScrollPosition = require('getScrollPosition');
+var getViewportDimensions = require('getViewportDimensions');
+var reactComponentExpect = require('reactComponentExpect');
+var {BOLD, NONE, ITALIC} = SampleDraftInlineStyle;
+
+var mockGetDecorations = jest.genMockFn();
+
+var DecoratorSpan = React.createClass({
+ render() {
+ return <span>{this.props.children}</span>;
+ },
+});
+
+var DraftEditorBlock = require('DraftEditorBlock.react');
+
+// Define a class to satisfy typechecks.
+class Decorator {
+ getDecorations() {
+ return mockGetDecorations();
+ }
+ getComponentForKey() {
+ return DecoratorSpan;
+ }
+ getPropsForKey() {
+ return {};
+ }
+}
+
+var mockLeafRender = jest.genMockFn().mockReturnValue(<span />);
+jest.setMock(
+ 'DraftEditorLeaf.react',
+ React.createClass({
+ render: function() {
+ return mockLeafRender();
+ },
+ })
+);
+
+var DraftEditorLeaf = require('DraftEditorLeaf.react');
+
+function returnEmptyString() {
+ return '';
+}
+
+function getHelloBlock() {
+ return new ContentBlock({
+ key: 'a',
+ type: 'unstyled',
+ text: 'hello',
+ characterList: Immutable.List(
+ Immutable.Repeat(CharacterMetadata.EMPTY, 5)
+ ),
+ });
+}
+
+function getSelection() {
+ return new SelectionState({
+ anchorKey: 'a',
+ anchorOffset: 0,
+ focusKey: 'a',
+ focusOffset: 0,
+ isBackward: false,
+ hasFocus: true,
+ });
+}
+
+function getProps(block, decorator) {
+ return {
+ block,
+ tree: BlockTree.generate(block, decorator),
+ selection: getSelection(),
+ decorator: decorator || null,
+ forceSelection: false,
+ direction: UnicodeBidiDirection.LTR,
+ blockStyleFn: returnEmptyString,
+ styleSet: NONE,
+ };
+}
+
+function arePropsEqual(renderedChild, leafPropSet) {
+ Object.keys(leafPropSet).forEach(key => {
+ expect(
+ Immutable.is(leafPropSet[key], renderedChild.instance().props[key])
+ ).toBeTruthy();
+ });
+}
+
+function assertLeaves(renderedBlock, leafProps) {
+ leafProps.forEach((leafPropSet, ii) => {
+ const child = renderedBlock.expectRenderedChildAt(ii);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, leafPropSet);
+ });
+}
+
+describe('DraftEditorBlock.react', () => {
+ Style.getScrollParent.mockReturnValue(window);
+ window.scrollTo = jest.genMockFn();
+ getElementPosition.mockReturnValue({
+ x: 0,
+ y: 600,
+ width: 500,
+ height: 16,
+ });
+ getScrollPosition.mockReturnValue({x: 0, y: 0});
+ getViewportDimensions.mockReturnValue({width: 1200, height: 800});
+
+ beforeEach(() => {
+ window.scrollTo.mockClear();
+ mockGetDecorations.mockClear();
+ mockLeafRender.mockClear();
+ });
+
+ describe('Basic rendering', () => {
+ it('must render a leaf node', () => {
+ var props = getProps(getHelloBlock());
+ var block = ReactTestUtils.renderIntoDocument(
+ <DraftEditorBlock {...props} />
+ );
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ assertLeaves(rendered, [
+ {
+ text: 'hello',
+ offsetKey: 'a-0-0',
+ start: 0,
+ styleSet: NONE,
+ isLast: true,
+ },
+ ]);
+ });
+
+ it('must render multiple leaf nodes', () => {
+ var boldLength = 2;
+ var helloBlock = getHelloBlock();
+ var characters = helloBlock.getCharacterList();
+ characters = characters
+ .slice(0, boldLength)
+ .map(c => CharacterMetadata.applyStyle(c, 'BOLD'))
+ .concat(characters.slice(boldLength));
+
+ helloBlock = helloBlock.set('characterList', characters.toList());
+
+ var props = getProps(helloBlock);
+ var block = ReactTestUtils.renderIntoDocument(
+ <DraftEditorBlock {...props} />
+ );
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ assertLeaves(rendered, [
+ {
+ text: 'he',
+ offsetKey: 'a-0-0',
+ start: 0,
+ styleSet: BOLD,
+ isLast: false,
+ },
+ {
+ text: 'llo',
+ offsetKey: 'a-0-1',
+ start: 2,
+ styleSet: NONE,
+ isLast: true,
+ },
+ ]);
+ });
+ });
+
+ describe('Allow/reject component updates', () => {
+ it('must allow update when `block` has changed', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ var updatedHelloBlock = helloBlock.set('text', 'hxllo');
+ var nextProps = getProps(updatedHelloBlock);
+
+ expect(updatedHelloBlock).not.toBe(helloBlock);
+ expect(props.block).not.toBe(nextProps.block);
+
+ ReactDOM.render(
+ <DraftEditorBlock {...nextProps} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+ });
+
+ it('must allow update when `tree` has changed', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ mockGetDecorations.mockReturnValue(
+ Immutable.List.of('x', 'x', null, null, null)
+ );
+ var decorator = new Decorator();
+
+ var newTree = BlockTree.generate(helloBlock, decorator);
+ var nextProps = {...props, tree: newTree, decorator};
+
+ expect(props.tree).not.toBe(nextProps.tree);
+
+ ReactDOM.render(
+ <DraftEditorBlock {...nextProps} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(3);
+ });
+
+ it('must allow update when `direction` has changed', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ var nextProps = {...props, direction: UnicodeBidiDirection.RTL};
+ expect(props.direction).not.toBe(nextProps.direction);
+
+ ReactDOM.render(
+ <DraftEditorBlock {...nextProps} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+ });
+
+ it('must allow update when forcing selection', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ // The default selection state in this test is on a selection edge.
+ var nextProps = {
+ ...props,
+ forceSelection: true,
+ };
+
+ ReactDOM.render(
+ <DraftEditorBlock {...nextProps} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+ });
+
+ it('must reject update if conditions are not met', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ // Render again with the exact same props as before.
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ // No new leaf renders.
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+ });
+
+ it('must reject update if selection is not on an edge', () => {
+ var helloBlock = getHelloBlock();
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+
+ // Move selection state to some other block.
+ var nonEdgeSelection = props.selection.merge({
+ anchorKey: 'z',
+ focusKey: 'z',
+ });
+
+ var newProps = {...props, selection: nonEdgeSelection};
+
+ // Render again with selection now moved elsewhere and the contents
+ // unchanged.
+ ReactDOM.render(
+ <DraftEditorBlock {...newProps} />,
+ container
+ );
+
+ // No new leaf renders.
+ expect(mockLeafRender.mock.calls.length).toBe(1);
+ });
+ });
+
+ describe('Complex rendering with a decorator', () => {
+ it('must split apart two decorated and undecorated', () => {
+ var helloBlock = getHelloBlock();
+
+ mockGetDecorations.mockReturnValue(
+ Immutable.List.of('x', 'x', null, null, null)
+ );
+ var decorator = new Decorator();
+ var props = getProps(helloBlock, decorator);
+
+ var container = document.createElement('div');
+ var block = ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ rendered
+ .expectRenderedChildAt(0)
+ .scalarPropsEqual({offsetKey: 'a-0-0'})
+ .toBeComponentOfType(DecoratorSpan)
+ .expectRenderedChild()
+ .toBeComponentOfType('span');
+
+ rendered
+ .expectRenderedChildAt(1)
+ .scalarPropsEqual({offsetKey: 'a-1-0'})
+ .toBeComponentOfType(DraftEditorLeaf);
+ });
+
+ it('must split apart two decorators', () => {
+ var helloBlock = getHelloBlock();
+
+ mockGetDecorations.mockReturnValue(
+ Immutable.List.of('x', 'x', 'y', 'y', 'y')
+ );
+
+ var decorator = new Decorator();
+ var props = getProps(helloBlock, decorator);
+
+ var container = document.createElement('div');
+ var block = ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ rendered
+ .expectRenderedChildAt(0)
+ .scalarPropsEqual({offsetKey: 'a-0-0'})
+ .toBeComponentOfType(DecoratorSpan);
+
+ rendered
+ .expectRenderedChildAt(1)
+ .scalarPropsEqual({offsetKey: 'a-1-0'})
+ .toBeComponentOfType(DecoratorSpan);
+ });
+ });
+
+ describe('Complex rendering with inline styles', () => {
+ it('must split apart styled spans', () => {
+ var helloBlock = getHelloBlock();
+ var characters = helloBlock.getCharacterList();
+ var newChars = characters.slice(0, 2).map(ch => {
+ return CharacterMetadata.applyStyle(ch, 'BOLD');
+ }).concat(characters.slice(2));
+
+ helloBlock = helloBlock.set('characterList', Immutable.List(newChars));
+ var props = getProps(helloBlock);
+
+ var container = document.createElement('div');
+ var block = ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(2);
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ let child = rendered.expectRenderedChildAt(0);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, {offsetKey: 'a-0-0', styleSet: BOLD});
+
+ child = rendered.expectRenderedChildAt(1);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, {offsetKey: 'a-0-1', styleSet: NONE});
+ });
+
+ it('must split styled spans apart within decorator', () => {
+ var helloBlock = getHelloBlock();
+ var characters = helloBlock.getCharacterList();
+ var newChars = Immutable.List([
+ CharacterMetadata.applyStyle(characters.get(0), 'BOLD'),
+ CharacterMetadata.applyStyle(characters.get(1), 'ITALIC'),
+ ]).concat(characters.slice(2));
+
+ helloBlock = helloBlock.set('characterList', Immutable.List(newChars));
+
+ mockGetDecorations.mockReturnValue(
+ Immutable.List.of('x', 'x', null, null, null)
+ );
+ var decorator = new Decorator();
+ var props = getProps(helloBlock, decorator);
+
+ var container = document.createElement('div');
+ var block = ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ expect(mockLeafRender.mock.calls.length).toBe(3);
+
+ var rendered = reactComponentExpect(block)
+ .expectRenderedChild()
+ .toBeComponentOfType('div');
+
+ var decoratedSpan = rendered
+ .expectRenderedChildAt(0)
+ .scalarPropsEqual({offsetKey: 'a-0-0'})
+ .toBeComponentOfType(DecoratorSpan)
+ .expectRenderedChild();
+
+ let child = decoratedSpan.expectRenderedChildAt(0);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, {offsetKey: 'a-0-0', styleSet: BOLD});
+
+ child = decoratedSpan.expectRenderedChildAt(1);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, {offsetKey: 'a-0-1', styleSet: ITALIC});
+
+ child = rendered.expectRenderedChildAt(1);
+ child.toBeComponentOfType(DraftEditorLeaf);
+ arePropsEqual(child, {offsetKey: 'a-1-0', styleSet: NONE});
+ });
+ });
+
+ describe('Scroll-to-cursor on mount', () => {
+ var props = getProps(getHelloBlock());
+
+ describe('Scroll parent is `window`', () => {
+ it('must scroll the window if needed', () => {
+ getElementPosition.mockReturnValueOnce({
+ x: 0,
+ y: 800,
+ width: 500,
+ height: 16,
+ });
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ var scrollCalls = window.scrollTo.mock.calls;
+ expect(scrollCalls.length).toBe(1);
+ expect(scrollCalls[0][0]).toBe(0);
+
+ // (current scroll position) + (block height) + (buffer)
+ expect(scrollCalls[0][1]).toBe(26);
+ });
+
+ it('must not scroll the window if unnecessary', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+ <DraftEditorBlock {...props} />,
+ container
+ );
+
+ var scrollCalls = window.scrollTo.mock.calls;
+ expect(scrollCalls.length).toBe(0);
+ });
+ });
+ });
+});
212 src/component/contents/__tests__/DraftEditorTextNode-test.js
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.dontMock('DraftEditorTextNode.react');
+
+var BLOCK_DELIMITER_CHAR = '\n';
+var TEST_A = 'Hello';
+var TEST_B = ' World!';
+
+var DraftEditorTextNode = require('DraftEditorTextNode.react');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var UserAgent = require('UserAgent');
+
+describe('DraftEditorTextNode', function() {
+ var container;
+
+ beforeEach(function() {
+ jest.resetModuleRegistry();
+ container = document.createElement('div');
+ });
+
+ function renderIntoContainer(element) {
+ return ReactDOM.render(element, container);
+ }
+
+ function initializeAsIE() {
+ UserAgent.isBrowser.mockImplementation(() => true);
+ }
+
+ function initializeAsNonIE() {
+ UserAgent.isBrowser.mockImplementation(() => false);
+ }
+
+ function expectPopulatedSpan(stub, testString) {
+ var node = ReactDOM.findDOMNode(stub);
+ expect(node.tagName).toBe('SPAN');
+ expect(node.childNodes.length).toBe(1);
+ expect(node.firstChild.textContent).toBe(testString);
+ }
+
+ it('must initialize correctly with an empty string, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{''}</DraftEditorTextNode>
+ );
+ expect(ReactDOM.findDOMNode(stub).tagName).toBe('BR');
+ });
+
+ it('must initialize correctly with an empty string, IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{''}</DraftEditorTextNode>
+ );
+ expectPopulatedSpan(stub, BLOCK_DELIMITER_CHAR);
+ });
+
+ it('must initialize correctly with a string, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+ expectPopulatedSpan(stub, TEST_A);
+ });
+
+ it('must initialize correctly with a string, IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+ expectPopulatedSpan(stub, TEST_A);
+ });
+
+ it('must update from empty to non-empty, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{''}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+ expectPopulatedSpan(stub, TEST_A);
+ });
+
+ it('must update from empty to non-empty, IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{''}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+ expectPopulatedSpan(stub, TEST_A);
+ });
+
+ it('must update from non-empty to non-empty, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A + TEST_B}</DraftEditorTextNode>
+ );
+
+ expectPopulatedSpan(stub, TEST_A + TEST_B);
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+ expectPopulatedSpan(stub, TEST_B);
+ });
+
+ it('must update from non-empty to non-empty, non-IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A + TEST_B}</DraftEditorTextNode>
+ );
+ expectPopulatedSpan(stub, TEST_A + TEST_B);
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+ expectPopulatedSpan(stub, TEST_B);
+ });
+
+ it('must skip updates if text already matches DOM, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ spyOn(stub, 'render').and.callThrough();
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+ expect(stub.render.calls.count()).toBe(0);
+
+ // Sanity check that updating is performed when appropriate.
+ renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+
+ expect(stub.render.calls.count()).toBe(1);
+ });
+
+ it('must skip updates if text already matches DOM, IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ spyOn(stub, 'render').and.callThrough();
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+ expect(stub.render.calls.count()).toBe(0);
+
+ // Sanity check that updating is performed when appropriate.
+ renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+
+ expect(stub.render.calls.count()).toBe(1);
+ });
+
+ it('must update from non-empty to empty, non-IE', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(<DraftEditorTextNode>{''}</DraftEditorTextNode>);
+
+ expect(ReactDOM.findDOMNode(stub).tagName).toBe('BR');
+ });
+
+ it('must update from non-empty to empty, IE', function() {
+ initializeAsIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ renderIntoContainer(<DraftEditorTextNode>{''}</DraftEditorTextNode>);
+
+ expectPopulatedSpan(stub, BLOCK_DELIMITER_CHAR);
+ });
+
+ it('must render properly into a parent DOM node', function() {
+ initializeAsNonIE();
+ renderIntoContainer(
+ <div><DraftEditorTextNode>{TEST_A}</DraftEditorTextNode></div>
+ );
+ });
+
+ it('must force unchanged text back into the DOM', function() {
+ initializeAsNonIE();
+ var stub = renderIntoContainer(
+ <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+ );
+
+ ReactDOM.findDOMNode(stub).textContent = TEST_B;
+
+ renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+ expect(ReactDOM.findDOMNode(stub).textContent).toBe(TEST_A);
+ });
+});
43 src/component/handlers/DraftEditorModes.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorModes
+ * @flow
+ */
+
+'use strict';
+
+export type DraftEditorModes = (
+ /**
+ * `edit` is the most common mode for text entry. This includes most typing,
+ * deletion, cut/copy/paste, and other behaviors.
+ */
+ 'edit' |
+
+ /**
+ * `composite` mode handles IME text entry.
+ */
+ 'composite' |
+
+ /**
+ * `drag` mode handles editor behavior while a drag event is occurring.
+ */
+ 'drag' |
+
+ /**
+ * `cut` mode allows us to effectively ignore all edit behaviors while the`
+ * browser performs a native `cut` operation on the DOM.
+ */
+ 'cut' |
+
+ /**
+ * `render` mode is the normal "null" mode, during which no edit behavior is
+ * expected or observed.
+ */
+ 'render'
+);
183 src/component/handlers/composition/DraftEditorCompositionHandler.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorCompositionHandler
+ * @flow
+ */
+
+'use strict';
+
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+const Keys = require('Keys');
+
+const getEntityKeyForSelection = require('getEntityKeyForSelection');
+const isSelectionAtLeafStart = require('isSelectionAtLeafStart');
+
+/**
+ * Millisecond delay to allow `compositionstart` to fire again upon
+ * `compositionend`.
+ *
+ * This is used for Korean input to ensure that typing can continue without
+ * the editor trying to render too quickly. More specifically, Safari 7.1+
+ * triggers `compositionstart` a little slower than Chrome/FF, which
+ * leads to composed characters being resolved and re-render occurring
+ * sooner than we want.
+ */
+const RESOLVE_DELAY = 20;
+
+/**
+ * A handful of variables used to track the current composition and its
+ * resolution status. These exist at the module level because it is not
+ * possible to have compositions occurring in multiple editors simultaneously,
+ * and it simplifies state management with respect to the DraftEditor component.
+ */
+let resolved = false;
+let stillComposing = false;
+let textInputData = '';
+
+var DraftEditorCompositionHandler = {
+ onBeforeInput: function(e: SyntheticInputEvent): void {
+ textInputData = (textInputData || '') + e.data;
+ },
+
+ /**
+ * A `compositionstart` event has fired while we're still in composition
+ * mode. Continue the current composition session to prevent a re-render.
+ */
+ onCompositionStart: function(): void {
+ stillComposing = true;
+ },
+
+ /**
+ * Attempt to end the current composition session.
+ *
+ * Defer handling because browser will still insert the chars into active
+ * element after `compositionend`. If a `compositionstart` event fires
+ * before `resolveComposition` executes, our composition session will
+ * continue.
+ *
+ * The `resolved` flag is useful because certain IME interfaces fire the
+ * `compositionend` event multiple times, thus queueing up multiple attempts
+ * at handling the composition. Since handling the same composition event
+ * twice could break the DOM, we only use the first event. Example: Arabic
+ * Google Input Tools on Windows 8.1 fires `compositionend` three times.
+ */
+ onCompositionEnd: function(): void {
+ resolved = false;
+ stillComposing = false;
+ setTimeout(() => {
+ if (!resolved) {
+ DraftEditorCompositionHandler.resolveComposition.call(this);
+ }
+ }, RESOLVE_DELAY);
+ },
+
+ /**
+ * In Safari, keydown events may fire when committing compositions. If
+ * the arrow keys are used to commit, prevent default so that the cursor
+ * doesn't move, otherwise it will jump back noticeably on re-render.
+ */
+ onKeyDown: function(e: SyntheticKeyboardEvent): void {
+ if (e.which === Keys.RIGHT || e.which === Keys.LEFT) {
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Keypress events may fire when committing compositions. In Firefox,
+ * pressing RETURN commits the composition and inserts extra newline
+ * characters that we do not want. `preventDefault` allows the composition
+ * to be committed while preventing the extra characters.
+ */
+ onKeyPress: function(e: SyntheticKeyboardEvent): void {
+ if (e.which === Keys.RETURN) {
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Attempt to insert composed characters into the document.
+ *
+ * If we are still in a composition session, do nothing. Otherwise, insert
+ * the characters into the document and terminate the composition session.
+ *
+ * If no characters were composed -- for instance, the user
+ * deleted all composed characters and committed nothing new --
+ * force a re-render. We also re-render when the composition occurs
+ * at the beginning of a leaf, to ensure that if the browser has
+ * created a new text node for the composition, we will discard it.
+ *
+ * Resetting innerHTML will move focus to the beginning of the editor,
+ * so we update to force it back to the correct place.
+ */
+ resolveComposition: function(): void {
+ if (stillComposing) {
+ return;
+ }
+
+ resolved = true;
+ const composedChars = textInputData;
+ textInputData = '';
+
+ const editorState = EditorState.set(this.props.editorState, {
+ inCompositionMode: false,
+ });
+
+ const currentStyle = editorState.getCurrentInlineStyle();
+ const entityKey = getEntityKeyForSelection(
+ editorState.getCurrentContent(),
+ editorState.getSelection()
+ );
+
+ const mustReset = (
+ !composedChars ||
+ isSelectionAtLeafStart(editorState) ||
+ currentStyle.size > 0 ||
+ entityKey !== null
+ );
+
+ if (mustReset) {
+ this.restoreEditorDOM();
+ }
+
+ this.exitCurrentMode();
+ this.removeRenderGuard();
+
+ if (composedChars) {
+ // If characters have been composed, re-rendering with the update
+ // is sufficient to reset the editor.
+ const contentState = DraftModifier.replaceText(
+ editorState.getCurrentContent(),
+ editorState.getSelection(),
+ composedChars,
+ currentStyle,
+ entityKey
+ );
+ this.update(
+ EditorState.push(
+ editorState,
+ contentState,
+ 'insert-characters'
+ )
+ );
+ return;
+ }
+
+ if (mustReset) {
+ this.update(
+ EditorState.set(editorState, {
+ nativelyRenderedContent: null,
+ forceSelection: true,
+ })
+ );
+ }
+ },
+};
+
+module.exports = DraftEditorCompositionHandler;
156 src/component/handlers/drag/DraftEditorDragHandler.js
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorDragHandler
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DataTransfer = require('DataTransfer');
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+
+const findAncestorOffsetKey = require('findAncestorOffsetKey');
+const getTextContentFromFiles = require('getTextContentFromFiles');
+const getUpdatedSelectionState = require('getUpdatedSelectionState');
+const nullthrows = require('nullthrows');
+
+import type SelectionState from 'SelectionState';
+
+/**
+ * Get a SelectionState for the supplied mouse event.
+ */
+function getSelectionForEvent(
+ event: Object,
+ editorState: EditorState
+): ?SelectionState {
+ let node: ?Node = null;
+ let offset: ?number = null;
+
+ if (document.caretRangeFromPoint) {
+ var dropRange = document.caretRangeFromPoint(event.x, event.y);
+ node = dropRange.startContainer;
+ offset = dropRange.startOffset;
+ } else if (event.rangeParent) {
+ node = event.rangeParent;
+ offset = event.rangeOffset;
+ } else {
+ return null;
+ }
+
+ node = nullthrows(node);
+ offset = nullthrows(offset);
+ const offsetKey = nullthrows(findAncestorOffsetKey(node));
+
+ return getUpdatedSelectionState(
+ editorState,
+ offsetKey,
+ offset,
+ offsetKey,
+ offset
+ );
+}
+
+var DraftEditorDragHandler = {
+ /**
+ * Drag originating from input terminated.
+ */
+ onDragEnd: function(): void {
+ this.exitCurrentMode();
+ },
+
+ /**
+ * Handle data being dropped.
+ */
+ onDrop: function(e: Object): void {
+ const data = new DataTransfer(e.nativeEvent.dataTransfer);
+
+ const editorState: EditorState = this.props.editorState;
+ const dropSelection: ?SelectionState = getSelectionForEvent(
+ e.nativeEvent,
+ editorState
+ );
+
+ e.preventDefault();
+ this.exitCurrentMode();
+
+ if (dropSelection == null) {
+ return;
+ }
+
+ const files = data.getFiles();
+ if (files.length > 0) {
+ if (this.props.handleDroppedFiles &&
+ this.props.handleDroppedFiles(dropSelection, files)) {
+ return;
+ }
+
+ getTextContentFromFiles(files, fileText => {
+ fileText && this.update(
+ insertTextAtSelection(
+ editorState,
+ nullthrows(dropSelection), // flow wtf
+ fileText
+ )
+ );
+ });
+ return;
+ }
+
+ if (this._internalDrag) {
+ this.update(moveText(editorState, dropSelection));
+ return;
+ }
+
+ this.update(
+ insertTextAtSelection(editorState, dropSelection, data.getText())
+ );
+ },
+
+};
+
+function moveText(
+ editorState: EditorState,
+ targetSelection: SelectionState
+): EditorState {
+ const newContentState = DraftModifier.moveText(
+ editorState.getCurrentContent(),
+ editorState.getSelection(),
+ targetSelection
+ );
+ return EditorState.push(
+ editorState,
+ newContentState,
+ 'insert-fragment'
+ );
+}
+
+/**
+ * Insert text at a specified selection.
+ */
+function insertTextAtSelection(
+ editorState: EditorState,
+ selection: SelectionState,
+ text: string
+): EditorState {
+ const newContentState = DraftModifier.insertText(
+ editorState.getCurrentContent(),
+ selection,
+ text,
+ editorState.getCurrentInlineStyle()
+ );
+ return EditorState.push(
+ editorState,
+ newContentState,
+ 'insert-fragment'
+ );
+}
+
+module.exports = DraftEditorDragHandler;
43 src/component/handlers/edit/DraftEditorEditHandler.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorEditHandler
+ * @flow
+ */
+
+'use strict';
+
+const onBeforeInput = require('editOnBeforeInput');
+const onBlur = require('editOnBlur');
+const onCompositionStart = require('editOnCompositionStart');
+const onCopy = require('editOnCopy');
+const onCut = require('editOnCut');
+const onDragOver = require('editOnDragOver');
+const onDragStart = require('editOnDragStart');
+const onFocus = require('editOnFocus');
+const onInput = require('editOnInput');
+const onKeyDown = require('editOnKeyDown');
+const onPaste = require('editOnPaste');
+const onSelect = require('editOnSelect');
+
+const DraftEditorEditHandler = {
+ onBeforeInput,
+ onBlur,
+ onCompositionStart,
+ onCopy,
+ onCut,
+ onDragOver,
+ onDragStart,
+ onFocus,
+ onInput,
+ onKeyDown,
+ onPaste,
+ onSelect,
+};
+
+module.exports = DraftEditorEditHandler;
80 src/component/handlers/edit/commands/SecondaryClipboard.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SecondaryClipboard
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+var getContentStateFragment = require('getContentStateFragment');
+var nullthrows = require('nullthrows');
+
+import type {BlockMap} from 'BlockMap';
+import type SelectionState from 'SelectionState';
+
+var clipboard: ?BlockMap = null;
+
+/**
+ * Some systems offer a "secondary" clipboard to allow quick internal cut
+ * and paste behavior. For instance, Ctrl+K (cut) and Ctrl+Y (paste).
+ */
+var SecondaryClipboard = {
+ cut: function(editorState: EditorState): EditorState {
+ var content = editorState.getCurrentContent();
+ var selection = editorState.getSelection();
+ var targetRange: ?SelectionState = null;
+
+ if (selection.isCollapsed()) {
+ var anchorKey = selection.getAnchorKey();
+ var blockEnd = content.getBlockForKey(anchorKey).getLength();
+
+ if (blockEnd === selection.getAnchorOffset()) {
+ return editorState;
+ }
+
+ targetRange = selection.set('focusOffset', blockEnd);
+ } else {
+ targetRange = selection;
+ }
+
+ targetRange = nullthrows(targetRange);
+ clipboard = getContentStateFragment(content, targetRange);
+
+ var afterRemoval = DraftModifier.removeRange(
+ content,
+ targetRange,
+ 'forward'
+ );
+
+ if (afterRemoval === content) {
+ return editorState;
+ }
+
+ return EditorState.push(editorState, afterRemoval, 'remove-range');
+ },
+
+ paste: function(editorState: EditorState): EditorState {
+ if (!clipboard) {
+ return editorState;
+ }
+
+ var newContent = DraftModifier.replaceWithFragment(
+ editorState.getCurrentContent(),
+ editorState.getSelection(),
+ clipboard
+ );
+
+ return EditorState.push(editorState, newContent, 'insert-fragment');
+ },
+};
+
+module.exports = SecondaryClipboard;
55 src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandBackspaceToStartOfLine
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+var expandRangeToStartOfLine = require('expandRangeToStartOfLine');
+var getDraftEditorSelectionWithNodes = require('getDraftEditorSelectionWithNodes');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+function keyCommandBackspaceToStartOfLine(
+ editorState: EditorState
+): EditorState {
+ var afterRemoval = removeTextWithStrategy(
+ editorState,
+ strategyState => {
+ var domSelection = global.getSelection();
+ var range = domSelection.getRangeAt(0);
+ range = expandRangeToStartOfLine(range);
+
+ var selection = getDraftEditorSelectionWithNodes(
+ strategyState,
+ null,
+ range.endContainer,
+ range.endOffset,
+ range.startContainer,
+ range.startOffset
+ ).selectionState;
+ return selection;
+ },
+ 'backward'
+ );
+
+ if (afterRemoval === editorState.getCurrentContent()) {
+ return editorState;
+ }
+
+ return EditorState.push(
+ editorState,
+ afterRemoval,
+ 'remove-range'
+ );
+}
+
+module.exports = keyCommandBackspaceToStartOfLine;
54 src/component/handlers/edit/commands/keyCommandBackspaceWord.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandBackspaceWord
+ * @flow
+ */
+
+'use strict';
+
+var DraftRemovableWord = require('DraftRemovableWord');
+var EditorState = require('EditorState');
+
+var moveSelectionBackward = require('moveSelectionBackward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Delete the word that is left of the cursor, as well as any spaces or
+ * punctuation after the word.
+ */
+function keyCommandBackspaceWord(editorState: EditorState): EditorState {
+ var afterRemoval = removeTextWithStrategy(
+ editorState,
+ strategyState => {
+ var selection = strategyState.getSelection();
+ var offset = selection.getStartOffset();
+ // If there are no words before the cursor, remove the preceding newline.
+ if (offset === 0) {
+ return moveSelectionBackward(strategyState, 1);
+ }
+ var key = selection.getStartKey();
+ var content = strategyState.getCurrentContent();
+ var text = content.getBlockForKey(key).getText().slice(0, offset);
+ var toRemove = DraftRemovableWord.getBackward(text);
+ return moveSelectionBackward(
+ strategyState,
+ toRemove.length || 1
+ );
+ },
+ 'backward'
+ );
+
+ if (afterRemoval === editorState.getCurrentContent()) {
+ return editorState;
+ }
+
+ return EditorState.push(editorState, afterRemoval, 'remove-range');
+}
+
+module.exports = keyCommandBackspaceWord;
52 src/component/handlers/edit/commands/keyCommandDeleteWord.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandDeleteWord
+ * @flow
+ */
+
+'use strict';
+
+var DraftRemovableWord = require('DraftRemovableWord');
+var EditorState = require('EditorState');
+
+var moveSelectionForward = require('moveSelectionForward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Delete the word that is right of the cursor, as well as any spaces or
+ * punctuation before the word.
+ */
+function keyCommandDeleteWord(editorState: EditorState): EditorState {
+ var afterRemoval = removeTextWithStrategy(
+ editorState,
+ strategyState => {
+ var selection = strategyState.getSelection();
+ var offset = selection.getStartOffset();
+ var key = selection.getStartKey();
+ var content = strategyState.getCurrentContent();
+ var text = content.getBlockForKey(key).getText().slice(offset);
+ var toRemove = DraftRemovableWord.getForward(text);
+
+ // If there are no words in front of the cursor, remove the newline.
+ return moveSelectionForward(
+ strategyState,
+ toRemove.length || 1
+ );
+ },
+ 'forward'
+ );
+
+ if (afterRemoval === editorState.getCurrentContent()) {
+ return editorState;
+ }
+
+ return EditorState.push(editorState, afterRemoval, 'remove-range');
+}
+
+module.exports = keyCommandDeleteWord;
26 src/component/handlers/edit/commands/keyCommandInsertNewline.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandInsertNewline
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+function keyCommandInsertNewline(editorState: EditorState): EditorState {
+ var contentState = DraftModifier.splitBlock(
+ editorState.getCurrentContent(),
+ editorState.getSelection()
+ );
+ return EditorState.push(editorState, contentState, 'split-block');
+}
+
+module.exports = keyCommandInsertNewline;
39 src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandMoveSelectionToEndOfBlock
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * See comment for `moveSelectionToStartOfBlock`.
+ */
+function keyCommandMoveSelectionToEndOfBlock(
+ editorState: EditorState
+): EditorState {
+ var selection = editorState.getSelection();
+ var endKey = selection.getEndKey();
+ var content = editorState.getCurrentContent();
+ var textLength = content.getBlockForKey(endKey).getLength();
+ return EditorState.set(editorState, {
+ selection: selection.merge({
+ anchorKey: endKey,
+ anchorOffset: textLength,
+ focusKey: endKey,
+ focusOffset: textLength,
+ isBackward: false,
+ }),
+ forceSelection: true,
+ });
+}
+
+module.exports = keyCommandMoveSelectionToEndOfBlock;
39 src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandMoveSelectionToStartOfBlock
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * Collapse selection at the start of the first selected block. This is used
+ * for Firefox versions that attempt to navigate forward/backward instead of
+ * moving the cursor. Other browsers are able to move the cursor natively.
+ */
+function keyCommandMoveSelectionToStartOfBlock(
+ editorState: EditorState
+): EditorState {
+ var selection = editorState.getSelection();
+ var startKey = selection.getStartKey();
+ return EditorState.set(editorState, {
+ selection: selection.merge({
+ anchorKey: startKey,
+ anchorOffset: 0,
+ focusKey: startKey,
+ focusOffset: 0,
+ isBackward: false,
+ }),
+ forceSelection: true,
+ });
+}
+
+module.exports = keyCommandMoveSelectionToStartOfBlock;
55 src/component/handlers/edit/commands/keyCommandPlainBackspace.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandPlainBackspace
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UnicodeUtils = require('UnicodeUtils');
+
+var moveSelectionBackward = require('moveSelectionBackward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Remove the selected range. If the cursor is collapsed, remove the preceding
+ * character. This operation is Unicode-aware, so removing a single character
+ * will remove a surrogate pair properly as well.
+ */
+function keyCommandPlainBackspace(editorState: EditorState): EditorState {
+ var afterRemoval = removeTextWithStrategy(
+ editorState,
+ strategyState => {
+ var selection = strategyState.getSelection();
+ var content = strategyState.getCurrentContent();
+ var key = selection.getAnchorKey();
+ var offset = selection.getAnchorOffset();
+ var charBehind = content.getBlockForKey(key).getText()[offset - 1];
+ return moveSelectionBackward(
+ strategyState,
+ charBehind ? UnicodeUtils.getUTF16Length(charBehind, 0) : 1
+ );
+ },
+ 'backward'
+ );
+
+ if (afterRemoval === editorState.getCurrentContent()) {
+ return editorState;
+ }
+
+ var selection = editorState.getSelection();
+ return EditorState.push(
+ editorState,
+ afterRemoval.set('selectionBefore', selection),
+ selection.isCollapsed() ? 'backspace-character' : 'remove-range'
+ );
+}
+
+module.exports = keyCommandPlainBackspace;
56 src/component/handlers/edit/commands/keyCommandPlainDelete.js
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandPlainDelete
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UnicodeUtils = require('UnicodeUtils');
+
+var moveSelectionForward = require('moveSelectionForward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Remove the selected range. If the cursor is collapsed, remove the following
+ * character. This operation is Unicode-aware, so removing a single character
+ * will remove a surrogate pair properly as well.
+ */
+function keyCommandPlainDelete(editorState: EditorState): EditorState {
+ var afterRemoval = removeTextWithStrategy(
+ editorState,
+ strategyState => {
+ var selection = strategyState.getSelection();
+ var content = strategyState.getCurrentContent();
+ var key = selection.getAnchorKey();
+ var offset = selection.getAnchorOffset();
+ var charAhead = content.getBlockForKey(key).getText()[offset];
+ return moveSelectionForward(
+ strategyState,
+ charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1
+ );
+ },
+ 'forward'
+ );
+
+ if (afterRemoval === editorState.getCurrentContent()) {
+ return editorState;
+ }
+
+ var selection = editorState.getSelection();
+
+ return EditorState.push(
+ editorState,
+ afterRemoval.set('selectionBefore', selection),
+ selection.isCollapsed() ? 'delete-character' : 'remove-range'
+ );
+}
+
+module.exports = keyCommandPlainDelete;
90 src/component/handlers/edit/commands/keyCommandTransposeCharacters.js
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandTransposeCharacters
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+var getContentStateFragment = require('getContentStateFragment');
+
+/**
+ * Transpose the characters on either side of a collapsed cursor, or
+ * if the cursor is at the end of the block, transpose the last two
+ * characters.
+ */
+function keyCommandTransposeCharacters(editorState: EditorState): EditorState {
+ var selection = editorState.getSelection();
+ if (!selection.isCollapsed()) {
+ return editorState;
+ }
+
+ var offset = selection.getAnchorOffset();
+ if (offset === 0) {
+ return editorState;
+ }
+
+ var blockKey = selection.getAnchorKey();
+ var content = editorState.getCurrentContent();
+ var block = content.getBlockForKey(blockKey);
+ var length = block.getLength();
+
+ // Nothing to transpose if there aren't two characters.
+ if (length <= 1) {
+ return editorState;
+ }
+
+ var removalRange;
+ var finalSelection;
+
+ if (offset === length) {
+ // The cursor is at the end of the block. Swap the last two characters.
+ removalRange = selection.set('anchorOffset', offset - 1);
+ finalSelection = selection;
+ } else {
+ removalRange = selection.set('focusOffset', offset + 1);
+ finalSelection = removalRange.set('anchorOffset', offset + 1);
+ }
+
+ // Extract the character to move as a fragment. This preserves its
+ // styling and entity, if any.
+ var movedFragment = getContentStateFragment(content, removalRange);
+ var afterRemoval = DraftModifier.removeRange(
+ content,
+ removalRange,
+ 'backward'
+ );
+
+ // After the removal, the insertion target is one character back.
+ var selectionAfter = afterRemoval.getSelectionAfter();
+ var targetOffset = selectionAfter.getAnchorOffset() - 1;
+ var targetRange = selectionAfter.merge({
+ anchorOffset: targetOffset,
+ focusOffset: targetOffset,
+ });
+
+ var afterInsert = DraftModifier.replaceWithFragment(
+ afterRemoval,
+ targetRange,
+ movedFragment
+ );
+
+ var newEditorState = EditorState.push(
+ editorState,
+ afterInsert,
+ 'insert-fragment'
+ );
+
+ return EditorState.acceptSelection(newEditorState, finalSelection);
+}
+
+module.exports = keyCommandTransposeCharacters;
52 src/component/handlers/edit/commands/keyCommandUndo.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandUndo
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+function keyCommandUndo(
+ e: SyntheticKeyboardEvent,
+ editorState: EditorState,
+ updateFn: (editorState: EditorState) => void
+): void {
+ var undoneState = EditorState.undo(editorState);
+
+ // If the last change to occur was a spellcheck change, allow the undo
+ // event to fall through to the browser. This allows the browser to record
+ // the unwanted change, which should soon lead it to learn not to suggest
+ // the correction again.
+ if (editorState.getLastChangeType() === 'spellcheck-change') {
+ var nativelyRenderedContent = undoneState.getCurrentContent();
+ updateFn(EditorState.set(undoneState, {nativelyRenderedContent}));
+ return;
+ }
+
+ // Otheriwse, manage the undo behavior manually.
+ e.preventDefault();
+ if (!editorState.getNativelyRenderedContent()) {
+ updateFn(undoneState);
+ return;
+ }
+
+ // Trigger a re-render with the current content state to ensure that the
+ // component tree has up-to-date props for comparison.
+ updateFn(EditorState.set(editorState, {nativelyRenderedContent: null}));
+
+ // Wait to ensure that the re-render has occurred before performing
+ // the undo action.
+ setTimeout(() => {
+ updateFn(undoneState);
+ }, 0);
+}
+
+module.exports = keyCommandUndo;
58 src/component/handlers/edit/commands/moveSelectionBackward.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule moveSelectionBackward
+ * @flow
+ */
+
+'use strict';
+
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Given a collapsed selection, move the focus `maxDistance` backward within
+ * the selected block. If the selection will go beyond the start of the block,
+ * move focus to the end of the previous block, but no further.
+ *
+ * This function is not Unicode-aware, so surrogate pairs will be treated
+ * as having length 2.
+ */
+function moveSelectionBackward(
+ editorState: EditorState,
+ maxDistance: number
+): SelectionState {
+ var selection = editorState.getSelection();
+ var content = editorState.getCurrentContent();
+ var key = selection.getStartKey();
+ var offset = selection.getStartOffset();
+
+ var focusKey = key;
+ var focusOffset = 0;
+
+ if (maxDistance > offset) {
+ var keyBefore = content.getKeyBefore(key);
+ if (keyBefore == null) {
+ focusKey = key;
+ } else {
+ focusKey = keyBefore;
+ var blockBefore = content.getBlockForKey(keyBefore);
+ focusOffset = blockBefore.getText().length;
+ }
+ } else {
+ focusOffset = offset - maxDistance;
+ }
+
+ return selection.merge({
+ focusKey,
+ focusOffset,
+ isBackward: true,
+ });
+}
+
+module.exports = moveSelectionBackward;
50 src/component/handlers/edit/commands/moveSelectionForward.js
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule moveSelectionForward
+ * @flow
+ */
+
+'use strict';
+
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Given a collapsed selection, move the focus `maxDistance` forward within
+ * the selected block. If the selection will go beyond the end of the block,
+ * move focus to the start of the next block, but no further.
+ *
+ * This function is not Unicode-aware, so surrogate pairs will be treated
+ * as having length 2.
+ */
+function moveSelectionForward(
+ editorState: EditorState,
+ maxDistance: number
+): SelectionState {
+ var selection = editorState.getSelection();
+ var key = selection.getStartKey();
+ var offset = selection.getStartOffset();
+ var content = editorState.getCurrentContent();
+
+ var focusKey = key;
+ var focusOffset;
+
+ var block = content.getBlockForKey(key);
+
+ if (maxDistance > (block.getText().length - offset)) {
+ focusKey = content.getKeyAfter(key);
+ focusOffset = 0;
+ } else {
+ focusOffset = offset + maxDistance;
+ }
+
+ return selection.merge({focusKey, focusOffset});
+}
+
+module.exports = moveSelectionForward;
51 src/component/handlers/edit/commands/removeTextWithStrategy.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule removeTextWithStrategy
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+
+import type ContentState from 'ContentState';
+import type {DraftRemovalDirection} from 'DraftRemovalDirection';
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * For a collapsed selection state, remove text based on the specified strategy.
+ * If the selection state is not collapsed, remove the entire selected range.
+ */
+function removeTextWithStrategy(
+ editorState: EditorState,
+ strategy: (editorState: EditorState) => SelectionState,
+ direction: DraftRemovalDirection
+): ContentState {
+ var selection = editorState.getSelection();
+ var content = editorState.getCurrentContent();
+ var target = selection;
+ if (selection.isCollapsed()) {
+ if (direction === 'forward') {
+ if (editorState.isSelectionAtEndOfContent()) {
+ return content;
+ }
+ } else if (editorState.isSelectionAtStartOfContent()) {
+ return content;
+ }
+
+ target = strategy(editorState);
+ if (target === selection) {
+ return content;
+ }
+ }
+ return DraftModifier.removeRange(content, target, direction);
+}
+
+module.exports = removeTextWithStrategy;
163 src/component/handlers/edit/editOnBeforeInput.js
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnBeforeInput
+ * @flow
+ */
+
+'use strict';
+
+var BlockTree = require('BlockTree');
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var getEntityKeyForSelection = require('getEntityKeyForSelection');
+var isSelectionAtLeafStart = require('isSelectionAtLeafStart');
+var nullthrows = require('nullthrows');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+// When nothing is focused, Firefox regards two characters, `'` and `/`, as
+// commands that should open and focus the "quickfind" search bar. This should
+// *never* happen while a contenteditable is focused, but as of v28, it
+// sometimes does, even when the keypress event target is the contenteditable.
+// This breaks the input. Special case these characters to ensure that when
+// they are typed, we prevent default on the event to make sure not to
+// trigger quickfind.
+var FF_QUICKFIND_CHAR = '\'';
+var FF_QUICKFIND_LINK_CHAR = '\/';
+var isFirefox = UserAgent.isBrowser('Firefox');
+
+function mustPreventDefaultForCharacter(character: string): boolean {
+ return (
+ isFirefox && (
+ character == FF_QUICKFIND_CHAR ||
+ character == FF_QUICKFIND_LINK_CHAR
+ )
+ );
+}
+
+/**
+ * Replace the current selection with the specified text string, with the
+ * inline style and entity key applied to the newly inserted text.
+ */
+function replaceText(
+ editorState: EditorState,
+ text: string,
+ inlineStyle: DraftInlineStyle,
+ entityKey: ?string
+): EditorState {
+ var contentState = DraftModifier.replaceText(
+ editorState.getCurrentContent(),
+ editorState.getSelection(),
+ text,
+ inlineStyle,
+ entityKey
+ );
+ return EditorState.push(editorState, contentState, 'insert-characters');
+}
+
+/**
+ * When `onBeforeInput` executes, the browser is attempting to insert a
+ * character into the editor. Apply this character data to the document,
+ * allowing native insertion if possible.
+ *
+ * Native insertion is encouraged in order to limit re-rendering and to
+ * preserve spellcheck highlighting, which disappears or flashes if re-render
+ * occurs on the relevant text nodes.
+ */
+function editOnBeforeInput(e: SyntheticInputEvent): void {
+ var chars = e.data;
+
+ // In some cases (ex: IE ideographic space insertion) no character data
+ // is provided. There's nothing to do when this happens.
+ if (!chars) {
+ return;
+ }
+
+ // Allow the top-level component to handle the insertion manually. This is
+ // useful when triggering interesting behaviors for a character insertion,
+ // Simple examples: replacing a raw text ':)' with a smile emoji or image
+ // decorator, or setting a block to be a list item after typing '- ' at the
+ // start of the block.
+ if (this.props.handleBeforeInput && this.props.handleBeforeInput(chars)) {
+ e.preventDefault();
+ return;
+ }
+
+ // If selection is collapsed, conditionally allow native behavior. This
+ // reduces re-renders and preserves spellcheck highlighting. If the selection
+ // is not collapsed, we will re-render.
+ var editorState = this.props.editorState;
+ var selection = editorState.getSelection();
+
+ if (!selection.isCollapsed()) {
+ e.preventDefault();
+ this.update(
+ replaceText(
+ editorState,
+ chars,
+ editorState.getCurrentInlineStyle(),
+ getEntityKeyForSelection(
+ editorState.getCurrentContent(),
+ editorState.getSelection()
+ )
+ )
+ );
+ return;
+ }
+
+ var mayAllowNative = !isSelectionAtLeafStart(editorState);
+ var newEditorState = replaceText(
+ editorState,
+ chars,
+ editorState.getCurrentInlineStyle(),
+ getEntityKeyForSelection(
+ editorState.getCurrentContent(),
+ editorState.getSelection()
+ )
+ );
+
+ if (!mayAllowNative) {
+ e.preventDefault();
+ this.update(newEditorState);
+ return;
+ }
+
+ var anchorKey = selection.getAnchorKey();
+ var anchorTree = editorState.getBlockTree(anchorKey);
+
+ // Check the old and new "fingerprints" of the current block to determine
+ // whether this insertion requires any addition or removal of text nodes,
+ // in which case we would prevent the native character insertion.
+ var originalFingerprint = BlockTree.getFingerprint(anchorTree);
+ var newFingerprint = BlockTree.getFingerprint(
+ newEditorState.getBlockTree(anchorKey)
+ );
+
+ if (
+ mustPreventDefaultForCharacter(chars) ||
+ originalFingerprint !== newFingerprint ||
+ (
+ nullthrows(newEditorState.getDirectionMap()).get(anchorKey) !==
+ nullthrows(editorState.getDirectionMap()).get(anchorKey)
+ )
+ ) {
+ e.preventDefault();
+ } else {
+ // The native event is allowed to occur.
+ newEditorState = EditorState.set(newEditorState, {
+ nativelyRenderedContent: newEditorState.getCurrentContent(),
+ });
+ }
+
+ this.update(newEditorState);
+}
+
+module.exports = editOnBeforeInput;
44 src/component/handlers/edit/editOnBlur.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnBlur
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var getActiveElement = require('getActiveElement');
+
+var isWebKit = UserAgent.isEngine('WebKit');
+
+function editOnBlur(e: SyntheticEvent): void {
+ // Webkit has a bug in which blurring a contenteditable by clicking on
+ // other active elements will trigger the `blur` event but will not remove
+ // the DOM selection from the contenteditable. We therefore force the
+ // issue to be certain, checking whether the active element is `body`
+ // to force it when blurring occurs within the window (as opposed to
+ // clicking to another tab or window).
+ if (isWebKit && getActiveElement() === document.body) {
+ global.getSelection().removeAllRanges();
+ }
+
+ var editorState = this.props.editorState;
+ var currentSelection = editorState.getSelection();
+ if (!currentSelection.getHasFocus()) {
+ return;
+ }
+
+ var selection = currentSelection.set('hasFocus', false);
+ this.props.onBlur && this.props.onBlur(e);
+ this.update(EditorState.acceptSelection(editorState, selection));
+}
+
+module.exports = editOnBlur;
29 src/component/handlers/edit/editOnCompositionStart.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCompositionStart
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * The user has begun using an IME input system. Switching to `composite` mode
+ * allows handling composition input and disables other edit behavior.
+ */
+function editOnCompositionStart(): void {
+ this.setRenderGuard();
+ this.setMode('composite');
+ this.update(
+ EditorState.set(this.props.editorState, {inCompositionMode: true})
+ );
+}
+
+module.exports = editOnCompositionStart;
35 src/component/handlers/edit/editOnCopy.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCopy
+ * @flow
+ */
+
+'use strict';
+
+var getFragmentFromSelection = require('getFragmentFromSelection');
+
+/**
+ * If we have a selection, create a ContentState fragment and store
+ * it in our internal clipboard. Subsequent paste events will use this
+ * fragment if no external clipboard data is supplied.
+ */
+function editOnCopy(e: SyntheticClipboardEvent): void {
+ var editorState = this.props.editorState;
+ var selection = editorState.getSelection();
+
+ // No selection, so there's nothing to copy.
+ if (selection.isCollapsed()) {
+ e.preventDefault();
+ return;
+ }
+
+ this.setClipboard(getFragmentFromSelection(this.props.editorState));
+}
+
+module.exports = editOnCopy;
71 src/component/handlers/edit/editOnCut.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCut
+ * @flow
+ */
+
+'use strict';
+
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+const Style = require('Style');
+
+const getFragmentFromSelection = require('getFragmentFromSelection');
+const getScrollPosition = require('getScrollPosition');
+
+/**
+ * On `cut` events, native behavior is allowed to occur so that the system
+ * clipboard is set properly. This means that we need to take steps to recover
+ * the editor DOM state after the `cut` has occurred in order to maintain
+ * control of the component.
+ *
+ * In addition, we can keep a copy of the removed fragment, including all
+ * styles and entities, for use as an internal paste.
+ */
+function editOnCut(e: SyntheticClipboardEvent): void {
+ const editorState = this.props.editorState;
+ const selection = editorState.getSelection();
+
+ // No selection, so there's nothing to cut.
+ if (selection.isCollapsed()) {
+ e.preventDefault();
+ return;
+ }
+
+ // Track the current scroll position so that it can be forced back in place
+ // after the editor regains control of the DOM.
+ const scrollParent = Style.getScrollParent(e.target);
+ const {x, y} = getScrollPosition(scrollParent);
+
+ const fragment = getFragmentFromSelection(editorState);
+ this.setClipboard(fragment);
+
+ // Set `cut` mode to disable all event handling temporarily.
+ this.setRenderGuard();
+ this.setMode('cut');
+
+ // Let native `cut` behavior occur, then recover control.
+ setTimeout(() => {
+ this.restoreEditorDOM({x, y});
+ this.removeRenderGuard();
+ this.exitCurrentMode();
+ this.update(removeFragment(editorState));
+ }, 0);
+}
+
+function removeFragment(editorState: EditorState): EditorState {
+ const newContent = DraftModifier.removeRange(
+ editorState.getCurrentContent(),
+ editorState.getSelection(),
+ 'forward'
+ );
+ return EditorState.push(editorState, newContent, 'remove-range');
+}
+
+module.exports = editOnCut;
24 src/component/handlers/edit/editOnDragOver.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnDragOver
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * Drag behavior has begun from outside the editor element.
+ */
+function editOnDragOver(e: SyntheticDragEvent): void {
+ this._internalDrag = false;
+ this.setMode('drag');
+ e.preventDefault();
+}
+
+module.exports = editOnDragOver;
23 src/component/handlers/edit/editOnDragStart.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnDragStart
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * A `dragstart` event has begun within the text editor component.
+ */
+function editOnDragStart(): void {
+ this._internalDrag = true;
+ this.setMode('drag');
+}
+
+module.exports = editOnDragStart;
36 src/component/handlers/edit/editOnFocus.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnFocus
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+function editOnFocus(e: SyntheticFocusEvent): void {
+ var editorState = this.props.editorState;
+ var currentSelection = editorState.getSelection();
+ if (currentSelection.getHasFocus()) {
+ return;
+ }
+
+ var selection = currentSelection.set('hasFocus', true);
+ this.props.onFocus && this.props.onFocus(e);
+
+ // When the tab containing this text editor is hidden and the user does a
+ // find-in-page in a _different_ tab, Chrome on Mac likes to forget what the
+ // selection was right after sending this focus event and (if you let it)
+ // moves the cursor back to the beginning of the editor, so we force the
+ // selection here instead of simply accepting it in order to preserve the
+ // old cursor position. See https://crbug.com/540004.
+ this.update(EditorState.forceSelection(editorState, selection));
+}
+
+module.exports = editOnFocus;
128 src/component/handlers/edit/editOnInput.js
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnInput
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var DraftOffsetKey = require('DraftOffsetKey');
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var findAncestorOffsetKey = require('findAncestorOffsetKey');
+var nullthrows = require('nullthrows');
+
+var isGecko = UserAgent.isEngine('Gecko');
+
+var DOUBLE_NEWLINE = '\n\n';
+
+/**
+ * This function is intended to handle spellcheck and autocorrect changes,
+ * which occur in the DOM natively without any opportunity to observe or
+ * interpret the changes before they occur.
+ *
+ * The `input` event fires in contentEditable elements reliably for non-IE
+ * browsers, immediately after changes occur to the editor DOM. Since our other
+ * handlers override or otherwise handle cover other varieties of text input,
+ * the DOM state should match the model in all controlled input cases. Thus,
+ * when an `input` change leads to a DOM/model mismatch, the change should be
+ * due to a spellcheck change, and we can incorporate it into our model.
+ */
+function editOnInput(): void {
+ var domSelection = global.getSelection();
+
+ var {anchorNode, isCollapsed} = domSelection;
+ if (anchorNode.nodeType !== Node.TEXT_NODE) {
+ return;
+ }
+
+ var domText = anchorNode.textContent;
+ var {editorState} = this.props;
+ var offsetKey = nullthrows(findAncestorOffsetKey(anchorNode));
+ var {blockKey, decoratorKey, leafKey} = DraftOffsetKey.decode(offsetKey);
+
+ var {start, end} = editorState
+ .getBlockTree(blockKey)
+ .getIn([decoratorKey, 'leaves', leafKey]);
+
+ var content = editorState.getCurrentContent();
+ var block = content.getBlockForKey(blockKey);
+ var modelText = block.getText().slice(start, end);
+
+ // Special-case soft newlines here. If the DOM text ends in a soft newline,
+ // we will have manually inserted an extra soft newline in DraftEditorLeaf.
+ // We want to remove this extra newline for the purpose of our comparison
+ // of DOM and model text.
+ if (domText.endsWith(DOUBLE_NEWLINE)) {
+ domText = domText.slice(0, -1);
+ }
+
+ // No change -- the DOM is up to date. Nothing to do here.
+ if (domText === modelText) {
+ return;
+ }
+
+ var selection = editorState.getSelection();
+
+ // We'll replace the entire leaf with the text content of the target.
+ var targetRange = selection.merge({
+ anchorOffset: start,
+ focusOffset: end,
+ isBackward: false,
+ });
+
+ var newContent = DraftModifier.replaceText(
+ content,
+ targetRange,
+ domText,
+ block.getInlineStyleAt(start)
+ );
+
+ var anchorOffset, focusOffset, startOffset, endOffset;
+
+ if (isGecko) {
+ // Firefox selection does not change while the context menu is open, so
+ // we preserve the anchor and focus values of the DOM selection.
+ anchorOffset = domSelection.anchorOffset;
+ focusOffset = domSelection.focusOffset;
+ startOffset = start + Math.min(anchorOffset, focusOffset);
+ endOffset = startOffset + Math.abs(anchorOffset - focusOffset);
+ anchorOffset = startOffset;
+ focusOffset = endOffset;
+ } else {
+ // Browsers other than Firefox may adjust DOM selection while the context
+ // menu is open, and Safari autocorrect is prone to providing an inaccurate
+ // DOM selection. Don't trust it. Instead, use our existing SelectionState
+ // and adjust it based on the number of characters changed during the
+ // mutation.
+ var charDelta = domText.length - modelText.length;
+ startOffset = selection.getStartOffset();
+ endOffset = selection.getEndOffset();
+
+ anchorOffset = isCollapsed ? endOffset + charDelta : startOffset;
+ focusOffset = endOffset + charDelta;
+ }
+
+ var contentWithAdjustedDOMSelection = newContent.merge({
+ selectionBefore: content.getSelectionAfter(),
+ selectionAfter: selection.merge({anchorOffset, focusOffset}),
+ });
+
+ this.update(
+ EditorState.push(
+ editorState,
+ contentWithAdjustedDOMSelection,
+ 'spellcheck-change'
+ )
+ );
+}
+
+module.exports = editOnInput;
135 src/component/handlers/edit/editOnKeyDown.js
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnKeyDown
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var Keys = require('Keys');
+var SecondaryClipboard = require('SecondaryClipboard');
+
+var keyCommandBackspaceToStartOfLine = require('keyCommandBackspaceToStartOfLine');
+var keyCommandBackspaceWord = require('keyCommandBackspaceWord');
+var keyCommandDeleteWord = require('keyCommandDeleteWord');
+var keyCommandInsertNewline = require('keyCommandInsertNewline');
+var keyCommandPlainBackspace = require('keyCommandPlainBackspace');
+var keyCommandPlainDelete = require('keyCommandPlainDelete');
+var keyCommandMoveSelectionToEndOfBlock = require('keyCommandMoveSelectionToEndOfBlock');
+var keyCommandMoveSelectionToStartOfBlock = require('keyCommandMoveSelectionToStartOfBlock');
+var keyCommandTransposeCharacters = require('keyCommandTransposeCharacters');
+var keyCommandUndo = require('keyCommandUndo');
+
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+
+/**
+ * Map a `DraftEditorCommand` command value to a corresponding function.
+ */
+function onKeyCommand(
+ command: DraftEditorCommand,
+ editorState: EditorState
+): EditorState {
+ switch (command) {
+ case 'redo':
+ return EditorState.redo(editorState);
+ case 'delete':
+ return keyCommandPlainDelete(editorState);
+ case 'delete-word':
+ return keyCommandDeleteWord(editorState);
+ case 'backspace':
+ return keyCommandPlainBackspace(editorState);
+ case 'backspace-word':
+ return keyCommandBackspaceWord(editorState);
+ case 'backspace-to-start-of-line':
+ return keyCommandBackspaceToStartOfLine(editorState);
+ case 'split-block':
+ return keyCommandInsertNewline(editorState);
+ case 'transpose-characters':
+ return keyCommandTransposeCharacters(editorState);
+ case 'move-selection-to-start-of-block':
+ return keyCommandMoveSelectionToStartOfBlock(editorState);
+ case 'move-selection-to-end-of-block':
+ return keyCommandMoveSelectionToEndOfBlock(editorState);
+ case 'secondary-cut':
+ return SecondaryClipboard.cut(editorState);
+ case 'secondary-paste':
+ return SecondaryClipboard.paste(editorState);
+ default:
+ return editorState;
+ }
+}
+
+/**
+ * Intercept keydown behavior to handle keys and commands manually, if desired.
+ *
+ * Keydown combinations may be mapped to `DraftCommand` values, which may
+ * correspond to command functions that modify the editor or its contents.
+ *
+ * See `getDefaultKeyBinding` for defaults. Alternatively, the top-level
+ * component may provide a custom mapping via the `keyBindingFn` prop.
+ */
+function editOnKeyDown(e: SyntheticKeyboardEvent): void {
+ var keyCode = e.which;
+ var editorState = this.props.editorState;
+
+ switch (keyCode) {
+ case Keys.RETURN:
+ e.preventDefault();
+ // The top-level component may manually handle newline insertion. If
+ // no special handling is performed, insert a newline.
+ if (!this.props.handleReturn || !this.props.handleReturn(e)) {
+ this.update(keyCommandInsertNewline(editorState));
+ }
+ return;
+ case Keys.ESC:
+ e.preventDefault();
+ this.props.onEscape && this.props.onEscape(e);
+ return;
+ case Keys.TAB:
+ this.props.onTab && this.props.onTab(e);
+ return;
+ case Keys.UP:
+ this.props.onUpArrow && this.props.onUpArrow(e);
+ return;
+ case Keys.DOWN:
+ this.props.onDownArrow && this.props.onDownArrow(e);
+ return;
+ }
+
+ var command = this.props.keyBindingFn(e);
+
+ // If no command is specified, allow keydown event to continue.
+ if (!command) {
+ return;
+ }
+
+ if (command === 'undo') {
+ // Since undo requires some special updating behavior to keep the editor
+ // in sync, handle it separately.
+ keyCommandUndo(e, editorState, this.update);
+ return;
+ }
+
+ // At this point, we know that we're handling a command of some kind, so
+ // we don't want to insert a character following the keydown.
+ e.preventDefault();
+
+ // Allow components higher up the tree to handle the command first.
+ if (this.props.handleKeyCommand && this.props.handleKeyCommand(command)) {
+ return;
+ }
+
+ var newState = onKeyCommand(command, editorState);
+ if (newState !== editorState) {
+ this.update(newState);
+ }
+}
+
+module.exports = editOnKeyDown;
0 src/component/handlers/edit/editOnPaste.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/handlers/edit/editOnSelect.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/handlers/edit/getFragmentFromSelection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/DOMDerivedSelection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/DraftOffsetKey.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/DraftOffsetKeyPath.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/__tests__/getDraftEditorSelection-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/expandRangeToStartOfLine.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/findAncestorOffsetKey.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getDraftEditorSelection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getDraftEditorSelectionWithNodes.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getRangeBoundingClientRect.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getRangeClientRects.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getSelectionOffsetKeyForNode.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/getUpdatedSelectionState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/isSelectionAtLeafStart.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/selection/setDraftEditorSelection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/DraftStyleDefault.css
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/KeyBindingUtil.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/getDefaultKeyBinding.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/getElementForBlockType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/getTextContentFromFiles.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/getWrapperTemplateForBlockType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/isSoftNewlineEvent.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/component/utils/splitTextIntoTextBlocks.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/constants/ComposedEntityMutability.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/constants/ComposedEntityType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/constants/DraftBlockType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/constants/DraftEditorCommand.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/constants/DraftRemovalDirection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/decorators/CompositeDraftDecorator.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/decorators/DraftDecorator.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/decorators/DraftDecoratorType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/decorators/__tests__/CompositeDraftDecorator-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/DraftStringKey.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/EntityRange.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/InlineStyleRange.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/RawDraftContentBlock.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/RawDraftContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/RawDraftEntity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/__tests__/decodeEntityRanges-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/__tests__/decodeInlineStyleRanges-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/__tests__/encodeEntityRanges-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/__tests__/encodeInlineStyleRanges-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/__tests__/sanitizeDraftText-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/convertFromDraftStateToRaw.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/convertFromRawToDraftState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/createCharacterList.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/decodeEntityRanges.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/decodeInlineStyleRanges.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/encodeEntityRanges.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/encodeInlineStyleRanges.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/encoding/sanitizeDraftText.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/DraftEntity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/DraftEntityInstance.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/DraftEntityMutability.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/DraftEntityType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/__mocks__/DraftEntity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/__tests__/DraftEntity-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/__tests__/getEntityKeyForSelection-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/getEntityKeyForSelection.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/entity/getTextAfterNearestEntity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/BlockMap.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/BlockMapBuilder.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/BlockTree.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/CharacterMetadata.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/ContentBlock.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/ContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/DefaultDraftInlineStyle.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/DraftInlineStyle.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/EditorBidiService.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/EditorChangeType.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/EditorState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/EditorStateCreationConfig.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/SampleDraftInlineStyle.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/SelectionState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/BlockTree-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/CharacterMetadata-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/ContentBlock-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/ContentState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/EditorBidiService-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/EditorState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/SelectionState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/__tests__/findRangesImmutable-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/immutable/findRangesImmutable.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/keys/generateBlockKey.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/DraftEntitySegments.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/DraftModifier.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/DraftRange.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/DraftRemovableWord.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/RichTextEditorUtil.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/__tests__/DraftRemovableWord-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/getCharacterRemovalRange.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/modifier/getRangesForDraftEntity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/paste/DraftPasteProcessor.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/paste/__mocks__/getSafeBodyFromHTML.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/paste/__tests__/DraftPasteProcessor-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/paste/getSafeBodyFromHTML.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/ContentStateInlineStyle.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/ContentStateInlineStyle-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/applyEntityToContentBlock-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/applyEntityToContentState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/insertIntoList-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/insertTextIntoContentState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/removeEntitiesAtEdges-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/removeRangeFromContentState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/__tests__/splitBlockInContentState-test.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/adjustBlockDepthForContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/applyEntityToContentBlock.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/applyEntityToContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/getContentStateFragment.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/getSampleStateForTesting.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/insertFragmentIntoContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/insertIntoList.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/insertTextIntoContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/removeEntitiesAtEdges.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/removeRangeFromContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/setBlockTypeForContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 src/model/transaction/splitBlockInContentState.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/README.md
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/DocsSidebar.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/H2.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/Header.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/HeaderLinks.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/Marked.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/Prism.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/Site.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/center.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/metadata.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/core/unindent.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/layout/DocsLayout.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/layout/PageLayout.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/package.json
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/publish.sh
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/server/convert.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/server/generate.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/server/server.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/css/draft.css
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-block-components.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-block-styling.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-decorators.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-entities.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-inline-styles.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-issues-and-pitfalls.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-key-bindings.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-managing-focus.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-nested-lists.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced-topics-text-direction.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/block-components.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/block-styling.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/decorators.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/entities.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/event-handling.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/inline-styles.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/key-bindings.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/nested-lists.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/performance.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/text-direction.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/undo-redo.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/advanced/unicode.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-character-metadata.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-content-block.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-content-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-data-conversion.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-editor-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-editor.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-entity.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-modifier.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api-reference-selection-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/character-metadata.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/content-block.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/content-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/data-conversion.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/drafteditor.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/editor-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/modifier.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/selection-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/api/transaction-functions.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/guides/an-immutable-model.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/guides/controlled-contenteditable.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/guides/controlling-contenteditable.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/guides/why-draft.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/model/overview.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/model/selection-state.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/overview.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart-api-basics.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart-rich-styling.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart/api-basics.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart/customizing-your-editor.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart/decorated-text.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart/rich-styling.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/docs/quickstart/the-basics.js
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/img/flux-simple-f8-diagram-1300w.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
0 website/src/draft-js/img/flux-simple-f8-diagram-explained-1300w.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
0 website/src/draft-js/img/flux-simple-f8-diagram-with-client-action-1300w.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
0 website/src/draft-js/img/flux_logo.svg
Sorry, we could not display the changes to this file because there were too many other changes to display.
0 website/src/draft-js/index.js
Sorry, we could not display the changes to this file because there were too many other changes to display.

0 comments on commit 342576b

Please sign in to comment.
Something went wrong with that request. Please try again.