内联样式和块样式并不是我们想要添加到编辑器中的唯一一种丰富样式。例如,Facebook的评论输入为mentions和hashtag提供蓝色的背景高亮显示。
为了支持定制富文本的灵活性,Draft提供了一个“装饰器(Decorators)”系统。tweet示例提供了一个可运行的装饰器示例。

CompositeDecorator(组合decorator)

装饰器的概念是基于对给定ContentBlock的内容进行扫描,寻找与已定义策略(比如一个正则)相匹配的文本范围,然后使用指定的React组件呈现它们。
您可以使用CompositeDecorator类来定义您想要的装饰器行为。这个类允许您提供多个DraftDecorator对象,并将依次使用每个策略搜索一个文本块。
装饰器存储在EditorState记录中。当创建一个新的EditorState对象时,例如通过EditorState. createEmpty(),可以选择传递一个装饰器参数。

额外说一点 当内容改变的时候,最终的EditorState对象会用它的装饰器重新计算新的ContentState ,并且定义要装饰的部分。渲染输出的时候会形成一个完整的有块元素、装饰器、内联元素的树。 用这种方式,我们能确保当内容改变的时候,被渲染的装饰器跟EditorState实时同步。

在这个“Tweet”编辑器的例子中,举个例子,我们用一个CompositeDecorator搜索@-handle字符串和#hashtag字符串

  1. const compositeDecorator = new CompositeDecorator([
  2. {
  3. strategy: handleStrategy,
  4. component: HandleSpan,
  5. },
  6. {
  7. strategy: hashtagStrategy,
  8. component: HashtagSpan,
  9. },
  10. ]);

这个复合的装饰器会首先扫描给定区域内的文字,找完@-handle后会找匹配matches的内容。

  1. // Note: these aren't very good regexes, don't use them!
  2. const HANDLE_REGEX = /\@[\w]+/g;
  3. const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
  4. function handleStrategy(contentBlock, callback, contentState) {
  5. findWithRegex(HANDLE_REGEX, contentBlock, callback);
  6. }
  7. function hashtagStrategy(contentBlock, callback, contentState) {
  8. findWithRegex(HASHTAG_REGEX, contentBlock, callback);
  9. }
  10. function findWithRegex(regex, contentBlock, callback) {
  11. const text = contentBlock.getText();
  12. let matchArr, start;
  13. while ((matchArr = regex.exec(text)) !== null) {
  14. start = matchArr.index;
  15. callback(start, start + matchArr[0].length);
  16. }
  17. }

这个策略函数会执行返回一个回调函数,回调函数传递匹配文字的start值和end值。

Decorator Components

为了要装饰部分文字,你必须定义一个用于渲染他们的React component。这些可以是一个带着CSS类或者样式的span标签。
在我们这个例子中,CompositeDecorator对象声明了HandleSpanHashtagSpan 用于装饰用的Component,这些是React的无状态组件。

  1. const HandleSpan = props => {
  2. return (
  3. <span {...props} style={styles.handle}>
  4. {props.children}
  5. </span>
  6. );
  7. };
  8. const HashtagSpan = props => {
  9. return (
  10. <span {...props} style={styles.hashtag}>
  11. {props.children}
  12. </span>
  13. );
  14. };

Decorator Component 在props中接收一系列元数据,包括contentState的副本,entityKey(如果有的话)和blockKey,有关提供给Decorator Component完整的props列表,请参考 DraftDecoratorComponentProps type
注意props.children被传递给渲染的输出。这样做是为了确保装饰后的文字会在span内渲染。
你可以用相同的方法处理链接,就像link example中那样做的。

Beyond CompositeDecorator

装饰器对象被用于EditorState,它仅仅需要匹配DraftDecoratorType的类型定义,这意味着你可以创建任何一个你期望的装饰器类,只要他们匹配期望的类型,你将不会被CompositeDecorator束缚。

Setting new decorators

未来,我们将可以在EditorState中设置新的decorator值,通过immutable的方法,在正常的状态下进行传递和设置。
这意味着在你的app工作流中,如果你的decorator成为无效或者需要一个修改,你可以创建一个新的装饰器对象或者用null删除所有的装饰器,并且EditorState.set()使用新的装饰器设置。
举个例子,因为某些原因,我们希望当用户操作编辑器的时候,禁用@-handle装饰器,它会变得很简单,就像下面这样做:

  1. function turnOffHandleDecorations(editorState) {
  2. const onlyHashtags = new CompositeDecorator([{
  3. strategy: hashtagStrategy,
  4. component: HashtagSpan,
  5. }]);
  6. return EditorState.set(editorState, {decorator: onlyHashtags});
  7. }

editorState中的ContentState会用新的装饰器重新计算,并且@-handle装饰器不会在下个渲染中呈现出来。
要再一次说明一下,因为使用持久化数据,它依然会是高效的。

  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 • Decorators</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 {CompositeDecorator, Editor, EditorState} = Draft;
  30. class TweetEditorExample extends React.Component {
  31. constructor() {
  32. super();
  33. const compositeDecorator = new CompositeDecorator([
  34. {
  35. strategy: handleStrategy,
  36. component: HandleSpan,
  37. },
  38. {
  39. strategy: hashtagStrategy,
  40. component: HashtagSpan,
  41. },
  42. ]);
  43. this.state = {
  44. editorState: EditorState.createEmpty(compositeDecorator),
  45. };
  46. this.focus = () => this.refs.editor.focus();
  47. this.onChange = (editorState) => this.setState({editorState});
  48. this.logState = () => console.log(this.state.editorState.toJS());
  49. }
  50. render() {
  51. return (
  52. <div style={styles.root}>
  53. <div style={styles.editor} onClick={this.focus}>
  54. <Editor
  55. editorState={this.state.editorState}
  56. onChange={this.onChange}
  57. placeholder="Write a tweet..."
  58. ref="editor"
  59. spellCheck={true}
  60. />
  61. </div>
  62. <input
  63. onClick={this.logState}
  64. style={styles.button}
  65. type="button"
  66. value="Log State"
  67. />
  68. </div>
  69. );
  70. }
  71. }
  72. /**
  73. * Super simple decorators for handles and hashtags, for demonstration
  74. * purposes only. Don't reuse these regexes.
  75. */
  76. const HANDLE_REGEX = /@[\w]+/g;
  77. const HASHTAG_REGEX = /#[\w\u0590-\u05ff]+/g;
  78. function handleStrategy(contentBlock, callback, contentState) {
  79. findWithRegex(HANDLE_REGEX, contentBlock, callback);
  80. }
  81. function hashtagStrategy(contentBlock, callback, contentState) {
  82. findWithRegex(HASHTAG_REGEX, contentBlock, callback);
  83. }
  84. function findWithRegex(regex, contentBlock, callback) {
  85. const text = contentBlock.getText();
  86. let matchArr, start;
  87. while ((matchArr = regex.exec(text)) !== null) {
  88. start = matchArr.index;
  89. callback(start, start + matchArr[0].length);
  90. }
  91. }
  92. const HandleSpan = (props) => {
  93. return (
  94. <span
  95. style={styles.handle}
  96. data-offset-key={props.offsetKey}
  97. >
  98. {props.children}
  99. </span>
  100. );
  101. };
  102. const HashtagSpan = (props) => {
  103. return (
  104. <span
  105. style={styles.hashtag}
  106. data-offset-key={props.offsetKey}
  107. >
  108. {props.children}
  109. </span>
  110. );
  111. };
  112. const styles = {
  113. root: {
  114. fontFamily: '\'Helvetica\', sans-serif',
  115. padding: 20,
  116. width: 600,
  117. },
  118. editor: {
  119. border: '1px solid #ddd',
  120. cursor: 'text',
  121. fontSize: 16,
  122. minHeight: 40,
  123. padding: 10,
  124. },
  125. button: {
  126. marginTop: 10,
  127. textAlign: 'center',
  128. },
  129. handle: {
  130. color: 'rgba(98, 177, 254, 1.0)',
  131. direction: 'ltr',
  132. unicodeBidi: 'bidi-override',
  133. },
  134. hashtag: {
  135. color: 'rgba(95, 184, 138, 1.0)',
  136. },
  137. };
  138. ReactDOM.render(
  139. <TweetEditorExample />,
  140. document.getElementById('target')
  141. );
  142. </script>
  143. </body>
  144. </html>