filler.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals window */
  6. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  7. import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
  8. /**
  9. * Set of utils related to block and inline fillers handling.
  10. *
  11. * Browsers do not allow to put caret in elements which does not have height. Because of it, we need to fill all
  12. * empty elements which should be selectable with elements or characters called "fillers". Unfortunately there is no one
  13. * universal filler, this is why two types are uses:
  14. *
  15. * * Block filler is an element which fill block elements, like `<p>`. CKEditor uses `<br>` as a block filler during the editing,
  16. * as browsers do natively. So instead of an empty `<p>` there will be `<p><br></p>`. The advantage of block filler is that
  17. * it is transparent for the selection, so when the caret is before the `<br>` and user presses right arrow he will be
  18. * moved to the next paragraph, not after the `<br>`. The disadvantage is that it breaks a block, so it can not be used
  19. * in the middle of a line of text. The {@link module:engine/view/filler~BR_FILLER `<br>` filler} can be replaced with any other
  20. * character in the data output, for instance {@link module:engine/view/filler~NBSP_FILLER non-breaking space}.
  21. *
  22. * * Inline filler is a filler which does not break a line of text, so it can be used inside the text, for instance in the empty
  23. * `<b>` surrendered by text: `foo<b></b>bar`, if we want to put the caret there. CKEditor uses a sequence of the zero-width
  24. * spaces as an {@link module:engine/view/filler~INLINE_FILLER inline filler} having the predetermined
  25. * {@link module:engine/view/filler~INLINE_FILLER_LENGTH length}. A sequence is used, instead of a single character to
  26. * avoid treating random zero-width spaces as the inline filler. Disadvantage of the inline filler is that it is not
  27. * transparent for the selection. The arrow key moves the caret between zero-width spaces characters, so the additional
  28. * code is needed to handle the caret.
  29. *
  30. * Both inline and block fillers are handled by the {@link module:engine/view/renderer~Renderer renderer} and are not present in the
  31. * view.
  32. *
  33. * @module engine/view/filler
  34. */
  35. /**
  36. * `<br>` filler creator. This is a function which creates `<br data-cke-filler="true">` element.
  37. * It defines how the filler is created.
  38. *
  39. * @see module:engine/view/filler~NBSP_FILLER
  40. * @function
  41. */
  42. export const BR_FILLER = domDocument => {
  43. const fillerBr = domDocument.createElement( 'br' );
  44. fillerBr.dataset.ckeFiller = true;
  45. return fillerBr;
  46. };
  47. /**
  48. * Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
  49. * It defines how the filler is created.
  50. *
  51. * @see module:engine/view/filler~BR_FILLER
  52. * @function
  53. */
  54. export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' );
  55. /**
  56. * Length of the {@link module:engine/view/filler~INLINE_FILLER INLINE_FILLER}.
  57. */
  58. export const INLINE_FILLER_LENGTH = 7;
  59. /**
  60. * Inline filler which is sequence of the zero width spaces.
  61. */
  62. export let INLINE_FILLER = '';
  63. for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
  64. INLINE_FILLER += '\u200b';
  65. }
  66. /**
  67. * Checks if the node is a text node which starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
  68. *
  69. * startsWithFiller( document.createTextNode( INLINE_FILLER ) ); // true
  70. * startsWithFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // true
  71. * startsWithFiller( document.createTextNode( 'foo' ) ); // false
  72. * startsWithFiller( document.createElement( 'p' ) ); // false
  73. *
  74. * @param {Node} domNode DOM node.
  75. * @returns {Boolean} True if the text node starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
  76. */
  77. export function startsWithFiller( domNode ) {
  78. return isText( domNode ) && ( domNode.data.substr( 0, INLINE_FILLER_LENGTH ) === INLINE_FILLER );
  79. }
  80. /**
  81. * Checks if the text node contains only the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
  82. *
  83. * isInlineFiller( document.createTextNode( INLINE_FILLER ) ); // true
  84. * isInlineFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // false
  85. *
  86. * @param {Text} domText DOM text node.
  87. * @returns {Boolean} True if the text node contains only the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
  88. */
  89. export function isInlineFiller( domText ) {
  90. return domText.data.length == INLINE_FILLER_LENGTH && startsWithFiller( domText );
  91. }
  92. /**
  93. * Get string data from the text node, removing an {@link module:engine/view/filler~INLINE_FILLER inline filler} from it,
  94. * if text node contains it.
  95. *
  96. * getDataWithoutFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ) == 'foo' // true
  97. * getDataWithoutFiller( document.createTextNode( 'foo' ) ) == 'foo' // true
  98. *
  99. * @param {Text} domText DOM text node, possible with inline filler.
  100. * @returns {String} Data without filler.
  101. */
  102. export function getDataWithoutFiller( domText ) {
  103. if ( startsWithFiller( domText ) ) {
  104. return domText.data.slice( INLINE_FILLER_LENGTH );
  105. } else {
  106. return domText.data;
  107. }
  108. }
  109. // Cache block fillers templates to improve performance.
  110. const templateBlockFillers = new WeakMap();
  111. /**
  112. * Checks if the node is an instance of the block filler of the given type.
  113. *
  114. * const brFillerInstance = BR_FILLER( document );
  115. * isBlockFiller( brFillerInstance, BR_FILLER ); // true
  116. *
  117. * @param {Node} domNode DOM node to check.
  118. * @param {Function} blockFiller Block filler creator.
  119. * @returns {Boolean} True if text node contains only {@link module:engine/view/filler~INLINE_FILLER inline filler}.
  120. */
  121. export function isBlockFiller( domNode, blockFiller ) {
  122. let templateBlockFiller = templateBlockFillers.get( blockFiller );
  123. if ( !templateBlockFiller ) {
  124. templateBlockFiller = blockFiller( window.document );
  125. templateBlockFillers.set( blockFiller, templateBlockFiller );
  126. }
  127. return domNode.isEqualNode( templateBlockFiller );
  128. }
  129. /**
  130. * Assign key observer which move cursor from the end of the inline filler to the beginning of it when
  131. * the left arrow is pressed, so the filler does not break navigation.
  132. *
  133. * @param {module:engine/view/view~View} view View controller instance we should inject quirks handling on.
  134. */
  135. export function injectQuirksHandling( view ) {
  136. view.document.on( 'keydown', jumpOverInlineFiller );
  137. }
  138. // Move cursor from the end of the inline filler to the beginning of it when, so the filler does not break navigation.
  139. function jumpOverInlineFiller( evt, data ) {
  140. if ( data.keyCode == keyCodes.arrowleft ) {
  141. const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
  142. if ( domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed ) {
  143. const domParent = domSelection.getRangeAt( 0 ).startContainer;
  144. const domOffset = domSelection.getRangeAt( 0 ).startOffset;
  145. if ( startsWithFiller( domParent ) && domOffset <= INLINE_FILLER_LENGTH ) {
  146. domSelection.collapse( domParent, 0 );
  147. }
  148. }
  149. }
  150. }