本文讨论了实体系统,Draft使用它来用元数据注释文本范围。实体引入了样式文本之外的丰富的文本显示。超链接、提及和嵌入的内容都可以使用实体来实现。
在Draft的代码存储库中,超链接文本编辑器实体demo提供了实际的代码示例,以帮助阐明如何使用实体以及它们的内置行为。
实体的api参考中,提供了有关创建、检索或更新实体对象时使用的静态方法的详细信息。
有关实体API最近更改的信息,以及如何更新应用程序的示例,请参阅我们的v0.10 API迁移指南

介绍

一个实体是一个在Draft编辑器中代表一个范围文本的元数据的对象。它有三个属性:

  • type:表示它是哪种实体的字符串。 ‘LINK’, ‘MENTION’, ‘PHOTO’,这三种类型中的其中一个
  • mutability:不要将其与immutable-js中的immutability混淆,该属性表示在编辑器中编辑文本范围时使用该实体对象注释的文本范围的行为。下面将更详细地讨论这个问题。
  • data:包含实体元数据的可选对象。例如,’LINK’实体可能包含一个数据对象,该对象包含该链接的href值。

所有实体都存储在ContentState记录中。实体由ContentState中的key键引用,而React组件用于修饰带注释的范围。(我们目前正在弃用以前用于访问实体的API;见问题# 839。)

使用decoratorscustom block components,您可以基于实体元数据向编辑器添加丰富的文本呈现。

创建和检索实体

实体应该使用contentState.createEntity,它接受上述三个属性作为参数。这个方法返回一个ContentState包含新创建的实体的新状态记录,也就是返回一个新的ContentState带上了新建的实体,然后你可以调用contentState.getLastCreatedEntityKey获取新创建的实体记录的键。

此键是将实体应用到内容时应该使用的值。例如,Modifier模块包含了一个applyEntity方法:

  1. const contentState = editorState.getCurrentContent();
  2. const contentStateWithEntity = contentState.createEntity('LINK', 'MUTABLE', {
  3. url: 'http://www.zombo.com',
  4. });
  5. const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
  6. const contentStateWithLink = Modifier.applyEntity(
  7. contentStateWithEntity,
  8. selectionState,
  9. entityKey,
  10. );
  11. const newEditorState = EditorState.push(editorState, {
  12. currentContent: contentStateWithLink,
  13. });

然后对于给定的文本范围,你可以使用ContentBlock对象上的getEntityAt()方法提取其文本相关联的实体key键,其中你可以在方法中传入目标文本中偏移量值。

  1. const contentState = editorState.getCurrentContent();
  2. const blockWithLinkAtBeginning = contentState.getBlockForKey('...');
  3. const linkKey = blockWithLinkAtBeginning.getEntityAt(0);
  4. const linkInstance = contentState.getEntity(linkKey);
  5. const {url} = linkInstance.getData();

“Mutability”

实体可能有三个”Mutability”值中的一个。它们之间的区别是当用户对它们进行编辑时它们的行为方式。注意DraftEntityInstance对象一直都是持久化的记录,并且这个属性意味着只能表明带注释的文字在编辑器里是如何突变的。(未来的更改可能会重命名此属性,以避免命名方面的潜在混乱)

Immutable

如果不从文字中删除整个实体,文字是不能被改变的。带这个属性的实体都是一个完整的粒子。
举个例子,在Facebook的输入框中,添加一个@功能,比如(@ mqyqingfeng)。然后,增加一个字符或者删掉一个字符,注意每当增加字符的时候,实体被删除,当删除字符的时候 ,整个实体被删除。
这个属性值非常有用当文字必须完全匹配它的相关的元数据,而且不能被改变。

Mutable

文字可以自由的改变。举个例子,链接的文字是可以设置成”mutable”的,因为链接的地址和文字不是强关联的。

Segmented

Segmented分段像”mutable”实体一样强关联对应的文字,但是允许自定义的删除。
举个例子,在Facebook的输入框中,@一个朋友,然后,给这个名字添加一个字符。注意,实体会被从整个字符串中移除,因为你@的朋友们没有你改后的名字。
接下来,在@的字符串中,尝试删除一个字符或者一个单词。注意只有你删除的那一部分实体会被删除,用这样方式。我们可以允许缩写名。

Modifying Entities

因为DraftEntityInstance记录是持久化的,你也许不能直接更新data属性。
相应的,两个Entity方法可以用来修改实体:mergeDatareplaceData。前者允许通过传入一个对象来合并来更新数据,而后者则完全交换新数据对象。
image.png

Using Entities for Rich Content

下一篇文章会讲到装饰器(decorator)对象的用法,这些对象可用于检索用于呈现渲染目的的实体。
link编辑器示例提供了一个可直接运行的实体创建和装饰器(decorator)的工作示例。

_

  1. <!--
  2. Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
  3. This file provided by Facebook is for non-commercial testing and evaluation
  4. purposes only. Facebook reserves all rights not expressly granted.
  5. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  6. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  7. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  8. FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  9. ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  10. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  11. -->
  12. <!DOCTYPE html>
  13. <html>
  14. <head>
  15. <meta charset="utf-8" />
  16. <title>Draft • Link Editor</title>
  17. <link rel="stylesheet" href="../../../dist/Draft.css" />
  18. </head>
  19. <body>
  20. <div id="target"></div>
  21. <script src="../../../node_modules/react/umd/react.development.js"></script>
  22. <script src="../../../node_modules/react-dom/umd/react-dom.development.js"></script>
  23. <script src="../../../node_modules/immutable/dist/immutable.js"></script>
  24. <script src="../../../node_modules/es6-shim/es6-shim.js"></script>
  25. <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js"></script>
  26. <script src="../../../dist/Draft.js"></script>
  27. <script type="text/babel">
  28. 'use strict';
  29. const {
  30. convertToRaw,
  31. CompositeDecorator,
  32. Editor,
  33. EditorState,
  34. RichUtils,
  35. } = Draft;
  36. class LinkEditorExample extends React.Component {
  37. constructor(props) {
  38. super(props);
  39. const decorator = new CompositeDecorator([
  40. {
  41. strategy: findLinkEntities,
  42. component: Link,
  43. },
  44. ]);
  45. this.state = {
  46. editorState: EditorState.createEmpty(decorator),
  47. showURLInput: false,
  48. urlValue: '',
  49. };
  50. this.focus = () => this.refs.editor.focus();
  51. this.onChange = (editorState) => this.setState({editorState});
  52. this.logState = () => {
  53. const content = this.state.editorState.getCurrentContent();
  54. console.log(convertToRaw(content));
  55. };
  56. this.promptForLink = this._promptForLink.bind(this);
  57. this.onURLChange = (e) => this.setState({urlValue: e.target.value});
  58. this.confirmLink = this._confirmLink.bind(this);
  59. this.onLinkInputKeyDown = this._onLinkInputKeyDown.bind(this);
  60. this.removeLink = this._removeLink.bind(this);
  61. }
  62. _promptForLink(e) {
  63. e.preventDefault();
  64. const {editorState} = this.state;
  65. const selection = editorState.getSelection();
  66. if (!selection.isCollapsed()) {
  67. const contentState = editorState.getCurrentContent();
  68. const startKey = editorState.getSelection().getStartKey();
  69. const startOffset = editorState.getSelection().getStartOffset();
  70. const blockWithLinkAtBeginning = contentState.getBlockForKey(startKey);
  71. const linkKey = blockWithLinkAtBeginning.getEntityAt(startOffset);
  72. let url = '';
  73. if (linkKey) {
  74. const linkInstance = contentState.getEntity(linkKey);
  75. url = linkInstance.getData().url;
  76. }
  77. this.setState({
  78. showURLInput: true,
  79. urlValue: url,
  80. }, () => {
  81. setTimeout(() => this.refs.url.focus(), 0);
  82. });
  83. }
  84. }
  85. _confirmLink(e) {
  86. e.preventDefault();
  87. const {editorState, urlValue} = this.state;
  88. const contentState = editorState.getCurrentContent();
  89. const contentStateWithEntity = contentState.createEntity(
  90. 'LINK',
  91. 'MUTABLE',
  92. {url: urlValue}
  93. );
  94. const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
  95. const newEditorState = EditorState.set(editorState, { currentContent: contentStateWithEntity });
  96. this.setState({
  97. editorState: RichUtils.toggleLink(
  98. newEditorState,
  99. newEditorState.getSelection(),
  100. entityKey
  101. ),
  102. showURLInput: false,
  103. urlValue: '',
  104. }, () => {
  105. setTimeout(() => this.refs.editor.focus(), 0);
  106. });
  107. }
  108. _onLinkInputKeyDown(e) {
  109. if (e.which === 13) {
  110. this._confirmLink(e);
  111. }
  112. }
  113. _removeLink(e) {
  114. e.preventDefault();
  115. const {editorState} = this.state;
  116. const selection = editorState.getSelection();
  117. if (!selection.isCollapsed()) {
  118. this.setState({
  119. editorState: RichUtils.toggleLink(editorState, selection, null),
  120. });
  121. }
  122. }
  123. render() {
  124. let urlInput;
  125. if (this.state.showURLInput) {
  126. urlInput =
  127. <div style={styles.urlInputContainer}>
  128. <input
  129. onChange={this.onURLChange}
  130. ref="url"
  131. style={styles.urlInput}
  132. type="text"
  133. value={this.state.urlValue}
  134. onKeyDown={this.onLinkInputKeyDown}
  135. />
  136. <button onMouseDown={this.confirmLink}>
  137. Confirm
  138. </button>
  139. </div>;
  140. }
  141. return (
  142. <div style={styles.root}>
  143. <div style={{marginBottom: 10}}>
  144. Select some text, then use the buttons to add or remove links
  145. on the selected text.
  146. </div>
  147. <div style={styles.buttons}>
  148. <button
  149. onMouseDown={this.promptForLink}
  150. style={{marginRight: 10}}>
  151. Add Link
  152. </button>
  153. <button onMouseDown={this.removeLink}>
  154. Remove Link
  155. </button>
  156. </div>
  157. {urlInput}
  158. <div style={styles.editor} onClick={this.focus}>
  159. <Editor
  160. editorState={this.state.editorState}
  161. onChange={this.onChange}
  162. placeholder="Enter some text..."
  163. ref="editor"
  164. />
  165. </div>
  166. <input
  167. onClick={this.logState}
  168. style={styles.button}
  169. type="button"
  170. value="Log State"
  171. />
  172. </div>
  173. );
  174. }
  175. }
  176. function findLinkEntities(contentBlock, callback, contentState) {
  177. contentBlock.findEntityRanges(
  178. (character) => {
  179. const entityKey = character.getEntity();
  180. return (
  181. entityKey !== null &&
  182. contentState.getEntity(entityKey).getType() === 'LINK'
  183. );
  184. },
  185. callback
  186. );
  187. }
  188. const Link = (props) => {
  189. const {url} = props.contentState.getEntity(props.entityKey).getData();
  190. return (
  191. <a href={url} style={styles.link}>
  192. {props.children}
  193. </a>
  194. );
  195. };
  196. const styles = {
  197. root: {
  198. fontFamily: '\'Georgia\', serif',
  199. padding: 20,
  200. width: 600,
  201. },
  202. buttons: {
  203. marginBottom: 10,
  204. },
  205. urlInputContainer: {
  206. marginBottom: 10,
  207. },
  208. urlInput: {
  209. fontFamily: '\'Georgia\', serif',
  210. marginRight: 10,
  211. padding: 3,
  212. },
  213. editor: {
  214. border: '1px solid #ccc',
  215. cursor: 'text',
  216. minHeight: 80,
  217. padding: 10,
  218. },
  219. button: {
  220. marginTop: 10,
  221. textAlign: 'center',
  222. },
  223. link: {
  224. color: '#3b5998',
  225. textDecoration: 'underline',
  226. },
  227. };
  228. ReactDOM.render(
  229. <LinkEditorExample />,
  230. document.getElementById('target')
  231. );
  232. </script>
  233. </body>
  234. </html>