You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

183 line
5.4 KiB

  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow strict-local
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. import type { BlockNodeRecord } from "./BlockNodeRecord";
  13. import type { DraftInlineStyle } from "./DraftInlineStyle";
  14. import type SelectionState from "./SelectionState";
  15. const DraftEditorTextNode = require("./DraftEditorTextNode.react");
  16. const React = require("react");
  17. const invariant = require("fbjs/lib/invariant");
  18. const isHTMLBRElement = require("./isHTMLBRElement");
  19. const setDraftEditorSelection = require("./setDraftEditorSelection").setDraftEditorSelection;
  20. type CSSStyleObject = {
  21. [property: string]: string | number,
  22. ...
  23. };
  24. type CustomStyleMap = {
  25. [name: string]: CSSStyleObject,
  26. ...
  27. };
  28. type CustomStyleFn = (style: DraftInlineStyle, block: BlockNodeRecord) => ?CSSStyleObject;
  29. type Props = {
  30. // The block that contains this leaf.
  31. block: BlockNodeRecord,
  32. // Mapping of style names to CSS declarations.
  33. customStyleMap: CustomStyleMap,
  34. // Function that maps style names to CSS style objects.
  35. customStyleFn: CustomStyleFn,
  36. // Whether to force the DOM selection after render.
  37. forceSelection: boolean,
  38. // Whether this leaf is the last in its block. Used for a DOM hack.
  39. isLast: boolean,
  40. offsetKey: string,
  41. // The current `SelectionState`, used to represent a selection range in the
  42. // editor
  43. selection: ?SelectionState,
  44. // The offset of this string within its block.
  45. start: number,
  46. // The set of style(s) names to apply to the node.
  47. styleSet: DraftInlineStyle,
  48. // The full text to be rendered within this node.
  49. text: string,
  50. ...
  51. };
  52. /**
  53. * All leaf nodes in the editor are spans with single text nodes. Leaf
  54. * elements are styled based on the merging of an optional custom style map
  55. * and a default style map.
  56. *
  57. * `DraftEditorLeaf` also provides a wrapper for calling into the imperative
  58. * DOM Selection API. In this way, top-level components can declaratively
  59. * maintain the selection state.
  60. */
  61. class DraftEditorLeaf extends React.Component<Props> {
  62. /**
  63. * By making individual leaf instances aware of their context within
  64. * the text of the editor, we can set our selection range more
  65. * easily than we could in the non-React world.
  66. *
  67. * Note that this depends on our maintaining tight control over the
  68. * DOM structure of the DraftEditor component. If leaves had multiple
  69. * text nodes, this would be harder.
  70. */
  71. leaf: ?HTMLElement;
  72. _setSelection(): void {
  73. const {
  74. selection
  75. } = this.props; // If selection state is irrelevant to the parent block, no-op.
  76. if (selection == null || !selection.getHasFocus()) {
  77. return;
  78. }
  79. const {
  80. block,
  81. start,
  82. text
  83. } = this.props;
  84. const blockKey = block.getKey();
  85. const end = start + text.length;
  86. if (!selection.hasEdgeWithin(blockKey, start, end)) {
  87. return;
  88. } // Determine the appropriate target node for selection. If the child
  89. // is not a text node, it is a <br /> spacer. In this case, use the
  90. // <span> itself as the selection target.
  91. const node = this.leaf;
  92. invariant(node, 'Missing node');
  93. const child = node.firstChild;
  94. invariant(child, 'Missing child');
  95. let targetNode;
  96. if (child.nodeType === Node.TEXT_NODE) {
  97. targetNode = child;
  98. } else if (isHTMLBRElement(child)) {
  99. targetNode = node;
  100. } else {
  101. targetNode = child.firstChild;
  102. invariant(targetNode, 'Missing targetNode');
  103. }
  104. setDraftEditorSelection(selection, targetNode, blockKey, start, end);
  105. }
  106. shouldComponentUpdate(nextProps: Props): boolean {
  107. const leafNode = this.leaf;
  108. invariant(leafNode, 'Missing leafNode');
  109. const shouldUpdate = leafNode.textContent !== nextProps.text || nextProps.styleSet !== this.props.styleSet || nextProps.forceSelection;
  110. return shouldUpdate;
  111. }
  112. componentDidUpdate(): void {
  113. this._setSelection();
  114. }
  115. componentDidMount(): void {
  116. this._setSelection();
  117. }
  118. render(): React.Node {
  119. const {
  120. block
  121. } = this.props;
  122. let {
  123. text
  124. } = this.props; // If the leaf is at the end of its block and ends in a soft newline, append
  125. // an extra line feed character. Browsers collapse trailing newline
  126. // characters, which leaves the cursor in the wrong place after a
  127. // shift+enter. The extra character repairs this.
  128. if (text.endsWith('\n') && this.props.isLast) {
  129. text += '\n';
  130. }
  131. const {
  132. customStyleMap,
  133. customStyleFn,
  134. offsetKey,
  135. styleSet
  136. } = this.props;
  137. let styleObj = styleSet.reduce((map, styleName) => {
  138. const mergedStyles = {};
  139. const style = customStyleMap[styleName];
  140. if (style !== undefined && map.textDecoration !== style.textDecoration) {
  141. // .trim() is necessary for IE9/10/11 and Edge
  142. mergedStyles.textDecoration = [map.textDecoration, style.textDecoration].join(' ').trim();
  143. }
  144. return Object.assign(map, style, mergedStyles);
  145. }, {});
  146. if (customStyleFn) {
  147. const newStyles = customStyleFn(styleSet, block);
  148. styleObj = Object.assign(styleObj, newStyles);
  149. }
  150. return <span data-offset-key={offsetKey} ref={ref => this.leaf = ref} style={styleObj}>
  151. <DraftEditorTextNode>{text}</DraftEditorTextNode>
  152. </span>;
  153. }
  154. }
  155. module.exports = DraftEditorLeaf;