8
0

getselectedcontent.js 5.1 KB

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