filler.js 7.1 KB

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