getselectedcontent.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/utils/getselectedcontent
  7. */
  8. import Range from '../range';
  9. import Position from '../position';
  10. /**
  11. * Gets a clone of the selected content.
  12. *
  13. * For example, for the following selection:
  14. *
  15. * <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
  16. *
  17. * It will return a document fragment with such a content:
  18. *
  19. * <quote><h>st</h></quote><p>se</p>
  20. *
  21. * @param {module:engine/model/model~Model} model The model in context of which
  22. * the selection modification should be performed.
  23. * @param {module:engine/model/selection~Selection} selection The selection of which content will be returned.
  24. * @returns {module:engine/model/documentfragment~DocumentFragment}
  25. */
  26. export default function getSelectedContent( model, selection ) {
  27. return model.change( writer => {
  28. const frag = writer.createDocumentFragment();
  29. const range = selection.getFirstRange();
  30. if ( !range || range.isCollapsed ) {
  31. return frag;
  32. }
  33. const root = range.start.root;
  34. const commonPath = range.start.getCommonPath( range.end );
  35. const commonParent = root.getNodeByPath( commonPath );
  36. // ## 1st step
  37. //
  38. // First, we'll clone a fragment represented by a minimal flat range
  39. // containing the original range to be cloned.
  40. // E.g. let's consider such a range:
  41. //
  42. // <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
  43. //
  44. // A minimal flat range containing this one is:
  45. //
  46. // <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
  47. //
  48. // We can easily clone this structure, preserving e.g. the <quote> element.
  49. let flatSubtreeRange;
  50. if ( range.start.parent == range.end.parent ) {
  51. // The original range is flat, so take it.
  52. flatSubtreeRange = range;
  53. } else {
  54. flatSubtreeRange = Range.createFromParentsAndOffsets(
  55. commonParent, range.start.path[ commonPath.length ],
  56. commonParent, range.end.path[ commonPath.length ] + 1
  57. );
  58. }
  59. const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
  60. // Clone the whole contents.
  61. for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
  62. if ( item.is( 'textProxy' ) ) {
  63. writer.appendText( item.data, item.getAttributes(), frag );
  64. } else {
  65. writer.append( item._clone( true ), frag );
  66. }
  67. }
  68. // ## 2nd step
  69. //
  70. // If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
  71. //
  72. // For example, for the range shown in the 1st step comment, we need to remove these pieces:
  73. //
  74. // <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
  75. //
  76. // So this will be the final copied content:
  77. //
  78. // <quote><h>st</h></quote><p>se</p>
  79. //
  80. // In order to do that, we remove content from these two ranges:
  81. //
  82. // [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
  83. if ( flatSubtreeRange != range ) {
  84. // Find the position of the original range in the cloned fragment.
  85. const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
  86. const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
  87. const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
  88. removeRangeContent( rightExcessRange, writer );
  89. removeRangeContent( leftExcessRange, writer );
  90. }
  91. return frag;
  92. } );
  93. }
  94. // After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed,
  95. // this function will, most likely, be able to rewritten using getMinimalFlatRanges().
  96. function removeRangeContent( range, writer ) {
  97. const parentsToCheck = [];
  98. Array.from( range.getItems( { direction: 'backward' } ) )
  99. // We should better store ranges because text proxies will lose integrity
  100. // with the text nodes when we'll start removing content.
  101. .map( item => Range.createOn( item ) )
  102. // Filter only these items which are fully contained in the passed range.
  103. //
  104. // E.g. for the following range: [<quote><p>y</p><h>fir]st</h>
  105. // the walker will return the entire <h> element, when only the "fir" item inside it is fully contained.
  106. .filter( itemRange => {
  107. // We should be able to use Range.containsRange, but https://github.com/ckeditor/ckeditor5-engine/issues/691.
  108. const contained =
  109. ( itemRange.start.isAfter( range.start ) || itemRange.start.isEqual( range.start ) ) &&
  110. ( itemRange.end.isBefore( range.end ) || itemRange.end.isEqual( range.end ) );
  111. return contained;
  112. } )
  113. .forEach( itemRange => {
  114. parentsToCheck.push( itemRange.start.parent );
  115. writer.remove( itemRange );
  116. } );
  117. // Remove ancestors of the removed items if they turned to be empty now
  118. // (their whole content was contained in the range).
  119. parentsToCheck.forEach( parentToCheck => {
  120. let parent = parentToCheck;
  121. while ( parent.parent && parent.isEmpty ) {
  122. const removeRange = Range.createOn( parent );
  123. parent = parent.parent;
  124. writer.remove( removeRange );
  125. }
  126. } );
  127. }