Permalink
Showing
with
9,776 additions
and 0 deletions.
- +6 โ0 .eslintignore
- +15 โ0 .eslintrc.js
- +8 โ0 .gitignore
- +14 โ0 .travis.yml
- +40 โ0 CONTRIBUTING.md
- +31 โ0 LICENSE
- +11 โ0 LICENSE-examples
- +33 โ0 PATENTS
- +53 โ0 README.md
- +157 โ0 docs/APIReference-CharacterMetadata.md
- +246 โ0 docs/APIReference-ContentBlock.md
- +265 โ0 docs/APIReference-ContentState.md
- +46 โ0 docs/APIReference-Data-Conversion.md
- +193 โ0 docs/APIReference-Editor.md
- +481 โ0 docs/APIReference-EditorState.md
- +117 โ0 docs/APIReference-Entity.md
- +207 โ0 docs/APIReference-Modifier.md
- +324 โ0 docs/APIReference-SelectionState.md
- +120 โ0 docs/Advanced-Topics-Block-Components.md
- +59 โ0 docs/Advanced-Topics-Block-Styling.md
- +152 โ0 docs/Advanced-Topics-Decorators.md
- +136 โ0 docs/Advanced-Topics-Entities.md
- +112 โ0 docs/Advanced-Topics-Inline-Styles.md
- +75 โ0 docs/Advanced-Topics-Issues-and-Pitfalls.md
- +100 โ0 docs/Advanced-Topics-Key-Bindings.md
- +40 โ0 docs/Advanced-Topics-Managing-Focus.md
- +22 โ0 docs/Advanced-Topics-Nested-Lists.md
- +35 โ0 docs/Advanced-Topics-Text-Direction.md
- +45 โ0 docs/Overview.md
- +75 โ0 docs/QuickStart-API-Basics.md
- +112 โ0 docs/QuickStart-Rich-Styling.md
- +219 โ0 examples/color/color.html
- +223 โ0 examples/entity/entity.html
- +178 โ0 examples/link/link.html
- +89 โ0 examples/plaintext/plaintext.html
- +62 โ0 examples/rich/RichEditor.css
- +218 โ0 examples/rich/rich.html
- +172 โ0 examples/tex/.eslintrc
- +25 โ0 examples/tex/js/app.js
- +176 โ0 examples/tex/js/components/TeXBlock.js
- +113 โ0 examples/tex/js/components/TeXEditorExample.js
- +68 โ0 examples/tex/js/data/content.js
- +98 โ0 examples/tex/js/modifiers/insertTeXBlock.js
- +39 โ0 examples/tex/js/modifiers/removeTeXBlock.js
- +22 โ0 examples/tex/package.json
- +100 โ0 examples/tex/public/TeXEditor.css
- +15 โ0 examples/tex/public/index.html
- +53 โ0 examples/tex/server.js
- +144 โ0 examples/tweet/tweet.html
- +134 โ0 gulpfile.js
- +85 โ0 package.json
- +20 โ0 scripts/babel/default-options.js
- +31 โ0 scripts/jest/preprocessor.js
- +17 โ0 src/.flowconfig
- +53 โ0 src/Draft.js
- +67 โ0 src/component/base/DraftEditor.css
- +448 โ0 src/component/base/DraftEditor.react.js
- +17 โ0 src/component/base/DraftEditorPlaceholder.css
- +64 โ0 src/component/base/DraftEditorPlaceholder.react.js
- +116 โ0 src/component/base/DraftEditorProps.js
- +18 โ0 src/component/base/DraftScrollPosition.js
- +15 โ0 src/component/base/DraftTextAlignment.js
- +224 โ0 src/component/contents/DraftEditorBlock.react.js
- +232 โ0 src/component/contents/DraftEditorContents.react.js
- +158 โ0 src/component/contents/DraftEditorLeaf.react.js
- +96 โ0 src/component/contents/DraftEditorTextNode.react.js
- +555 โ0 src/component/contents/__tests__/DraftEditorBlock.react-test.js
- +212 โ0 src/component/contents/__tests__/DraftEditorTextNode-test.js
- +43 โ0 src/component/handlers/DraftEditorModes.js
- +183 โ0 src/component/handlers/composition/DraftEditorCompositionHandler.js
- +156 โ0 src/component/handlers/drag/DraftEditorDragHandler.js
- +43 โ0 src/component/handlers/edit/DraftEditorEditHandler.js
- +80 โ0 src/component/handlers/edit/commands/SecondaryClipboard.js
- +55 โ0 src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js
- +54 โ0 src/component/handlers/edit/commands/keyCommandBackspaceWord.js
- +52 โ0 src/component/handlers/edit/commands/keyCommandDeleteWord.js
- +26 โ0 src/component/handlers/edit/commands/keyCommandInsertNewline.js
- +39 โ0 src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js
- +39 โ0 src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
- +55 โ0 src/component/handlers/edit/commands/keyCommandPlainBackspace.js
- +56 โ0 src/component/handlers/edit/commands/keyCommandPlainDelete.js
- +90 โ0 src/component/handlers/edit/commands/keyCommandTransposeCharacters.js
- +52 โ0 src/component/handlers/edit/commands/keyCommandUndo.js
- +58 โ0 src/component/handlers/edit/commands/moveSelectionBackward.js
- +50 โ0 src/component/handlers/edit/commands/moveSelectionForward.js
- +51 โ0 src/component/handlers/edit/commands/removeTextWithStrategy.js
- +163 โ0 src/component/handlers/edit/editOnBeforeInput.js
- +44 โ0 src/component/handlers/edit/editOnBlur.js
- +29 โ0 src/component/handlers/edit/editOnCompositionStart.js
- +35 โ0 src/component/handlers/edit/editOnCopy.js
- +71 โ0 src/component/handlers/edit/editOnCut.js
- +24 โ0 src/component/handlers/edit/editOnDragOver.js
- +23 โ0 src/component/handlers/edit/editOnDragStart.js
- +36 โ0 src/component/handlers/edit/editOnFocus.js
- +128 โ0 src/component/handlers/edit/editOnInput.js
- +135 โ0 src/component/handlers/edit/editOnKeyDown.js
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 |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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()`. |
@@ -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()`. |
@@ -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. |
@@ -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 | ||
+``` |
@@ -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). |
@@ -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`. |
@@ -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`. |
@@ -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. |
@@ -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. |
@@ -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; | ||
+} | ||
+``` |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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'`. |
@@ -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. |
@@ -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. |
@@ -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. |
@@ -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 || | ||
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.
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.
Wouldn't this be the same as
ReactDOM.findDOMNode(this)
? Why the ref?