modifyselection.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/controller/modifyselection
  7. */
  8. import Position from '../model/position';
  9. import TreeWalker from '../model/treewalker';
  10. import Range from '../model/range';
  11. import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
  12. /**
  13. * Modifies the selection. Currently, the supported modifications are:
  14. *
  15. * * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
  16. * Possible values for `unit` are:
  17. * * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
  18. * character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
  19. * with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
  20. * letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
  21. * selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
  22. * why `'character'` value is most natural and common method of modifying selection.
  23. * * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
  24. * selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
  25. * However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
  26. * two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
  27. * For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
  28. * outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  29. * extension will include whole "surrogate pair".
  30. *
  31. * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
  32. *
  33. * @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which
  34. * the selection modification should be performed.
  35. * @param {module:engine/model/selection~Selection} selection The selection to modify.
  36. * @param {Object} [options]
  37. * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
  38. * @param {'character'|'codePoint'} [options.unit='character'] The unit by which selection should be modified.
  39. */
  40. export default function modifySelection( dataController, selection, options = {} ) {
  41. const schema = dataController.model.schema;
  42. const isForward = options.direction != 'backward';
  43. const unit = options.unit ? options.unit : 'character';
  44. const focus = selection.focus;
  45. const walker = new TreeWalker( {
  46. boundaries: getSearchRange( focus, isForward ),
  47. singleCharacters: true,
  48. direction: isForward ? 'forward' : 'backward'
  49. } );
  50. const data = { walker, schema, isForward, unit };
  51. let next;
  52. while ( ( next = walker.next() ) ) {
  53. if ( next.done ) {
  54. return;
  55. }
  56. const position = tryExtendingTo( data, next.value );
  57. if ( position ) {
  58. selection.setFocus( position );
  59. return;
  60. }
  61. }
  62. }
  63. // Checks whether the selection can be extended to the the walker's next value (next position).
  64. function tryExtendingTo( data, value ) {
  65. // If found text, we can certainly put the focus in it. Let's just find a correct position
  66. // based on the unit.
  67. if ( value.type == 'text' ) {
  68. return getCorrectPosition( data.walker, data.unit );
  69. }
  70. // Entering an element.
  71. if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
  72. // If it's an object, we can select it now.
  73. if ( data.schema.objects.has( value.item.name ) ) {
  74. return Position.createAt( value.item, data.isForward ? 'after' : 'before' );
  75. }
  76. // If text allowed on this position, extend to this place.
  77. if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
  78. return value.nextPosition;
  79. }
  80. }
  81. // Leaving an element.
  82. else {
  83. // If leaving a limit element, stop.
  84. if ( data.schema.limits.has( value.item.name ) ) {
  85. // NOTE: Fast-forward the walker until the end.
  86. data.walker.skip( () => true );
  87. return;
  88. }
  89. // If text allowed on this position, extend to this place.
  90. if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
  91. return value.nextPosition;
  92. }
  93. }
  94. }
  95. // Finds a correct position by walking in a text node and checking whether selection can be extended to given position
  96. // or should be extended further.
  97. function getCorrectPosition( walker, unit ) {
  98. const textNode = walker.position.textNode;
  99. if ( textNode ) {
  100. const data = textNode.data;
  101. let offset = walker.position.offset - textNode.startOffset;
  102. while ( isInsideSurrogatePair( data, offset ) || ( unit == 'character' && isInsideCombinedSymbol( data, offset ) ) ) {
  103. walker.next();
  104. offset = walker.position.offset - textNode.startOffset;
  105. }
  106. }
  107. return walker.position;
  108. }
  109. function getSearchRange( start, isForward ) {
  110. const root = start.root;
  111. const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
  112. if ( isForward ) {
  113. return new Range( start, searchEnd );
  114. } else {
  115. return new Range( searchEnd, start );
  116. }
  117. }